共计 1746 个字符,预计需要花费 5 分钟才能阅读完成。
栈帧在函数调用中的核心作用
在 8086 架构中,栈帧是实现函数调用的关键数据结构。每个函数调用都会在栈上创建一个独立的栈帧,用于存储局部变量、函数参数和返回地址。栈帧通过两个指针寄存器协同工作:

- 栈指针(SP):始终指向栈顶,随数据压栈 / 弹栈动态变化
- 基址指针(BP):固定指向当前栈帧的基准位置,用于访问参数和局部变量
相比 RISC 架构的寄存器调用约定,CISC 架构的 8086 更依赖栈传递参数,这是由其有限的通用寄存器数量 (仅 AX/BX/CX/DX/SI/DI/BP/SP) 决定的。这种设计虽然增加了内存访问开销,但提供了更灵活的参数传递方式。
CALL/RET 指令与栈帧生命周期
调用阶段(CALL 指令)
- 将下一条指令地址 (返回地址) 压入栈
- 跳转到目标函数地址
此时栈状态变化示例:
; 调用前 SP=0xFFFE
CALL myFunction ; SP=0xFFFC, [SS:SP]= 返回地址
栈帧建立(函数入口)
典型序言 (prologue) 代码:
myFunction PROC
PUSH BP ; 保存调用者 BP (SP-=2)
MOV BP, SP ; 建立新栈帧
SUB SP, 4 ; 分配局部变量空间(SP-=4)
此时栈布局为:
+-----------------+
| 局部变量 2 | [BP-4]
+-----------------+
| 局部变量 1 | [BP-2]
+-----------------+
| 旧 BP 值 | [BP]
+-----------------+
| 返回地址 | [BP+2]
+-----------------+
| 参数 1 | [BP+4]
+-----------------+
| 参数 2 | [BP+6]
+-----------------+
返回阶段(RET 指令)
- 弹出返回地址到 IP
- 调整 SP(如果是带立即数 RET)
调用约定实践对比
cdecl 调用约定
- 调用方负责平衡栈
- 参数从右向左压栈
- 示例代码:
; 调用方
PUSH 3 ; 最后一个参数
PUSH 2 ; 中间参数
PUSH 1 ; 第一个参数
CALL addNumbers
ADD SP, 6 ; 调用方清栈
; 被调用方
addNumbers PROC
PUSH BP
MOV BP, SP
MOV AX, [BP+4] ; 取第一个参数
ADD AX, [BP+6] ; 加第二个参数
ADD AX, [BP+8] ; 加第三个参数
POP BP
RET ; 不带立即数
addNumbers ENDP
stdcall 调用约定
- 被调用方负责平衡栈
- 参数同样从右向左压栈
- 示例代码:
; 调用方
PUSH 3
PUSH 2
PUSH 1
CALL stdcallAdd
; 被调用方
stdcallAdd PROC
PUSH BP
MOV BP, SP
MOV AX, [BP+4] ; 参数 1
ADD AX, [BP+6] ; 参数 2
ADD AX, [BP+8] ; 参数 3
POP BP
RET 6 ; 带立即数清栈
stdcallAdd ENDP
调试器实战观察
使用 DEBUG.EXE 观察栈变化:
- 编译并加载程序
- 在函数入口设置断点
- 单步执行并记录寄存器 / 内存变化
典型调试会话示例:
-r
AX=0000 BX=0000 CX=0042 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
-t
AX=0000 BX=0000 CX=0042 DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000
-d ss:fffc
0BEE:FFFC 34 12 00 00 00 00 00 00-00 00 00 00 00 00 00 00
内存显示小端格式:返回地址为 1234h。
常见问题与防护措施
栈对齐问题
8086 虽无严格对齐要求,但未对齐访问会导致:
- 性能下降(需要额外总线周期)
- 某些扩展指令执行失败
解决方案:
SUB SP, 2 ; 强制对齐到字边界
AND SP, 0xFFFE ; 明确对齐操作
缓冲区溢出攻击
典型栈溢出攻击路径:
- 覆盖局部变量
- 覆盖保存的 BP 值
- 覆盖返回地址
防护建议:
- 严格校验输入长度
- 使用安全字符串操作
- 关键函数添加栈检查
实验与延伸思考
建议通过 Turbo Debugger 或 DOSBox- X 重现本文实验,特别注意观察:
- CALL/RET 执行前后 SP 的变化
- BP 如何建立栈帧访问链
- 不同调用约定下的栈平衡方式
现代 CPU 的栈保护机制(如 Canary 值)可对比思考:
- 编译器如何在函数入口 / 出口插入检测代码
- 如何利用段保护机制增强栈安全性
- 返回地址混淆 (ASLR) 的实现原理
通过理解这些基础机制,可以更深入地掌握现代系统安全防护的设计思想。
正文完
发表至: 未分类
近一天内
