兵器入门
该篇章将帮助我们理解数据在内存中是如何组织的。
首先复习 底层心法 章节的知识点:
我们常称 AX、BX、CX、DX 为通用寄存器,但它们实际上是“斜杠青年”,担任隐形的兼职,如:
| 寄存器 | 全称 | 隐形兼职(默认用途) |
|---|---|---|
| AX | Accumulator | 累加器。 乘除法指令的默认操作数和结果存放地。 |
| BX | Base | 基址寄存器。常用来存放内存地址(mov ax, [bx])。 |
| CX | Count | 计数器。loop 指令和字符串处理指令的次数。 |
| DX | Data | 数据寄存器。乘除法溢出的高位,或 I/O 端口号。 |
在 8086CPU 的时代,寄存器是非常昂贵的硬件资源,因此采取了这种硬件的高效率(不需要额外解码要操作的元器件)和低成本策略。随着时代发展,为了向下兼容以及执行效率,这些经典的“兼职”规则依然刻在 CPU 的电路板和编译器的逻辑里。
从现代角度看,这种设计略显捉襟见肘,所以 64位的 CPU 增加了更多的通用寄存器,如 R8-R15
1 指令介绍
接下来,我们将学习更多的指令工具,丰富程序处理数据的手段。
1.1 loop 指令
如果程序需要计算 $2^2$ 的结果,可以通过mov ax,2; add ax,ax来实现,但如果需要计算 $2^9$ 的结果,我们显然不希望重复的执行add ax,ax指令,这里介绍一个新的指令:使用loop来简化我们的程序。
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
当 CPU 执行loop s的时候,需要进行两步操作:
- (CX) = (CX) - 1
- 判断 CX 中的值,如果值为 0则执行下一条指令,否则转至标号 s 标识的地址处执行。
① 符号
(..)表示取单元..的值/数据 ② 这段程序里 CX 担任“计数器的兼职”
1.2 inc 指令
如果我们需要计算ffff:0到ffff:b单元中所有数据的和,并将结果存储到DX中,应当如何执行?
首先需要分析明确:
- 内存按照字节(Byte)寻址,求和取 0 到 11(十六进制b)这 12个独立内存单元的数据累加。
- 由于 DX是 16位,而内存单元的数据是 8位,命令
add dx,ds:[0]是非法的,而利用 DL进行累加,则计算结果可能超界。
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov bx,0
mov dx,0 ;初始化累加寄存器
mov cx,12 ;初始化循环计数器
s: add al,[bx]
mov ah,0 ;清零高位,确保ax高位没有垃圾数据
add dx,ax ;间接向dx中加上((ds)*16+(bx))单元的数值
inc bx ;ds:bx指向下一个单元
loop s
mov ax,4c00h
int 21h
code ends
end
这个案例中,利用 AX的低位寄存器 AL充当“中转站”,顺利解决了类型不匹配的问题。
同时,能看到有两个新的用法可以学习:
-
指令
inc的本质是原位加1,相反的dec表示自减-
最常见的是针对寄存器的操作,通常用于修改偏移地址(如
inc bx指向下一个内存单元)或循环计数。 -
它还能直接操作内存地址,当对内存地址操作时,需要使用操作符来明确宽度,如:
inc byte ptr [bx]将ds:bx指向的内存单元(8位)数值自增1;inc word ptr [bx]将ds:bx指向的连续两个内存单元(16位)数值自增1。
-
-
命令
add al,[bx]相比以往的add al,[0]更加灵活,利用了寄存器间接寻址,访问特定的内存位置在 8086 汇编中,DS 是内存寻址的缺省段寄存器。如,
ds:[bx]常被简写为[bx],此时,DS充当段前缀,而 BX负责提供偏移地址。这种缺省关系并非固定,也可以通过显式声明,使用其他寄存器作为段前缀(如
cs:、ss:、es:),从而强制 CPU 跨越默认规则去访问特定的段空间。
1.3 and 和 or 指令
前文我们使用[0]、[bx]等方法来定位内存单元的地址。接下来介绍两条更灵活的指令:and 和 or,它们主要用于对定位好的内存单元数据进行逻辑修改:
-
and 指令:逻辑与,按位进行与运算
mov al,01100011B and al,00111011B ;结果 al=00100011B指令将操作对象相应位设置为0,其他位不变。
-
or 指令:逻辑或,按位进行或运算
mov al,01100011B or al,00111011B ;结果 al=01111011B指令将操作对象相应位设置为1,其他位不变。
1.4 div 指令
这是一个除法指令,但使用div做除法的时候应该注意以下问题(以 8086CPU为例):
- 除数:有 8位和 16位,在一个寄存器或内存单元中
- 被除数:默认放在 AX 或 DX和 AX 中
- 如果除数为 8位,被除数则为 16位,默认在 AX中存放;
- 如果除数为 16位,被除数则为 32位,在 DX和 AX中存放,DX存放高 16位,AX存放低 16位。
- 结果:如果除数为8位,则 AL存储除法操作的商,AH存储除法操作的余数。
我们以 16位除法为例子进行分析,看案例:100001 / 100。
-
除数:100,放在 16位寄存器 BX中
-
被除数:100001 超过 16位,必须放在 DX和 AX中
100001 的十六进制为
186A1H,那么高 16位0001H放入 DX,低 16位86A1H放入 AX中
mov dx, 1 ; 被除数高位
mov ax, 86A1h ; 被除数低位 (DX:AX = 186A1h = 100001)
mov bx, 100 ; 除数
div bx ; 执行除法:(DX*10000H + AX) / BX
最后,结果的商和余数,也会自动存入对应的寄存器:
- 商:1000(十六进制
03E8H)自动存入 AX中 - 余数:1(十六进制
0001H)自动存入 DX中
Tips:当被除数很小,不足 16 位时,也要记得先把
dx归零噢 ~
2 数据处理
计算机中,所有的信息都是二进制的,人能理解的信息是已经具有约定意义的字符。世界上有很多编码方案,其中ASCII 编码在计算机系统中被广泛采用。
在汇编程序中,可以用 '...' 的方式指明数据是以字符的形式给出的,编译器将把他们转化成相对应的 ASCII码存储在内存的制定空间中。
2.1 多段程序
在 段与段寄存器 小节中,我们了解了段的基本概念。在实际的应用场景中,一个应用程序通常包含多个段。我们可以将它们定义出来:
assume cs:code, ds:data
data segment
db 'unIx' ; 偏移 0, 1, 2, 3 (4个字节)
db 'foRK' ; 偏移 4, 5, 6, 7 (紧随其后)
dw 0 ; 偏移 8 (预留一个字存结果或间隙)
data ends
code segment
start:
; --- 第一步:激活段地址 ---
mov ax, data
mov ds, ax ; 核心!让 DS 指向仓库 'data'
; --- 第二步:定位与读取 ---
mov al, [0] ; 读取的是 'u' (75H)
mov bl, [4] ; 读取的是 'f' (66H)
; --- 第三步:逻辑修改 (以此前学的 and/or 为例) ---
; 假设我们要把 'u' 变成大写 'U'
and al, 11011111B ; 对定位好的数据进行位运算
mov [0], al ; 写回内存,现在内存里是 'UnIx'
mov ax, 4c00h
int 21h
code ends
end start
上述代码中,我们不在把所有东西挤在一起,而是通过 segment 关键字进行"逻辑分区":
-
assume是一个条编译器指令,用来建立逻辑上的映射关系。assume cs:code:告诉编译器,code段里的内容要按“指令”来解析,并关联到CS。assume ds:data:告诉编译器,当我使用[bx]这种内存寻址时,默认去data段找,并关联到DS。 -
物理上的绑定
然而
assume是伪指令,真正进行物理绑定”激活“的,仍然是汇编代码mov ax, data ; mov ds, ax。
2.1.1 数据定义
代码中除了进行内存空间的分段,我们还根据数据的大小,选择了不同宽度的”容器“来定义数据:
| 指令 | 全称 | 长度 | 适用场景 |
|---|---|---|---|
| db | Define Byte | 8 位 (1 字节) | 字符串(如 'unIx')、小型数值。 |
| dw | Define Word | 16 位 (2 字节) | 8086 的标准字长、16位整数。 |
| dd | Define Doubleword | 32 位 (4 字节) | 现代 32 位整数、长地址指针。 |
| dq | Define Quadword | 64 位 (8 字节) | 现代 64 位架构中的地址指针。 |
2.2 伪指令和操作符
在逐渐学习中,我们接触了汇编指令,如 mov、div 等,这些指令有一一对应的机器码,CPU 的执行单元能直接识别并处理它们。
2.2.1 伪指令
而伪指令是给编译器(如MASM/TASM)看的,它们的作用是”占位“和”初始化内容“。上个小节中的 db, dw, dd, segment 等,都属于伪指令,它们通常占据一整行的主导地位,它告诉编译器“我要做什么样的内存规划”。
2.2.2 操作符
抛开伪指令,还有一些指令被称为操作符,它们通常嵌套在伪指令或硬指令之中,它负责“对数据进行加工或修饰”。
-
dup:该操作符和 db、dw 等数据定义伪指令配合使用,用来进行数据的重复定义。
如:
db 3 dup(0)表示定义 3个 值为 0的字节型数据。
2.3 寻址方式
在 8086CPU中,只有 BX、SI、DI和 BP这 4个寄存器可以用在[..]中来进行内存单元的寻址。寄存器 SI和 DI是和 BX功能相近的寄存器,但 SI和 DI不能够分成两个 8位寄存器来使用。
我们可以将寻址方式总结为以下几种类型:
| 名称 | 寻址方式 | 常用格式举例 |
|---|---|---|
| 直接寻址 | [idata] | idata表示常量 |
| 寄存器间接寻址 | [bx] 或 [si] 等 | [bx] |
| 寄存器相对寻址 | [bx + idata] 等 | 用于结构体:[bx].idata 用于数组:idata[bx] 或二维数组: |
| 基址变址寻址 | [bx + si] 等 | 用于二维数组: |
| 相对基址变址寻址 | [bx + si + idata] 等 | 用于表格(结构)中的数组项 |
只要在 [..] 中使用寄存器 BP,而指令中没有显性地给出段地址,段地址默认在 SS中。