3

I am trying to understand some basic assembly code concepts and am getting stuck on how the assembly code determines where to place things on the stack and how much space to give it.

To start playing around with it, I entered this simple code in godbolt.org's compiler explorer.

int main(int argc, char** argv) {
  int num = 1;  
  num++;  
  return num;
}

and got this assembly code

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-20], edi
        mov     QWORD PTR [rbp-32], rsi
        mov     DWORD PTR [rbp-4], 1
        add     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret

So a couple questions here:

  1. Shouldn't the parameters have been placed on the stack before the call? Why are argc and argv placed at offset 20 and 32 from the base pointer of the current stack frame? That seems really far down to put them if we only need room for the one local variable num. Is there a reason for all of this extra space?

  2. The local variable is stored at 4 below the base pointer. So if we were visualizing this in the stack and say the base pointer currently pointed at 0x00004000 (just making this up for an example, not sure if that's realistic), then we place the value at 0x00003FFC, right? And an integer is size 4 bytes, so does it take up the memory space from 0x00003FFC downward to 0x00003FF8, or does it take up the memory space from 0x00004000 to 0x00003FFC?

  3. It looks like stack pointer was never moved down to allow room for this local variable. Shouldn't we have done something like sub rsp, 4 to make room for the local int?

And then if I modify this to add more locals to it:

int main(int argc, char** argv) {
  int num = 1; 
  char *str1 = {0};
  char *str2 = "some string"; 
  num++;  
  return num;
}

Then we get

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-36], edi
        mov     QWORD PTR [rbp-48], rsi
        mov     DWORD PTR [rbp-4], 1
        mov     QWORD PTR [rbp-16], 0
        mov     QWORD PTR [rbp-24], OFFSET FLAT:.LC0
        add     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret

So now the main arguments got pushed down even further from base pointer. Why is the space between the first two locals 12 bytes but the space between the second two locals 8 bytes? Is that because of the sizes of the types?

AlyT
  • 261
  • 1
  • 10
  • 2
    Size and alignment. Also, unoptimized code. The compiler is free to use whatever layout it wants anyway. – Jester Apr 29 '19 at 20:38
  • 1
    The compiler has apparently decided that since nothing is called from `main`, it can use that stack space without adjusting the stack pointer. – 1201ProgramAlarm Apr 29 '19 at 20:42
  • @1201ProgramAlarm, thanks, that does explain the stack pointer not being adjusted. I played around with adding different function calls and noticed that it did adjust the stack pointer accordingly. Interesting. – AlyT Apr 29 '19 at 20:53
  • 4
    That's the red zone, a 128 byte region you are allowed to use under the stack pointer. Assuming you don't call other functions of course. – Jester Apr 29 '19 at 20:54
  • main() stack questions have been asked and answered many times here, usually the same observation. If you rename the function something else do you see the same stack setup? also try to optimize which means need to be creative with incoming variables and calling another function helps... – old_timer Apr 29 '19 at 21:11
  • 2
    On question 2.: the conventional way to talk about this is "it takes up memory from 0x00003FFC to 0x00003FFF", not the other way round. On x86 (rather, x64 in your example), which is little-endian, the LSB of the word will be at 0x00003FFC and MSB at 0x00003FFF. But regardless of the direction the stack grows it is very unusual to talk about its individual elements memory ranges the way you did. – tum_ Apr 29 '19 at 21:42
  • re: 3 [Why do we need stack allocation when we have a red zone?](//stackoverflow.com/q/37941779) – Peter Cordes Apr 29 '19 at 22:49
  • also a good answer here: [Why does the x86-64 GCC function prologue allocate less stack than the local variables?](https://stackoverflow.com/questions/13201644/why-does-the-x86-64-gcc-function-prologue-allocate-less-stack-than-the-local-var?rq=1) – tum_ Apr 29 '19 at 22:58
  • 2
    @Jester - it should be pointed out that existence of the red-zone is OS dependent. For example, Windows does not have a red-zone. – rcgldr Apr 30 '19 at 00:36

2 Answers2

3

I'm only going to answer this part of the question:

Shouldn't the parameters have been placed on the stack before the call? Why are argc and argv placed at offset 20 and 32 from the base pointer of the current stack frame?

The parameters to main are indeed set up by the code that calls main.

This appears to be code compiled according to the 64-bit ELF psABI for x86, in which the first several parameters to any function are passed in registers, not on the stack. When control reaches the main: label, argc will be in edi, argv will be in rsi, and a third argument conventionally called envp will be in rdx. (You didn't declare that argument, so you can't use it, but the code that calls main is generic and always sets it up.)

The instructions I believe you are referring to

    mov     DWORD PTR [rbp-20], edi
    mov     QWORD PTR [rbp-32], rsi

are what compiler nerds call spill instructions: they are copying the initial values of the argc and argv parameters from their original registers to the stack, just in case those registers are needed for something else. As several other people pointed out, this is unoptimized code; these instructions are unnecessary and would not have been emitted if you had turned optimization on. Of course, if you'd turned optimization on you'd have gotten code that doesn't touch the stack at all:

main:
    mov     eax, 2
    ret

In this ABI, the compiler is allowed to put the "spill slots," to which register values are saved, wherever it wants within the stack frame. Their locations do not have to make sense, and may vary from compiler to compiler, from patchlevel to patchlevel of the same compiler, or with apparently-unconnected changes to the source code.

(Some ABIs do specify stack frame layout in some detail, e.g. IIRC the 32-bit Windows ABI does this, to facilitate "unwinding", but that's not important right now.)

(To underline that the arguments to main are in registers, this is the assembly I get at -O1 from

int main(int argc) { return argc + 1; }

:

main:
    lea     eax, [rdi+1]
    ret

Still doesn't do anything with the stack! (Besides ret.))

zwol
  • 135,547
  • 38
  • 252
  • 361
2

This is "compiler 101" and what you want to research is "calling convention" and "stack frame". The details are compiler/OS/optimizations dependent. Briefly, incoming parameters may be in registers or on stack. When a function is entered, it may create a stack frame to save some of the registers. And then it may define a "frame pointer" to reference stack locals and stack parameters off the frame pointer. Sometimes the stack pointer is used as a frame pointer as well.

As for registers, usually someone (company) would define a calling convention and specifies which registers are "volatile", meaning that they can be used by a routine without issues, and "preserved", meaning that if a routine uses them, they will have to be saved and restored on function entry and exit. The calling convention also specifies which registers (if any) are used for parameter passing and function return.

  • 1
    All of the registers used in the `gcc -O0` output are call-clobbered except RBP. This answer omits the key point that storing register args to the stack is totally optional in the first place. There's zero reason to do that other than so a debugger can find them there. So while everything you say is true, I don't think it really answers *this* question. (And yes, x86-64 System V passes integer args in registers. It was designed GCC devs, not a company: [Why does Windows64 use a different calling convention from all other OSes on x86-64?](//stackoverflow.com/a/35619528)) – Peter Cordes Apr 30 '19 at 02:30
  • Also you forgot to mention the x86-64 System V red-zone. – Peter Cordes Apr 30 '19 at 02:33
  • Actually there is at least one other reasons to put arguments on stack, and that is to support varargs. In fact, debuggers can find register arguments just fine, they do not have to be on stack for that reasons. – Richard at ImageCraft May 01 '19 at 06:12