目录

兵器入门

系列 - 汇编语言

该篇章将帮助我们理解数据在内存中是如何组织的

首先复习 底层心法 章节的知识点:

我们常称 AX、BX、CX、DX 为通用寄存器,但它们实际上是“斜杠青年”,担任隐形的兼职,如:

寄存器 全称 隐形兼职(默认用途)
AX Accumulator 累加器。 乘除法指令的默认操作数和结果存放地。
BX Base 基址寄存器。常用来存放内存地址(mov ax, [bx])。
CX Count 计数器loop 指令和字符串处理指令的次数。
DX Data 数据寄存器。乘除法溢出的高位,或 I/O 端口号。

在 8086CPU 的时代,寄存器是非常昂贵的硬件资源,因此采取了这种硬件的高效率(不需要额外解码要操作的元器件)和低成本策略。随着时代发展,为了向下兼容以及执行效率,这些经典的“兼职”规则依然刻在 CPU 的电路板和编译器的逻辑里。

从现代角度看,这种设计略显捉襟见肘,所以 64位的 CPU 增加了更多的通用寄存器,如 R8-R15

接下来,我们将学习更多的指令工具,丰富程序处理数据的手段。

如果程序需要计算 $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的时候,需要进行两步操作:

  1. (CX) = (CX) - 1
  2. 判断 CX 中的值,如果值为 0则执行下一条指令,否则转至标号 s 标识的地址处执行。

① 符号(..)表示取单元..的值/数据 ② 这段程序里 CX 担任“计数器的兼职”

如果我们需要计算ffff:0ffff:b单元中所有数据的和,并将结果存储到DX中,应当如何执行?

首先需要分析明确:

  1. 内存按照字节(Byte)寻址,求和取 0 到 11(十六进制b)这 12个独立内存单元的数据累加。
  2. 由于 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充当“中转站”,顺利解决了类型不匹配的问题。

同时,能看到有两个新的用法可以学习:

  1. 指令inc的本质是原位加1,相反的dec表示自减

    • 最常见的是针对寄存器的操作,通常用于修改偏移地址(如 inc bx 指向下一个内存单元)或循环计数

    • 它还能直接操作内存地址,当对内存地址操作时,需要使用操作符来明确宽度,如:

      inc byte ptr [bx]ds:bx指向的内存单元(8位)数值自增1;

      inc word ptr [bx]ds:bx指向的连续两个内存单元(16位)数值自增1。

  2. 命令add al,[bx]相比以往的add al,[0]更加灵活,利用了寄存器间接寻址,访问特定的内存位置

    在 8086 汇编中,DS 是内存寻址的缺省段寄存器。如,ds:[bx]常被简写为[bx],此时,DS充当段前缀,而 BX负责提供偏移地址

    这种缺省关系并非固定,也可以通过显式声明,使用其他寄存器作为段前缀(如 cs:ss:es:),从而强制 CPU 跨越默认规则去访问特定的段空间。

前文我们使用[0][bx]等方法来定位内存单元的地址。接下来介绍两条更灵活的指令:andor,它们主要用于对定位好的内存单元数据进行逻辑修改

  1. and 指令:逻辑与,按位进行与运算

    mov al,01100011B
    and al,00111011B
    ;结果 al=00100011B

    指令将操作对象相应位设置为0,其他位不变。

  2. or 指令:逻辑或,按位进行或运算

    mov al,01100011B
    or  al,00111011B
    ;结果 al=01111011B

    指令将操作对象相应位设置为1,其他位不变。

这是一个除法指令,但使用div做除法的时候应该注意以下问题(以 8086CPU为例):

  1. 除数:有 8位和 16位,在一个寄存器或内存单元中
  2. 被除数:默认放在 AX 或 DX和 AX 中
    • 如果除数为 8位,被除数则为 16位,默认在 AX中存放;
    • 如果除数为 16位,被除数则为 32位,在 DX和 AX中存放,DX存放高 16位,AX存放低 16位。
  3. 结果:如果除数为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 归零噢 ~

计算机中,所有的信息都是二进制的,人能理解的信息是已经具有约定意义的字符。世界上有很多编码方案,其中ASCII 编码在计算机系统中被广泛采用。

在汇编程序中,可以用 '...' 的方式指明数据是以字符的形式给出的,编译器将把他们转化成相对应的 ASCII码存储在内存的制定空间中。

段与段寄存器 小节中,我们了解了段的基本概念。在实际的应用场景中,一个应用程序通常包含多个段。我们可以将它们定义出来:

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

代码中除了进行内存空间的分段,我们还根据数据的大小,选择了不同宽度的”容器“来定义数据:

指令 全称 长度 适用场景
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 位架构中的地址指针。

在逐渐学习中,我们接触了汇编指令,如 movdiv 等,这些指令有一一对应的机器码,CPU 的执行单元能直接识别并处理它们。

伪指令是给编译器(如MASM/TASM)看的,它们的作用是”占位“和”初始化内容“。上个小节中的 db, dw, dd, segment 等,都属于伪指令,它们通常占据一整行的主导地位,它告诉编译器“我要做什么样的内存规划”。

抛开伪指令,还有一些指令被称为操作符,它们通常嵌套在伪指令或硬指令之中,它负责“对数据进行加工或修饰”。

  • dup:该操作符和 db、dw 等数据定义伪指令配合使用,用来进行数据的重复定义

    如:db 3 dup(0) 表示定义 3个 值为 0的字节型数据。

在 8086CPU中,只有 BX、SI、DI和 BP这 4个寄存器可以用在[..]中来进行内存单元的寻址。寄存器 SI和 DI是和 BX功能相近的寄存器,但 SI和 DI不能够分成两个 8位寄存器来使用。

我们可以将寻址方式总结为以下几种类型:

名称 寻址方式 常用格式举例
直接寻址 [idata] idata表示常量
寄存器间接寻址 [bx] 或 [si] 等 [bx]
寄存器相对寻址 [bx + idata] 等 用于结构体:[bx].idata
用于数组:idata[bx] 或二维数组:bxbxidataidata
基址变址寻址 [bx + si] 等 用于二维数组:bxbxsisi
相对基址变址寻址 [bx + si + idata] 等 用于表格(结构)中的数组项

只要在 [..] 中使用寄存器 BP,而指令中没有显性地给出段地址,段地址默认在 SS中。