1

I have tried to understand this basing on a square function in c++ at godbolt.org . Clearly, return, parameters and local variables use “rbp - alignment” for this function. Could someone please explain how this is possible? What then would rbp + alignment do in this case?

int square(int num){
    int n = 5;// just to test how locals are treated with frame pointer
    return num * num;
}

Compiler (x86-64 gcc 11.1)

Generated Assembly:

square(int):
    push rbp
    mov rbp, rsp 
    mov DWORD PTR [rbp-20], edi. ;\\Both param and local var use rbp-*
    mov DWORD PTR[rbp-4], 5.     ;//
    mov eax, DWORD PTR [rbp-20]
    imul eax, eax
    pop rbp
    ret

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 2
    The 64 bit calling convention uses registers to pass parameters (conditions apply). As such the `num` is in `edi`, it is later stored into a temporary local variable. – Jester Jul 16 '21 at 11:18
  • Background reading - [Old New Thing - The great thing about calling conventions on the x86 platform is that there are so many to choose from!](https://devblogs.microsoft.com/oldnewthing/20040102-00/?p=41213) And also [Itanium C++ ABI](https://itanium-cxx-abi.github.io/cxx-abi/abi.html) – Richard Critten Jul 16 '21 at 12:05
  • 1
    If those were memory locations were for parameters, the caller would have done the stores. When the callee stores into variables, they are not parameters from the caller, but storage of the callee. We think of it more as `rbp - offset` rather than an alignment thing. – Erik Eidt Jul 16 '21 at 14:47
  • Related, near duplicate: [Why does the x86-64 System V calling convention pass args in registers instead of just the stack?](https://stackoverflow.com/q/51976465) - the spill / reload is a consequence of compiling without optimization. – Peter Cordes Sep 15 '22 at 05:41

2 Answers2

5

This is one of those cases where it’s handy to distinguish between parameters and arguments. In short: arguments are the values given by the caller, while parameters are the variables holding them.

When square is called, the caller places the argument in the rdi register, in accordance with the standard x86-64 calling convention. square then allocates a local variable, the parameter, and places the argument in the parameter. This allows the parameter to be used like any other variable: be read, written into, having its address taken, and so on. Since in this case it’s the callee that allocated the memory for the parameter, it necessarily has to reside below the frame pointer.

With an ABI where arguments are passed on the stack, the callee would be able to reuse the stack slot containing the argument as the parameter. This is exactly what happens on x86-32 (pass -m32 to see yourself):

square(int):                             # @square(int)
        push    ebp
        mov     ebp, esp
        push    eax
        mov     eax, dword ptr [ebp + 8]
        mov     dword ptr [ebp - 4], 5
        mov     eax, dword ptr [ebp + 8]
        imul    eax, dword ptr [ebp + 8]
        add     esp, 4
        pop     ebp
        ret

Of course, if you enabled optimisations, the compiler would not bother with allocating a parameter on the stack in the callee; it would just use the value in the register directly:

square(int):                             # @square(int)
        mov     eax, edi
        imul    eax, edi
        ret
user3840170
  • 26,597
  • 4
  • 30
  • 62
  • Thanks a lot. Now just to be sure, your point is -this behavior depends on the architecture. – Mwesigwa Shalom Jul 16 '21 at 19:38
  • It depends on the calling convention, which depends on the ABI, which depends on the architecture. Any of those can change without the next changing. – user3840170 Jul 17 '21 at 05:56
0

GCC allows "leaf" functions, those that don't call other functions, to not bother creating a stack frame. The free stack is fair game to do so as these fns wish.

G Huxley
  • 1,130
  • 14
  • 19
  • It's really the x86-64 System V ABI that allows it, guaranteeing that 128 bytes below RSP (the [red zone](https://stackoverflow.com/tags/red-zone/info)) is safe from asynchronous clobbers (e.g. by signal handers, debuggers, or anything else). GCC targeting Windows can't use space below the stack pointer, because Windows x64 does *not* have a red-zone. But yes, GCC does know how to take advantage of the red-zone for leaf functions on ABIs that have them. – Peter Cordes Sep 15 '22 at 05:33
  • Also note that this doesn't answer the question asked: the only difference in the asm with `gcc -mno-red-zone` would be a `sub rsp, 24` in the prologue, and a `leave` instead of `pop rbp` at the end. (https://godbolt.org/z/x6h3938as). The incoming register arg would still be spilled to a negative offset relative from RBP ([because this was compiled without optimization](https://stackoverflow.com/questions/51976465/why-does-the-x86-64-system-v-calling-convention-pass-args-in-registers-instead-o)), the difference would just be that RSP is below that. It's a useful footnote to the other answer. – Peter Cordes Sep 15 '22 at 05:40