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:
- /2011/11/09/a-low-level-curriculum-for-c-and-c/
- /2011/11/24/c-c-low-level-curriculum-part-2-data-types/
- /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:
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):
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!