在 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=0xffffd840EIP的地址保存在栈中(CALL操作数后面的指令的地址)。然后ESP减4。此时,ESP=0xffffd83c。然后遇到入栈操作数。按照书上的说法,堆栈指针先递减,然后寄存器的内容被压入堆栈。所以现在 ESP=0xffffd838 和 1 被压入堆栈。

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

现在,我的问题是,是否跳过了 0xffffd83c?根据书上的说法,ESPcall之后保存下一条指令后递减,然后在数据从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>,所以在你的情况下,如果 esp0xffffd840 领先于 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 段限制)。