1

It is known that %rsp points to the top of the stack frame and %rbp points to the base of the stack frame. Then I can't understand why %rbp is 0x0 in this piece of code:

(gdb) x/4xg $rsp
0x7fffffffe170: 0x00000000004000dc  0x0000000000000010
0x7fffffffe180: 0x0000000000000001  0x00007fffffffe487
(gdb) disas HelloWorldProc 
Dump of assembler code for function HelloWorldProc:
=> 0x00000000004000b0 <+0>: push   %rbp
   0x00000000004000b1 <+1>: mov    %rsp,%rbp
   0x00000000004000b4 <+4>: mov    $0x1,%eax
   0x00000000004000b9 <+9>: mov    $0x1,%edi
   0x00000000004000be <+14>:    movabs $0x6000ec,%rsi
   0x00000000004000c8 <+24>:    mov    $0xd,%edx
   0x00000000004000cd <+29>:    syscall 
   0x00000000004000cf <+31>:    leaveq 
   0x00000000004000d0 <+32>:    retq   
End of assembler dump.
(gdb) x/xg $rbp
0x0:    Cannot access memory at address 0x0

And why is it "saving" (pushing) %rbp to the stack if it points to nothing?

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
alacerda
  • 155
  • 1
  • 8
  • Share your original code – Alex Jone Jun 21 '17 at 23:29
  • 2
    Actually, RSP points to the top of the stack frame, but the bottom of the procedure frame and RBP points to the top of procedure frame. GDB under Linux begins applications with all registers except RSP set to NULL. Trace to 4000B4 then 'x /xg $rsp' will work. – Shift_Left Jun 21 '17 at 23:36
  • @Shift_Left you were right. But to be truth I couldn't understand the reason for that behavior. If the system need to know the range/size of the stack how is it possible to %rbp starts with zero? Don't you agree that it is impossible to get to know where the stack frame starts at the beginning of the program? – alacerda Jun 21 '17 at 23:59
  • 1
    The system doesn't need to know the range/size of the stack frame. – Ross Ridge Jun 22 '17 at 00:18
  • 2
    @alacerda This is a peculiarity of GDB, probably so it's easier to know what registers have or haven't been used in your application. Design an app that displays RBP (**not using GDB**) on invocation and you might find the mechanism that reads the ELF header and launches the application passes a non-zero value in EBP – Shift_Left Jun 22 '17 at 01:24

1 Answers1

19

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.

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
  • https://www.phoronix.com/scan.php?page=article&item=fedora-frame-pointer&num=1 recently tested the performance cost of `-O2 -fno-omit-frame-pointer` with GCC12.1 on a Zen3 laptop CPU for multiple open-source programs, as proposed for Fedora 37. Most of them had performance regressions, a few of them very serious (probably some kind of bad thing happening at a key hotspot, probably didn't have to be that bad but happened to be.) – Peter Cordes Jul 02 '22 at 07:06
  • It's not GDB that zeros the registers, it's the Linux kernel that does that before entering user-space, to avoid leaking information via old values left in registers. If you're writing your own `_start`, the registers will still be `0` when your code gets control, if your code is statically linked. (Otherwise the dynamic linker runs first.) – Peter Cordes Apr 23 '23 at 21:55