第8讲:堆栈段与函数调用
本周核心:理解栈段的定义和作用,掌握 call/ret 的执行机制,学会用栈传递参数。
一、堆栈段的定义
1.1 为什么需要栈段?
栈用于:
- 保存函数返回地址(call/ret)
- 传递函数参数
- 保存局部变量
- 保存寄存器的值
asm
stk segment stack ; ★ stack 关键字告诉链接器这是栈段
db 100h dup('S') ; 100h 字节的栈空间(dup = duplicate 重复)
stk ends如果源程序不定义栈段,DOS 会让 ss = 程序首段的段地址,sp = 0,此时栈从段末尾往下长。
1.2 程序启动时的寄存器状态
(1) 程序开始执行时:
ds = es = 首段的段地址 - 10h(即 PSP 的段地址)
PSP (Program Segment Prefix) 是 DOS 加载程序时创建的
100h 字节的前缀块,位于首段前一个内存块
byte ptr PSP:[80h] = 命令行参数的长度
PSP:81h 开始 = 命令行参数的内容
(2) ss = 栈段的段地址, sp = 栈的大小
(3) cs = 代码段的段地址, ip = main 的偏移地址1.3 栈段内存布局示例
假设 data 段地址 = 1000h,段长度 = 10h,code 段长度 = 20h:
1000:0000 ~ 1000:000F 数据段
1000:0010 ~ 1000:002F 代码段
1000:0030 ~ 1000:FFFF ← 空闲空间(可被栈使用)
2000:0000 ~ 9000:FFFF 也属于当前程序(DOS 分配)二、多个数据段的访问
当程序需要多个数据段时:
asm
data1 segment
abc dw 1234h, 5678h
data1 ends
data2 segment
xyz dw 89ABh, 0CDEFh
data2 ends
code segment
assume cs:code, ds:data1, es:data2
main:
mov ax, data1
mov ds, ax ; ds → data1
mov ax, data2
mov es, ax ; es → data2
mov ax, abc[0] ; 编译成: mov ax, ds:[0]
mov xyz[0], ax ; 编译成: mov es:[0], ax
; 不能直接写: mov xyz[0], abc[0]
; 因为一条指令中两个操作数不能都是内存操作数
push abc[2] ; push word ptr ds:[2]
pop xyz[2] ; pop word ptr es:[2]
; push + pop 效果: xyz[2] = abc[2]
mov ah, 4Ch
mov al, 0
int 21h
code ends
stk segment stack
db 100h dup('S')
stk ends
end main三、函数定义与调用
3.1 最基本的 call / ret(用寄存器传参)
asm
code segment
assume cs:code
f: ; 用标号定义函数
shl ax, 1 ; 返回值放在 ax 中
ret ; CPU 执行 ret 时: pop ip
main:
mov ax, 3 ; 用寄存器 ax 传参
call f
; CPU 执行 call f 时:
; ① push offset back (压入返回地址)
; ② jmp f (跳转到目标地址)
back:
mov ah, 4Ch
mov al, 0
int 21h
code ends
stk segment stack
db 100h dup('S')
stk ends
end maincall 执行过程:
call f →
① push offset back ; 把 call 下一条指令的偏移地址压栈
② jmp f ; 跳转到函数
ret →
① pop ip ; 从栈中弹出返回地址 → ip3.2 用栈传递参数
当参数多了,寄存器不够用,需要用栈传参。
原型:f(参数1, 参数2),参数从右往左压栈:
asm
code segment
assume cs:code
f: ; f(a, b) = a - b
push bp ; (4) 保存调用者的 bp
mov bp, sp ; ★ 建立栈帧:bp 指向当前栈顶
mov ax, ss:[bp+4]; 读取参数 1(a)
sub ax, ss:[bp+6]; ax = 参数 1 - 参数 2(b)
pop bp ; (5) 恢复调用者的 bp
ret ; (6) 返回
main:
mov ax, 3 ; 参数 2 = 3
push ax ; (1) 先压参数 2
mov ax, 5 ; 参数 1 = 5
push ax ; (2) 再压参数 1
call f ; (3) 调用函数
back:
add sp, 4 ; (7) 清理栈中的 2 个参数
mov ah, 4Ch
mov al, 0
int 21h
code ends
stk segment stack
db 100h dup('S')
stk ends
end main3.3 栈帧详解
设程序运行时 ss = 2000h, sp = 1000h,则执行过程中栈的变化:
调用前 (main):
ss:1000 ← sp
push 参数2 (3) 后:
ss:0FFE 0003h ← sp
push 参数1 (5) 后:
ss:0FFC 0005h ← sp
call f 后:
ss:0FFA back 的偏移 ← sp
push bp 后:
ss:0FF8 old bp ← bp ← sp
mov bp, sp 后:
bp = 0FF8, 此时访问参数:
ss:[bp+4] = ss:[0FFC] = 5 ← 参数 1
ss:[bp+6] = ss:[0FFE] = 3 ← 参数 2栈帧结构(从高地址到低地址):
ss:[bp+6] = 参数 2 (先压入的)
ss:[bp+4] = 参数 1 (后压入的)
ss:[bp+2] = 返回地址 (call 压入的)
ss:[bp] = old bp (push bp 压入的)关键规律:在栈帧中,参数从
[bp+4]开始,因为[bp+2]存放返回地址,[bp]存放 old bp。
四、本周易错点
- 不定义栈段时栈空间有限,递归/深层调用可能溢出
- 忘记
push bp和pop bp,导致调用者的 bp 被破坏 - 参数偏移算错:
[bp+4]不是[bp+2](bp+2 是返回地址) - 调用后忘记
add sp, n清理参数(内存泄漏) - 一条指令中两个操作数不能都是内存操作数(如
mov xyz[0], abc[0]非法)
五、速查表
| 操作 | 写法 |
|---|---|
| 定义栈段 | stk segment stack + db N dup('S') |
| 用寄存器传参 | mov ax, value → call f → 函数内直接用 ax |
| 用栈传参 | push param2 → push param1 → call f |
| 建立栈帧 | push bp / mov bp, sp |
| 访问参数 | ss:[bp+4](参数1), ss:[bp+6](参数2) |
| 清理参数 | add sp, 参数总字节数 |
| 函数返回 | pop bp / ret |