In x86 Assembly language:
I assume that I have a normal function prologue, read
push ebp
mov ebp,esp
I know I can read or write registers by accessing a memory destination operand, let's say I wanted the first argument. I would do
mov eax,[ebp +8]
to get f.e. an integer parameter from the stack.
So why wouldn't I work with the stackpointer directly?
add esp,8 ; point ESP at the data we want
pop eax
sub esp,12 ; restore ESP to its original position
Does this lead to errors ? Is this used in any case?
I know of course that the first operation is smaller in size since it is only one opcode namely mov
instead of three, but that is not the point of the question.
(editor's note: mov eax, [ebp+8]
is a 3-byte instruction in x86 machine code. add
/sub
esp, imm8 are 3 bytes each, pop eax
is 1 byte.
mov eax, [esp+8]
is a 4-byte instruction: unlike in 16-bit addressing modes, ESP can be a base register. But it does require a SIB byte to encode it.
These are all single-uop instructions on modern CPU, not counting extra stack-sync uops.)
Why is it bad practise to do so?
You can work with ESP
as a pointer directly.
However if any pushing or popping takes place then ESP turns into a moving target, making your calculations a bit more difficult.
For this reason we put a copy of the stack pointer in EBP so we don't have to worry about ESP changing.
If, however, you are not going to do anything to alter the stack pointer then it's perfectly fine to use ESP
instead of EBP
.
And if you do alter ESP
you can of course alter the offsets from ESP accordingly.
add esp,8
mov ecx,[esp-4] //never access data outside the actual stack.
pop eax
sub esp,12
Remember an interrupt can happen at any time.
The interrupt will assume anything below the stack pointer can be altered. If you manually increase the stack pointer and then access the data below it as if it was still in the stack you might find that the data there has already been replaced by the interrupt handler (Oops).
Rule: anything north of ESP is safe, anything south of ESP is marked for death
This is why routines create a stack frame
. By lowering the stack pointer (remember the stack grows down) a region of memory is protected because it is now inside the stack.
The semantics of the stack means that any data above ESP is safe and any data below ESP is fair game.
If you violate either of these two principles by
A - using a non-fixed ESP as a base pointer, or
B - accessing data below ESP.
You'll risk A: corrupting someone else's data or B: working with corrupted data yourself.
Is this bad practise?
add esp,8 //equivalent to pop anyreg, pop anyreg pop eax //pop from the (new) top of the stack. sub esp,12 //reset the stack back to where is was.
If an interrupt happens before the sub esp,12
the 3 integers stored in this bit of stack space will be altered, causing data corruption in your app.
Use the following code instead.
mov eax,[esp+8]
This code is A: safe, B: faster, C: does not clobber the flags register, D: shorter, and E: encodes in fewer bytes.
A note on add/sub
If you have something useful in FLAGS, you can avoid clobbering it by using LEA
for addition instead. If not, add
/sub
are at least as fast (e.g. running on more execution ports on some mainstream CPU, while LEA can only run on 2 of the 4 integer ALU execution units on Ryzen, and Haswell and later). There's no code-size advantage either way.
lea esp,[esp+8] == add esp,8 (but without altering the flags).
lea edx, [esp+8] ; copy-and-add replacing mov + add is very useful
Definitely use LEA when it can replace 2 or more other instructions, but not just to replace an add/sub unless you have a use for preserving FLAGS.