RBP
is a general-purpose register, so it can contain any value that you (or your compiler) wants it to contain. It is only by convention that RBP
is used to point to the procedure frame. According to this convention, the stack looks like this:
Low |====================|
addresses | Unused space |
| |
|====================| ← RSP points here
↑ | Function's |
↑ | local variables |
↑ | | ↑ RBP - x
direction |--------------------| ← RBP points here
of stack | Original/saved RBP | ↓ RBP + x
growth |--------------------|
↑ | Return pointer |
↑ |--------------------|
↑ | Function's |
| parameters |
| |
|====================|
| Parent |
| function's data |
|====================|
| Grandparent |
High | function's data |
addresses |====================|
As such, the boilerplate prologue code for a function is:
push %rbp
mov %rsp, %rbp
This first instruction saves the original value of RBP
by pushing it onto the stack, and then the second instruction sets RBP
to the original value of RSP
. After this, the stack looks exactly like the one depicted above, in the beautiful ASCII art.
The function then does its thing, executing whatever code it wants to execute. As suggested in the drawing, it can access any parameters it was passed on the stack by using positive offsets from RBP
(i.e., RBP+x
), and it can access any local variables it has allocated space for on the stack by using negative offsets from RBP
(i.e., RBP-x
). If you understand that the stack grows downward in memory (addresses get smaller), then this offsetting scheme makes sense.
Finally, the boilerplate epilogue code to end a function is:
leaveq
or, equivalently:
mov %rbp, %rsp
pop %rbp
This first instruction sets RSP
to the value of RBP
(the working value used throughout the function's code), and the second instruction pops the "original/saved RBP" off the stack, into RBP
. It is no coincidence that this is precisely the opposite of what was done in the prologue code we looked at above.
Note, though, that this is merely a convention. Unless required by the ABI, the compiler is free to use RBP
as a general-purpose register, with no relation to the stack pointer. This works because the compiler can just calculate the required offsets from RSP
at compile time, and it is a common optimization, known as "frame pointer elision" (or "frame pointer omission"). It is especially common in 32-bit mode, where the number of available general-purpose registers is extremely small, but you'll sometimes see it in 64-bit code, too. When the compiler has elided the frame pointer, it doesn't need the prologue and epilogue code to manipulate it, so this can be omitted, too.
The reason you see all of this frame-pointer book-keeping is because you're analyzing unoptimized code, where the frame pointer is never elided because having it around often makes debugging easier (and since execution speed is not a significant concern).
The reason why it RBP
is 0 upon entry to your function appears to be a peculiarity of GDB, and not something that you really need to concern yourself with. As Shift_Left notes in the comments, GDB under Linux pre-initializes all registers (except RSP
) to 0 before handing off control to an application. If you had run this program outside of the debugger, and simply printed the initial value of RBP
to stdout, you'd see that it would be non-zero.
But, again, the exact value shouldn't matter to you. Understanding the schematic drawing of the call stack above is the key. Assuming that frame pointers have not been elided, the compiler has no idea when it generates the prologue and epilogue code what value RBP
will have upon entry, because it doesn't know where on the call stack the function will end up being called.