3.14 控制转移
控制转移指令 call 和 ret 实现函数调用和返回的流程跳转,同时维护栈帧的正确链接。
call 指令
call 指令执行两个操作:
- 压入返回地址:将下一条指令的地址(
call指令的下一条指令)压入栈 - 跳转:跳转到被调函数的起始地址
直接调用
call func # 调用函数 func
# 返回后从这里继续执行间接调用
call *%rax # 以 %rax 中的值作为函数地址调用
call *(%rdi) # 从内存中读取函数地址并调用call 的等价操作
# call func 等价于:
pushq $return_addr # 压入返回地址
jmp func # 跳转到函数
return_addr: # call 之后的指令地址
# ...ret 指令
ret 指令执行与 call 相反的操作:
- 弹出返回地址:从栈顶弹出地址
- 跳转:跳转到该地址继续执行
ret 的等价操作
# ret 等价于:
popq %rip # 弹出栈顶值到指令指针(实际无法直接访问 %rip)
%rip(指令指针寄存器)保存当前正在执行指令的地址,程序员不能直接修改。ret通过将弹出值加载到%rip实现跳转。
PC 在 call/ret 中的变化
调用过程
执行 call func 之前:
%rip = 0x400100 (call 指令地址)
%rsp = 0x2000
执行 call func:
[1] 计算返回地址:0x400105 (下一条指令地址)
[2] %rsp = 0x1FF8 (压栈前减 8)
[3] 内存[0x1FF8] = 0x400105 (保存返回地址)
[4] %rip = 0x400200 (func 的地址,跳转)
执行后:
%rip = 0x400200 (func 的第一条指令)
%rsp = 0x1FF8
栈:[0x1FF8] = 0x400105 (返回地址)
返回过程
执行 ret 之前(在 func 内部):
%rip = 0x400250 (func 的 ret 指令)
%rsp = 0x1FF8
栈:[0x1FF8] = 0x400105 (返回地址)
执行 ret:
[1] %rip = 内存[0x1FF8] = 0x400105 (弹出返回地址)
[2] %rsp = 0x2000 (弹出后加 8)
执行后:
%rip = 0x400105 (返回到 call 的下一条指令)
%rsp = 0x2000
完整的调用-返回周期
# 调用者代码
movq $10, %rdi # 设置参数
call my_function # 1. call: 压入返回地址并跳转
.Lreturn: # ← 返回地址指向这里
movq %rax, %rbx # 5. 使用返回值
# 被调函数
my_function:
pushq %rbp # 2. 函数序言
movq %rsp, %rbp
# ... 函数体 ...
movq %rbp, %rsp
popq %rbp # 3. 函数尾声
ret # 4. ret: 弹出返回地址并跳转时间轴:
调用者环境 → call → 被调函数环境 → ret → 调用者环境
栈帧的嵌套
main:
call A # main → A
A:
# ... A 的函数体 ...
call B # A → B
# ... 返回到 A ...
ret # A → main
B:
# ... B 的函数体 ...
ret # B → A栈的变化:
main 调用 A 后:
栈顶 → [返回地址到 main] ← %rsp
A 调用 B 后:
栈顶 → [返回地址到 A]
[返回地址到 main]
...
B 执行 ret 后:
栈顶 → [返回地址到 main] ← %rsp
...
A 执行 ret 后:
栈顶 → 最初的状态
call/ret 与条件码
call 和 ret 不修改 。这意味着在函数调用前后,条件码的状态保持不变(除非被调函数内部的指令修改了它们)。
相对于其他跳转指令
| 指令 | 影响栈 | 用途 | 是否支持间接 |
|---|---|---|---|
jmp | 否 | 无条件跳转 | 是 (jmp *%rax) |
call | 是(压入返回地址) | 函数调用 | 是 (call *%rax) |
ret | 是(弹出返回地址) | 函数返回 | 不适用 |
je/jne/... | 否 | 条件跳转 | 否 |
相关笔记
-
- call/ret 对栈的影响
-
- 完整的栈帧管理
-
- 与其他跳转指令的对比
链接到
- 上一个知识点:3.13 过程与栈帧
- 下一个知识点:3.15 参数传递