Discovering the Arguments Passed to Windows API Functions with Public Symbols

I've been talking at a bit of a higher level lately, and today I'm just in the mood to go into some of the deeper debugging aspect that you may run across when looking at Windows Vista application compatibility. (Well, perhaps not deep to some, but for most of us, we haven't spent much time in a low-level debugger lately so the skills have frequently atrophied somewhat.)

When I talk about debugging for application compatibility, I always emphasize this point: the application itself is the same - Windows is now different. So the entry and exit points from Windows take on an exaggerated importance in these debugging sessions. However, internally, we have a bit of an advantage: those entry points are more expressive. You see, we have private symbols to Windows, so the Locals window in WinDbg is quite helpful. Everyone else just gets public symbols. So, those entry points are a little bit more mysterious. Of course, that doesn't mean you are stuck, it just means you have to do a touch more work.

For this exercise, I'm just going to use an application that I know checks the version number in Windows, the MSDN library, and the Debugging Tools for Windows. We're going to get the same information internal folks get without requiring private symbols!

Going in, I know that the application has an error where it tells me that it's expecting an older version of the operating system (in this case, Windows XP). Let's dissect that entry point.

MSDN tells us that the function is implemented in Kernel32.dll, so let's set our breakpoint:

0:000> bp kernel32!GetVersionExW

Now, let's run it up to that breakpoint. While we're here, let's take a look at the function.

0:000> uf kernel32!getversionexw
769248d8 8bff         mov     edi,edi
769248da 55           push    ebp
769248db 8bec         mov     ebp,esp
769248dd 56           push    esi
769248de 8b7508       mov     esi,dword ptr [ebp+8]
769248e1 8b06         mov     eax,dword ptr [esi]
769248e3 57           push    edi
769248e4 bf1c010000   mov     edi,11Ch
769248e9 3bc7         cmp     eax,edi
769248eb 740b         je      kernel32!GetVersionExW+0x2b (769248f8)
769248ed 3d14010000   cmp     eax,114h
769248f2 0f858e8d0200 jne     kernel32!GetVersionExW+0x1c (7694d686)
769248f8 56           push    esi
769248f9 ff15f0139076 call    dword ptr [kernel32!_imp__RtlGetVersion (769013f0)]
769248ff 85c0         test    eax,eax
76924901 0f85868d0200 jne     kernel32!GetVersionExW+0x23 (7694d68d)
76924907 393e         cmp     dword ptr [esi],edi
76924909 7409         je      kernel32!GetVersionExW+0x3a (76924914)
7692490b 33c0         xor     eax,eax
7692490d 40           inc     eax
7692490e 5f           pop     edi
7692490f 5e           pop     esi
76924910 5d           pop     ebp
76924911 c20400       ret     4
76924914 a046ef9c76   mov     al,byte ptr [kernel32!BaseRCNumber (769cef46)]
76924919 88861b010000 mov     byte ptr [esi+11Bh],al
7692491f ebea         jmp     kernel32!GetVersionExW+0x45 (7692490b)
7694d686 6a7a         push    7Ah
7694d688 e8eeeeffff   call    kernel32!SetLastError (7694c57b)
7694d68d 33c0         xor     eax,eax
7694d68f e97a72fdff   jmp     kernel32!GetVersionExW+0x25 (7692490e)

We want to esablish the calling convention. If we were using _fastcall, we'd be passing arguments in registers (ECX and EDX), but we don't even use those registers here. We're calling ret 4 to unwind our own stack, so we can't be using a cdecl function (where the caller is responsible for unwinding the stack). We're looking at a stdcall function.

(While you're looking at it - the first instruction - mov edi,edi - is an instruction that does absolutely nothing. It's a 2-byte filler. Why? To enable hot patching. With two byes, we can add a short jump, from which we can long jump to a new implementation of the function.)

Now, we could set up our stack and begin looking at the arguments passed, but in this case, we don't really care much about what was passed. What's interesting is what we return. So, let's get out of this function and back into our main function and have a look.

First, let's look at our return value. Again, looking at MSDN, we see that it returns a BOOL, returning non-zero upon success. This appears in the EAX register. Let's see how we did.

0:000> r eax

OK, so the function succeeded. However, you're probably far more interested in the LPOSVERSIONINFO argument that the function modified! How do we get at that?

Well, first we need to know where it sits. Because the arguments are passed on the stack, the stack pointer tells us where to look. We look at ESP+8 ESP+4, and we'll begin to find our arguments. And, fortunately, we can use the dt command to format it nicely for us.

0:000> dt OSVERSIONINFO esp+8
   +0x000 dwOSVersionInfoSize : 6
   +0x004 dwMajorVersion   : 0
   +0x008 dwMinorVersion   : 0x1771
   +0x00c dwBuildNumber    : 2
   +0x010 dwPlatformId     : 0x650053
   +0x014 szCSDVersion     : [128]  "rvice Pack 1"

0:000> dt OSVERSIONINFO esp+4
   +0x000 dwOSVersionInfoSize : 0x114
   +0x004 dwMajorVersion   : 6
   +0x008 dwMinorVersion   : 0
   +0x00c dwBuildNumber    : 0x1771
   +0x010 dwPlatformId     : 2
   +0x014 szCSDVersion     : [128]  "Service Pack 1"

So, this is how the function is coming back to us. And now we can see our first argument. Fortunately for us, this is our only argument, and our job is done.

(You may be scratching your head as to why szCSDVersion seems to be missing the letters Se - I'm doing the same thing. Actually, no more head scratching. I mixed up esp+4 outside with ebp+8 inside - fixed this.)

That wasn't so hard, was it? A little help from MSDN, and we are on our way.

Let's do one more, for good measure. This application happens to display a message box, so let's fast forward to that API call. MSDN tells us that this is found in user32.dll, so let's set our breakpoint.

0:000> bp user32!messageboxw

Now that we're in the function, this time we want to stay in it. The arguments here are interesting going in. Let's take a look at this function:

0:000> uf user32!messageboxw
76d5d667 8bff            mov     edi,edi
76d5d669 55              push    ebp
76d5d66a 8bec            mov     ebp,esp
76d5d66c 833da89cd67600  cmp     dword ptr [USER32!gfEMIEnable (76d69ca8)],0
76d5d673 7424            je      USER32!MessageBoxW+0x32 (76d5d699)
76d5d675 64a118000000    mov     eax,dword ptr fs:[00000018h]
76d5d67b 6a00            push    0
76d5d67d ff7024          push    dword ptr [eax+24h]
76d5d680 6824a3d676      push    offset USER32!gdwEMIThreadID (76d6a324)
76d5d685 ff150412d076    call    dword ptr [USER32!_imp__InterlockedCompareExchange (76d01204)]
76d5d68b 85c0            test    eax,eax
76d5d68d 750a            jne     USER32!MessageBoxW+0x32 (76d5d699)
76d5d68f c70520a3d67601000000 mov dword ptr [USER32!gpReturnAddr (76d6a320)],1
76d5d699 6a00            push    0
76d5d69b ff7514          push    dword ptr [ebp+14h]
76d5d69e ff7510          push    dword ptr [ebp+10h]
76d5d6a1 ff750c          push    dword ptr [ebp+0Ch]
76d5d6a4 ff7508          push    dword ptr [ebp+8]
76d5d6a7 e849ffffff      call    USER32!MessageBoxExW (76d5d5f5)
76d5d6ac 5d              pop     ebp
76d5d6ad c21000          ret     10h

The first few lines set up our stack frame. We can step through the first 3 lines to set it up ourselves, or we can just use the existing stack pointer (ESP). Since it saves us some time, let's do that.

Looking at MSDN, the first argument is the HWND of the window that owns the message box. This will be a 4-byte HWND value. If we had stepped through our stack frame setup, we'd be starting at EBP+8, but since we haven't called PUSH EBP yet, we're going to be starting at ESP+4. So, our first argument is:

0:000> dc esp+4
0012fdd0  00000000

OK, so it's passing a NULL HWND. On to argument #2: a pointer to the text to display in the message box. We'll just walk 4 more bytes on the stack to grab that pointer:

0:000> ddu esp+8
0012fdd4  00402160 "This application requires Windows XP"

And what are they using as the caption in argument 3?

0:000> ddu esp+c
0012fdd8  00402134 "Unsupported Version"

And our last argument tells us what buttons it's going to support:

0:000> dc esp+10
0012fddc  00000010

Ah - this one is a bit harder to crack, because it's expressed as a UINT instead of a handy #define'd flag. Fortunately, MSDN also tells us where the API is defined (winuser.h), so we can just head there and search for MB_ - they're typically all grouped together. So, let's translate. 10 is MB_ICONHAND (which also happens to be MB_ICONERROR and MB_ICONSTOP if you continue reading the header), and, since we have to have buttons, we must have MB_OK (0x0). So, we must be calling this as MB_ICONHAND | MB_OK.

So, with a little help from the SDK header files, and a lot of help from MSDN, we were able to point our debugger at Windows APIs with public symbols and gather just as much information about arguments as the private symbols would give you.

Updated 2/23/2008: I'm so used to typing EBP+8 inside a method that I typed ESP+8 outside. I corrected this.

Comments (5)

  1. Aleks says:

    Hi Chris,

    nice job … but I’ve always missed a good/fast tool from Microsoft for monitoring Windows API calls. That’s why I built such one – you can download it from my site:

    The tool is called Windows API Logger. I would appreciate your comments on the tool.


    Aleksandar Colovic

  2. cjacks says:

    Aleks – we actually have such a tool internally, but I don’t think we’ve ever shipped it? I’ll have to look into that…. I couldn’t resolve your site.

  3. I send out a lot of links to my articles in response to questions that come up, but the other day I had

Skip to main content