Can I pop from the middle of a stack?
Asked Answered
A

2

6

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?

Amsden answered 24/3, 2017 at 4:40 Comment(1)
I assume you mean add instead of inc and sub instead of dec. One issue with this is if you are running with interrupts on and an interrupt occurs between pop eax and the sub esp,12 then anything below ESP will likely be clobbered (the interrupt will cause data to be pushed onto the stack). An interrupt between inc esp, 8 and pop eax will be a potential problem for the same reason. If you still need the data to be preserved below ESP then this will be a problem. Don't know how you use stack data after dec esp,12 so hard to say if it would be an issue or not.Corody
G
5

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.

enter image description here Warning
You should never do:

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.

YES! This is bad

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.

Gharry answered 24/3, 2017 at 8:1 Comment(3)
For user-space (and not kernel code) anything "south of ESP" can be perfectly safe - the CPU switches to a different stack when an IRQ occurs. EBP is mostly used as "frame pointer" to make it easier to write debuggers capable of figuring out the stack frame; and (excluding for some debuggers) it's always better to not use EBP as "frame pointer" and use it as just another general purpose register (especially for 32-bit 80x86 where the number of general purpose registers is quite limited).Enfranchise
@Enfranchise I don't think the OP did specify target platform, so if he is running some weird 32b mode setup (for example his own custom OS), he may have shared stack. ;) IMO it never hurts to learn them first this classic way, and after that to introduce the common x86 quirks and convenience, principles first, they may hit the issue on other platform later.Pseudo
@Ped7g: I prefer to cover all possibilities, and find that that people who are taught to write assembly like a compiler rarely recover from "assembly must be limited by the restrictions of a completely different language" initial assumptions. Also note that use of "south of RSP" became an official part of at least one modern calling convention (the "red zone" for AMD64 ABI).Enfranchise
E
3

So why would'nt i work with the stack pointer directly?

EBP is used as a frame pointer so that it's easier to write debuggers (or, easier for debuggers to figure out the current stack frame and determine where local variables and arguments are).

Using EBP as a frame pointer makes it unusable for anything else, and this is bad for performance - less registers you can use means more time spent moving temporary values to/from stack. Good code (and good debuggers) do not use or need EBP as a frame pointer. Good compilers typically support an option to omit/disable the use of EBP as a frame pointer (e.g. the "--fomit-frame-pointer" in GCC).

This code is inefficient:

    add esp,8
    pop eax
    sub esp,12

However, depending on how the code is being used it can be very dangerous or perfectly safe. More specifically, it depends on whether or not the code has to cater to asynchronous events (signals, IRQs) that assume data can be pushed onto the stack without corrupting existing data on the stack.

Better is something like:

    mov eax,[esp+8]

This is more efficient, and it's always safe.

Note: Under some (relatively extreme) conditions it can also be perfectly safe to use ESP as just another general purpose register. For a simple scenario, consider this:

;Copy array somewhere else, while reversing the order of elements
;
;Input
; ecx    Number of elements in array
; esi    Address of source array
; edi    Address of destination array

reverseArray:
    mov ebx,esp           ;ebx = stack top
    lea esp,[edi+ecx*4]   ;esp = address of byte after array
    cld
.next:
    lodsd                 ;eax = next element in source array
    push eax              ;Store it in destination array
    loop .next            ;Do all elements

    mov esp,ebx           ;Restore stack
    ret
Enfranchise answered 24/3, 2017 at 10:13 Comment(3)
I understand that a compiler (like the mentioned gcc) is more efficient by not using ebp, but when writing assembly code by hand i imagine it to be much harder to figure out all addresses.Amsden
@Amsden while you write asm by hand, you usually do that for performance reasons.. and at that point you avoid stack (and any stack calling convention) if possible, so you need to solve this only rarely, like when you are interfacing some C library, or something, where you can't enforce your own calling convention ignoring stack.Pseudo
@clockw0rk: When writing assembly by hand you might add a bunch of definitions (e.g. %define .myLocalVar1 [esp+8]) in a nice orderly group near the start of a routine (and that's no harder than figuring out offsets from EBP). It only gets a little messy if you're pushing/popping in the middle of a routine. However, with parameters passed in registers and careful use of registers (to avoid local variables on the stack) often you don't need the address of anything on the stack at all.Enfranchise

© 2022 - 2024 — McMap. All rights reserved.