多段程序设计

一个完整的汇编程序通常包含代码段数据段栈段三个部分,各司其职。理解段的划分和设置是写好汇编程序的关键。

前置知识

本文假设你已经了解 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:段名代表什么?

段名(如 datacodestack)本质上就是一个标号,代表该段的段地址

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

相关