8086函数调用机制详解:从栈帧结构到参数传递实战

1次阅读
没有评论

共计 1481 个字符,预计需要花费 4 分钟才能阅读完成。

image.webp

理解函数调用机制是掌握操作系统和编译器设计的基础。就像搭积木需要知道每块的连接方式,搞清楚 CALL/RET 背后的栈操作,才能写出可靠的汇编代码,也能更好地理解高级语言的工作原理。

8086 函数调用机制详解:从栈帧结构到参数传递实战

一、栈帧结构:函数调用的舞台

每次函数调用时,CPU 都会在栈上创建一块称为 ” 栈帧 ” 的内存区域。以 CALL myFunc 指令为例(机器码0xE8+ 偏移量):

  1. 调用前:假设 SS:SP=0x1000:0xFFFE
  2. 执行 CALL 时:
  3. 先将返回地址 CS:IP 压栈(占用 4 字节)
  4. SP 自动减 4 变为 0xFFFA
  5. 进入函数后:
  6. 典型开场白PUSH BP(机器码 0x55)
  7. MOV BP, SP(机器码 0x89 0xE5)
  8. 这时栈布局:
    SS:0xFFFA -> 旧 IP
    SS:0xFFFC -> 旧 CS
    SS:0xFFFE -> 旧 BP  <- BP 现在指向这里

二、调用约定:参数传递的交通规则

在 8086 汇编中,常见两种调用约定:

  1. Cdecl 约定(C 语言默认):
  2. 参数从右向左压栈
  3. 调用者清理栈
  4. 示例:

    ; 调用方
    PUSH 1234    ; 参数 2
    PUSH 5678    ; 参数 1
    CALL add
    ADD SP, 4    ; 清理栈
    
    ; 函数方
    add PROC
        PUSH BP
        MOV BP, SP
        MOV AX, [BP+4]  ; 取参数 1
        ADD AX, [BP+6]  ; 加参数 2
        POP BP
        RET
    add ENDP

  5. StdCall 约定(WinAPI 常用):

  6. 参数同样从右向左
  7. 被调函数自己清理栈
  8. 对应的 RET 指令变为RET n(机器码 0xC2+n)

三、参数传递实战:WORD 与 DWORD 的区别

处理不同尺寸参数时容易出错,关键要记住:

  1. WORD 参数(16 位):
  2. 直接 PUSH 一个寄存器或内存字
  3. PUSH AX(机器码 0x50)

  4. DWORD 参数(32 位):

  5. 在 8086 上需要分两次 PUSH
  6. 示例传递 0x12345678:
    PUSH 5678h   ; 高字
    PUSH 1234h   ; 低字
  7. 函数内通过 [BP+4] 访问低字,[BP+6]访问高字

四、完整代码示例

下面这个可编译程序演示了参数传递和栈跟踪:

.MODEL SMALL
.STACK 100h
.CODE

main PROC
    MOV AX, @data
    MOV DS, AX

    ; 准备参数
    PUSH 3       ; 第二个参数
    PUSH 2       ; 第一个参数
    CALL sum     ; 机器码 E8 0007
    ADD SP, 4    ; 清理栈

    MOV AH, 4Ch
    INT 21h
main ENDP

; 求和函数
sum PROC
    PUSH BP      ; 55
    MOV BP, SP   ; 89 E5

    ; 现在栈结构:; BP+6 -> 参数 2
    ; BP+4 -> 参数 1
    ; BP+2 -> 返回 IP
    ; BP+0 -> 旧 BP

    MOV AX, [BP+4]
    ADD AX, [BP+6]

    POP BP       ; 5D
    RET          ; C3
sum ENDP

END main

五、避坑指南

  1. 栈对齐问题
  2. 8086 没有硬性对齐要求,但保持 WORD 对齐(SP 为偶地址)能提升性能
  3. 中断发生时,CPU 会自动将 FLAGS、CS、IP 压栈(共 6 字节)

  4. 寄存器保存

  5. 函数内修改 BX/SI/DI/BP 等寄存器前应先 PUSH 保存
  6. 示例:

    myFunc PROC
        PUSH SI
        PUSH DI
        ; 函数体
        POP DI
        POP SI
        RET
    myFunc ENDP

  7. 跨模块调用

  8. 确保所有模块使用相同内存模型(TINY/SMALL/COMPACT 等)
  9. 外部函数声明要一致:EXTRN someFunc:PROC

六、调试技巧(DOSBox)

  1. 用 DEBUG.EXE 加载程序后:
  2. U命令反汇编查看代码
  3. T单步执行
  4. D SS:SP查看栈内容

  5. 关键断点设置:

    -g 0x1234  ; 执行到 CS:IP=0x1234
    -p         ; 跳过 CALL/RET

思考题

  1. 可变参数函数(如 printf)如何通过栈确定参数个数?
  2. 当 CS 发生变化时的 FAR CALL 与普通 NEAR CALL 在机器码上有何区别?
  3. 递归调用超过栈空间时会出现什么现象?如何检测?
正文完
 0
评论(没有评论)