In x86 assembly, is ESP decremented twice after a

2019-07-10 12:15发布

Long story short, I'm studying a book titled "The 8088 and 8086 Microprocessors" by Singh and Triebel, to learn old assembly for those specific CPUs. Now, the computer I'm practicing on is my main computer, which I recently built, so the registers are bigger.

That said, the book (which I find extremely helpful) says that the call label operand causes the address of the instruction following the call to be placed on the stack, and THEN SP is decremented by 2 (ESP, and decremented by 4 on my CPU). In some code I'm studying, a call operand is immediately followed by a push. When the CPU encounters a push, the book states that SP is decremented by two (again, ESP is decremented by 4 on my CPU).

; ESP=0xffffd840 right now
call iprint
mov eax, 0Ah

iprint:
push eax ; say eax contains 1

Now, say ESP=0xffffd840 before the call. The address of EIP is saved on the stack (the address of the instruction that follows the CALL operand). Then ESP is decremented by 4. At this point, ESP=0xffffd83c. Then the push operand is encountered. Going by what the book says, the stack pointer is decremented first, and then the contents of the register are pushed onto the stack. So now ESP=0xffffd838 and 1 is pushed onto the stack.

If it helps:
Stack addr  Contents
********** ********
0xffffd840  address of mov eax, 0Ah
0xffffd83c  ?
0xffffd838  1

Now, my question is, is 0xffffd83c skipped? According to the book, ESP is decremented after saving the next instruction after the call, and then before data is put on the stack from the push, it's decremented again.

I've been debugging a similar scenario for a while now, paying close attention to the values of the registers, but I just can't tell if the debugger adheres to what the book says (decrementing before doing an operation, or after).

Is this because in some cases a parameter is given after RET in a subroutine, causing the stack pointer to increment? If the stack pointer is indeed decremented twice before having data put on it, this is the only reason I can see.

Could someone please confirm or explain this if I have this wrong?

Thank you

2条回答
甜甜的少女心
2楼-- · 2019-07-10 12:22

EDIT: Sorry, my previous answer was wrong for your first question. My x86 is rusty. The best way to check is to debug your program. Here's a similar one (NASM syntax):

global _start

section .text

test:
push eax
pop eax
ret

_start:
mov eax, 1
call test

You can compile this on Linux as follows:

$ nasm -f elf32 -g test.s && ld -m elf_i386 -g test.o

Now let's use GDB to debug it:

$ gdb -q a.out
Reading symbols from a.out...done.

Disassemble _start function to see its addresses

(gdb) disas _start
Dump of assembler code for function _start:
   0x08048063 <+0>: mov    $0x1,%eax
   0x08048068 <+5>: call   0x8048060 <test>
End of assembler dump.

Put a breakpoint at the start

(gdb) b *0x08048063
Breakpoint 1 at 0x8048063

And run it!

(gdb) r
Starting program: /home/yasin/Downloads/a.out 

Breakpoint 1, 0x08048063 in _start ()

Check ESP starting value

(gdb) i r esp
esp            0xffffce00   0xffffce00

And put a breakpoint on test function.

(gdb) disas test
Dump of assembler code for function test:
   0x08048060 <+0>: push   %eax
   0x08048061 <+1>: pop    %eax
   0x08048062 <+2>: ret    
End of assembler dump.
(gdb) b *0x08048060
Breakpoint 2 at 0x8048060

Let's continue

(gdb) c
Continuing.

Breakpoint 2, 0x08048060 in test ()

Ok, stopped at start of test, before push, let's check ESP value and its contents

(gdb) i r esp
esp            0xffffcdfc   0xffffcdfc
(gdb) x /1xw $esp
0xffffcdfc: 0x0804806d

It has been decremented by 4 and the call return value pushed there. Let's put another breakpoint after push and see what happens.

(gdb) b *0x08048061
Breakpoint 3 at 0x8048061
(gdb) c
Continuing.

Breakpoint 3, 0x08048061 in test ()
(gdb) i r esp
esp            0xffffcdf8   0xffffcdf8
(gdb) x /1xw $esp
0xffffcdf8: 0x00000001

It has decremented ESP by 4 and pushed 1. So now stack looks like this

(gdb) x /2xw $esp
0xffffcdf8: 0x00000001  0x0804806d

So resuming: what you were missing is that CALL also behaves like PUSH: it decrements ESP by 4 before pushing the value into the stack.

And for the rest of the questions:

decremented by 4 on my CPU

Assemblers usually default to 32 bits and not 16 bits, that's why it is decrementing 4 bytes and not 2. You can force your assembler to use 16-bit instructions instead.

In some code I'm studying, a call operand is immediately followed by a push

In fact that is the common case, it is called the function/routine entry protocol.

I've been debugging a similar scenario for a while now, paying close attention to the values of the registers, but I just can't tell if the debugger adheres to what the book says.

The step by step run I've made should have clarified how to check register and memory/stack values. If you have any further questions about this, let me know.

查看更多
虎瘦雄心在
3楼-- · 2019-07-10 12:39

The call <address> is like: push eip jmp <address>, so in your case if esp is 0xffffd840 is ahead of call, the returning address of next instruction is pushed to 0xffffd83c (Because the pseudo "push eip" will first decrement the esp to create new top of stack, then it will store the current value of eip there (BTW, the eip already points at next instruction, as the fetch+decode instruction phase of call was finished, so it is actually the value which will be needed for ret).

You can also in debugger view memory. And "stack" is just ordinary memory. So if you have esp equal to 0xffffd840, you can open for example memory view at 0xffffd824, and you will see 32 bytes of stack memory, with the 28 bytes not-used-yet and last 4 bytes being current "top of the stack".

I'm using group of 4 bytes everywhere, as that's the native size of CPU "word" (dword in x86 terminology, word is 16 bit only) in 32b protected mode. IIRC you can still enforce the CPU to do push ax or use sub/add esp,immediate to even move it by single byte, but usually it involves performance penalties, and in 64b modes several calling conventions even require 16 byte alignment, so I would recommend to stick with +-4 esp operations in 32b mode.

But if your book is about 8086, you may want to use dosbox to emulate the old DOS 16 bit environment, to save you some platform specific problems in the beginning. Although maybe you should instead find some 32/64 bit recent book for your OS, as the 32b protected mode on x86 is much easier to learn (only the graphics output is not as straightforward as it was back in DOS era, but if you will mix your asm files with C++ "loader", which would for example initialize some window surface as ARGB memory array, you can pass that pointer down to the asm routines and toy around with pixels, in the same simple way, how the old 320x200 "mode 13h" in DOS worked. Even easier (no palette and no 64k segment limits).

查看更多
登录 后发表回答