汇编语言:包含多个段的程序


之前为了存放数据,我们需要借用代码段空间或一段安全区域(0:200),这是不规范且不可扩展的。因此需要令程序合法地获取任意大小的内存空间,以及将代码、数据、栈逻辑分离。

在代码段中使用数据

考虑一个将 8 个给定的字数据(0123H, 0456H...)累加,结果存入 AX 的程序。可以使用 dw(Define Word) 伪指令,在 code segment 的开头使用了 dw 0123h, 0456h...,以告诉编译器将这些数据作为代码段的一部分。当程序被编译连接后,这些数据会以二进制形式紧跟在程序头后面,成为可执行文件的一部分。加载入内存时,它们占据 CS:0000CS:000F 这 16 个字节。

assume cs:code
code segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

    mov bx,0
    mov ax,0

    mov cx,8
s:  add ax,cs:[bx]
    add bx,2
    loop s

    mov ax,4c00h
    int 21h
code ends
end

使用 Debug 加载后,如果直接用 u 命令从 CS:0000 查看将看到实际上是数据机器码的一堆乱码。
image
只有用 d 命令才能看到这些数据。
image

由于数据占据了代码段的前 16 个字节,真正的指令从偏移地址 10H 开始。如果直接运行程序,CPU 会从 CS:0000 开始执行,把数据当成指令跑将导致错误。
image

为了解决这个问题,需要引入 end start 伪指令组合,在代码开头设置标号 start,并在 end 后面加上这个标号。

assume cs:code
code segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

start:  mov bx,0
        mov ax,0

        mov cx,8
    s:  add ax,cs:[bx]
        add bx,2
        loop s

        mov ax,4c00h
        int 21h
code ends
end start

这样能明确地告知编译器程序的入口,编译器会将这个信息写入可执行文件的头部(描述信息),DOS 加载程序时读取该信息,自动将 CS:IP 指向 start 标号处(即 CS:0010),从而跳过了数据区。
image
image

引入包含段的程序之后,标准的汇编程序框架模板乳腺癌:

assume cs:code
code segment
        ; 数据定义区 (dw ...)
start:  ; 程序入口
        ; 初始化代码
        ; 主逻辑代码
        mov ax,4c00h
        int 21h
code ends
end start

在现代操作系统(如 Linux/Windows 的 PE 文件)中,数据嵌在代码段的做法对应着 .text 段(代码段)中的只读数据区,现代编译器通常会自动将字符串常量等放入 .rodata 段。在实模式下,由于没有操作系统的复杂加载器支持,需要手动管理这种布局。end start 的机制类似于现代链接脚本(Linker Script)中的 ENTRY(start) 指令,都是为了确定程序的复位向量(Reset Vector)或入口点。

在代码段中使用栈

考虑一个利用栈将程序中定义的数据逆序存放的任务,思路是利用 dw 伪指令在代码段尾部预留一块全零的内存区域,并将其强制指定为栈。首先用 dw 0,0... 定义 16 个字(32 字节)的空间,设置 ss=cssp=30h,将 cs:10hcs:2Fh 这 32 个字节定义为栈段,并将栈顶指针指向栈底 cs:30h。接下来先循环 8 次将 cs:0 开始的 8 个数据依次 push 入栈,第二步是重置偏移地址再循环 8 次,通过 pop 指令将栈中的数据依次弹回原地址。

assume cs:code
code segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

start:  mov ax,cs
        mov ss,ax
        mov sp,30h

        mov bx,0
        mov cx,8
s0:     push cs:[bx]
        add bx,2
        loop s0

        mov bx,0
        mov cx,8
s1:     pop cs:[bx]
        add bx,2
        loop s1

        mov ax,4c00h
        int 21h
code ends
end start

image

需要特别注意 sp 的初始化值,数据区位于 cs:0cs:F(16 字节),预留的栈空间从 cs:10h 开始。因为栈是向低地址方向增长的,且初始空栈时 SP 指向栈底,所以 sp 须设置为 32(即 20H)而不是 10h。如果错误地设置为 10h,第一次压栈就会覆盖数据区的最后一个元素,导致数据损坏。
image
image

早期系统对内存保护是较为缺失的,在现代操作系统中,代码段通常是只读且不可执行的(NX bit),不允许在代码段里开辟栈空间。现代程序的栈是由操作系统在加载时动态分配在独立的虚拟内存页中的,程序员无需手动计算 sp 的偏移量。

将数据、代码、栈放入不同的段

当程序规模变大,或者数据/栈空间超过 64KB 时,将所有内容塞进一个段将变得不可维护。同样是利用栈将程序中定义的数据逆序存放的任务,以下程序引入了多段结构,使用 data segment...data endsstack segment...stack ends 定义独立的数据段和栈段。

assume cs:code,ds:data,ss:stack

data segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends

code segment
start:  mov ax,stack
        mov ss,ax
        mov sp,20h

        mov ax,data
        mov ds,ax

        mov bx,0
        mov cx,8
s1:     push [bx]
        add bx,2
        loop s1

        mov bx,0
        mov cx,8
s2:     pop [bx]
        add bx,2
        loop s2

        mov ax,4c00h
        int 21h

code ends
end start

data, stack 的段名,在编译后会被替换为其对应的段地址数值。例如 mov ax,data 并不是把字符串 “data” 给 AX,而是把 data 段的段地址送入 AX。
image

assume cs:code, ds:data, ss:stack 这条伪指令仅仅是告知编译器:让 CS 指向 code 段,让 DS 指向 data 段,让 SS 指向 stack 段。编译器据此来检查指令的合法性,但是 CPU 本身并不认识 assume,CPU的段寄存器(CS, DS, SS, ES)不会因为 assume 的存在而自动赋值。因此需要在代码段的开头(start 处)手动进行初始化:mov ax, stack / mov ss, ax 设置栈段寄存器;用 mov ax, data / mov ds, ax 设置数据段寄存器;用 mov sp, 20h 设置栈顶。

段名只是助记符,真正决定程序行为的是寄存器与内存地址的映射关系,因此将段名改为 a, b, c,将入口改为 d 这样的操作是合法的。只要初始化代码正确,即 ss 指向了栈段、ds 指向了数据段、cs 指向了代码段,程序就能正确运行。

多段结构对应了现代可执行文件(如 PE 或 ELF 格式)中的 Section(节区)概念,.text 对应代码段、.data 对应数据段、.bss 对应未初始化数据(类似栈段预留空间)。现代操作系统加载程序时,会根据文件头中的信息自动设置 CS、DS、SS 等段寄存器,或在保护模式下设置段选择子,不再需要程序员在程序开头手动写这些初始化指令。

实验 5

多段程序分析

以下程序定义了数据段(8 个字)、栈段(8 个字)和代码段,然后会将 ds:[0](0123H)和 ds:[2](0456H)入栈,再依次弹出到 ds:[2]ds:[0]。运行完的效果是交换了前两个字单元的数据,因此程序返回前 data 段中的数据变为 0456H、0123H,后续数据保持不变。

assume cs:code,ds:data,ss:stack

data segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
    dw 0,0,0,0,0,0,0,0
stack ends

code segment
start:  mov ax,stack
        mov ss,ax
        mov sp,16

        mov ax,data
        mov ds,ax

        push ds:[0]
        push ds:[2]
        pop ds:[2]
        pop ds:[0]

        mov ax,4c00h
        int 21h
code ends
end start

程序初始化时 ss 被赋值为 stack 的段地址、ds 被赋值为 data 的段地址。设 code 段地址为 X,则 data 段地址为 X-2,stack 段地址为 X-1。DOS 加载程序时按照段的定义顺序分配内存,该程序是按代码段、数据段、栈段的顺序。每个段最小以 16 字节(10H)为单位分配,code 段很小(不足 16B),占一个最小单位;data 段定义 8 个字(16B),占一个单位;stack 段定义 8 个字(16B),占一个单位。因此逻辑上段地址相差 1,即物理地址相差 16 字节。
image
image

段大小与空间占用

以下程序将 data 段缩减为 2 个字,stack 段缩减为 2 个字。但在 DOS 中加载后,每个段实际占用的空间必须是 16 的倍数。因此即使只定义了 4 字节数据,该段实际占有的空间仍为 \(\lceil N/16 \rceil \times 16\),其中 N 为数据字节数。

assume cs:code,ds:data,ss:stack

data segment
    dw 0123h,0456h
data ends

stack segment
    dw 0,0
stack ends

code segment
start:  mov ax,stack
        mov ss,ax
        mov sp,16

        mov ax,data
        mov ds,ax

        push ds:[0]
        push ds:[2]
        pop ds:[2]
        pop ds:[0]

        mov ax,4c00h
        int 21h
code ends
end start

image

段定义顺序的影响

以下程序改变了段的定义顺序,将 code 段放在最前面,data 和 stack 段置于其后。此时设 code 段地址为 X,则 data 段地址为 X+1,stack 段地址为 X+2。段地址的分配取决于源程序中 segment 出现的先后顺序,而非 segment 的名字。

assume cs:code,ds:data,ss:stack

code segment
start:  mov ax,stack
        mov ss,ax
        mov sp,16

        mov ax,data
        mov ds,ax

        push ds:[0]
        push ds:[2]
        pop ds:[2]
        pop ds:[0]

        mov ax,4c00h
        int 21h
code ends

data segment
    dw 0123h,0456h
data ends

stack segment
    dw 0,0
stack ends

end start

image

若不指明入口,将 end start 改为 end,则只有这个程序仍能正确执行。end start 告诉编译器程序的入口在 start 标号处,编译器会在可执行文件头中记录该入口地址,DOS 加载后会直接跳转到该地址执行。如果省略 start,DOS 会默认从程序在内存中的起始偏移 0 处开始执行。之前的 2 个程序的代码段前面定义了 data 和 stack 段,内存起始处是数据,CPU 将数据误当指令执行必然出错。而这个小节中,代码段 code segment 恰好位于源程序的最前端,加载后内存起始处就是指令。因此即使不指定入口,CPU 也能碰巧执行正确的指令,但这属于不良编程习惯。

字节型数据相加

考虑一个将 a 段和 b 段中的 8 个字节数据相加,结果存入 c 段的程序。

assume cs:code
a segment
	db 1,2,3,4,5,6,7,8
a ends

b segment
	db 1,2,3,4,5,6,7,8
b ends

d segment
	db 0,0,0,0,0,0,0,0
d ends

code segment
start:  mov ax,a
	    mov ds,ax
        mov ax,d
        mov es,ax

        mov bx,0
        mov cx,8
s0:     mov ax,ds:[bx]
        mov es:[bx], ax
        inc bx
        loop s0

        mov ax,b
	    mov ds,ax
        mov bx,0
        mov cx,8
s1:     mov ax,ds:[bx]
        add es:[bx], ax
        inc bx
        loop s1

        mov ax,4c00h
        int 21h

code ends
end start

image
image
image

字型数据逆序存储

考虑一个将 a 段中的前 8 个字型数据(16 字节),利用栈的特性逆序存储到 b 段中。

assume cs:code
a segment
    dw 1,2,3,4,5,6,7,8,9,0ah,0bh,0ch,0dh,0eh,0fh,0ffh
a ends

b segment
    dw 0,0,0,0,0,0,0,0
b ends

code segment
start:  mov ax,a
        mov ds,ax
        mov ax,b
        mov ss,ax
        mov sp,16
        mov bx,0
        mov cx,8

s:      push [bx]
        add bx,2
        loop s

        mov ax,4c00h
        int 21h
code ends
end start

通过将目标段 b 设置为栈段,并执行 push 操作,数据在存入内存时已经完成了逆序排列,不需要再进行额外的弹出和写入操作。
image
image

参考资料

《汇编语言(第四版)》,王爽 编著,清华大学出版社

posted @ 2026-08-10 00:30  乌漆WhiteMoon  阅读(4)  评论(0)    收藏  举报