Stupid debugger tricks: Calling functions and methods


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.

Comments (14)
  1. Anonymous says:

    Neat trick. I’ll bookmark that for the future, but I’ve never been hardcore enough to use ntsd, cdb or windbg. I’m just fishing, but is there a similar technique to insert a temporary function call under VC++ (any recent version) without recompiling and using Edit and Continue?

  2. Asztal says:

    Chris: the Immediate Window is your friend. When it works, that is ;)

  3. Anonymous says:

    This is the coolest thing I’ve seen in a long time. Thank you.

  4. Anonymous says:

    It’s been ~3 years since I’ve fired up windbg (either for a crash-dump analysis or to debug a driver over a serial cable. hah. Old boot param changes).

    Do you find yourself doing this a lot ? (if that’s an answerable question in and of itself).. I mean, this is really neat stuff, but in my application experience, I’ve never had to do anything close to this. (Granted, I also don’t use COM, which from looking at your previous debugging posts, might be a good thing). Are you doing it for internal code, or external code ? (since you hint at "if you don’t have the source … ")

    As others said, I now know where to look if I come across such a problem, but I feel too shortsighted to know/see where I would find myself in such a situation.

  5. Anonymous says:

    Raymond, aren’t you allocating a buffer of 1000 bytes and then passing it as being 0x1000 bytes long?

    Of course, I’m assuming the third parameter to CAlphaStream::Read is the size of the buffer passed, and I could be completely wrong here.

    [My default radix is 16. I don’t know what the default radix is for C++ expressions, so I played it safe and put the 0x prefix on. -Raymond]
  6. Anonymous says:

    Is there an undo function for .call?

    If I thought I’d call something then realized I passed bad arguments, can I revert the register without actually doing the call?

    (Maybe it’s in some documentation somewhere, maybe not. Beside, the answer will help millions of developpers. Or, at least, one.)

    [If you know how to do what I described in the opening paragraph (and I’m assuming that you do), then you should know how to undo a .call. -Raymond]
  7. Anonymous says:

    Pierre: read the documentation for .call.  It’s call /c.

    I wish there were a way of doing .call even if you have no symbols at all (it would make scripts easier to manage)…

  8. Anonymous says:

    Mark Steward:

    You could do an extension !call that accepts a calling convention in addition to a list of parameters.  It’d be very hard to do this entirely without symbols though…  trying to reverse engineer something interesting? :)

  9. Anonymous says:

    BTW, the ? command that lets you evaluate expressions also allow calling functions. It existed in SYMDEB, but did not allow you to call functions until CodeView.

  10. Anonymous says:

    Hooray for Visual Studio.

  11. Anonymous says:

    Am I doing something wrong or do the public symbols not suffice to do this for the Win32 API?

    [Please read the article again, particularly the second half. -Raymond]
  12. Anonymous says:

    Is this supported on Win64?

  13. Anonymous says:

    8irik66w9p4y6my 397n53dy 85j5erh9wopnxfax 65hkv649yb

  14. Anonymous says:

    8irik66w9p4y6my 85j5erh9wopnxfax 65hkv649yb

Comments are closed.