Welcome to the 4th part of my C/C++ Low Level Curriculum – more Stack!

The last post was a mammoth and took me ages so this post is going to be significantly shorter, and will consequently cover less ground. Specifically we’re going to look at how more than one parameter is passed in compiler generated unoptimised x86 assembler using the stdcall calling convention.

This post assumes that you have read the previous post on the Stack (or know how the Stack works in “vanilla” x86 assembler already).

If you missed the previous posts here are backlinks:

  1. http://altdevblogaday.com/2011/11/09/a-low-level-curriculum-for-c-and-c/
  2. http://altdevblogaday.com/2011/11/24/c-c-low-level-curriculum-part-2-data-types/
  3. http://altdevblogaday.com/2011/12/14/c-c-low-level-curriculum-part-3-the-stack/

I’ve also dropped in a good link to some IBM resources on the PowerPC ABI which explain in detail (and at the assembler level) how the Stack is used on PowerPC based CPUs as it is actually more different from x86 than I remembered. You may find this particularly useful if you work on Current Gen consoles and want to understand how they use the Stack, call functions, and pass parameters.

 

More than one function parameter

As before, I’m using a win32 console application made by the “new project” wizard in VS2010 with the default options. The disassembly we’ll be looking at is from the debug build configuration, which generates vanilla unoptimised stdcall x86 code.

The only change I make is to turn off “Basic Runtime Checks” to make the generated assembler more legible (and significantly faster…) see the previous post for details on how to do this.

We’re going to update the very simple program used for the last article so that the function it calls requires 3 parameters.

int SumOf( int iParamOne, int iParamTwo, int iParamThree )
{
    int iLocal = iParamOne + iParamTwo + iParamThree;
    return iLocal;
}

int main( int argc, char** argv )
{
    int iValOne   = 1;
    int iValTwo   = 2;
    int iValThree = 4;
    int iResult   = SumOf( iValOne, iValTwo, iValThree );
    return 0;
}

and here’s the assembler it generates for main() (as before the addresses of the instructions will almost certainly differ for you):

     7: int main( int argc, char** argv )
     8: {
00401280  push        ebp
00401281  mov         ebp,esp
00401283  sub         esp,50h
00401286  push        ebx
00401287  push        esi
00401288  push        edi
     9:     int iValOne        = 1;
00401289  mov         dword ptr [ebp-4],1
    10:     int iValTwo        = 2;
00401290  mov         dword ptr [ebp-8],2
    11:     int iValThree    = 4;
00401297  mov         dword ptr [ebp-0Ch],4
    12:     int iResult        = SumOf( iValOne, iValTwo, iValThree );
0040129E  mov         eax,dword ptr [ebp-0Ch]
004012A1  push        eax
004012A2  mov         ecx,dword ptr [ebp-8]
004012A5  push        ecx
004012A6  mov         edx,dword ptr [ebp-4]
004012A9  push        edx
004012AA  call        00401127
004012AF  add         esp,0Ch
004012B2  mov         dword ptr [ebp-10h],eax
    13:     return 0;
004012B5  xor         eax,eax
    14: }
004012B7  pop         edi
004012B8  pop         esi
004012B9  pop         ebx
004012BA  mov         esp,ebp
004012BC  pop         ebp
004012BD  ret

 

Calling SumOf()

As we saw in the last article, we know we can safely ignore the function preamble and postamble (lines 3-8, and lines 28-33; also known as the prologue and epilogue respectively) which set up and tear down the function’s Stack Frame as we know they’re not involved in passing the parameters to SumOf().

A quick look at the disassembly initialising the local variables tells us that iValOne, iValTwo, and iValThree are stored at [ebp-4], [ebp-8], and [ebp-0Ch] respectively.

The disassembly relevant to the function call and the assignment of its return value is this part:

    12:     int iResult        = SumOf( iValOne, iValTwo, iValThree );
0040129E  mov         eax,dword ptr [ebp-0Ch]
004012A1  push        eax
004012A2  mov         ecx,dword ptr [ebp-8]
004012A5  push        ecx
004012A6  mov         edx,dword ptr [ebp-4]
004012A9  push        edx
004012AA  call        00401127
004012AF  add         esp,0Ch
004012B2  mov         dword ptr [ebp-10h],eax

As in the case with a single argument, copies of the function parameters’ values are pushed onto the Stack – but note that they are pushed on in the opposite order to the order in which the function’s parameter list expects them in the C++ code.

The final thing to note, is that following the call instruction on line 22 (i.e. immediately before the assembler for SumOf() is executed) the copy of iValOne that was pushed onto the stack in line 21 is at [esp+4] because call pushes the return address onto the Stack.

Just in case, here’s what the stack looks like immediately after line 22 is executed, but before any code in SumOf() is executed:

Stack after line 22, before any of SumOf() is executed

 

Accessing the parameters

Here’s the disassembly for SumOf():

     1: int SumOf( int iParamOne, int iParamTwo, int iParamThree )
     2: {
00401250  push        ebp
00401251  mov         ebp,esp
00401253  sub         esp,44h
00401256  push        ebx
00401257  push        esi
00401258  push        edi
     3:     int iLocal = iParamOne + iParamTwo + iParamThree;
00401259  mov         eax,dword ptr [ebp+8]
0040125C  add         eax,dword ptr [ebp+0Ch]
0040125F  add         eax,dword ptr [ebp+10h]
00401262  mov         dword ptr [ebp-4],eax
     4:     return iLocal;
00401265  mov         eax,dword ptr [ebp-4]
     5: }
00401268  pop         edi
00401269  pop         esi
0040126A  pop         ebx
0040126B  mov         esp,ebp
0040126D  pop         ebp
0040126E  ret

We can see that the function prologue codeĀ pushes ebp which moves esp on another 4 bytes, then moves esp to ebp – so after line 4 the copy of iValOne’s value is now at [ebp+8].

Here’s another Stack snapshot showing the state after the function prologue (i.e. after line 8):

apshot1

state of the Stack after the prologue of SumOf()

Looking at lines 10-12 we can see that the assembler is accessing the function parameters as follows:

  • iParamOne (iValOne) from [ebp+8]
  • iParamTwo (iValTwo) from [ebp+0Ch]
  • iParamThree (iValThree) from [ebp+10h]

Which, unsurprisingly, is exactly where the values main() pushed onto the Stack before calling this function ended up after the function prologue.

Now we can see why the function parameters are pushed onto the stack in reverse order by main() – because functions called expect them to be stored in the Stack in parameter list order starting from [ebp+8] and incrementing in offset from ebp for each parameter.

As before the return value (iLocal, stored at [ebp-4]) is moved into eax before the function’s epilogue code in order to return it to main(), and since we know how the epilogue and return work from the last article we’re done with vanilla stdcall with multiple parameters. Joy.

 

Summary

We’ve looked in some detail at how the Stack is used to call functions in vanilla unoptimised compiler generated stdcall x86 assembler, this should leave you in a pretty good place to go mooching about in disassembly windows with a fair idea of which parts of the disassembly for each function is most likely to be relevant.

For extra information, and to show you how different the Stack use can be (whilst still being basically the same in principle), here’s a link to the 4th in a series of articles on the IBM Technical Library site dealing with PowerPC assembler, and in particular with the 64 bit PowerPC ABI:

http://www.ibm.com/developerworks/linux/library/l-powasm4/index.html

In all likelihood you’ll need to read the first three articles to make sense of the 4th, but the 4th one is where most of the juicy info is :)

 

Next Time

Next time, I’m going to look at the x86 thiscall calling convention used when C++ member functions (where the ‘this’ pointer is passed in ecx), and we’ll also have a look in overview at how the exciting sounding ‘fastcall’ x86 calling convention uses the Stack.

Oh, and Merry Christmas!