7.4 堆栈、过程与参数传递
第 7.3 节说明,存储器通过共同遵守的布局契约获得结构。过程调用对栈采用完全相同的做法:处理器自动压入返回地址,调用者与被调用者共同约定参数、保存的寄存器和局部存储放在哪里。
CALL 与 RET 建立返回路径
过程(procedure)是具有明确入口与返回规则的可复用指令序列。近 CALL target 完成两个相互关联的动作:
1. 把 CALL 后一条指令的偏移量作为返回 IP 压栈。
2. 把目标偏移量装入 IP。
近 RET 从栈顶弹出这个偏移量并装入 IP。压入与弹出必须对应同一活动栈布局,否则 RET 会把一个从未打算作为指令地址的字装入 IP。
远调用因为跨越代码段,还会保存 CS。本节使用近调用,所以返回地址只占一个字。相同的栈记账原则也适用于返回地址更大的远调用。
BP 在 SP 移动时提供稳定基准
栈帧(stack frame)是一个过程调用当前占用的栈区域,其中保存该次调用的状态、返回信息、参数和局部变量。SP 指向当前栈顶,每次压栈或分配局部空间都会变化。BP 可以从 SP 复制后保持不变,使每个字段都有固定的 BP 相对偏移。
假设调用者压入一个字参数,再执行近调用:
PUSH 0005H
CALL double_word
ADD SP,2 ; 调用者移除自己的参数过程建立并拆除栈帧:
double_word PROC NEAR
PUSH BP
MOV BP,SP
SUB SP,2 ; [BP-2] 的一个局部字
MOV AX,[BP+4] ; 参数
SHL AX,1
MOV [BP-2],AX
MOV SP,BP
POP BP
RET
double_word ENDP若 SP 初值为 0100H,参数压在 00FEH,CALL 把返回 IP 放在 00FCH,PUSH BP 把旧 BP 放在 00FAH。执行 MOV BP,SP 与 SUB SP,2 后,BP 仍为 00FAH,SP 变为 00F8H。因此即使 SP 变化,参数仍然是 [BP+4]。
MOV SP,BP 一次释放所有局部变量;POP BP 恢复调用者的帧基址;RET 消耗返回 IP;调用者的 ADD SP,2 再移除参数。最终 SP 必须等于调用前的值。
调用约定是双方共同遵守的契约
调用约定(calling convention)规定分别编写的调用者与被调用者怎样合作。即使是小型课程程序,也必须回答这些问题:
- 参数放在寄存器、栈、存储器,还是混合使用?
- 返回值放在哪里?
- 被调用者可以覆盖哪些寄存器,又必须恢复哪些寄存器?
- 谁负责移除栈参数?
- 多字参数按什么顺序放置?
8086 没有唯一通用的调用约定。项目可以选择一种,但每个调用点与每个过程必须采用同一选择。
对于由调用者清理参数、并要求被调用者保存 BX 的栈约定,可以写成:
twice PROC NEAR
PUSH BP
MOV BP,SP
PUSH BX
MOV BX,[BP+4]
MOV AX,BX
SHL AX,1
POP BX
POP BP
RET
twice ENDP随后调用者执行 ADD SP,2。如果被调用者改用 RET 2,其中的立即数 2 表示:弹出返回地址后,再丢弃一个参数字。这属于被调用者清理。同时使用 RET 2 与调用者的 ADD SP,2 会把同一参数移除两次,使 SP 最终比原值高 2。
调用者保存寄存器(caller-saved register)允许被调用者覆盖,因此调用者需要时自行保存。被调用者保存寄存器(callee-saved register)在返回时必须保持原值,因此过程负责保存与恢复。所有权由契约决定,而不是由寄存器名称决定。
嵌套调用会加强检验。如果 twice 又调用辅助过程,当前栈帧内部还会压入新的返回地址。保存的寄存器与参数必须仍然可达,每条退出路径都必须恢复相同的 SP 和保存值。只有 AX 结果正确,并不能证明调用安全。
过程以明确的栈和寄存器成本提供运行时复用。第 7.5 节将把过程与源码级宏展开比较,再把程序组织扩展到分别汇编的多个模块。