在 x86 汇编中,ESP 是否在调用后递减两次,然后在将数据保存到堆栈之前推送?
In x86 assembly, is ESP decremented twice after a call and then push, before data is saved on the stack?
长话短说,我正在研究 Singh 和 Triebel 的一本名为 "The 8088 and 8086 Microprocessors" 的书,以学习 old 那些特定 CPU 的程序集。现在,我正在练习的计算机是我最近组装的主计算机,所以寄存器更大。
就是说,这本书(我觉得非常有帮助)说调用标签操作数导致调用之后的指令地址被放置在堆栈上,然后 SP
递减 2 (ESP
,并在我的 CPU 上减去 4)。在我正在研究的某些代码中,调用操作数后紧跟 push
。当 CPU 遇到 push
时,书上说 SP
减 2(同样, ESP
在我的 CPU 上减 4)。
; ESP=0xffffd840 right now
call iprint
mov eax, 0Ah
iprint:
push eax ; say eax contains 1
现在,在通话前说 ESP=0xffffd840
。 EIP
的地址保存在栈中(CALL
操作数后面的指令的地址)。然后ESP
减4。此时,ESP=0xffffd83c
。然后遇到入栈操作数。按照书上的说法,堆栈指针先递减,然后寄存器的内容被压入堆栈。所以现在 ESP=0xffffd838
和 1 被压入堆栈。
If it helps:
Stack addr Contents
********** ********
0xffffd840 address of mov eax, 0Ah
0xffffd83c ?
0xffffd838 1
现在,我的问题是,是否跳过了 0xffffd83c
?根据书上的说法,ESP
在call
之后保存下一条指令后递减,然后在数据从push
放入堆栈之前,它再次递减。
我已经调试了一段时间类似的场景,密切关注寄存器的值,但我无法判断调试器是否遵循书中所说的(在执行操作之前递减) , 或之后).
这是不是因为某些情况下子程序在RET
之后给了一个参数,导致栈指针自增?如果在将数据放入堆栈指针之前堆栈指针确实递减了两次,这是我能看到的唯一原因。
如果我有错,能否请大神确认一下或解释一下?
谢谢
编辑:抱歉,我之前的回答对您的第一个问题是错误的。我的 x86 生锈了。最好的检查方法是调试您的程序。这是一个类似的(NASM 语法):
global _start
section .text
test:
push eax
pop eax
ret
_start:
mov eax, 1
call test
您可以在 Linux 上编译它,如下所示:
$ nasm -f elf32 -g test.s && ld -m elf_i386 -g test.o
现在让我们用GDB调试一下:
$ gdb -q a.out
Reading symbols from a.out...done.
反汇编 _start
函数以查看其地址
(gdb) disas _start
Dump of assembler code for function _start:
0x08048063 <+0>: mov [=13=]x1,%eax
0x08048068 <+5>: call 0x8048060 <test>
End of assembler dump.
在开头打个断点
(gdb) b *0x08048063
Breakpoint 1 at 0x8048063
还有运行呢!
(gdb) r
Starting program: /home/yasin/Downloads/a.out
Breakpoint 1, 0x08048063 in _start ()
检查 ESP 起始值
(gdb) i r esp
esp 0xffffce00 0xffffce00
并在 test
函数上设置断点。
(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
让我们继续
(gdb) c
Continuing.
Breakpoint 2, 0x08048060 in test ()
好的,在 test
开始时停止,在 push
之前,让我们检查 ESP 值及其内容
(gdb) i r esp
esp 0xffffcdfc 0xffffcdfc
(gdb) x /1xw $esp
0xffffcdfc: 0x0804806d
它已经减了 4 并且 call return 值被推到那里。让我们在 push
之后放置另一个断点,看看会发生什么。
(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
它已将 ESP 减 4 并压入 1。所以现在堆栈看起来像这样
(gdb) x /2xw $esp
0xffffcdf8: 0x00000001 0x0804806d
所以恢复:你错过的是 CALL
也表现得像 PUSH
:它在将值压入堆栈之前将 ESP
递减 4。
其余问题:
decremented by 4 on my CPU
汇编程序通常默认为 32 位而不是 16 位,这就是为什么它递减 4 个字节而不是 2 个字节。您可以强制您的汇编程序改为使用 16 位指令。
In some code I'm studying, a call operand is immediately followed by a
push
事实上这是常见的情况,它被称为 function/routine 入口协议。
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.
我所做的逐步 运行 应该阐明了如何检查寄存器和 memory/stack 值。如果您对此有任何疑问,请告诉我。
call <address>
就像:push eip
jmp <address>
,所以在你的情况下,如果 esp
是 0xffffd840
领先于 call
,下一条指令的返回地址被压入0xffffd83c
(因为伪“push eip
”将首先递减esp
以创建新的堆栈顶部,然后它将存储[的当前值=19=] 那里(顺便说一句,eip
已经指向下一条指令,因为 call
的获取+解码指令阶段已经完成,所以它实际上是 [=22 需要的值=]).
您也可以在调试器中查看内存。而"stack"只是普通的内存。因此,如果 esp
等于 0xffffd840
,您可以打开 0xffffd824
处的内存视图,您将看到 32 字节的堆栈内存,其中 28 字节尚未使用最后 4 个字节是当前 "top of the stack".
我到处都使用 4 字节组,因为那是 CPU "word" 的原始大小(dword
在 x86 术语中,word
仅是 16 位) 在 32b 保护模式下。 IIRC 您仍然可以强制 CPU 执行 push ax
或使用 sub/add esp,immediate
甚至将其移动一个字节,但通常它会涉及性能损失,并且在 64b 模式下,几个调用约定甚至需要 16字节对齐,所以我建议在 32b 模式下坚持 +-4 esp
操作。
但是如果你的书是关于 8086 的,你可能想要使用 dosbox
来模拟旧的 DOS 16 位环境,以在开始时避免一些特定于平台的问题。虽然也许你应该为你的 OS 找一些 32/64 位的新书,因为 x86 上的 32b 保护模式更容易学习(只是图形输出不像 D[= 中那样简单47=] 时代,但是如果你将你的 asm 文件与 C++ "loader" 混合,例如将一些 window 表面初始化为 ARGB 内存数组,你可以将该指针向下传递给 asm 例程和玩具围绕像素,以同样简单的方式,DOS 中旧的 320x200 "mode 13h" 是如何工作的。甚至更容易(没有调色板,也没有 64k 段限制)。
长话短说,我正在研究 Singh 和 Triebel 的一本名为 "The 8088 and 8086 Microprocessors" 的书,以学习 old 那些特定 CPU 的程序集。现在,我正在练习的计算机是我最近组装的主计算机,所以寄存器更大。
就是说,这本书(我觉得非常有帮助)说调用标签操作数导致调用之后的指令地址被放置在堆栈上,然后 SP
递减 2 (ESP
,并在我的 CPU 上减去 4)。在我正在研究的某些代码中,调用操作数后紧跟 push
。当 CPU 遇到 push
时,书上说 SP
减 2(同样, ESP
在我的 CPU 上减 4)。
; ESP=0xffffd840 right now
call iprint
mov eax, 0Ah
iprint:
push eax ; say eax contains 1
现在,在通话前说 ESP=0xffffd840
。 EIP
的地址保存在栈中(CALL
操作数后面的指令的地址)。然后ESP
减4。此时,ESP=0xffffd83c
。然后遇到入栈操作数。按照书上的说法,堆栈指针先递减,然后寄存器的内容被压入堆栈。所以现在 ESP=0xffffd838
和 1 被压入堆栈。
If it helps:
Stack addr Contents
********** ********
0xffffd840 address of mov eax, 0Ah
0xffffd83c ?
0xffffd838 1
现在,我的问题是,是否跳过了 0xffffd83c
?根据书上的说法,ESP
在call
之后保存下一条指令后递减,然后在数据从push
放入堆栈之前,它再次递减。
我已经调试了一段时间类似的场景,密切关注寄存器的值,但我无法判断调试器是否遵循书中所说的(在执行操作之前递减) , 或之后).
这是不是因为某些情况下子程序在RET
之后给了一个参数,导致栈指针自增?如果在将数据放入堆栈指针之前堆栈指针确实递减了两次,这是我能看到的唯一原因。
如果我有错,能否请大神确认一下或解释一下?
谢谢
编辑:抱歉,我之前的回答对您的第一个问题是错误的。我的 x86 生锈了。最好的检查方法是调试您的程序。这是一个类似的(NASM 语法):
global _start
section .text
test:
push eax
pop eax
ret
_start:
mov eax, 1
call test
您可以在 Linux 上编译它,如下所示:
$ nasm -f elf32 -g test.s && ld -m elf_i386 -g test.o
现在让我们用GDB调试一下:
$ gdb -q a.out
Reading symbols from a.out...done.
反汇编 _start
函数以查看其地址
(gdb) disas _start
Dump of assembler code for function _start:
0x08048063 <+0>: mov [=13=]x1,%eax
0x08048068 <+5>: call 0x8048060 <test>
End of assembler dump.
在开头打个断点
(gdb) b *0x08048063
Breakpoint 1 at 0x8048063
还有运行呢!
(gdb) r
Starting program: /home/yasin/Downloads/a.out
Breakpoint 1, 0x08048063 in _start ()
检查 ESP 起始值
(gdb) i r esp
esp 0xffffce00 0xffffce00
并在 test
函数上设置断点。
(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
让我们继续
(gdb) c
Continuing.
Breakpoint 2, 0x08048060 in test ()
好的,在 test
开始时停止,在 push
之前,让我们检查 ESP 值及其内容
(gdb) i r esp
esp 0xffffcdfc 0xffffcdfc
(gdb) x /1xw $esp
0xffffcdfc: 0x0804806d
它已经减了 4 并且 call return 值被推到那里。让我们在 push
之后放置另一个断点,看看会发生什么。
(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
它已将 ESP 减 4 并压入 1。所以现在堆栈看起来像这样
(gdb) x /2xw $esp
0xffffcdf8: 0x00000001 0x0804806d
所以恢复:你错过的是 CALL
也表现得像 PUSH
:它在将值压入堆栈之前将 ESP
递减 4。
其余问题:
decremented by 4 on my CPU
汇编程序通常默认为 32 位而不是 16 位,这就是为什么它递减 4 个字节而不是 2 个字节。您可以强制您的汇编程序改为使用 16 位指令。
In some code I'm studying, a call operand is immediately followed by a push
事实上这是常见的情况,它被称为 function/routine 入口协议。
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.
我所做的逐步 运行 应该阐明了如何检查寄存器和 memory/stack 值。如果您对此有任何疑问,请告诉我。
call <address>
就像:push eip
jmp <address>
,所以在你的情况下,如果 esp
是 0xffffd840
领先于 call
,下一条指令的返回地址被压入0xffffd83c
(因为伪“push eip
”将首先递减esp
以创建新的堆栈顶部,然后它将存储[的当前值=19=] 那里(顺便说一句,eip
已经指向下一条指令,因为 call
的获取+解码指令阶段已经完成,所以它实际上是 [=22 需要的值=]).
您也可以在调试器中查看内存。而"stack"只是普通的内存。因此,如果 esp
等于 0xffffd840
,您可以打开 0xffffd824
处的内存视图,您将看到 32 字节的堆栈内存,其中 28 字节尚未使用最后 4 个字节是当前 "top of the stack".
我到处都使用 4 字节组,因为那是 CPU "word" 的原始大小(dword
在 x86 术语中,word
仅是 16 位) 在 32b 保护模式下。 IIRC 您仍然可以强制 CPU 执行 push ax
或使用 sub/add esp,immediate
甚至将其移动一个字节,但通常它会涉及性能损失,并且在 64b 模式下,几个调用约定甚至需要 16字节对齐,所以我建议在 32b 模式下坚持 +-4 esp
操作。
但是如果你的书是关于 8086 的,你可能想要使用 dosbox
来模拟旧的 DOS 16 位环境,以在开始时避免一些特定于平台的问题。虽然也许你应该为你的 OS 找一些 32/64 位的新书,因为 x86 上的 32b 保护模式更容易学习(只是图形输出不像 D[= 中那样简单47=] 时代,但是如果你将你的 asm 文件与 C++ "loader" 混合,例如将一些 window 表面初始化为 ARGB 内存数组,你可以将该指针向下传递给 asm 例程和玩具围绕像素,以同样简单的方式,DOS 中旧的 320x200 "mode 13h" 是如何工作的。甚至更容易(没有调色板,也没有 64k 段限制)。