汇编语言:内存间接寻址和循环


之前的指令只是在操作寄存器和固定内存单元,通过引入内存间接寻址循环控制可以令程序实现动态访问内存和批量处理数据。

[BX]与内存单元的描述

之前使用的类似 mov ax,[0] 的指令中的 [0] 被称为直接寻址,偏移地址是固定的常量。我们可以将内存单元的偏移地址存放在 BX 寄存器中,并用 [BX] 代表寄存器间接寻址。即可以通过改变 BX 的值,用一条指令访问不同的内存单元。例如 mov ax,[bx] 的逻辑含义是:将 DS 段中,偏移地址为 BX 值的内存字单元内容送入 AX,即为:\((ax)=((ds)\times16+(bx))\)

需要注意的是,内存操作的长度不再由 [bx] 决定,而是由另一个操作对象决定。如果是 mov ax,[bx] 则为字操作,如果是 mov al,[bx] 则为字节操作。例如在如下程序中:

assume cs:code
code segment
    mov ax,2000H
    mov ds,ax
    mov bx,1000H
    mov cx,00BEH
    mov [bx],cx

    mov ax,[bx]
    inc bx
    inc bx
    mov [bx],ax
    inc bx
    inc bx
    mov [bx],ax
    inc bx
    mov [bx],al
    inc bx
    mov [bx],al
    
    mov ax, 4c00h
    int 21h
code ends
end

DS=2000HBX=1000H 时,执行 mov ax,[bx],AX 得到的是 2000:1000 处的字数据。inc bx 指令的功能是使得 BX 每次递增 1(字节级移动),通过 mov [bx],axmov [bx],al,分别控制数据是按字(2 字节)还是按字节写入内存。
image

Loop指令

loop 指令是汇编语言中最基础的循环指令,其工作机制为:先将 CX 寄存器的值减 1,然后判断 CX 是否为 0。如果不为 0,则跳转到指定的标号处继续执行;如果为 0,则执行 loop 之后的指令。例如如果不使用循环实现计算 \(2^{12}\),需要手写 11 条 add ax,ax;而令 s 作为标号使用 loop s 指令,仅需几行代码即可完成:

assume cs:code
code segment
    mov ax, 2
    mov cx, 11
s:  add ax, ax
    loop s

    mov ax, 4c00h
    int 21h

code ends
end

image

使用 loop 实现循环有如下要求:

  1. CX 中存放循环次数。
  2. 因为 loop 是向后跳转,标号必须定义在循环体的上方。
  3. 循环体位于标号和 loop 指令之间。

在现代 x86 汇编(如 x86-64)中,loop 指令其实已经很少被使用了。主要原因有两个:

  1. 现代 CPU 拥有深度的流水线,loop 指令(涉及减法、判断、跳转)的效率往往不如使用 dec ecx / jnz label 的组合,后者更有利于 CPU 的分支预测;
  2. 64 位模式下,循环指令默认使用的 ECX 寄存器在处理 64 位数据时不够直观。

考虑一个较为复杂的任务:计算 ffff:0ffff:b 共 12 个字节单元的和,将结果存放在 DX 中。由于不能直接将 8 位内存数据加到 16 位的 DX 中,也不能只累加到 8 位的 DL 中(会导致溢出)。此时需要使用一个 16 位的中间寄存器如 AX,每次先从内存取出 8 位数据,将其放入 AL 同时将 AH 清零(mov ah,0),构造出一个合法的 16 位数据再累加到 DX 中。通过引入 loop 和 [bx],可以避免硬编码产生的冗长代码:

assume cs:code
code segment
    mov ax,0ffffh
    mov ds,ax
    mov dx,0
    mov bx,0
    mov cx,12

s:  mov al,ds:[bx]
    mov ah,0
    add dx,ax
    add bx,1
    loop s

    mov ax,4c00h
    int 21h
code ends
end

image

在现代 x86-64 架构中,比例变址寻址(Scaled Index Addressing)已经取代了上述的遍历方式。例如 [rax + rbx*4] 允许索引寄存器乘以一个比例因子(1、2、4、8),以此适应不同大小的数据类型(char、short、int、long)。此外,现代 CPU 拥有强大的 SIMD 指令集(如 SSE、AVX),可以一次性读取 128 位或 256 位数据并进行并行加法运算,效率远超逐字节累加的方式。

Debug 跟踪用 loop 指令

以下程序实现了计算 ffff:0006 单元中的数乘以 3,将结果存于 DX 中。在数据类型方面,ffff:0006 是字节单元(8 位),而 AX 是 16 位寄存器。为了保证数值大小不变,需要用 mov al,[bx]mov ah,0 进行高位清零操作。这样无论内存中是 32H 还是 FFH,存入 AX 后都会变成 0032H00FFH,避免符号扩展导致的错误。设置循环次数 (cx)=3,利用 add dx,ax 完成累加。

assume cs:code
code segment
    mov ax, 0ffffh
    mov ds, ax
    mov bx, 6

    mov al, [bx]
    mov ah, 0
    mov cx, 3
    mov dx, 0

s:  add dx, ax
    loop s

    mov ax, 4c00h
    int 21h
code ends
end

在 Debug 加载程序后,观察 u 命令的结果会发现,源程序中的标号 s 已经被编译器转化为具体的偏移地址(0012)。在编译阶段,标号会被换算成相对于代码段起始的偏移地址,即完成了地址绑定操作。
image

使用 t 命令可以看到 loop 指令执行的第一步是将 CX 减 1,然后再判断是否为 0。
image

当程序逻辑较为复杂或循环次数较多时,逐行使用 t 命令会变得低效。可以使用以下两个命令:

命令 功能
g 命令(Go) 可以跳过无需关心的代码段,例如 g 0012 会让程序持续执行,直到 IP 寄存器的值等于 0012H 才停下。
p 命令(Proceed) 用于快速执行循环。当遇到 loop 指令时,使用 p 命令 Debug 会自动执行直到 CX 减为 0,停在循环结束后的下一条指令。

image

在 8086 实模式下,因为操作系统未对内存访问进行保护,对 FFFF:0006 内存地址的直接读写是合法的。但在现代操作系统中,直接访问物理内存的行为会引发段错误(Segmentation Fault),因为用户态程序运行在虚拟地址空间中,受到 MMU(内存管理单元)和保护模式的严格隔离。此外,现代编译器在处理 byteint 的转换时,会自动处理零扩展(Zero Extension)符号扩展(Sign Extension),而无需手动设置 ah=0

段前缀

Debug 解释器与 MASM 编译器对带有方括号 [] 的指令有不同的解析逻辑。在 Debug 中直接使用 a 命令输入 mov al,[0],Debug 会将其理解为“从 DS:0 内存单元取数据送入 AL”。
image

而在汇编源程序(.asm 文件)中编写 mov al,[0],MASM 编译器会将其解释为 mov al,0,即将常量 0 送入 AL 而非访问内存。该差异源于编译器为了语法严谨性,将单独的 [idata] 视为立即数,除非显式指定段寄存器。
image

解决这一问题需要引入段前缀,它是指在访问内存单元的指令中,显式地用于指明内存单元段地址的标识符,常见的包括 ds:cs:ss:es:。即对于上述问题,在源程序中写成 mov al,ds:[0] 明确告知编译器 [0] 是一个内存地址,且段地址在 DS 中。同理,ss:[0]es:[bx] 分别强制指定了栈段和附加段作为数据来源。如果省略段前缀仅使用 [bx][0],编译器默认段地址为 DS,但对于常量偏移量 [0] 时该默认机制失效,必须显式声明。
image

Debug 与编译器不一致的现象在现代汇编开发中依然存在,但在 NASM(Netwide Assembler)或 GAS(GNU Assembler)中处理方式略有不同。例如 NASM 语法区分了直接寻址 mov ax, var和间接寻址 mov ax, [var],逻辑更为统一。在 MASM/TASM 的语法体系中,这种设计主要是为了兼容早期的语法解析器。

例如现在需要将 ffff:0~ffff:b 的数据复制到 0:200~0:20b。最直接的思路是每次循环都切换 DS 的值,先用 DS 指向源地址 FFFFH 段读取数据,再修改 DS 指向目标地址 0020H 段写入数据。

assume cs:code

code segment
    mov bx,0
    mov cx,12

s:  mov ax,0ffffh
    mov ds,ax
    mov dl,ds:[bx]

    mov ax,0020h
    mov ds,ax
    mov ds:[bx],dl

    inc bx
    loop s

    mov ax,4c00h
    int 21h
code ends

end

image
image

这种方法虽然逻辑正确,但每次循环都要执行两条修改 DS 的指令。可以利用 ES 附加段寄存器进行改进,初始化时将源段地址 FFFFH 送入 DS,将目标段地址 0020H 送入 ES。在循环体内,使用mov dl,[bx]读取源数据,同时使用 mov es:[bx],dl 将数据写入目标地址。

assume cs:code

code segment
    mov bx,0
    mov cx,12

    mov ax,0ffffh
    mov ds,ax
    mov ax,0020h
    mov es,ax

s:  mov dl,ds:[bx]
    mov es:[bx],dl
    inc bx
    loop s

    mov ax,4c00h
    int 21h
code ends
end

一段安全的空间

在实模式编程中,随意向内存写入数据存在风险,可能会破坏操作系统或其他关键程序的数据。以如下程序为例,主要操作是mov ds:[26h],ax 试图向 0:0026 写入数据。
image

在 DOSBOX debug 时运行时会卡死,这是因为0:0026 存放着重要的中断向量或系统数据。
image

学习汇编的目的是直接操作硬件,但又必须在操作系统提供的环境中运行工具,此时可以使用一段操作系统和其他合法程序都不会使用的内存空间。在 DOS 环境下,通常认为 0:200~0:2FF(即 00200H~002FFH)这 256 个字节是安全的。这段区域位于中断向量表之后、DOS 和系统程序数据之前,一般不会被占用。

需要注意的是,这种做法在现代操作系统中是不可行的。现代操作系统运行在 CPU 的保护模式长模式下,每个进程都拥有独立的虚拟地址空间,并由内存管理单元(MMU)进行的权限检查。任何试图访问未分配或受保护地址的代码都会立即触发通用保护错误(General Protection Fault)段错误(Segmentation Fault),导致进程被强制终止。王爽老师说明的安全空间是实模式下特有的。

实验4

实验要求在安全空间 0:200~0:23F(共 64 个字节)中依次填入 0~63,只能使用 9 条指令(含返回指令)。思路是让 BX 从 0 递增到 3FH,同时将 BX 的值作为数据写入对应的内存单元。目标地址 0:200 使用段前缀 ds:,或直接设置 ds=0020h 均可。

assume cs:code

code segment
    mov ax, 0020h
    mov ds, ax
    mov bx, 0
    mov cx, 64

s:  mov ds:[bx], bx
    inc bx
    loop s

    mov ax, 4c00h
    int 21h
code ends
end

image
image

接着需要实现的是将以下程序的开头到 mov ax,4c00h 之前的所有指令,复制到 0:200 处。复制的是机器码,源地址是从 CS:0 开始,此时关键在于知道 mov ax,4c00hint 21h 这两条指令在内存中占多少个字节,然后在复制时跳过它们。可以通过 Debug 的 U 命令查看,观察发现 mov ax,4c00h 对应机器码 B8004C(3 字节),int 21h 对应 CD 21(2 字节)。因此循环次数 CX 应该等于总代码长度减去这 5 个字节。
image

补全后的程序如下:

assume cs:code
code segment
    mov ax, cs
    mov ds, ax
    mov ax, 0020h
    mov es, ax
    mov bx, 0
    mov cx, 0017h

s:  mov al, [bx]
    mov es:[bx], al
    inc bx
    loop s

    mov ah, 4ch
    int 21h
code ends
end

“代码自复制”操作在早期病毒和引导扇区编程中非常常见,但现代操作系统由于引入了 NX(No-eXecute)位地址空间布局随机化(ASLR),数据段通常不可执行,代码段通常不可写。因此直接在内存中修改或移动代码的行为会被硬件直接拦截,以防止恶意代码注入。

参考资料

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

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