集会:Y86 堆栈和调用、pushl/popl 和 ret 指令
题
除非我抄错了,上面的代码是一个学生在老师的帮助/纠正下在课堂上的黑板上写的:
int array[100], sum, i;
void ini() {
for(i = 0; i < 100; i++)
array[i] = i;
}
int main() {
ini();
sum = 0;
for(i = 0; i < 100; i++)
sum += array[i];
}
.pos 0
irmovl Stack, %esp
rrmovl Stack, %ebp
jmp main
array:
.pos 430
sum: .long 0
i: .long 0
main:
call ini //
irmovl $0, %eax // %eax = 0
irmovl sum, %esi // %esi = 0xsum
rmmovl %eax, 0(%esi) // 0(%esi) = %eax <=> 0(0xsum) = 0 [sum = 0]
rmmovl %eax, 4(%esi) // 4(%esi) = %eax <=> 4(0xsum) = 0 [i = 0]
compare:
irmovl $100, %ebx // %ebx = 100
subl %eax, %ebx // %ebx = %ebx - %eax <=> %ebx = 100 - i
jle finish // Jumps to "finish" if SF=1 pr ZF=0
mrmovl 0(%esi), %edx // %edx = 0(%esi) <=> %edx = 0(0xsum) = sum
addl %eax, %edx // %edx = %edx + %eax <=> %edx = sum + i => sum
rmmovl %edx, 0($esi) // 0(%esi) = %edx <=> 0(0xsum) = sum
irmovl $1, %ecx // %ecx = 1
addl %ecx, %eax // %eax = %eax + %ecx <=> %eax = i + 1 => i
rmmovl %eax, 4(%esi) // 4($esi) = %eax <=> 4(0xsum) = i
jmp compare // Jumps unconditionally to "compare"
ini:
pushl %ebp //
rrmovl %esp, %ebp //
pushl %ebx //
pushl %eax //
irmovl $0, %eax // %eax = 0
rmmovl %eax, -8(%ebp) //
ini_compare:
irmovl $100, %ecx // %ecx = 100
subl %eax, %ecx // %ecx = %ecx - %eax <=> %ecx = 100 - i
jle ini_finish // Jumps to "ini_finish" if SF=1 pr ZF=0
rrmovl %eax, %ebx // %ebx = %eax <=> %ebx = i
addl %eax, $ebx // %ebx = %ebx + %eax <=> %ebx = i + i = 2i
addl %ebx, %ebx // %ebx = %ebx + %ebx <=> %ecx = 2i + 2i = 4i
rmmovl %eax, array(%ebx) // array(%ebx) = %eax <=> array(0x4i) = i
irmovl %1, %ecx // %ecx = 1
addl %ecx, %eax // %eax = %eax + %ecx <=> %eax = i + 1 => i
rmmovl %eax, -8(%ebp) //
jmp ini_compare // Jumps unconditionally to "ini_compare"
ini_finish:
irmovl $4, %ebx //
addl %ebx, %esp //
popl %ebx //
popl %ebp //
ret //
.pos 600
Stack .long 0
正如你所看到的,所有指令中有一堆注释,我(我认为)得到了其中的大部分,令我困惑的是 call、pushl/popl 和 ret 指令。我不太明白它们,也不明白堆栈发生了什么以及所有记录都指向哪里。基本上,带有注释 (//) 的行上没有写任何内容。
了解这一切是如何运作的非常重要,希望你们中的一些人能够阐明所有这些混乱的情况。
关于我的评论的一些注释:
- 0x总和: 这并不意味着地址是“总和”,这是不可能的。它只是一种在不使用确切内存地址的情况下理解我正在谈论的内容的方法。
- [总和= 0]: 这意味着在我们的 C 代码中,变量 sum 此时将被设置为 0。
- 我 + 1 => 我: 这意味着我们将“i”的值增加一,并且在下一行中“i”实际上将代表该增加的值。
解决方案
让我们看一些代码:
main:
call ini
这会将指令指针的值压入堆栈(以便稍后可以返回到代码中的此位置),并跳转到 ini 标签的地址。“ret”指令使用存储在堆栈上的值从子例程返回。
以下是子程序的初始化顺序。它将一些寄存器的值保存在堆栈上,并通过将堆栈指针(esp)复制到基址指针寄存器(ebp)来建立堆栈帧。如果子程序有局部变量,则堆栈指针递减,为堆栈上的变量腾出空间,基指针用于访问堆栈帧中的局部变量。在示例中,唯一的局部变量是(未使用的)返回值。
推送指令将堆栈指针 (esp) 递减为要推送的数据大小,然后将值存储在该地址。pop 指令执行相反的操作,首先获取值,然后递增堆栈指针。(请注意,堆栈向下增长,因此当堆栈增长时,堆栈指针地址会变低。)
ini:
pushl %ebp // save ebp on the stack
rrmovl %esp, %ebp // ebp = esp (create stack frame)
pushl %ebx // save ebx on the stack
pushl %eax // push eax on the stack (only to decrement stack pointer)
irmovl $0, %eax // eax = 0
rmmovl %eax, -8(%ebp) // store eax at ebp-8 (clear return value)
代码遵循标准模式,因此当没有局部变量并且有未使用的返回值时看起来有点尴尬。如果存在局部变量,将使用减法来递减堆栈指针,而不是压入 eax。
以下是子程序的退出顺序。它将堆栈恢复到创建堆栈帧之前的位置,然后返回到调用子例程的代码。
ini_finish:
irmovl $4, %ebx // ebx = 4
addl %ebx, %esp // esp += ebx (remove stack frame)
popl %ebx // restore ebx from stack
popl %ebp // restore ebp from stack
ret // get return address from stack and jump there
针对您的评论:
ebx 寄存器被压入和弹出以保留其值。编译器显然总是把这段代码放在那里,可能是因为寄存器非常常用,只是不在这段代码中。同样,即使实际上并不需要,堆栈帧也总是通过将 esp 复制到 ebp 来创建。
压入 eax 的指令仅用于递减堆栈指针。对于小量减量,它是这样做的,因为它比减去堆栈指针更短、更快。它保留的空间是用于返回值的,即使不使用返回值,编译器显然也总是这样做。
在您的图中,esp 寄存器始终指向内存中过高的四个字节。请记住,堆栈指针在压入一个值后会递减,因此它将指向压入的值,而不是下一个值。(内存地址也很偏离,它类似于 0x600 而不是 0x20,因为这是声明 Stack 标签的地方。)