要回答这些编号问题:
1) subl $24,%esp
means esp = esp - 24
GNU AS 使用 AT&T 语法,与 Intel 语法相反。 AT&T 的目的地位于右侧,Intel 的目的地位于左侧。 AT&T 也明确指出了论据的大小。英特尔试图推断它或强迫您明确说明。
堆栈在内存中向下增长,内存在和之后esp是堆栈内容,低于esp的地址是未使用的堆栈空间。 esp 指向最后压入堆栈的内容。
2)x86 指令编码主要允许以下内容:
movl rm,r ' move value from register or memory to a register
movl r,rm ' move a value from a register to a register or memory
movl imm,rm ' Move immediate value.
没有内存到内存的指令格式。 (严格来说,您可以使用以下命令进行内存到内存的操作movs
or by push mem
, pop mem
,但都不在同一条指令上使用两个内存操作数)
“立即”意味着该值被直接编码到指令中。例如,要将 15 存储在 ebx 中的地址处:
movl $15,(%ebx)
15 是“立即”值。
括号使其使用寄存器作为指向内存的指针。
3) movl 8(%ebp),%eax
means,
- 取ebp的值
- 添加 8(但不修改 ebp),
- 将其用作地址(括号),
- 从该地址读取 32 位值,
- 并将值存储在eax中
esp是堆栈指针。
在 32 位模式下,堆栈上的每次压入和弹出都是 4 个字节宽。通常,大多数变量无论如何都会占用 4 个字节。所以你可以说 8(%ebp) 意味着,从堆栈顶部开始,将值 2 (4 x 2 = 8) int 放入堆栈中。
通常,32 位代码使用 ebp 来指向函数中局部变量的开头。在 16 位 x86 代码中,无法将堆栈指针用作指针(很难相信,对吧?)。所以人们所做的就是复制sp
to bp
并使用 bp 作为本地帧指针。当 32 位模式出现(80386)时,这变得完全没有必要了,它确实有办法直接使用堆栈指针。不幸的是,ebp 使调试变得更容易,因此我们最终继续在 32 位代码中使用 ebp(如果使用 ebp,则很容易进行堆栈转储)。
值得庆幸的是,amd64 给了我们一个新的 ABI,它不使用 ebp 作为帧指针,64 位代码通常使用 esp 来访问局部变量,ebp 可用于保存变量。
4)上面解释过
5) leave
是一条旧指令,它只是做movl %ebp,%esp
and popl %ebp
并节省一些代码字节。它实际上所做的是撤消对堆栈的更改并恢复调用者的 ebp。被调用的函数必须保留ebp
在 x86 ABI 中。
在进入该函数时,编译器执行了 subl $24,%esp 来为局部变量以及有时没有足够寄存器来保存的临时存储腾出空间。
“想象”栈帧的最佳方式你的想法是将其视为位于堆栈上的结构。虚数结构的第一个成员是最近“推送”的值。因此,当您推送到堆栈时,想象一下在结构的开头插入一个新成员,而其他成员都没有移动。当您从堆栈中“弹出”时,您将获得虚构结构的第一个成员的值,并且该结构的该(第一)行从存在中消失。
堆栈帧操作主要只是移动堆栈指针,以便在我们称为堆栈帧的虚构结构中腾出更多或更少的空间。从堆栈指针中减去只是一步将多个虚构成员放在结构的开头。添加到堆栈指针会使前这么多成员消失。
您发布的代码的结尾并不典型。那jmp
通常是一个ret
。编译器对此很聪明,并进行了“尾部调用优化”,这意味着它只是清理对堆栈所做的操作并跳转到f
. When f(2)
返回,它实际上会直接返回给调用者(而不是返回到您发布的代码)