x86 汇编入门(05):函数调用与递归阶乘
call 和 ret 是汇编里最重要的「接力棒」。没有它们,代码只能从上到下一条道走到黑。这一篇我们拆开函数调用的完整机制,并用递归计算 5 的阶乘——在汇编里亲眼看到栈是怎么一层层长高的。
这是「x86 汇编入门」系列的第 5 篇。上一篇用跳转实现了循环。这一篇通过
05_function.asm,理解 call / ret、栈帧和 x86_64 调用约定。
一、call 和 ret 做了什么?
1 | call factorial ; ① 把「下一条指令地址」压栈 ② 跳到 factorial |
可以把它想成:call 留下回城坐标,ret 按坐标回去。栈就是存放这些坐标的地方。
二、栈帧:函数自己的「工作台」
每次进入函数,标准开场是:
1 | factorial: |
这叫栈帧(Stack Frame)。rbp 指向当前帧底部,局部数据和保存的寄存器都相对它定位。
三、x86_64 调用约定(精简版)
Linux x86_64 下,整数/指针参数按顺序放:
| 参数位置 | 寄存器 |
|---|---|
| 第 1 个 | rdi |
| 第 2 个 | rsi |
| 第 3 个 | rdx |
| 第 4 个 | rcx |
| 第 5 个 | r8 |
| 第 6 个 | r9 |
返回值放在 rax。
05_function.asm 调用 factorial(5):
1 | mov rdi, 5 |
四、递归阶乘走读
数学定义:
1 | factorial(0) = 1 |
汇编实现:
1 | factorial: |
关键点:
- 用
rbx保存n,因为递归call会改掉rdi - 基础情况必须能停下来,否则无限递归直到栈溢出
- 每次
call多一层栈帧,5!最多嵌 6 层,完全可控
五、caller-saved vs callee-saved
| 类型 | 寄存器 | 谁负责保存 |
|---|---|---|
| caller-saved | rax, rcx, rdx, rsi, rdi, r8–r11 |
调用者 |
| callee-saved | rbx, rbp, r12–r15 |
被调用函数 |
调用函数前,如果 caller-saved 寄存器里还有重要数据,调用者要自己 push;被调用函数若使用 callee-saved 寄存器,进入时保存、返回前恢复。
六、运行输出
1 | make |
1 | 5! = 120 |
用 gdb 单步跟进去,能看到栈指针 rsp 随每次 call 下降,随每次 ret 回升——比任何图示都直观。
七、小结
本篇要点:
call压入返回地址,ret弹出并跳转- 栈帧用
push rbp/mov rbp, rsp建立 - 第一个整型参数用
rdi,返回值用rax - 递归 = 同样的函数 + 更小的参数 + 可终止的基础情况
x86 汇编入门系列第 5 篇完。下一篇是系列收官——用文件系统调用读写磁盘上的文件,把前面所有技能串起来。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 WALL-E`s Blog!







