汇编语言中的函数调用

时间:2022-01-15 00:55:13

C语言从原则上来说,只能在函数内执行代码。
所以任何 text 段都对应有自己的帧栈。
本文主要谈一下 call leave ret 三条与函数调用紧密相关的指令。

call 指令

call 的不同形式

call Label 所谓直接跳转
call *operand 所谓间接跳转

080483f7 <caller>: 804840c: e8 dc ff ff ff call 80483ed <callee>

上边代码段中 caller 中 call 80483ed <callee> 就是直接跳转

call 之前的准备

080483f7 <caller>: 80483fa: 83 ec 08 sub $0x8,%esp 80483fd: c7 44 24 04 1c a0 04 movl $0x804a01c,0x4(%esp) 8048404: 08 8048405: c7 04 24 01 00 00 00 movl $0x1,(%esp)

gcc ABI约定被调函数的参数保存在调用者的栈帧(frame)上,所以 caller 需要将 callee 的参数放在自己的栈帧上。这个过程分两步完成。

  • 开栈。
    将栈指针向下(栈由高位向下扩展)移动 8 bytes。这是因为两个参数一个是指针类型,一个是整数类型,均需要 4 bytes 来存储。事实上由于对齐的要求,即使参数类型小于 4 bytes 编译器还是会为其分配 4 bytes 的栈空间,
  • 反向保存参数。
    gcc ABI规定,反向保存参数,故栈顶保存最后一个参数。如果参数类型大于 4 bytes,IA32 需要用两条 movl 指令来传递参数。
    值得注意的是,ABI只规定了参数在栈上存储的空间顺序,并没有规定参数压入栈中的时间顺序

call 干了什么

存储返回地址。
call 指令将 (%eip) 对应指令之后的那条指令的起始地址放在栈上,也就是把 %eip + n 放在 (%esp),其中 n 为 (%eip) 中指令的长度。然后跳转到 call 的操作数所指的地址。

call之后发生了什么

080483ed <callee>: 80483ed: 55 push %ebp // sub $0x4,%esp // mov %ebp,(%esp) 80483ee: 89 e5 mov %esp,%ebp 80483f0: 83 ec 2c sub $0x2c,%esp 8048405: c7 45 e8 01 00 00 00 movl $0x1,-0x18(%ebp) 804840c: c7 45 ec 02 00 00 00 movl $0x2,-0x14(%ebp) 8048413: c7 45 f0 03 00 00 00 movl $0x3,-0x10(%ebp) 804841a: c7 45 f4 04 00 00 00 movl $0x4,-0xc(%ebp) 8048421: c7 45 f8 05 00 00 00 movl $0x5,-0x8(%ebp) 8048428: c7 45 fc 06 00 00 00 movl $0x6,-0x4(%ebp)
  • 切换栈帧。
    被调函数首先将旧的栈底指针 %ebp 压到自己的栈帧上,然后以其地址(而非内容)作为自己的栈底指针的内容,此时新的栈帧已经形成了,由于 %esp == %ebp,故新的栈帧暂时没有使用栈内存。
  • 开栈。
    当局部变量数量太大时,编译器会选择将局部变量放在栈帧上。gcc的ABI约定,函数栈帧的大小必须 16 bytes 对齐,所以sub指令所减去的16进制数以c结尾(栈帧上已经有上一帧 %ebp ) 。
  • 初始化局部变量。
    这里对局部变量的初始化是以栈底指针为基准的,此处值得注意的是 (%ebp) 中存储的是上一帧的 %ebp

leave 指令

8048411: c9 leave

leave 所做的工作是还原上一帧的栈底指针与栈顶指针,等效于

mov  %ebp,%esp // 把栈顶指针置为本帧的栈底(同时也是存储上一帧栈底指针内容的地址),
popl %ebp      // 还原上一帧的栈底指针,此时 %esp 指向返回地址

ret 指令

8048412: c3 ret

ret 所做的工作是弹出栈顶的返回地址,并跳转到此地址。此时 %esp 指向调用函数所存储的被调函数的最后一个参数。

杂记

一个完整的栈帧上会有什么?
从底到顶依次是:

1. 上一帧的 `%ebp` 1. ABI 约定被调用者保存(如果有)的调用者的三个寄存器的内容 `%ebx` `%esi` `%edi` 2. 局部变量 2. 对齐空白 3. ABI 约定调用者保存(如果有)的自己的三个寄存器的内容 `%eax` `%edx` `%ecx` 3. 所调用的函数的参数 3. 返回地址(本帧的 %esp所指,下一帧的 0x4(%ebp))