April, 2007

  • The Old New Thing

    Everyone should have a house pianist

    • 18 Comments

    Last year, a request was sent out to a social mailing list that went something like this:

    Hi, everybody. My girlfriend is coming to visit for a week, and she's a pianist. Does anybody have a piano they would be willing to let her practice on while she's here?

    I offered the piano in my living room and gave him the key to my house. I came home one day to hear the house filled with the resonant chords of a Beethoven piano sonata. You can have all the recordings you want. There's nothing like experiencing music played live right in front of you. The music fills your body in a way recordings simply are not capable of reproducing.

    Everyone should have a house pianist.

    (The wife of one of my relatives has a degree in piano performance. He doesn't know how good he's got it. It's also her birthday today. Happy birthday!)

  • The Old New Thing

    The default verb is not necessarily "open"

    • 19 Comments

    If you hunt around the Internet you'll find lots of people† who hard-code the string "open" as the second parameter to the ShellExecute function, indicating that they want to open the document specified as the third parameter. While that's a nice thing to want to do, it might not be the right thing.

    When the user double-clicks a document, the shell executes the default verb, which is not necessarily "open". For example, for audio files, the default verb is often "play", and for pictures, it's often "view". If you want to launch the document as if the user had double-clicked it, then you need to pass NULL as the lpOperation.

    Commenter brad asked how the default verb is determined. This is explained in MSDN in the section titled, not surprisingly, Verbs and File Associations:

    The primary verb is specified by the default value of the shell key, or the open key if the shell key has no default value. ... The primary verb is also referred to as the default verb.

    Nitpicker's corner

    †The phrase "lots of people" means "lots of people". It doesn't mean "lots of people not including Microsoft."

  • The Old New Thing

    News Flash: Not being able to watch Jay Leno is not an emergency

    • 10 Comments

    The Mesa (Arizona) Police Department reminds us in a press release that 9-1-1 is for emergencies, and the inability to watch Jay Leno due to a power outage does not qualify as an emergency.

    (Noting, because everybody else will, that this is not the same as the unconfirmed of the woman who called 9-1-1 for a cheeseburger emergency.)

    Update: First link appears to be dead. A snippet of the article can be found here; search for "Mesa Police".

    Update 2: First link repaired thanks to commenter Maurits.

  • The Old New Thing

    Stupid debugger tricks: Calling functions and methods

    • 14 Comments

    Back in the old days, if you wanted to call a function from inside the debugger, you had to do it by hand: Save the registers, push the parameters onto the stack (or into registers if the function uses fastcall or thiscall) push the address of the ntdll!DbgBreakPoint function, move the instruction pointer to the start of the function you want to call, then hit "g" to resume execution. The function runs then returns to the ntdll!DbgBreakPoint, where the debugger regains control and you can look at the results. Then restore the registers (including the original instruction pointer) and resume debugging. (That paragraph was just a quick recap; I'm assuming you already knew that.)

    The Windows symbolic debugger engine (the debugging engine behind ntsd, cdb and windbg) can now automate this process. Suppose you want to call this function:

    int DoSomething(int i, int j);
    

    You can ask the debugger to do all the heavy lifting:

    0:001> .call ABC!DoSomething(1,2)
    Thread is set up for call, 'g' will execute.
    WARNING: This can have serious side-effects,
    including deadlocks and corruption of the debuggee.
    0:001> r
    eax=7ffde000 ebx=00000001 ecx=00000001 edx=00000003 esi=00000004 edi=00000005
    eip=10250132 esp=00a7ffbc ebp=00a7fff4 iopl=0         nv up ei pl zr na po nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
    ABC!DoSomething:
    10250132 55               push    ebp
    0:001> dd esp
    00a7ffbc  00a7ffc8 00000001 00000002 ccfdebcc
    

    Notice that the debugger nicely pushed the parameters onto the stack and set the eip register for you. All you have to do is hit "g" and the DoSomething function will run. Once it returns, the debugger will restore the original state.

    This technique even works with C++ methods:

    // pretend that we know that 0x00131320 is an IStream pointer
    0:001> .dvalloc 1000
    Allocated 1000 bytes starting at 00a80000
    0:001> .call ABC!CAlphaStream::Read(0x00131320, 0xa80000, 0x1000, 0)
    Thread is set up for call, 'g' will execute.
    WARNING: This can have serious side-effects,
    including deadlocks and corruption of the debuggee.
    

    Notice that when calling a nonstatic C++ method, you have to pass the "this" parameter as an explicit first parameter. The debugger knows what calling convention to use and puts the registers in the correct location. In this case, it knew that CAlphaStream::Read uses the stdcall calling convention, so the parameters have all been pushed onto the stack.

    And what's with that .dvalloc command? That's another debugger helper function that allocates some memory in the debugged process's address space. Here, we used it to allocate a buffer that we want to read into.

    But what if you want to call a method on an interface, and you don't have the source code to the implementation? For example, you want to read from a stream that was passed to you from some external component. Well, you can play a little trick. You can pretend to call a function that you do have the source code to, one that has the same function signature, and then move the eip register to the desired entry point.

    // pretend that we know that 0x00131320 is an IStream pointer
    0:000>  dp 131320 l1
    00131320  77f6b5e8 // vtable
    0:000> dps 77f6b5e8 l4
    77f6b5e8  77fbff0e SHLWAPI!CFileStream::QueryInterface
    77f6b5ec  77fb34ed SHLWAPI!CAssocW2k::AddRef
    77f6b5f0  77f6b670 SHLWAPI!CFileStream::Release
    77f6b5f4  77f77474 SHLWAPI!CFileStream::Read
    0:000> .call SHLWAPI!CFileStream::Read(0x00131320, 0xa80000, 0x1000, 0)
                    ^ Symbol not a function in '.call SHLWAPI!CFileStream::Read'
    

    That error message is the debugger's somewhat confusing way of saying, "I don't have enough information available to make that function call." But that's okay, because we have a function that's "close enough", namely CAlphaStream::Read:

    0:001> .call ABC!CAlphaStream::Read(0x00131320, 0xa80000, 0x1000, 0)
    Thread is set up for call, 'g' will execute.
    WARNING: This can have serious side-effects,
    including deadlocks and corruption of the debuggee.
    0:000> r eip=SHLWAPI!CFileStream::Read
    0:000> r
    eax=00131320 ebx=0007d628 ecx=00130000 edx=0013239e esi=00000000 edi=00000003
    eip=77f77474 esp=0007d384 ebp=0007d3b0 iopl=0         nv up ei pl zr na po nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
    SHLWAPI!CFileStream::Read:
    77f77474 8bff             mov     edi,edi
    

    Woo-hoo! We got ABC!CAlphaStream::Read to push all the parameters for us, and then whoosh we swap out that function and slip CFileStream::Read in its place. Now you can hit "g" to execute the CFileStream::Read call.

    This just skims the surface of what you can do with the .call command. Mix in some C++ expression evaluation and you've got yourself a pretty nifty "pseudo-immediate mode" expression evaluator.

  • The Old New Thing

    The intermediate value theory helps your table but not necessarily your beer

    • 28 Comments

    An Australian mathematician applied intermediate value theory to the problem of keeping a table from wobbling: Just rotate the table and you'll eventually find a spot.

    A few things struck me about that article. First, that it explains that intermediate value theory "is the same principle underlying the fact that there will always be two points with exactly the same temperature somewhere on Earth." While that's true, it's way overkill. Intermediate value theory lets you find two points on any circle with the same temperature; you don't need a whole sphere. (Perhaps they got the scenario mixed up with the Borsuk-Ulam theorem, which shows the significantly stronger result that you can find two antipodal points on the Earth with the same temperature and pressure.)

    The next thing that struck me is that the author twice used beer to illustrate the article. But then again, it was an Australian article, so maybe that's just normal for Australia.

    Finally, the article concludes that the theorem may help you place your refrigerator. Well, sure, if you don't care which direction the door faces.

  • The Old New Thing

    Using the "gu" debugger command to find the infinite loop

    • 15 Comments

    Somebody says, "Your program is consuming 100% CPU" and hands you a debug session. Usually, this happens because one thread has gotten stuck in an infinite loop. And if you're lucky it's the type of infinite loop that's easy to diagnose because it's just one function that isn't returning. (The more complicated types are where a function does some work and then returns, and then some of that work has a delayed effect that causes the function to run again, and so on.) Let's assume we're lucky because, well, debugging is an exercise in optimism.

    The first step is to find the thread that is using all the CPU. That's actually pretty easy with the help of the !runaway debugger extension.

    0:011> !runaway
     User Mode Time
     Thread    Time
     192c      0 days 0:05:22.457
     1384      0 days 0:00:16.063
     14ac      0 days 0:00:08.392
     48c       0 days 0:00:03.955
     1db0      0 days 0:00:00.010
     1888      0 days 0:00:00.010
     1078      0 days 0:00:00.000
     1470      0 days 0:00:00.000
     1f84      0 days 0:00:00.000
     1d60      0 days 0:00:00.000
     1850      0 days 0:00:00.000
     134c      0 days 0:00:00.000
     19fc      0 days 0:00:00.000
     1b4       0 days 0:00:00.000
    

    Wow, thread 0x192c has sure used a lot of CPU time, but that doesn't mean that it's the thread that is in a 100% CPU loop, because the CPU time is cumulative over the lifetime of the thread. Maybe that thread has a lot of CPU time because it's been around the longest. What you need to do is resume execution for a little while, then break in again and see whose CPU time has increased.

    0:011> g
    ^C
    (1928.1d34): Break instruction exception - code 80000003 (first chance)
    eax=7ffd9000 ebx=00000001 ecx=00000002 edx=00000003 esi=00000004 edi=00000005
    eip=7c901230 esp=0124ffcc ebp=0124fff4 iopl=0         nv up ei pl zr na po nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
    ntdll!DbgBreakPoint:
    7c901230 cc               int     3
    0:011> !runaway
     User Mode Time
     Thread    Time
     192c      0 days 0:05:23.679
     1384      0 days 0:00:16.063
     14ac      0 days 0:00:08.392
     48c       0 days 0:00:03.955
     1db0      0 days 0:00:00.010
     1888      0 days 0:00:00.010
     1078      0 days 0:00:00.000
     1470      0 days 0:00:00.000
     1ea4      0 days 0:00:00.000
     1d60      0 days 0:00:00.000
     1850      0 days 0:00:00.000
     134c      0 days 0:00:00.000
     19fc      0 days 0:00:00.000
     1b4       0 days 0:00:00.000
    

    Aha, we see that thread 0x192c is the only one who gained any noticeable amount of CPU time. That's probably the one that's responsible for the 100% CPU usage.

    0:011> ~~[192c]s
    eax=00000000 ebx=77d5e581 ecx=0012daa0 edx=0000000c esi=01d18140 edi=00000000
    eip=77d5e590 esp=0012da78 ebp=0012da88 iopl=0         nv up ei pl zr na po nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
    USER32!FindWindowA+0xf:
    77d5e590 50               push    eax
    ChildEBP RetAddr
    0012da88 1000714b USER32!FindWindowA+0xf
    0012dbc8 100061f3 ABC!CAlpha::FindTarget+0x27f
    0012dbf4 603517e8 ABC!CBeta::TransferData+0x18a
    0012dc28 10002d9d DEF!CGamma:TransferData+0xc
    0012dc48 00505303 ABC!CBeta::BeginAsync+0x51
    0012dc5c 0090a21a GHI!CPrintSession::Open+0x2a51
    0012dd20 009099d8 GHI!CPrintSession::Init+0x252
    0012e060 009097e6 GHI!CPrintOptions::GetSettings+0x24a
    0012e0a4 0090973d GHI!CPrintOptions::OpenSettings+0x248
    0012e130 00909664 GHI!CDocumentMenu::OnInvoke+0x24
    ...
    

    Now this is where the magical "gu" command comes in. You type "gu" to run the current function until it returns. If you get another prompt back, then type "gu" again, to run that function until it returns. And so on, until you find the function that doesn't return.

    That's the one with the infinite loop.

    Now you can start investigating why that function is stuck.

  • The Old New Thing

    There's something about Christopher Walken

    • 12 Comments

    Actor Christopher Walken has such a peculiar speaking style, he's inspired a theater show: All About Walken. The run ends soon, so if you're going to see it, you'll have to act fast.

  • The Old New Thing

    Identifying an object whose underlying DLL has been unloaded

    • 10 Comments

    Okay, so I gave it away in the title, but follow along anyway.

    Your program chugs along and then suddenly it crashes like this:

    eax=06bad8e8 ebx=00000000 ecx=1e1cfdf0 edx=00000000 esi=06b9a680 edi=01812950
    eip=1180ab57 esp=001178b4 ebp=001178c0 iopl=0         nv up ei pl nz na pe nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
    ABC!FunctionX+0x1f:
    1180ab57 ff5108          call    dword ptr [ecx+8]    ds:0023:1e1cfdf8=????????
    0:000>>
    

    Instantly you recognize the following:

    • This is a virtual method call. (Call indirect through register plus offset.) — Very high confidence.
    • The vtable is in ecx. (That is the base register of the indirect call.) — Very high confidence.
    • The underlying DLL for this object has been unloaded. (The memory that contains the vtable is not valid and its address is consistent with once having been in valid code.) — High confidence.
    • This is a IUnknown::Release call. (Release is the third function of IUnknown and therefore resides at offset 8 on x86.) — High confidence.

    Of course, all of the above "instant conclusions" are merely "highly-educated guesses", but life is full of highly-educated guesses. (Every morning, I guess that my plates are still in the cupboard.)

    Let's run with our theory that the object was in an unloaded DLL and look for confirmation.

    0:000> lm
    start    end        module name
    ...
    Unloaded modules:
    10340000 10348000   DEF.DLL
    1e1c0000 1e781000   GHI.DLL
    25a90000 25a96000   JKL.DLL
    0:000>
    

    Aha, our presumed vtable address lies right inside the address space where GHI.DLL used to be loaded. Let's see what used to be loaded at that address. For this, I borrow a trick from Doron, namely loading a module as a dump file. This "virtually loads" the library so you can poke around inside it.

    C:\Program Files\ABC> ntsd -z GHI.DLL
    
    Microsoft (R) Windows Debugger
    Copyright (c) Microsoft Corporation. All rights reserved.
    
    Loading Dump File [C:\Program Files\ABC\GHI.DLL]
    ...
    ModLoad: 15800000 15dc1000   C:\Program Files\ABC\GHI.DLL
    eax=00000000 ebx=00000000 ecx=00000000 edx=00000000 esi=00000000 edi=00000000
    eip=15807366 esp=00000000 ebp=00000000 iopl=0         nv up di pl nz na pe nc
    cs=0000  ss=0000  ds=0000  es=0000  fs=0000  gs=0000             efl=00000000
    GHI!_DllMainCRTStartup:
    15807366 8bff             mov     edi,edi
    0:000>
    

    That module-load notification tells you where the DLL got virtually-loaded; in our case, it got loaded to 0x15800000. This isn't the same address as it was in our crashed process, so we'll have to do some mental arithmetic to account for the discrepancy.

    Going back to the original register dump, we see that our putative vtable is at ecx=1e1cfdf0 relative to the load address 1e1c0000. Since our DLL-loaded-as-a-dump-file was loaded at 0x1580000 we need to adjust the address to be relative to the new location.

    // working with the second copy of ntsd
    0:000> ln 0x1580fdf0
    (1580fdf0)   GHI!CAlphaStream::`vftable'
    

    That magic number 0x1580fdf0 is just the result of some mental arithmetic. First:

    0x1e1cfdf0
    -0x1e1c0000
    0x0000fdf0

    This is the address of the vtable in the crashed process relative to the load address of the DLL in the crashed process. Next:

    0x15800000
    +0x0000fdf0
    0x1580fdf0

    This is the address of the vtable in the DLL-loaded-as-a-dump-file relative to the load address of the DLL in the DLL-loaded-as-a-dump-file. The math really isn't that hard, as you can see, since a lot of things cancel out. This happens a lot.

    When we asked the debugger to tell us what symbol is nearest to that address, we hit the jackpot: It is exactly a vtable for the CAlphaStream object. This confirms our original theory. We can even confirm the IUnknown::Release theory by dumping the vtable.

    0:000> dds 1580fdf0
    1580fdf0  159234b3 GHI!CAlphaStream::QueryInterface
    1580fdf4  15810539 GHI!CBetaState::AddRef
    1580fdf8  15923cfc GHI!CAlphaStream::Release
    1580fdfc  15923d30 GHI!CAlphaStream::Read
    ...
    

    Yup, that's a CAlphaStream vtable all right.

    Since I'm not familiar with the GHI.DLL file, let's ask the debugger where the source code is so we can take a closer look:

    0:000> .lines
    Line number information will be loaded
    0:000> dds 1580fdf0
    1580fdf0  159234b3 GHI!CAlphaStream::QueryInterface
                       [c:\dev\fabricam\synergy\proactive\winwin.cpp @ 2624]
    1580fdf4  15810539 GHI!CBetaState::AddRef
                       [c:\dev\fabricam\leverage\paradigm\initiative.cpp @ 427]
    1580fdf8  15923cfc GHI!CAlphaStream::Release
                       [c:\dev\fabricam\synergy\proactive\winwin.cpp @ 2638]
    1580fdfc  15923d30 GHI!CAlphaStream::Read
                       [c:\dev\fabricam\synergy\proactive\winwin.cpp @ 2649]
    

    Now that we know where the source code to CAlphaStream is, we can hop on over to take a quick peek and confirm that, oh look, the object doesn't increment the DLL object count when it is constructed (or decrement it when it is destructed). As a result, when COM calls DllCanUnloadNow, the GHI.DLL says, "Sure, go ahead!" The DLL is unloaded even though ABC still has a reference to it, and then when ABC goes to release that reference, we crash because GHI is already gone.

    After I wrote this up, I discovered that Tony Schreiner went through pretty much the same exercise with a third-party Internet Explorer toolbar, except he had the extra bonus challenge of not having source code for the plug-in!

  • The Old New Thing

    How much time does it take for a pedestrian to cross the street?

    • 28 Comments

    It sounds like the set-up to an old joke, but it's not. It's just one of the random bits of trivia that I wondered about.

    For intersections with both high pedestrian and high vehicle volumes, I was able to find the Federal Highway Administration recommendation, which is to give pedestrians a head start to allow them to cross one lane of traffic before vehicles are given a green light, and the formula they use for determining how much time to allow is 2.8 feet per second.

    In the city of Bellevue, planners assume a pedestrian speed of four feet per second.

    At Microsoft campus, there is one dedicated pedestrian crossing, and the planners who designed the crossing came up with a clever hack to reduce the amount of time that traffic is stopped while still adhering to the four-feet-per-second rule. As you can see from the photo, there is an island in the road. Traffic is stopped long enough to allow a four-feet-per-second pedestrian to reach the center island. The pedestrian can then hit a crossing control button on the island to get a second chunk of time to complete the crossing. On the other hand, a faster pedestrian can complete the entire crossing within one cycle and the center crossing control is not needed.

    Before the pedestrian crossing was installed there, you would often seen Microsoft employees jaywalking at that location by playing a game of real-life Frogger. You'd be driving along and catch a head peeping out of the trees on the median and have a brief moment of panic as you realized you were travelling too fast to stop in time if the person decided to take that moment to resume their crossing. Signs and warnings didn't stop people from jaywalking, so the traffic planners installed an official crossing, for which I am thankful. Now I no longer panic when I drive past that area.

    (For completeness, here is the Washington State Crosswalk Law in pictures.)

  • The Old New Thing

    What is the underlying object behind a COM interface pointer?

    • 9 Comments

    When you're debugging, you might have a pointer to a COM interface and want to know what the underlying object is. Now, sometimes this trick won't work because the interface pointer actually points to a stub or proxy, but in the case where no marshalling is involved, it works great. (This technique also works for many C++ compilers for any object that has virtual methods and therefore a vtable.)

    Recall that the layout of a COM object requires that the pointer to a COM interface point to the object's vtable, and it's the vtable that is the key.

    0:000> dv
                pstm = 0x000c7568
    0:000> dt psf
    Local var @ 0x7cc2c Type IStream*
    0x000c7568
       +0x000 __VFN_table : 0x1c9c8e84
    

    Okay, so far all we know is that our IStream * lives at 0x000c7568 and its vtable is 0x1c9c8e84. Whose stream implementation is it?

    0:000> ln 0x1c9c8e84
    (1c9c8e84)   ABC!CAlphaStream::`vftable'
    

    Aha, it's a CAlphaStream from ABC.DLL. Let's take a look at it:

    0:000> dt ABC!CAlphaStream 0x000c7568
       +0x000 __VFN_table : 0x1c9c8e84 // our vtable
       +0x004 m_cRef           : 480022128
       +0x008 lpVtbl           : 0x1c9d2d30
       +0x00c lpVtbl           : 0x00000014
       +0x010 m_pszName        : 0x000c7844 "??????????"
       +0x014 m_dwFlags        : 0x3b8
       +0x018 m_pBuffer        : 0x00000005
       +0x01c m_cbBuffer       : 705235565
       +0x020 m_cbPos          : 2031674
    

    "Hey, how did you get the debugger to dump m_pszName as a string?" If you issue the .enable_unicode 1 command, then the debugger will treat pointers to unsigned short as if they were pointers to Unicode strings. (By default, only pointers to wchar_t are treated as pointers to Unicode strings.)

    Okay, back to the structure dump. It doesn't look right at all. The reference count is some absurd value, the vtable at offset 0x00c is a bogus pointer, the name in m_pszName is garbage, pretty much every field aside from the initial vtable and the vtable at offset 0x008 is blatantly wrong.

    What happened? Well, clearly we were given a "q" pointer; i.e., a pointer to one of the vtables other than the first one. We have to adjust the pointer so it points to the start of the object instead of the middle.

    How do we do this adjustment? There's the methodical way and the quick-and-dirty way.

    The methodical way is to use the adjustor thunks to tell you how much the pointer needs to be adjusted in order to move from a secondary vtable to the primary one. (This assumes that the primary IUnknown implementation is the first base class. This is not guaranteed to be the case but it usually is.)

    0:000> dps 1c9c8e84 l1
    1c9c8e84  1c9eb08e ABC![thunk]:CAlphaStream::QueryInterface`adjustor{8}'
    

    Aha, this adjustors adjust by eight bytes, so we just need to subtract eight from our pointer to get the object's starting address.

    0:000> dt ABC!CAlphaStream 0x000c7560-8
       +0x000 __VFN_table : 0x1c9c8ee8
       +0x004 m_cRef           : 2
       +0x008 lpVtbl           : 0x1c9c8e84
       +0x00c lpVtbl           : 0x1c9c8e70
       +0x010 m_pszName        : 0x1c9d2d30 "Scramble"
       +0x014 m_dwFlags        : 0x14
       +0x018 m_pBuffer        : 0x000c7844
       +0x01c m_cbBuffer       : 952
       +0x020 m_cbPos          : 5
    

    Ah, that looks much nicer. Notice that the reference count is a more reasonable value of two, the name pointer looks good, the buffer size and position appear to be much more realistic.

    Now, I don't bother with the whole adjustor thunk thing. Instead I rely on the principle of "Assume it's mostly correct": Assume that the object is not corrupted and just adjust the pointer by eye until the fields line up. Let's take another look at the original (bad) dump:

    0:000> dt ABC!CAlphaStream 0x000c7568
       +0x000 __VFN_table : 0x1c9c8e84
       +0x004 m_cRef           : 480022128
       +0x008 lpVtbl           : 0x1c9d2d30
       +0x00c lpVtbl           : 0x00000014
       +0x010 m_pszName        : 0x000c7844 "??????????"
       +0x014 m_dwFlags        : 0x3b8
       +0x018 m_pBuffer        : 0x00000005
       +0x01c m_cbBuffer       : 705235565
       +0x020 m_cbPos          : 2031674
    

    This obviously doesn't smell right, but what do we have to do to get things to line up? Well, we know that the vtable we have must go into one of the other two vtable slots, either the one at offset 0x008 or the one at offset 0x00c. If we moved it to offset 0x00c, then that would move the 0x00000014 currently at offset 0x00c down twelve bytes, placing it at offset 0x018, right at m_pBuffer. But obviously 0x00000014 is not a valid buffer pointer, so 0x00c can't be the correct adjustment. On the other hand, if we put our vtable at offset 0x008, then that would move 0x000c7844 into the m_pBuffer position, which is not too unreasonable. Therefore, I would guess that the adjustor is eight, yielding the same structure dump that we got by dumping the vtable to see the adjustor.

    In real life, I tend to pay attention to the vtables, the reference count, and any string members because it's usually pretty easy to see whether you got them right. (Vtables reside in code. Reference counts tend to be small integers. Strings are, well, strings.)

Page 1 of 4 (38 items) 1234