xxx222
xxx222

Reputation: 3244

Try to understand calling process in assembly code

I wrote a very simple program in C and try to understand the function calling process.

#include "stdio.h"

void Oh(unsigned x) {
    printf("%u\n", x);
}

int main(int argc, char const *argv[])
{
    Oh(0x67611c8c);
    return 0;
}

And its assembly code seems to be

0000000100000f20 <_Oh>:
   100000f20:   55                      push   %rbp
   100000f21:   48 89 e5                mov    %rsp,%rbp
   100000f24:   48 83 ec 10             sub    $0x10,%rsp
   100000f28:   48 8d 05 6b 00 00 00    lea    0x6b(%rip),%rax        # 100000f9a <_printf$stub+0x20>
   100000f2f:   89 7d fc                mov    %edi,-0x4(%rbp)
   100000f32:   8b 75 fc                mov    -0x4(%rbp),%esi
   100000f35:   48 89 c7                mov    %rax,%rdi
   100000f38:   b0 00                   mov    $0x0,%al
   100000f3a:   e8 3b 00 00 00          callq  100000f7a <_printf$stub>
   100000f3f:   89 45 f8                mov    %eax,-0x8(%rbp)
   100000f42:   48 83 c4 10             add    $0x10,%rsp
   100000f46:   5d                      pop    %rbp
   100000f47:   c3                      retq
   100000f48:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
   100000f4f:   00

0000000100000f50 <_main>:
   100000f50:   55                      push   %rbp
   100000f51:   48 89 e5                mov    %rsp,%rbp
   100000f54:   48 83 ec 10             sub    $0x10,%rsp
   100000f58:   b8 8c 1c 61 67          mov    $0x67611c8c,%eax
   100000f5d:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
   100000f64:   89 7d f8                mov    %edi,-0x8(%rbp)
   100000f67:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
   100000f6b:   89 c7                   mov    %eax,%edi
   100000f6d:   e8 ae ff ff ff          callq  100000f20 <_Oh>
   100000f72:   31 c0                   xor    %eax,%eax
   100000f74:   48 83 c4 10             add    $0x10,%rsp
   100000f78:   5d                      pop    %rbp
   100000f79:   c3                      retq

Well, I don't quite understand the argument passing process, since there is only one parameter passed to Oh function, I could under stand this

100000f58:  b8 8c 1c 61 67          mov    $0x67611c8c,%eax

So what does the the code below do? Why rbp? Isn't it abandoned in X86-64 assembly? If it is a x86 style assembly, how can I generate the x86-64 style assembly using clang? If it is x86, it doesn't matter, could any one explains the below code line by line for me?

100000f5d:  c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
100000f64:  89 7d f8                mov    %edi,-0x8(%rbp)
100000f67:  48 89 75 f0             mov    %rsi,-0x10(%rbp)
100000f6b:  89 c7                   mov    %eax,%edi
100000f6d:  e8 ae ff ff ff          callq  100000f20 <_Oh>

Upvotes: 3

Views: 720

Answers (2)

dave
dave

Reputation: 4922

As Jester mentions you still have frame pointers on (to aid debugging)so stepping through main:

0000000100000f50 <_main>:

First we enter a new stack frame, we have to save the base pointer and move the stack to the new base. Also, in x86_64 the stack frame has to be aligned to a 16 byte boundary (hence moving the stack pointer by 0x10).

       100000f50:   push   %rbp        
       100000f51:   mov    %rsp,%rbp
       100000f54:   sub    $0x10,%rsp

As you mention, x86_64 passes parameters by register, so load the param in to the register:

       100000f58:   mov    $0x67611c8c,%eax

??? Help needed

       100000f5d:   movl   $0x0,-0x4(%rbp)

From here: "Registers RBP, RBX, and R12-R15 are callee-save registers", so if we want to save other resisters then we have to do it ourselves ....

       100000f64:   mov    %edi,-0x8(%rbp)
       100000f67:   mov    %rsi,-0x10(%rbp)

Not really sure why we didn't just load this in %edi where it needs to be for the call to begin with, but we better move it there now.

       100000f6b:   mov    %eax,%edi

Call the function:

       100000f6d:   callq  100000f20 <_Oh>

This is the return value (passed in %eax), xor is a smaller instruction than load 0, so is a cmmon optimization:

       100000f72:   xor    %eax,%eax

Clean up that stack frame we added earlier (not really sure why we saved those registers on it when we didn't use them)

       100000f74:   add    $0x10,%rsp
       100000f78:   pop    %rbp
       100000f79:   retq

Upvotes: 2

Davislor
Davislor

Reputation: 15134

You might get cleaner code if you turned optimizations on, or you might not. But, here’s what that does.

The %rbp register is being used as a frame pointer, that is, a pointer to the original top of the stack. It’s saved on the stack, stored, and restored at the end. Far from being removed in x86_64, it was added there; the 32-bit equivalent was %ebp.

After this value is saved, the program allocates sixteen bytes off the stack by subtracting from the stack pointer.

There then is a very inefficient series of copies that sets the first argument of Oh() as the second argument of printf() and the constant address of the format string (relative to the instruction pointer) as the first argument of printf(). Remember that, in this calling convention, the first argument is passed in %rdi (or %edi for 32-bit operands) and the second in %rsi This could have been simplified to two instructions.

After calling printf(), the program (needlessly) saves the return value on the stack, restores the stack and frame pointers, and returns.

In main(), there’s similar code to set up the stack frame, then the program saves argc and argv (needlessly), then it moves around the constant argument to Oh into its first argument, by way of %eax. This could have been optimized into a single instruction. It then calls Oh(). On return, it sets its return value to 0, cleans up the stack, and returns.

The code you’re asking about does the following: stores the constant 32-bit value 0 on the stack, saves the 32-bit value argc on the stack, saves the 64-bit pointer argv on the stack (the first and second arguments to main()), and sets the first argument of the function it is about to call to %eax, which it had previously loaded with a constant. This is all unnecessary for this program, but would have been necessary had it needed to use argc and argv after the call, when those registers would have been clobbered. There’s no good reason it used two steps to load the constant instead of one.

Upvotes: 3

Related Questions