- 位操作指令
- 字符形式的数据
- 带偏移量的内存寻址
- SI 和 DI 寄存器
- 二重寻址
- 三重寻址
- 参考资料
位操作指令
and 和 or 指令是进行位操作的基础指令。
| 指令 | 功能 | 样例 |
|---|---|---|
| and 指令 | 按位逻辑与,用途是将特定位设为 0 | 例如 and al, 11011111B,通过让第 5 位(从 0 开始数)与 0 相与,无论该位原来是什么结果都是 0,而其他位与 1 相与保持不变。 |
| or 指令 | 按位逻辑或,用途是将特定位设为 1 | 例如 or al, 00100000B,通过让第5位与1相或,无论该位原来是什么结果都是 1,其他位与 0 相或保持不变。 |
字符形式的数据
计算机存储字符本质是存储数字,ASCII 码就是字符与数字的映射表。在汇编源程序中可以直接使用单引号 '...' 来定义字符数据。例如如下程序中的 db 'unIX',编译器会自动将其转换为对应的 ASCII 码值(75H, 6EH, 49H, 58H)。
assume cs:code, ds:datadata segment
db 'unIX'
db 'foRK'
data endscode segment
start: mov ax, data
mov ds, ax
mov ax, 4c00h
int 21h
code endsend start
在 Debug 中查看内存时,既可以看其十六进制数值,也可以看其右侧对应的 ASCII 字符。

字符的大小写转换的是一个经典的位操作案例,考虑一个将 datasg 中的第一个字符串转为大写,第二个转为小写的程序。一种方式是,可以通过观察发现小写字母比大写字母的 ASCII 码值大 20H(如'a'=61H, 'A'=41H)。如果采用这个规律,程序必须先判断字母是大写还是小写,再决定加减 20H。
但在当时掌握的指令集中,目前缺乏高效的判断指令。观察 ASCII 码的二进制形式,发现关键在第 5 位(bit 5):大写字母(A-Z)的第 5 位为 0 (如 'A': 41H -> 010000001);小写字母(a-z)第 5 位为 1 (如 'a': 61H -> 011000001),其他位都相同。
| 大写 | 大写十六进制 | 大写二进制 (bit7~bit0) | 小写 | 小写十六进制 | 小写二进制 (bit7~bit0) | 第5位(bit5) |
|---|---|---|---|---|---|---|
| A | 41H | 010000001 | a | 61H | 011000001 | 0 → 1 |
| B | 42H | 010000010 | b | 62H | 011000010 | 0 → 1 |
| C | 43H | 010000011 | c | 63H | 011000011 | 0 → 1 |
| D | 44H | 010000100 | d | 64H | 011000100 | 0 → 1 |
因此使用如下程序完成,该程序使用了两个循环。
assume cs:code, ds:datadata segment
db 'BaSic'
db 'iNfOrMaTion'
data endscode segment
start: mov ax, data
mov ds, ax mov bx, 0
mov cx, 5
s0: mov al, [bx]
and al, 11011111b
mov [bx], al
inc bx
loop s0 mov bx, 5
mov cx, 11
s1: mov al, [bx]
or al, 00100000b
mov [bx], al
inc bx
loop s1 mov ax, 4c00h
int 21h
code ends
end start

转为大写可以使用 and al, 11011111B (即 DFH) 将第 5 位清零,这样无论原来是大写(已是 0)还是小写(变为 0),结果都变为大写。

转为小写可以使用 or al, 00100000B (即 20H) 将第 5 位置 1,无论原来是小写(已是 1)还是大写(变为 1),结果都变为小写。

利用位运算处理字符大小写在现代编程中依然有效,尤其是在底层库或高性能代码中。不过,现代高级语言(如 C/Java)通常提供了标准的库函数(如 toupper, tolower)来封装这些细节。此外,现代字符编码(如 UTF-8)远比ASCII复杂,简单的位操作已不足以应对国际化和Unicode字符的大小写转换。
带偏移量的内存寻址
基址加偏移的寻址模式表示为 [bx+idata],意为内存单元的偏移地址等于 (bx) + idata。例如 mov ax,[bx+200],其数学描述为:\((ax)=((ds)\times16+(bx)+200)\)。编译器支持多种等价格式,如 200[bx] 或 [bx].200,其中 200[bx] 在可读性上更接近高级语言的风格。这和 C 语言中的数组访问是对应的,高级语言数组访问的底层实现逻辑是一个基地址加上一个变址。
带偏移量的寻址方式可以令处理多个数组变得更加简洁。例如之前的程序要将 datasg 中第一个字符串转为大写,第二个转为小写。现在可以利用 [0+bx] 定位第一个字符串,用 [5+bx] 定位第二个字符串,这样只需要一个循环然后用 BX 作为索引变量递增即可。
assume cs:code, ds:datadata segment
db 'BaSic'
db 'MinIX'
data endscode segment
start: mov ax, data
mov ds, ax mov bx, 0
mov cx, 5
s: mov al, [bx]
and al, 11011111b
mov [bx], al
mov al, [5+bx]
or al, 00100000b
mov 5[bx], al
inc bx
loop s mov ax, 4c00h
int 21h
code ends
end start

SI 和 DI 寄存器
SI(Source Index)和 DI(Destination Index) 是 8086 CPU 中两个用于在内存之间传输数据的寄存器,功能与 BX 类似。SI 和 DI 不能像 BX 那样拆分为两个 8 位寄存器,也就是在 8086 中它们只能作为 16 位寄存器使用,用来存放内存偏移地址。
考虑一个将 datasg 中的 'welcome to masm!' 复制到其后的数据区的程序,源地址是 datasg:0,目标地址是 datasg:16(字符串长 16 字节)。使用 SI 指向源地址(0),SI + 16 指向目标地址(16)。由于是字操作,需要循环 8 次每次复制 2 个字节。
assume cs:code, ds:datadata segmentdb 'welcome to masm!'db '................'
data endscode segment
start: mov ax, datamov ds, axmov si, 0mov cx, 8
s: mov ax, [si]mov 16[si], axadd si, 2loop smov ax, 4c00hint 21h
code ends
end start


在现代 x86-64 架构中,SI 和 DI 演变成了 RSI 和 RDI,并且增加了 RIP 相对寻址(RIP-relative addressing)。现代 CPU 拥有专门的字符串操作指令,如 MOVSB/MOVSW 和重复前缀(REP),配合 SI 和 DI 可以一行指令完成上述循环。此外,SI 和 DI 在 64 位模式下终于可以被拆分为低 8 位(SIL, DIL)和低 16 位(SIW, DIW)使用。
二重寻址
二重寻址方式引入了两个变量来确定内存地址,格式为 [bx+si],其物理地址计算公式为 \((ds)\times16+(bx)+(si)\)。编译器同样支持简洁写法 [bx][si],在逻辑上对应了高级语言中的二维数组访问。
考虑一个程序,它的 datasg 段包含 '1. file' 等字符串,每行 16 字节。需要将 datasg 段中每个单词的头一个字母改为大写字母,目标字符位于每行的第 4 列。此时即可使用 [bx+idata],BX 作为行指针(每次 +16),常量 3 作为列偏移。
assume cs:code, ds:datadata segment
db '1. file '
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
data endscode segment
start: mov ax, data
mov ds, ax mov bx, 0
mov cx, 6
s: mov al, [bx].3
and al, 11011111b
mov [bx].3, al
add bx, 16
loop s mov ax, 4c00h
int 21h
code ends
end start


三重寻址
结合基址寄存器(BX)、变址寄存器(SI/DI)和位移量(idata)可以实现形如 [bx+si+idata] 的三重寻址,指令 mov ax,[bx+si+idata] 的数学描述为:\((ax)=((ds)\times16+(bx)+(si)+idata)\)。常用写法如 idata[bx][si]、[bx].idata[si] 等,该结构适合用于访问结构体内部的数组,或者数组内的结构体成员。目前涉及到的几种寻址方式的小结如下:
| 寻址方式 | 示例 | 适用场景 |
|---|---|---|
| 直接寻址 | [idata] |
访问单个静态变量 |
| 寄存器间接 | [bx] |
访问一维数组(单指针) |
| 寄存器相对 | [bx+idata] |
访问结构体成员或一维数组(带起始偏移) |
| 基址变址 | [bx+si] |
访问二维数组(双指针) |
| 相对基址变址 | [bx+si+idata] |
访问结构体数组或复杂嵌套数据 |
考虑一个程序,它的 datasg 段包含 'ibm' 等 4 行字符串,每行前 3 列为字母,需要将 datasg 段中每个字符串都改为大写字母。由于二重循环需要两个计数器,但只有一个 CX。如果在内层循环中直接使用 loop,外层 CX 会被覆盖。可以考虑用 DX 暂存 CX,代码如下:
assume cs:code, ds:datadata segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
data endscode segment
start: mov ax, data
mov ds, ax
mov bx, 0
mov cx, 4s0: mov dx, cx
mov si, 0
mov cx, 3
s1: mov al, [bx+si]
and al, 11011111b
mov [bx+si], al
inc si
loop s1
mov cx, dx
add bx, 16
loop s0 mov ax, 4c00h
int 21h
code ends
end start



但如果 DX 也需要被内层使用,则该方法失效。虽然也可以在 Data Segment 中预留内存单元(如 dw 0)暂存 CX,但是这样容易混乱,不易扩展。较好的方式是使用栈,利用先进后出特性,在进入内层循环前 push cx,在内层循环结束后 pop cx。
assume cs:code, ds:data, ss:stackdata segmentdb 'ibm 'db 'dec 'db 'dos 'db 'vax '
data endsstack segmentdb 0,0,0,0,0,0,0,0
stack endscode segment
start: mov ax, datamov ds, axmov bx, 0mov cx, 4s0: push cxmov si, 0mov cx, 3
s1: mov al, [bx+si]and al, 11011111bmov [bx+si], alinc siloop s1pop cxadd bx, 16loop s0mov ax, 4c00hint 21h
code ends
end start

最后考虑一个程序,将 '1. display ' 等字符串中单词的前 4 个字母改为大写。方法是令 BX 作为行指针,每次 +16。因为单词前的 '1. ' 占 3 字节,用常量 3 加上变址寄存器 SI 遍历前 4 个字母。
assume cs:code, ds:data, ss:stackdata segmentdb '1. display 'db '2. brows 'db '3. replace 'db '4. modify '
data endsstack segmentdb 0,0,0,0,0,0,0,0
stack endscode segment
start: mov ax, datamov ds, axmov bx, 0mov cx, 4s0: push cxmov si, 0mov cx, 4
s1: mov al, [bx+si+3]and al, 11011111bmov [bx+si+3], alinc siloop s1pop cxadd bx, 16loop s0mov ax, 4c00hint 21h
code ends
end start
在现代编译器中,二重循环和栈的使用是隐式的。在 C 语言中写 for(i=0;i<4;i++) { for(j=0;j<3;j++) {...} } 时,编译器会自动生成类似上面的代码:将外层循环变量 i 保存到栈帧(Stack Frame)中,执行内层循环后再恢复 i。因此递归过深会导致栈溢出,正是因为保存现场的次数超过了栈的容量。
参考资料
《汇编语言(第四版)》,王爽 编著,清华大学出版社