A reader wrote to us after last month’s article about walking up stack frames suggesting that we should do a write-up describing the basics of how arguments are actually passed on the stack. This seemed like a useful follow-on and thus this article describes (briefly) the calling conventions used in Windows on the x86 platform (the details are different, of course, for IA-64 and AMD-64, and are not described in this article).
The x86 architecture does not define a “canonical” form for a function call. Instead, it allows the programmer to pass arguments in whatever fashion is appropriate for the given use. When a programmer uses a higher level programming language the actual argument passing mechanism is defined by the code generated by the compiler. There are generally two major calling conventions used by the Microsoft ‘C’ compiler. The first, is where all arguments are passed on the stack. In this convention, the return value from the function (if any) is passed in the EAX register (see Figure 1).
Figure 1 – All Arguments Pass on the Stack
In the second calling convention (the “fastcall” convention) two of the arguments are passed in registers (the ECX and EDX registers) while the remaining arguments (if any) are passed on the stack (noted in Figure 2).
Figure 2 – The “fastcall” Convention
Understanding these calling conventions helps when you are using the debugger and attempting to unravel the implementation of a given piece of code. For example, the following code is ExfInterlockedAddUlong:
kd> u ExfInterlockedAddUlong
nt!ExfInterlockedAddUlong:
805128f3 9c pushfd
805128f4 fa cli
805128f5 8b01 mov eax,[ecx]
805128f7 0111 add [ecx],edx
805128f9 9d popfd
805128fa c20400 ret 0x4
Note that the first thing it does is to use ECX. Understanding the calling convention, it is straight-forward to move from the function declaration to the corresponding code. The function is declared as:
NTKERNELAPI
ULONG
FASTCALL
ExInterlockedAddUlong (
IN PULONG Addend,
IN ULONG Increment,
IN PKSPIN_LOCK Lock
);
and:
#define ExInterlockedAddUlong ExfInterlockedAddUlong
Of course, the #define is why the function name changes in the kernel. Indeed, the kernel team uses a convention for naming their functions, by including an extra “f” in the prefix (hence, Exf represents an Executive function using the fastcall convention).
Understanding this eases debugging because it is now possible to “figure out” what the code is doing. In the case of this function it becomes clear. The pushfd saves the current EFLAGS register, with the cli disabling interrupts (roughly equivalent to raising the IRQL on the processor to HIGH_LEVEL). The mov saves the current value pointed to by ECX in the EAX register (so we return the correct value) and then adds the value in EDX (second parameter) to the value pointed to by ECX. The popfd then restores the EFLAGS register (and restores interrupt state to what it was when the call was made). The ret 0x4 then indicates that four bytes should be discarded from the stack upon return – that is the third parameter of the function. In the uni-processor OS build (which is where we obtained this code) the spinlock parameter is not used. In the multi-processor OS build it is used and the code is commensurately complex.
An older convention (the “pascal” convention) passes parameters in a different fashion. In this case, it pushes them on the stack from left to right (as they are presented in the function parentheses). See Figure 3.
Figure 3 – The “pascal” Convention
This calling convention is unusual (the __pascal keyword is no longer even supported by the C compiler) but is an equally valid way of passing parameters – just one we do not see very often.
Of course, these are still only conventions. For those writing code in assembler, the calling conventions are not required. In mixed-language programming, the assembly language writer is responsible for understanding the nuances of the calling conventions and handling it appropriately. While those writing in higher level languages may have some control (the C compiler provides a mechanism for creating your own prolog and epilog code) it is considerably more cumbersome and still requires a thorough understanding of the conventions.