Function pa­ra­me­ters seen at ma­chine code level

Created: Wed Feb 27 01:55:53 CET 2019

Last mod­i­fied: Wed Feb 27 10:09:09 CET 2019


This is the third post in my se­ries about (dis)assembly.

In this post, ex­am­ples are com­piled down to ob­ject files us­ing the following com­mand: clang -c -o object.o source.c.

(The same can be ac­com­plished with gcc.)

I chose not to build full work­ing bi­na­ries be­cause it’s not worth it. Too much noise whereas ob­ject files are sim­pler and can be dis­as­sem­bled in just the same way.

We will fo­cus on the fol­low­ing C pro­gram,

int bar(int c)
{
    return 0x67 + c;
}

int foo(int c)
{
    return bar(0x32) + c;
}

int main(void)
{
    return foo(0x15);
}

Save it as foo.c and com­pile it.

Disassembling it us­ing objdump -M intel -d foo.o, we get the fol­low­ing output,

First, bar()’s body,

0000000000000000 <bar>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
   7:   8b 7d fc                mov    edi,DWORD PTR [rbp-0x4]
   a:   83 c7 67                add    edi,0x67
   d:   89 f8                   mov    eax,edi
   f:   5d                      pop    rbp
  10:   c3                      ret    
  11:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  18:   00 00 00 
  1b:   0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

then foo()’s,

0000000000000020 <foo>:
  20:   55                      push   rbp
  21:   48 89 e5                mov    rbp,rsp
  24:   48 83 ec 10             sub    rsp,0x10
  28:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
  2b:   bf 32 00 00 00          mov    edi,0x32
  30:   e8 cb ff ff ff          call   0 <bar>
  35:   03 45 fc                add    eax,DWORD PTR [rbp-0x4]
  38:   48 83 c4 10             add    rsp,0x10
  3c:   5d                      pop    rbp
  3d:   c3                      ret    
  3e:   66 90                   xchg   ax,ax

then main()’s,

0000000000000040 <main>:
  40:   55                      push   rbp
  41:   48 89 e5                mov    rbp,rsp
  44:   48 83 ec 10             sub    rsp,0x10
  48:   c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
  4f:   bf 15 00 00 00          mov    edi,0x15
  54:   e8 c7 ff ff ff          call   20 <foo>
  59:   48 83 c4 10             add    rsp,0x10
  5d:   5d                      pop    rbp
  5e:   c3                      ret

I rec­om­mend that you copy and paste this in an­other win­dow for fur­ther references.

You can try to spot all the con­stants we de­lib­er­ately placed in our source file, such as 0x67 for ex­am­ple.

You might have no­ticed al­ready that when­ever a func­tion is called with arguments, in­struc­tions such as mov DWORD PTR [rbp-0x4], edi start to appear at the be­gin­ning of the func­tion’s body.

For ex­am­ple, look­ing at main()’s in­struc­tion 0x4f: the ar­gu­ment to foo(), 0x15, is stored in EDI, 0x15 be­ing a 32 bit in­te­ger value.

Then there is a call to foo() and if you ig­nore the lines un­til instruction 0x28, that is to say, mov DWORD PTR [rbp-0x4],edi we can clearly in­fer that au­to­matic vari­ables are - at least some­times - stored in reg­is­ters by the call­ing func­tion (here main) be­fore be­ing copied over at ad­dress rbp-X by the callee (foo); af­ter some setup is made on RSP and RBP.

A few hints to get you started,

We will start our analy­sis at in­struc­tion 0x21 and ig­nore every­thing that hap­pened be­fore that.

mov    rbp,rsp

So RBP and RSP are now equal. On to next in­struc­tion.

sub    rsp,0x10

RSP is now 16 less than RBP. RSP (the mem­ory area it points to to be exact) is on the left of RBP.

mov    DWORD PTR [rbp-0x4],edi

Now what­ever that was in EDI is copied at ad­dress RBP-0x4, that is to say, in be­tween RSP and RBP. We know that EDI con­tains 0x15 but we don’t need this in­for­ma­tion for now.

Because EDI con­tains a 32 bit value, which is four bytes, 0x4 is just the right amount of mem­ory to store its value.

mov    edi,0x32
call   0 ; bar

The ar­gu­ment to bar() is stored in EDI and the pro­gram jumps at ad­dress 0.

push   rbp

We can re­place this in­struc­tion with the fol­low­ing ones:

sub    rsp, 0x8
mov [rsp], rbp

Meaning that RSP is moved fur­ther to the left (64 bits) and that RBPs value is copied where RSP is point­ing to.

mov    rbp,rsp

RBP and RSP are equal again. The old value of RBP which was copied at [rsp] is now on the right of both RBP and RSP. The ad­dress pointed by RBP moved down in mem­ory.

mov    DWORD PTR [rbp-0x4],edi

Now, the value of EDI, 0x32 is moved ont the left of both RBP and RSP, which are still equal.

We will stop there and take a look at the fol­low­ing schematic,

Memory grows to the right and time grows to the bot­tom of the schematic.

If you look at the state of both RSP and RBP on top, you might no­tice that RSP is on the left of RBP, which means that we are in the same state as af­ter in­struc­tion 0x24 was ex­e­cuted.

We stopped our men­tal com­pu­ta­tion in the mid­dle of the schematic so you can ig­nore the sec­ond half of the schematic.

The pur­ple ar­eas are stacks frames; whereas the whole schematic represent a part of the pro­gram’s stack. bp0 refers to the value of RBP, copied there by the push in­struc­tion.

As you prob­a­bly guessed, [rbp-0x4] is an ad­dress lo­cated in the stack frame. Now this stack frame changes de­pend­ing on which func­tion we are in.

In fact, be­cause RSP was cut down by 16, we have enough stor­age in there to store four 32 bits val­ues. We only stored one though; which is 0x15, the value which was passed down by main().

Moving your fin­ger down by a sin­gle line from the first one, we reach the point af­ter the push in­struc­tion of bar() has been ex­e­cuted.

RBP is copied on the right of [rsp].

Then RSP is copied in RBP, which is in­struc­tion 0x1; and we reach the third state, where RSP and RBP are con­founded.

The value of EDI, which was set by foo() since we are now in bar()’s body, is stored the left of [rbp]; which is still con­founded with RSP.

RSPs value changes on two con­di­tions:

  1. when there is a push,
  2. when the cur­rent func­tion needs to pass pa­ra­me­ters down to an­other function.

When there is a push, the value will be re­turned to nor­mal by the next pop in the same func­tion as long as the value of the reg­is­ter operand.

If bar() had re­quired two ar­gu­ments, the dis­as­sem­bler’s out­put would have looked like this:

   4:   89 7d fc             mov    DWORD PTR [rbp-0x4],edi
   7:   89 75 f8             mov    DWORD PTR [rbp-0x8],esi

Basically, when a func­tion calls an­other, it asks it­self the fol­low­ing question:

If I were to put all of my au­to­matic (local) vari­ables side-by-side (which it does), how much mem­ory would I need ?”

And it sub­tract RSP by the an­swer, or some­times bit more, so that the space on the right can be used to store its own vari­ables whereas the space on the left can be used by the func­tions it calls.

The sec­ond half of the schematic un­does all the changes that were made to RSP and RBP. The last state is right be­fore the in­struc­tion 0x59, which adds 0x10 back to RSP, is ex­e­cuted. The au­to­matic vari­ables are now sim­ply un­reach­able un­til an­other func­tion call over­writes them.

Conclusion

Every time a func­tion calls an­other, the same process is re­peated. Of course, there are ex­cep­tions, when a func­tion does­n’t take any ar­gu­ment for ex­am­ple. It also de­pends on the level of op­ti­miza­tion of your com­piler.

A ques­tion I haven’t ad­dressed yet is How does the pro­gram know that it is not over­writ­ing pre­ex­ist­ing data on the left of RBP ?”

It does­n’t. The op­er­at­ing sys­tem pro­vides the pro­gram with a stack, which the pro­gram pro­ceeds to fill up from top to bot­tom with re­spect to memory ad­dresses. If func­tion calls are made in se­quence (foo(); foo();) then every­thing’s fine: a stack frame is cre­ated, then re­moved between each calls. Now if calls are im­bri­cated in a bad way, for example, if foo() calls it­self, stack frames will keep be­ing cre­ated until the ker­nel-pro­vided size limit is ex­ceeded lead­ing to the pro­gram terminating.

In our ex­am­ple code, two stack frames were in use at the same time when we were still in bar(): foo()’s frame and bar()’s frame.

Here is the dis­as­sem­bly for foo(int c){foo(c);}:

0000000000000000 <foo>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 10             sub    rsp,0x10
   8:   89 7d f8                mov    DWORD PTR [rbp-0x8],edi
   b:   8b 7d f8                mov    edi,DWORD PTR [rbp-0x8]
   e:   e8 ed ff ff ff          call   0 <foo>

The in­struc­tions re­quired to un­wind a stack frame are all lo­cated after 0xe; and are thus un­reach­able.

It’s worth not­ing that our de­pic­tion of the process was­n’t the most exact avail­able. To il­lus­trate, I did­n’t in­clude what the call mnemonic has to do with the stack in my schematic.

Anyway thank you for read­ing, I hope you learned some­thing.

Do not hes­i­tate to com­ment (again, you don’t need a valid email ad­dress), even if it’s just to tell you (dis)liked the ar­ti­cle.

source code