enter
also needs a number which is the amount of space to allocate, which is the key to your question: these instructions are made to set up space for your function's local variables.
Local variables are referred to through the EBP register. Let me show you an example:
import core.stdc.stdio;
void main() {
int a = 8;
a += 8;
printf("%d\n", 8);
}
(This is D code but that's not really relevant)
Disassembly of section .text._Dmain:
00000000 <_Dmain>:
0: 55 push ebp
1: 8b ec mov ebp,esp
3: 83 ec 04 sub esp,0x4
6: b8 08 00 00 00 mov eax,0x8
b: 89 45 fc mov DWORD PTR [ebp-0x4],eax
e: 01 45 fc add DWORD PTR [ebp-0x4],eax
11: 50 push eax
12: b9 00 00 00 00 mov ecx,"%d\n"
17: 51 push ecx
18: e8 fc ff ff ff call printf
1d: 31 c0 xor eax,eax
1f: 83 c4 08 add esp,0x8
22: c9 leave
23: c3 ret
Let's break this up into each part:
0: 55 push ebp
1: 8b ec mov ebp,esp
3: 83 ec 04 sub esp,0x4
This is the function prolog, setting up ebp. The sub esp, 0x4
pushed the stack 4 bytes away - this makes room for our local int a
variable, which is 4 bytes long.
The enter
instruction is rarely used, but I believe enter 4,0
does this same thing - enter a function with 4 bytes of local variable space. edit: The other 0 is the nesting level, I've never seen it used... enter is generally slower than just doing the steps yourself as the compiler does here. /edit
6: b8 08 00 00 00 mov eax,0x8
b: 89 45 fc mov DWORD PTR [ebp-0x4],eax
This is the a=8
line - the second line stores the value in the local variable's memory.
e: 01 45 fc add DWORD PTR [ebp-0x4],eax
Then, we add to it in a+=8
(the compiler reused eax here since it recognized that the
number is the same...)
After that, it calls printf by pushing its arguments to the stack, then zeroes out the eax register (xor eax, eax
), which is how D returns 0 from a function.
11: 50 push eax
12: b9 00 00 00 00 mov ecx,"%d\n"
17: 51 push ecx
18: e8 fc ff ff ff call printf
1d: 31 c0 xor eax,eax
1f: 83 c4 08 add esp,0x8
Note that the add esp, 0x8
here is part of the call to printf: the caller is responsible for cleaning up the arguments after calling the function. This is needed because only the caller knows how many args it actually sent - this is what enables printf's variable arguments.
Anyway, finally, we clean up our local variables and return from the function:
22: c9 leave
23: c3 ret
edit: leave
btw expands to mov esp, ebp; pop ebp;
- it is exactly the opposite of the setup instructions, and like Aki Suihkonen said in the other answer, a nice thing here is the stack is restored to how it was at function entrance, no matter what happened inside the function (well, unless the function totally destroyed the stack's contents, in which case your program will most likely soon crash). /edit
So, bottom line, the ebp stuff is all about your local variables. It uses esp to get started so it has a nice memory space that won't step on other functions (it is on the call stack), but moves it to ebp so your locals remain at a consistent offset throughout the function - the variable a
is ALWAYS [EBP-4] in this function, even as the stack is manipulated.
It is easiest to see in action by disassembling a function you write in C or something, like we did here. The linux command objdump -d -M intel somefile.o
is what I used (then I manually fixed up some minor things to make it more readable. If you disassemble a .o file not all library calls are resolved yet so it can look kinda weird, but that doesn't affect the local variable stuff!)
enter
isenter 0, 0
. The first parameter is the number of bytes to reserve for local variables (sub esp, ???). The second parameter is "lex level" - you don't wanna know, just make it zero. – Frank Kotlerenter
. It's very slow compared topush
/mov
, and only saves a tiny bit of code bytes if you can also replace asub esp, imm
to reserve some stack space. It's larger (32b) or the same size (64b) as just push / mov.leave
doesn't have potential crazy-CISC semantics, so it isn't slow and is worth using on some microarchitectures. – Peter Cordes