多段程序设计
一个完整的汇编程序通常包含代码段、数据段、栈段三个部分,各司其职。理解段的划分和设置是写好汇编程序的关键。
前置知识
本文假设你已经了解 01-寄存器 中的段寄存器(CS/DS/SS/ES),以及 03-数据定义与伪指令 中的 segment/ends。
问题:为什么不能把数据放在代码段最前面?
如果我们直接在代码段开头定义数据:
assume cs:code
code segment
dw 0123H, 0456H, 0789H ; 定义了3个字的数据
start:
mov bx, 0
mov ax, 0
mov cx, 3
s: add ax, cs:[bx]
add bx, 2
loop s
mov ax, 4c00H
int 21H
code ends
end start ; 注意这里有 start!
这段程序能正确运行——因为有 end start 指定了入口。如果没有 end start,只有 end,CPU 会从段的偏移地址 0 开始执行,把 dw 0123H 等数据当成指令来执行,程序崩溃!
关键结论
- 用
dw定义的数据会在编译时被写入 .exe 文件,和代码一样占据内存空间- 程序从哪里开始执行由
end 标号指定,标号所在位置就是 CS:IP 的初始指向- 如果没有指定入口(只有
end没有标号),默认从段的第一个字节开始执行
危险的写法
没有
end start,且数据定义在代码段最前面时,CPU 会把数据当指令执行,导致崩溃。 即使代码在最前面,也必须以mov ax, 4c00H / int 21H正确退出,否则 CPU 会冲入数据区。
规范做法:定义多个段
把数据、栈、代码分开到不同的段中是最清晰、最安全的写法:
assume cs:code, ds:data, ss:stack
data segment
dw 0123H, 0456H, 0789H ; 数据放在 data 段
data ends
stack segment
dw 0, 0, 0, 0, 0, 0, 0, 0 ; 栈空间:16个字节(8个字)
stack ends
code segment
start:
; 初始化段寄存器(必须手动设置!)
mov ax, stack
mov ss, ax ; SS 指向 stack 段
mov sp, 16 ; SP 指向栈顶(16 = 栈空间大小)
mov ax, data
mov ds, ax ; DS 指向 data 段
; 现在可以写业务逻辑了
mov bx, 0
mov ax, 0
mov cx, 3
s: add ax, [bx] ; 默认从 DS 段读数据
add bx, 2
loop s
mov ax, 4c00H
int 21H
code ends
end start
段的三个关键问题
问题1:段名代表什么?
段名(如 data、code、stack)本质上就是一个标号,代表该段的段地址。
mov ax, data ; 将 data 段的段地址送入 AX
mov ds, ax ; DS = data 段的段地址
在编译器眼里,data1 dw 1234H 中的 data1 也只是一个偏移地址常量,不是什么”变量”。它代表 data1 相对于所在段起始位置的偏移量。
问题2:assume 到底做了什么?
assume cs:code, ds:data, ss:stack
assume 是伪指令,它不会自动设置 CS/DS/SS 的值!CPU 根本不知道 assume 的存在。
它的作用是告诉编译器:
- “当代码中引用标号时,如果涉及段地址,请默认用这个段寄存器”
- 如果没有
assume ds:data,写mov ax, data1时编译器会报错,因为它不知道 data1 在哪个段
真正设置段寄存器的是代码中的 mov 指令,不是 assume。
| 段寄存器 | 谁设置的? | 怎么设置的? |
|---|---|---|
| CS:IP | 操作系统(DOS加载程序时) | 根据 end start 中的入口自动设置 |
| DS | 你自己 | 必须手动 mov ax, data / mov ds, ax |
| SS:SP | 你自己 | 必须手动设置 |
| ES | 你自己 | 需要时手动设置 |
为什么 CS 不用手动设置?
end start告诉了编译器程序入口在哪里,这个信息会被写入 .exe 文件的头部。DOS 加载程序时读取这个信息,自动将 CS:IP 设置到入口处。所以 CS 是操作系统帮你设置的。
问题3:“代码段”、“数据段”、“栈段”是 CPU 规定的吗?
不是! 完全是我们自己安排的。
- 把段命名为
data并不能让 CPU 知道这是数据段 - CPU 判断一段内存是什么,完全取决于你把哪个段地址放到了哪个段寄存器中
- 如果把 SS 指向
data段,那 data 段就变成了栈段 - 这些名字只是给人看的,让程序更容易理解
PSP:程序段前缀
DOS 加载 .exe 程序时,会在程序前面自动分配 256 字节(100H) 的 PSP(Program Segment Prefix,程序段前缀),存放系统信息:
- 程序退出入口(前两个字节是
CD 20,即int 20h) - 命令行参数(偏移 80H 处)
- 文件句柄表
- 父进程信息
这也是为什么程序加载后 DS 和 ES 默认指向 PSP 段,而不是你的数据段——所以你必须自己把 DS 设置到 data 段。
程序的内存布局
低地址 ┌─────────────┐ ← DS(初始值), ES(初始值) │ PSP │ 256字节 ├─────────────┤ ← CS(程序加载后) │ 代码段 │ ├─────────────┤ │ 数据段 │ ├─────────────┤ │ 栈段 │ └─────────────┘ ← SS:SP(设置后指向栈顶) 高地址实际上段的顺序不一定是这样排列的,具体取决于链接器。
段的对齐:16字节一节
8086 规定每个段的起始地址必须是 16 的倍数(即段地址 × 16 = 节对齐)。
编译器的规则:
- 即使一个段里只有 1 个字节,编译器也会为它分配至少 16 字节(1节)的空间
- 如果段内容超过 16 字节,按实际大小分配,但起始地址仍是 16 的倍数
data segment
db 4 dup(0) ; 只有4个字节
data ends
; 实际占用 16 字节(1节),下一个段的段地址比 data 大 1(1×16 = 16 字节偏移)
完整模板
assume cs:code, ds:data, ss:stack
; ---------- 数据段 ----------
data segment
; 在这里定义数据
msg db 'Hello, World!$'
data ends
; ---------- 栈段 ----------
stack segment
dw 32 dup(0) ; 64字节栈空间(根据需要调整大小)
stack ends
; ---------- 代码段 ----------
code segment
start:
; 1. 初始化栈段
mov ax, stack
mov ss, ax
mov sp, 64 ; sp = 栈的字节大小
; 2. 初始化数据段
mov ax, data
mov ds, ax
; 3. 初始化附加段(如果需要)
; mov ax, data
; mov es, ax
; 4. 你的代码写在这里...
; 5. 退出程序
mov ax, 4c00H
int 21H
code ends
end start
相关
- 04-第一个程序 — 环境搭建和 debug 调试
- 06-栈 — 栈的工作原理,为什么要设置 SS:SP
- 01-寄存器 — 段寄存器回顾
- 03-数据定义与伪指令 — db/dw/dd 数据定义