了解递归 IA32 汇编调用
Understanding a recursive IA32 assembly call
我正在尝试做一些练习以更加熟悉 IA32 汇编,并且在将这个递归的汇编代码片段转换为可理解的 C 代码方面遇到了一些困难。他们给了我们一个提示,代码中的所有函数只给出一个参数,但是我对IA32堆栈的理解还是有点差。
.globl bar
.type bar, @function
bar:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl , %eax
popl %ebp
ret
.globl foo
.type foo, @function
foo:
pushl %ebp
movl %esp, %ebp
subl , %esp
movl %ebx, -8(%ebp)
movl %esi, -4(%ebp)
movl 8(%ebp), %ebx
movl , %eax
cmpl , %ebx
jle .L5
movl %ebx, (%esp)
call bar
movl %eax, %esi
subl , %ebx
movl %ebx, (%esp)
call foo
imull %esi, %eax
.L5:
movl -8(%ebp), %ebx
movl -4(%ebp), %esi
movl %ebp, %esp
popl %ebp
ret
bar 函数看起来很简单 - 它将参数加 10 和 returns。 "foo" 函数是我迷路的地方。我理解 subl $24,esp 保留 space 的 24 个字节,但从那里我开始迷失自己。我们似乎每次递归调用 "foo" 并递减参数(我们称它为 x)直到它达到 1,然后将 bar(x) 的结果乘以 bar(x-1) 的结果,但其他所有内容那是怎么回事我似乎不明白。
具体来说,这两个命令的作用是什么?他们紧跟着 subl $24,%esp,我认为这是理解整个事情的关键。
movl %ebx,-8(%ebp)
movl %esi, -4(%ebp)
在 x86 中,堆栈向下增长(朝向更小的地址)。在 IA32 中,寄存器是 32 位或 4 个字节,指令指针 (EIP) 也是如此。堆栈指针寄存器 (ESP) 指向 "top of stack",即压入堆栈的最后一项 - 因为堆栈向较低地址增长,所以这是堆栈上具有有效数据的最低地址。 push 指令将 ESP 减 4,然后在该地址存储 32 位(4 字节)。调用指令有一个隐含的 4 字节推送(return 地址,即调用后的地址)。
那么函数入口处的栈顶是什么?那就是 ESP 指向的 4 个字节,它包含 return 地址。 ESP 的偏移 +4 处是什么?那将是压入堆栈的倒数第二个东西,如果函数有一个参数,它将是那个参数:
| |
+----------------+
| parameter 1 | ESP + 4
+----------------+
| return address | <===== ESP (top of stack)
+----------------+
为了展开堆栈(正如调试器想要做的那样),编译器构建了一个 "EBP chain" - EBP 指向堆栈上存储调用者的 EBP 的位置...那个 EBP 在turn 指向调用者的调用者保存的 EBP,依此类推堆栈。这就是为什么在函数的开头您会看到:
pushl %ebp # save caller's EBP on the stack
movl %esp, %ebp # EBP now points to caller's EBP on the stack
有些函数在自动存储中没有变量...在这种情况下,函数序言已完成...但是,假设您有 6 个变量,每 4 个字节,那么您需要 6x4 = 24 个字节的自动存储在堆栈上...这是通过从 ESP 中减去 24 来实现的,然后您有 6 个局部变量的空间 (local-var-0...local-var-5):
subl , %esp
堆栈现在看起来像这样:
| |
+----------------+
| parameter 1 | 8(ebp)
+----------------+
| return address | 4(ebp)
+----------------+
| caller's EBP | 0(ebp)
+----------------+
| local-var-0 | -4(ebp)
+----------------+
| local-var-1 | -8(ebp)
+----------------+
| local-var-2 | -12(ebp)
+----------------+
| local-var-3 | -16(ebp)
+----------------+
| local-var-4 | -20(ebp)
+----------------+
| local-var-5 | -24(ebp)
+----------------+
如你所见,-8(ebp)是local-var-1的地址,-4(ebp)是local-var-0的地址,所以这段代码将寄存器保存在栈中
movl %ebx, -8(%ebp) # save %ebx in local-var-1
movl %esi, -4(%ebp) # save %esi in local-var-0
并且它们在 returning:
之前恢复
.L5:
movl -8(%ebp), %ebx # restore %ebx from local-var-1
movl -4(%ebp), %esi # restore %esi from local-var-0
根据 IA32 调用约定,通用寄存器 %ebx 和 %esi(以及 %edi 和 %ebp)由 "callee" 保存。请参阅 Table 4, Chapter 6 Register Usage of Agner Fog's Calling Conventions 文档。
我正在尝试做一些练习以更加熟悉 IA32 汇编,并且在将这个递归的汇编代码片段转换为可理解的 C 代码方面遇到了一些困难。他们给了我们一个提示,代码中的所有函数只给出一个参数,但是我对IA32堆栈的理解还是有点差。
.globl bar
.type bar, @function
bar:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl , %eax
popl %ebp
ret
.globl foo
.type foo, @function
foo:
pushl %ebp
movl %esp, %ebp
subl , %esp
movl %ebx, -8(%ebp)
movl %esi, -4(%ebp)
movl 8(%ebp), %ebx
movl , %eax
cmpl , %ebx
jle .L5
movl %ebx, (%esp)
call bar
movl %eax, %esi
subl , %ebx
movl %ebx, (%esp)
call foo
imull %esi, %eax
.L5:
movl -8(%ebp), %ebx
movl -4(%ebp), %esi
movl %ebp, %esp
popl %ebp
ret
bar 函数看起来很简单 - 它将参数加 10 和 returns。 "foo" 函数是我迷路的地方。我理解 subl $24,esp 保留 space 的 24 个字节,但从那里我开始迷失自己。我们似乎每次递归调用 "foo" 并递减参数(我们称它为 x)直到它达到 1,然后将 bar(x) 的结果乘以 bar(x-1) 的结果,但其他所有内容那是怎么回事我似乎不明白。
具体来说,这两个命令的作用是什么?他们紧跟着 subl $24,%esp,我认为这是理解整个事情的关键。 movl %ebx,-8(%ebp) movl %esi, -4(%ebp)
在 x86 中,堆栈向下增长(朝向更小的地址)。在 IA32 中,寄存器是 32 位或 4 个字节,指令指针 (EIP) 也是如此。堆栈指针寄存器 (ESP) 指向 "top of stack",即压入堆栈的最后一项 - 因为堆栈向较低地址增长,所以这是堆栈上具有有效数据的最低地址。 push 指令将 ESP 减 4,然后在该地址存储 32 位(4 字节)。调用指令有一个隐含的 4 字节推送(return 地址,即调用后的地址)。
那么函数入口处的栈顶是什么?那就是 ESP 指向的 4 个字节,它包含 return 地址。 ESP 的偏移 +4 处是什么?那将是压入堆栈的倒数第二个东西,如果函数有一个参数,它将是那个参数:
| |
+----------------+
| parameter 1 | ESP + 4
+----------------+
| return address | <===== ESP (top of stack)
+----------------+
为了展开堆栈(正如调试器想要做的那样),编译器构建了一个 "EBP chain" - EBP 指向堆栈上存储调用者的 EBP 的位置...那个 EBP 在turn 指向调用者的调用者保存的 EBP,依此类推堆栈。这就是为什么在函数的开头您会看到:
pushl %ebp # save caller's EBP on the stack
movl %esp, %ebp # EBP now points to caller's EBP on the stack
有些函数在自动存储中没有变量...在这种情况下,函数序言已完成...但是,假设您有 6 个变量,每 4 个字节,那么您需要 6x4 = 24 个字节的自动存储在堆栈上...这是通过从 ESP 中减去 24 来实现的,然后您有 6 个局部变量的空间 (local-var-0...local-var-5):
subl , %esp
堆栈现在看起来像这样:
| |
+----------------+
| parameter 1 | 8(ebp)
+----------------+
| return address | 4(ebp)
+----------------+
| caller's EBP | 0(ebp)
+----------------+
| local-var-0 | -4(ebp)
+----------------+
| local-var-1 | -8(ebp)
+----------------+
| local-var-2 | -12(ebp)
+----------------+
| local-var-3 | -16(ebp)
+----------------+
| local-var-4 | -20(ebp)
+----------------+
| local-var-5 | -24(ebp)
+----------------+
如你所见,-8(ebp)是local-var-1的地址,-4(ebp)是local-var-0的地址,所以这段代码将寄存器保存在栈中
movl %ebx, -8(%ebp) # save %ebx in local-var-1
movl %esi, -4(%ebp) # save %esi in local-var-0
并且它们在 returning:
之前恢复.L5:
movl -8(%ebp), %ebx # restore %ebx from local-var-1
movl -4(%ebp), %esi # restore %esi from local-var-0
根据 IA32 调用约定,通用寄存器 %ebx 和 %esi(以及 %edi 和 %ebp)由 "callee" 保存。请参阅 Table 4, Chapter 6 Register Usage of Agner Fog's Calling Conventions 文档。