问题
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
回答1:
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).
回答2:
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.
来源:https://stackoverflow.com/questions/44129645/in-x86-assembly-is-esp-decremented-twice-after-a-call-and-then-push-before-dat