2
votes

Enter in function, standard prologue

push rbp
mov rbp, rsp
sub rsp, 128 ; large space for storing doubles, for example

How to reference local variables now, via rsp + positive offset, or via rbp + negative offset?

Reading https://en.wikibooks.org/wiki/X86_Disassembly/Functions_and_Stack_Frames, indeed quite understandable. It writes

...the value of esp cannot be reliably used to determine (using the appropriate offset) the memory location of a specific local variable. To solve this problem, many compilers access local variables using negative offsets from the ebp registers.

Why not reliable? Until that question I was accessing local variables via rsp, like this:

mov rax, [rsp+.length] ; get length of array
mov [rsp+8], rax ; store sum at the stack

everything goes quite nicely using rsp for stack referencing.

1
The stack pointer can often be used to determine the address of variables on the stack. However, if the function uses variable-length arrays or the equivalent of alloca(), using offsets from the stack pointer may no longer be possible.EOF
@EOF, so one should use addressing relative to rsp in all cases except that two?Bulat M.

1 Answers

4
votes

Look at gcc output. It defaults to -fomit-frame-pointer when optimizing, only making a stack frame when functions use variable-length arrays or it needs to align the stack to more than 16B.

That wiki page is basically wrong. There's no scary weird stuff that makes it "unreliable". The only time you can't do it is when you need to modify RSP by an amount that isn't an assemble-time constant.


However, if you do make a stack frame with push rbp / mov rbp, rsp, you should use RBP-relative addressing modes. It's more efficient, because [rsp + 8] takes an extra byte to encode (vs. [rbp - 8]). Addressing modes with RSP as the base register always need a SIB byte, even when there's no index register.

The point of using RSP-relative addressing modes is that you can avoid wasting instructions making a stack frame, so RBP is just another call-saved register (like RBX) that you can save/restore and use for whatever you want.


The other big advantage to RBP-relative addressing is that the offset from RBP to a given variable stays constant for the entire function. Unlike compilers, we puny humans are easily confused by pushes and pops which change RSP inside a function. Of course, 64-bit code hardly ever changes RSP inside a function between the prologue and epilogue, because both ABIs pass args in registers. Saving/restoring some call-preserved registers (like RBX or R12-R15) in the prologue/epilogue is often better than having push/pop inside the function (and definitely better than inside a loop). When you need to spill/reload, mov for random access is usually best.

In 32-bit code, making a stack frame in hand-written code often makes more sense, esp. for maintainability. In 64-bit code, it's usually not much of an issue. Although saving/restoring an extra register with an extra pair of push/pop does change the stack layout, which does matter if any args were passed on the stack (e.g. a large struct by value, but write your function to take a const-pointer arg instead!).