汇编语言中的函数调用

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))
posted @ 2019-07-20 11:20  叕叒双又  阅读(1910)  评论(0编辑  收藏  举报