3

I am having some trouble understanding how the esp and ebp registers are used.

Why do we do:

pushl %ebp 
movl %esp, %ebp

at the start of every function? What is ebp holding when it is pushed for the first time?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 2
    ESP points to where you are going to allocate more stack space. Which happens when you call another function that has parameters or use a function like alloca(). EBP is stable and therefore nice to have to address local variables and arguments. And notably nice for a debugger since it doesn't have to know how ESP changes. The pushed value of EBP points to the stack frame of the caller, again nice for the debugger and useful in languages that support nested functions, like Pascal. x64 code generators typically do it the hard way, using only RSP. – Hans Passant Mar 16 '19 at 13:10
  • When you say "for the first time", do you mean in `_start`, the process entry point that doesn't have a caller? In that case, the i386 System V ABI doc recommends pushing `0` (as a sentinel that this is the outer-most scope), not whatever garbage is in EBP. – Peter Cordes Mar 16 '19 at 13:12

3 Answers3

4

Why do we do:

This has historic reasons. In 16-bit code ...

  • ... the x86 CPUs did not allow all registers to be used for memory addressing.
  • ... addresses were relative to the "segments" (e.g. 16*ss or 16*ds).

Because sp could not be used to access memory directly (e.g. 10(%sp) - this is not possible in 16-bit code), you first had to copy sp to another register and then access the memory (e.g. copy sp to bp and then do 10(%bp)).

Of course it would also have been possible to use bx, si or di instead of bp.

However, the second problems are the segments: Using one of those registers would access the segment specified by the ds register. To access memory on the stack, we would have to do ss:10(%bx) instead of 10(%bx). Using bp implicitly accesses the segment that contains the stack (which is faster and the instruction is one byte shorter compared to explicitly specifying a segment).

In 32-bit (or 64-bit) code all this is not necessary any more. I just compiled a function with a modern C compiler. The result was:

movl    12(%esp), %eax
imull   8(%esp), %eax
addl    4(%esp), %eax
ret

As you can see, the ebp register is not used.

However, there are two reasons why ebp is still used in modern code:

  • It's easier to create the function. You know that the first argument is always located at 8(%ebp) even if your function contains push and pop instructions. Using esp the location of the first argument changes with every push or pop operation.
  • Use of the alloca function: This function will modify the esp register in a way that may even be unpredictable for the compiler! So you'll need a copy of the original esp register.

An example for the use of alloca:

push %ebp
mov %esp, %ebp
call GetMemorySize  # This will set %eax

# ---- Start of alloca() ----
# The alloca "function" will reserve N bytes on the
# stack while the value N is calculated during
# the run-time of the program (here: by the function
# GetMemorySize)
or $3, %al
inc %eax

# This has the same effect as multiple "push"
# instructions. However, we don't know how many
# "push" instructions!
sub %eax, %esp

mov %esp, %eax
# From this moment on, we would not be able to "restore"
# the original stack any more if we didn't have a copy
# of the stack pointer!
# ---- End of alloca() ----

push %eax
mov 8(%ebp), %eax
push %eax
call ProcessSomeData
mov %ebp, %esp
pop %ebp

# Of course we need to restore the original value
# of %esp before we can do a "ret".
ret
Martin Rosenau
  • 17,897
  • 3
  • 19
  • 38
4

At the start of every function ebp is pointing wherever the calling function wanted it, it's not relevant to the current function until the code for the current function chooses to use it. ebp is just a stack frame pointer in case you choose to have a stack frame. The notion is that YOU CAN use ebp to have a non-moving reference to the stack for your function while you are free to continue to add or remove items on the stack using esp. If you were to not use a stack pointer and were to continue to use esp as the reference to the stack then where a particular item on the stack is over the course of your function varies relative to esp. If you set ebp before you start using the stack (other than to save ebp) then you have a fixed relative address to the parameters on the stack that your function cares about, like passed parameters, local variables, etc.

You are perfectly free to use eax or edx or any other register as a stack frame pointer within your function, ebp is there as a general purpose register for you to use for stack frames since x86 has historically had a stack dependency (return addresses, and old calling conventions were stack based). Other instruction sets with more registers might simply choose a register for the compiler implementation as the function pointer/stack frame pointer. If you have the option and choose to use a stack frame. It burns a register you could be using for other things, burns more code and execution time. Like using other general purpose registers, ebp is non volatile per the calling conventions being used today, you need to preserve it and return it the way you found it. So what it points to is specific to the function. What it pointed to when your function was entered was specific to the calling function.

A particular compiler implementation may choose to have stack frames and may choose how it uses ebp. And if it is always used the same way when enabled then with that toolchain you might have a debugger or other tool that can take advantage of that. For example if the first thing in the function is to push ebp on the stack then the return address to the calling function within any function relative to ebp is fixed (well unless there was some tail optimization then maybe its the caller of the caller (of the caller (of the caller))). You are burning a register and stack space and code space for this feature but, like compiling for debugging you can compile with a stack frame during development to use these features.

The reason why you start with a push is that is a good way to use the frame pointer and define a consistent location. Pushing it on the stack as the first thing you do 1) preserves ebp so you don't crash the calling function(s) 2) defines a consistent reference point addresses below ebp are the return address and calling parameters at a fixed offset for the duration of the function. Local variables are at fixed addresses above ebp for a scheme like this. Compilers, as well as humans, are more than capable of not needing to do this, my first parameter might be at esp-20 at one point in the code and I may then go push 8 bytes more on the stack now that same parameter is at esp-28, just code it as such.

But for debugging purposes debugging the code produced and at times for example finding the return address at a fixed offset. Burning another register, is IMO lazy but, can definitely help debug and increase the quality of the compiler output. Find bugs in the output of the compiler faster, and help folks trying to read the code understand it faster with less effort. With a stack frame pointer used properly all parameters and local variables are at a fixed offset to the stack frame pointer through the duration of the function between the points where the stack frame pointer is setup and cleaned up. Push pointer to save it set frame pointer to stack pointer with or without an offset. To the pop of the frame pointer before a return.

halfer
  • 19,824
  • 17
  • 99
  • 186
old_timer
  • 69,149
  • 8
  • 89
  • 168
2

During execution of the function, various objects can be pushed onto the stack. The push decrements %esp (or %rsp if you are using 64-bit hardware) to point to the next available memory on the stack, while %ebp (or %rbp) maintains an unchanging pointer to the start of the function's stack frame so that, relative to %ebp, the function is able to find its various objects already stored on the stack.

Early, 8-bit CPUs like the old 6502 from the 1970s and 1980s had no %ebp. Lacking %epb, consider this C code:

int a = 10;
++a;
{
    int b = 20;
    --b;
    a += b;
}

The a is stored at the 0(%esp), except that, when b is pushed onto the stack, the a, which has not actually moved, is now at 4(%esp). Do you see the problem?

Using %ebp, the a is always at -4(%ebp) and the b when in scope is at -8(%ebp).

thb
  • 13,796
  • 3
  • 40
  • 68