The NT Insider

Don't Call Us -- Calling Conventions for the x86
(By: The NT Insider, Vol 10, Issue 1, Jan-Feb 2003 | Published: 15-Feb-03| Modified: 11-Mar-03)


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.

This article was printed from OSR Online http://www.osronline.com

Copyright 2017 OSR Open Systems Resources, Inc.