├── .github └── workflows │ └── build-pdf.yml ├── 0-base.md ├── 1-machine.md ├── 2-address.md ├── 3-instruction.md ├── 4-program.md ├── 5-string.md ├── README.md ├── assets ├── 8086.png └── datapath.svg └── experiment.md /.github/workflows/build-pdf.yml: -------------------------------------------------------------------------------- 1 | # name: Build PDF 2 | 3 | # on: 4 | # push: 5 | # branches: 6 | # - master 7 | 8 | # env: 9 | # title: x86汇编程序设计笔记 10 | # docs: "1-machine.md 2-address.md 3-instruction.md 4-program.md 5-string.md experiment.md" 11 | 12 | # jobs: 13 | # build_pdf: 14 | # name: Build PDF 15 | # runs-on: ubuntu-latest 16 | # steps: 17 | # - uses: actions/checkout@v2 18 | # - name: setup chinese fonts 19 | # run: | 20 | # sudo apt-get update 21 | # sudo apt-get install -y ttf-mscorefonts-installer fontconfig 22 | # wget "https://github.com/adobe-fonts/source-han-sans/releases/download/2.004R/SourceHanSansCN.zip" 23 | # sudo mkdir -p /usr/share/fonts/adobe-source-han-sans/ 24 | # unzip SourceHanSansCN.zip 25 | # sudo cp SubsetOTF/CN/* /usr/share/fonts/adobe-source-han-sans/ 26 | # mkfontscale /usr/share/fonts/adobe-source-han-sans/ 27 | # mkfontdir /usr/share/fonts/adobe-source-han-sans/ 28 | # fc-cache -v -f /usr/share/fonts/adobe-source-han-sans/ 29 | # fc-list :lang=zh-cn 30 | # - name: setup node.js 31 | # uses: actions/setup-node@v3 32 | # with: 33 | # node-version: '17' 34 | # - name: install markdown-pdf 35 | # run: sudo npm install -g markdown-pdf 36 | # - name: convert doc to pdf 37 | # run: | 38 | # pids=() 39 | # docs="${{ env.docs }}" 40 | # for doc in $docs; do 41 | # pdf=`echo $doc | sed 's/.md/.pdf/g'` 42 | # markdown-pdf -o $pdf $doc -m '{"html":true}' & 43 | # pids+=($!) 44 | # done 45 | # for pid in ${pids[@]}; do 46 | # wait $pid 47 | # done 48 | # - name: conbine pdf 49 | # run: | 50 | # sudo apt-get install poppler-utils 51 | # find . -name '*.pdf' -type f | sort | sed '$a ${{ env.title }}.pdf' | xargs pdfunite 52 | # - uses: actions/upload-artifact@v3 53 | # with: 54 | # name: ${{ env.title }} 55 | # path: ${{ env.title }}.pdf 56 | -------------------------------------------------------------------------------- /0-base.md: -------------------------------------------------------------------------------- 1 | # 数制 2 | 3 | 由于程序设计课和计算机组成原理课已经讲过,此处不再赘述,只需要注意 MASM 中十六进制数的表示方法:以 `h` 或 `H` 结尾,如果以字母 `A-F` / `a-f` 开头还需加前缀 `0` 以避免和标识符(变量/标签/寄存器)混淆。 4 | 5 | 例如: `0AH`, `0FFFFFh`, `10h`,分别对应 C 语言中的 `0xa`, `0xFFFFF`, `0x10`。其中 `0AH` 和 `0FFFFFh` 的前缀 0 是为了区分寄存器名称 `AH` 与标识符名称 `FFFFFh`。 -------------------------------------------------------------------------------- /1-machine.md: -------------------------------------------------------------------------------- 1 | # 8086 机器 2 | 3 | ## 基本设定 4 | 5 | 结构图(非常重要) 6 | 7 |  8 | 9 | 机器字长: 16 位 (ALU,寄存器等的位宽) 10 | 11 | 地址线宽: 20 位(寻址空间为 1MB) 12 | 13 | 数据总线: 16 位 (8086), 8 位 (8088) 14 | 15 | ## **寄存器**(非常重要) 16 | 17 | - 通用寄存器: 共 4 个,每个寄存器 16 位 (`*X`),分为两个 8 位 (`*H`, `*L`) 18 | - `AX (AH, AL)` : 累加器 (**A**dd) 19 | - `BX (BH, BL)` : 基址寄存器 (**B**ase) 20 | - `CX (CH, CL)` : 计数寄存器 (**C**ount) 21 | - `DX (DH, DL)` : 数据寄存器 (**D**ata) 22 | - 指针寄存器: 2 个 23 | - `SP`: 16 位堆栈指针 24 | - `BP`: 16 位基址指针 25 | - 变址寄存器 (字符串指针) 26 | - `SI`: 源串 (**S**ource) 27 | - `DI`: 目的串 (**D**estination) 28 | - 段寄存器: 4 个 29 | - `CS`: 代码段 (**C**ode) 30 | - `DS`: 数据段 (**D**ata) 31 | - `SS`: 堆栈段 (**S**tack) 32 | - `ES`: 附加段 (**E**xtra) 33 | - 指令指针 34 | - `IP`, 相当于 RISC 中的 `PC` 35 | - 标志寄存器 `PSW` 36 | 37 | `SS:SP` 组成堆栈 (`SS` 是堆栈基地址, `SP` 是栈顶相对基地址的偏移) 38 | 39 | `CS:IP` 组成当前可执行点 (`CS` 是代码段基地址, `IP` 是当前指令相对基地址的偏移) 40 | 41 | ## 标志寄存器 42 | 43 | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 44 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 45 | | | | | |OF|DF|IF|TF|SF|ZF| |AF| |PF| |CF| 46 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 47 | 48 | 标志含义: 49 | 50 | - `OF` 溢出标志 51 | - `DF` 方向标志 (地址递增/递减) 52 | - `IF` 中断标志 53 | - `TF` 陷阱标志 54 | - `SF` 符号标志 55 | - `ZF` 结果为零标志 56 | - `AF` 辅助进位标志 57 | - `PF` 奇偶标志 58 | - `CF` 进位标志 59 | 60 | ## 存储器 61 | 62 | 8086 地址线共有 20 位,可寻址空间为 1MB。 63 | 64 | ### 总体结构 65 | 66 | +------------+ --- --- --- 67 | | 00000h | | Byte | | 68 | +------------+ --- | Word | 69 | | 00001h | | | 70 | +------------+ --- | DoubleWord 71 | | 00002h | | 72 | +------------+ | 73 | | 00003h | | 74 | +------------+ --- 75 | | | 76 | | ...... | 77 | | | 78 | +------------+ 79 | | FFFFFh | 1MB 80 | +------------+ 81 | 82 | 内存的基本单元为一个**字节** (Byte, 8 位),按线性顺序存放;两个字节组合为一个**字** (Word, 16 位),两个字组合成为一个**双字** (Double Word, 32 位)。 83 | 84 | 内存中可以任意组合存放字节、字、双字,**无需字对齐** (与 MIPS 不同)。 85 | 86 | 在内存中存放一个字时,低字节在前,高字节在后; 存放双字时,低字在前,高字在后(即: 按字节小端序存储) 87 | 88 | 例: 在 `00000h` 地址存放一个双字 `12345678h` 89 | 90 | addr : data 91 | 00000h : 78h 92 | 00001h : 56h 93 | 00002h : 34h 94 | 00003h : 12h 95 | 96 | ### 字节、字、双字的存取 97 | 98 | 例题: `CL` 中有字节 `'A'`, `BX` 中有字 `2000h`, `DX:AX` 中有双字 `12345678h`。 99 | 100 | - 依次将 `CL`, `BX`, `DX:AX` 中的内容按照字节、字、双字的结构存入地址 `30000h` 处。 101 | 102 | addr : data 103 | 30000h : 41h ; 'A' 104 | 30001h : 00h ; 2000h 105 | 30002h : 20h ; 106 | 30003h : 78h ; 12345678h 107 | 30004h : 56h ; 108 | 30005h : 34h ; 109 | 30006h : 12h ; 110 | 111 | - 将内存 `30000h` 处的一个字取出来送入 `BX` 中,将 `30002` 处的一个双字送入 `DX:AX` 中 112 | 113 | BX = 0041h [BH = 00h, BL = 41h] 114 | DX = 3456h [DH = 34h, DL = 56h] 115 | AX = 7820h [AH = 78h, AL = 20h] 116 | DX:AX = 34567820h 117 | 118 | ### 逻辑地址和物理地址 119 | 120 | 用 16 位的地址寄存器来表示地址,最多可寻址 64KB 空间。要表示 20 位地址,需要对内存进行**分段**,用一个寄存器 (段寄存器) 表示段地址,用另一个寄存器 (指针寄存器) 表示段内地址偏移。 121 | 122 | 将 1MB 内存分段,每段最大 64KB。 123 | 124 | - 物理地址: 20 位二进制,与内存单元 (字节) 一一对应 125 | - 逻辑地址: 用**段地址**和**偏移值**组合表示内存地址, 常写作 `段地址:偏移值` 的形式 126 | - 物理地址 = 段地址 x 16D(10h) + 偏移地址,一个物理地址可能有多个逻辑地址的组合。 127 | - 典型的程序在内存中执行时,一般都有**代码段**,**数据段**,**堆栈段**,其段地址分别用 `CS`, `DS`, `SS` 来存放。 128 | 129 | ### 堆栈的组成和操作 130 | 131 | 堆栈: 由 `SS` 和 `SP` 确定的一块特殊区域,严格按照**先进后出**的方式工作。增长方向为从高地址向低地址(与 MIPS 相似) 132 | 133 | 例: `SS = 2000h, SP = 0100h` 134 | 135 | - 堆栈区域为 `2000h:0000h` 至 `2000h:00FFh` 136 | - 栈顶指针值为 `0100h` (注意栈顶指针值本身不属于堆栈) 137 | - 压栈操作 `PUSH op` 138 | 1. `SP = SP - 2` 139 | 2. `op -> SS:[SP]` 140 | - 出栈操作 `POP op` 141 | 1. `op <- SS:[SP]` 142 | 2. `SP = SP + 2` 143 | -------------------------------------------------------------------------------- /2-address.md: -------------------------------------------------------------------------------- 1 | # 指令格式与寻址方式 2 | 3 | ## 指令格式 4 | 5 | Intel 8086/8088 基本指令格式: 6 | 7 | op dst, src ; 由 "源" 至 "目的", 结果在目的操作数中 8 | 9 | (分号 `;` 以后的内容为行注释) 10 | 11 | 指令可以有 2 个, 1 个或 0 个操作数: 12 | 13 | - `op op2, op1` 14 | - `op op1` 15 | - `op` 16 | 17 | 指令编码由 1-7 个字节组成,为变长编码。编码要素: 18 | 19 | - 操作码字节 20 | - 寻址方式字节 21 | - 段超越字节 22 | - 操作数 23 | 24 | ## 寻址方式 25 | 26 | 46 | 47 | ## 与数据有关的寻址方式 (6 种) 48 | 49 | (目的, 源) 操作数可以来自: 50 | 51 | - 寄存器: 8 个通用寄存器, 4 个段寄存器, 1 个标志寄存器 52 | - 立即数: 指令本身给出的立即数 (常量) 53 | - 内存单元: 直接寻址/间接寻址 54 | - 定义操作数: 55 | - `EQU`: 常量 56 | - `DB`: 字节 57 | - `DW`: 字 58 | - `DD`: 双字 59 | 60 | 寻址方式: 61 | 62 | - 立即寻址: 指令操作数包含在指令中,为一个常量或常数 63 | - 寄存器寻址: 指令操作数为 CPU 的寄存器 64 | - 直接寻址: 操作数偏移地址 `EA` 在指令中给出,如**变量名** 65 | - 寄存器间接寻址: 操作数地址 `EA` 位于**间指**寄存器 (`BX`, `BP`, `SI`, `DI`) 中 66 | - 寄存器相对寻址: 操作数地址 `EA` 由间指寄存器 + 8 位或 16 位的常量组成 67 | - 基址变址寻址: 操作数地址 `EA` 为一个基址寄存器和一个变址寄存器之和 68 | 69 | 掌握和理解寻址要点: 70 | 71 | 1. 寄存器的使用规则 72 | 2. 类型匹配 73 | 3. 数据通路 74 | 4. 操作的是"内容"还是"地址"(指针) 75 | 76 | ### 立即寻址 77 | 78 | 指令所需操作数直接包含在指令代码中,可以是一个**常量** (由 `EQU` 定义) 或者一个**常数**,成为**立即数**。 79 | 80 | 立即数可以是 8 位或 16 位,需要看与之对应的另一个操作数的类型(二者需要匹配) 81 | 82 | VALUE EQU 512 ; 定义一个常量, 名称为 VALUE, 值为 512 83 | MOV AL, 05H ; AL = 05H 84 | MOV AL, 00000101b ; AL = 05H, b 表示二进制 85 | MOV AX, 512 ; AX = 0200H (512 的十六进制) 86 | MOV AX, 512 ; AX = 0200H (VALUE 是常量,值是 512) 87 | 88 | 错误示例: 89 | 90 | MOV AL, 100H ; 100H 超出了 1 字节的范围 91 | MOV BL, VALUE ; VALUE = 512, 超出了 1 字节的范围 92 | MOV AX, 10000H ; 10000H 超出了 16 位 (一个字) 的范围 93 | 94 | ### 寄存器寻址 95 | 96 | 指令中所需的操作数是 CPU 的某个寄存器,取操作数完全在 CPU 内部进行,不需要访存。 97 | 98 | - 对于 8 位操作数,寄存器可以是 `AH`, `AL`, `BH`, `BL`, `CH`, `CL`, `DH`, `DL` 中的一个。 99 | - 对于 16 位操作数,寄存器可以是 `AX`, `BX`, `CX`, `DX`, `SP`, `BP`, `SI`, `DI` 及 `CS`, `DS`, `SS`, `ES` 的任何一个(没有 `IP`) 100 | 101 | 寄存器寻址方式示例: 102 | 103 | MOV AX, BX ; 源操作数和目的操作数都是寄存器寻址 104 | MOV AX, 1234H ; 目的操作数是寄存器寻址 105 | ADD X, AX ; 目的操作数是寄存器寻址 106 | PUSH DS ; 源操作数是寄存器寻址 107 | 108 | 109 | 110 | 当使用 `CS`, `DS`, `SS`, `ES` 段寄存器时,必须遵循数据通路要求。 111 | 112 | 操作数中的寄存器可能是隐含的寄存器(没有明确出现在指令的源或目的操作数中),例如: 113 | 114 | PUSHF ; PSW (标志寄存器) 作为源操作数,是寄存器寻址方式 115 | STD ; 设置 DF = 1, 目的操作数是寄存器寻址方式 116 | 117 | ### 直接寻址 118 | 119 | 操作数的偏移地址直接在指令中给出。例如: 120 | 121 | MOV AX, [2000H] ; 源操作数 [2000H] 为直接寻址 122 | ; 相当于 AX = *(uint16_t*)(0x2000) 123 | 124 | 以上示例中源操作数 `[2000H]` 是直接寻址。(加 `[]` 表示其内部的 `2000H` 是地址,如不加则是立即寻址) 125 | 126 | 如没有段超越,通常以这种方式直接寻址去操作数都是相对数据段 `DS` 的。 127 | 128 | 如使用变量(符号)定义内存中的单元,在指令中直接使用符号也是直接寻址,虽然操作数中并未直接出现地址,但汇编语言程序经过汇编器后,汇编器计算出符号的偏移值并进行替换。例如: 129 | 130 | ; 以下两行为伪指令, 定义了两个变量 131 | x DW ? ; 定义一个字变量(DW), ? 表示未指定初始值 132 | c DB 'A' ; 定义一个字节变量, 初始值为 41H 133 | ; 以下指令为直接寻址 134 | MOV AX, x ; 将变量 x 的字存入 AX 寄存器 135 | MOV AL, c ; 将变量 c 的字节存入 AL 寄存器 136 | ; 以下指令也是直接寻址 137 | MOV AX, x+1 ; 将内存 x+1 单元的字存入 AX 寄存器 138 | 139 | ### 寄存器间接寻址 140 | 141 | 操作数的有效地址 EA 不位于指令中,而是位于**基址寄存器 `BX`, `BP`**或**变址寄存器 `SI`, `DI`**中(不能是其他寄存器)。因为地址值未在指令中直接指出,而是通过一个寄存器来指明,因此称为间接寻址。效果上,这个寄存器相当于一个地址指针。 142 | 143 | 例如下面指令的源操作数的寻址方式都是间接寻址: 144 | 145 | MOV AX, [BX] ; 内存操作数的偏移地址位于 BX 中,在 DS 段内 146 | MOV BH, [BP] ; 内存操作数的偏移地址位于 BP 中,在 SS 段内 147 | MOV CX, [SI] ; 内存操作数的偏移地址位于 SI 中,在 DS 段内 148 | MOV DL, [DI] ; 内存操作数的偏移地址位于 DI 中,在 DS 段内。 149 | 150 | 错误示例: 151 | 152 | MOV AX, [DX] ; DX 不能作为间接寻址寄存器 153 | MOV DL, [BL] ; 只能用完整的 BX, 不能用 BL 作为间接寻址寄存器, 因为偏移值是 16 位 154 | 155 | #### 隐含段规则 156 | 157 | 使用以上 4 个寄存器进行间接寻址时,如果未显式指定段寄存器,则 `BX`, `SI`, `DI` 是相对 `DS` 段的偏移地址,而 `BP` 是相对 `SS` 段的偏移地址。 158 | 159 | 上述四条指令分别与下面四条指令等价: 160 | 161 | MOV AX, DS:[BX] 162 | MOV BH, SS:[BP] 163 | MOV CX, DS:[SI] 164 | MOV DL, DS:[DI] 165 | 166 | **注意**: 堆栈指针 `SP` 不可以用来间接寻址! 167 | 168 | #### 段超越 169 | 170 | 如果寻址是不用寄存器默认隐含的段,而是显式地指定一个段寄存器,则称为**段超越**。例如: 171 | 172 | MOV AX, SS:[BX] ; BX 默认相对段 DS,此处指定用段 SS 代替默认的 DS 173 | MOV CS:[BP], DX ; 指定段 CS 代替 BP 寄存器默认的 SS, 该指令会修改代码段,有一定危险性 174 | 175 | ### 寄存器相对寻址 176 | 177 | 操作数的有效地址 EA 是一个基址寄存器或变址寄存器的内容和指令中指定的 8 位和 16 位位移量之和。 178 | 179 | 即: EA = 间址寄存器的值 + 8 位或 16 位常量 180 | 181 | 也就是在间接寻址的基础上增加了一个常量(间接寻址 + 相对寻址)。 182 | 183 | 可用来寻址的寄存器与隐含段规则同间接寻址, `BX`, `SI`, `DI` 寄存器寻址的默认段是数据段 `DS`, `BP` 寄存器寻址的默认段是堆栈段 `SS`。 184 | 185 | 寄存器相对寻址示例(以下指令中的源操作数): 186 | 187 | MOV AX, [SI+10H] ; SI 的值加 10H 形成偏移地址, 在 DS 段内寻址 188 | 189 | 其中 `[SI+10H]` 也可以写成 `10H[SI]`,即上面的指令和下面的指令等价: 190 | 191 | MOV AX, 10H[SI] 192 | 193 | 与直接寻址一样,相对寻址的 16 位偏移量也可以是个符号名或变量名。因为符号名和变量名在段内的位置(偏移值)是固定的,所以作用等同于常量。 194 | 195 | 采用符号名进行相对寻址的示例: 196 | 197 | MOV AX, ARRAY[SI] ; EA = SI 的值 + ARRAY 相对 DS 的偏移值 198 | MOV TABLE[DI], AL ; EA = DI 的值 + TABLE 相对 DS 的偏移值 199 | MOV TABLE[DI+1], AL ; EA = DI 的值 + TABLE 的偏移值 + 1 200 | 201 | ### 基址变址寻址 202 | 203 | 操作数的有效地址 EA 等于一个基址寄存器和一个变址寄存器的内容之和。 204 | 205 | 显著特点: 两个寄存器均出现在指令中。 206 | 207 | 基址寄存器为 `BX` 或 `BP`, 变址寄存器为 `SI` 或 `DI`。如果基址寄存器为 `BX`, 则缺省段寄存器为 `DS`; 如果基址寄存器为 `BP`, 则缺省段寄存器为 `SS`。 208 | 209 | 示例: 210 | 211 | MOV AX, [BX][SI] ; 源操作数 EA = BX + SI, 段为 DS 212 | MOV AX, [BX+SI] ; 等同上一条指令 213 | MOV ES:[BX+SI], AL ; 目的操作数 EA = BX + SI, 段为 ES (采用了段超越) 214 | MOV [BP+DI], AX ; 目的操作数 EA = BP + DI, 段为 SS 215 | 216 | 可以将基址变址寻址方式理解为寄存器相对寻址方式加上一个变址寄存器。例如: 217 | 218 | MOV AX, [BX+SI+200] ; 源操作数的 EA = BX + SI + 200, 段为 DS 219 | MOV ARRAY[BP + SI], AX ; 目的操作数 EA = BP + SI + ARRAY 的偏移量, 段为 SS 220 | 221 | **注意**: 基址变址寻址方式的基址寄存器**只能为 `BX` 或 `BP`**, 变址寄存器**只能是 `SI` 或 `DI`**。 222 | 223 | 错误示例: 224 | 225 | MOV [BX+CX], AX ; CX 不能作为变址寄存器 226 | MOV [BX+BP], AX ; BP 只能用作基址寄存器, 不能用于变址寄存器 227 | MOV [BX+DI], ARRAY ; 如果 ARRAY 为变量, 则源和目的操作数都在内存中, 不合法 228 | 229 | 最后一条指令如果 ARRAY 是变量, 则源操作数是直接寻址, 源和目的操作数不能都在内存中(此部分详见**数据通路**),但 ARRAY 如果是立即数,则源操作数是立即寻址,没问题。 230 | 231 | ## 与转移地址有关的寻址方式 232 | 233 | 与转移地址有关的寻址方式主要运用于转移指令 `JMP` 和过程调用指令 `CALL`,寻址方式共有四种: 234 | 235 | - 段内直接寻址 236 | - 段内间接寻址 237 | - 段间直接寻址 238 | - 段间间接寻址 239 | 240 | ### 标号与过程名 241 | 242 | 标号示例: 243 | 244 | ... 245 | l1: MOV AX, ARRAY[SI] 246 | ... 247 | 248 | 上例中 `l1` 是一个标号,后面跟有一个冒号,一般位于一条指令的前面。标号的作用与变量名类似,确定了标号后的指令在代码段中的偏移地址。 249 | 250 | 过程名示例: 251 | 252 | ... 253 | p1 PROC near 254 | ... 255 | RET 256 | p1 ENDP 257 | 258 | 或 259 | 260 | ... 261 | p2 PROC far 262 | ... 263 | RET 264 | p2 ENDP 265 | 266 | 过程位于程序代码中,上述两个例子的 `p1` 和 `p2` 是过程名,确定了该过程第一条指令在代码段中的偏移值。其中 `p2` 还同时指出了它所处的 `CS` 段值(用 `far` 表示) 267 | 268 | ### 段内直接寻址 269 | 270 | 要转向(由 `JMP`, 条件转移,`CALL` 等)指令实际的有效地址是当前 `IP` 寄存器的内容和指令中指定的 8 位或 16 位位移量之和。 271 | 272 | 在定义了前面的标号 `l1` 或子程序名 `p1`, `p2` 后,段内直接寻址的示例: 273 | 274 | JMP l1 ; 转移至标号 l1 处 275 | CALL p1 ; 先保存 CALL 下一条指令的偏移地址至堆栈中,然后转移至 p1 处 276 | 277 | 与操作数的直接寻址方式不同的是,上述指令在汇编后,指令的机器码不会直接出现 `l1` 或 `p1` 的偏移地址,而是相对于当前 IP 的位移量,是一种相对寻址。 278 | 279 | 根据位移量是 8 位还是 16 位,可以加 `SHORT` 和 `NEAR PTR` 操作符, 如下例所示: 280 | 281 | JMP SHORT l1 ; l1 与当前 IP 的位移量是一个 8 位值 282 | JMP NEAR PTR l1 ; l1 与当前 IP 的位移量是一个 16 位值 283 | 284 | 对于**条件转移指令,只能是 8 位位移量**,省略 `SHORT` 操作符。 285 | 286 | 如果 JMP 指令省略了 `NEAR PTR` 或 `SHORT` 操作符,则使用 16 位位移量。使用 8 位位移量的转移称为短跳转。 287 | 288 | ### 段内间接寻址 289 | 290 | 转向的有效地址是一个**寄存器**或**存储单元**的内容。 291 | 292 | - 寄存器: 可以是 `AX`, `BX`, `CX`, `DX`, `SI`, `DI`, `BP`, `SP` 中的任何一个。 293 | - 存储单元: 位于数据段中,可以使用四种内存寻址方式中的任何一种。 294 | 295 | 在转移指令中使用寄存器间接寻址时,寄存器保存的是相对**代码段 `CS`**而不是数据段的偏移值。(与数据相关的寻址不同!) 296 | 297 | 示例: 用寄存器 `AX` 作为间接寻址寄存器 298 | 299 | MOV AX, OFFSET p1 ; 获取 p1 过程在代码段内的偏移值 300 | CALL AX 301 | 302 | 上例中 `AX` 也可以用 `BX`, `CX`, `DX`, `BP`, `SI`, `DI` 来代替, 且都是相对于代码段的。使用 `SP` 在语法上也允许,但逻辑上通常不这样使用。 303 | 304 | 注意段内间接寻址和数据寄存器间接寻址的差别: 305 | 306 | MOV AX, [BX] ; 数据寄存器间接寻址 307 | JMP BX ; 转移指令的段内间接寻址 308 | 309 | 数据寻址的寄存器间接方式中, `[BX]` 有方括号,相对于数据段寻址。 310 | 311 | 当间接寻址使用内存单元存放时, 要转移的偏移地址位于数据段的某个位置,而要转移至的位置则位于代码段中。 312 | 313 | 在下列指令中,转移地址同样使用的是段内间接转移: 314 | 315 | MOV AX, OFFSET p1 ; 获取过程 p1 的偏移地址,存入 AX 316 | MOV ADD1, AX ; 将 AX 内容送入数据段内的 ADD1 处 317 | CALL ADD1 ; 转移至 ADD1 中存放的偏移地址处 318 | MOV BX, OFFSET ADD1 ; 获取数据 ADD1 的偏移地址,存入 BX 319 | CALL [BX] ; 转移地址的偏移地址位于数据段中, 通过 BX 间接寻址获取 320 | 321 | 对于 `CALL ADD1` 指令,当 `ADD1` 是数据段中的一个地址而不是一个过程名/标号时,获取转移地址的方式是间接的,而不是直接的。它先从数据段 `ADD1` 确定的偏移地址中取出要转移去执行的地方的偏移值,然后再转移到代码段的此地址中。所以,当 `ADD1` 为变量名, `p1` 为过程名时,如下两条指令的差别是很大的: 322 | 323 | CALL p1 324 | CALL ADD1 325 | 326 | 前者是段 (CS) 内直接转移,后者是段内间接转移,其转移地址存放在数据段的 ADD1 处。 327 | 328 | 如果假设 `p1` 在代码段内的偏移值为 `000AH`, `ADD1` 在数据段内的偏移值为 `000AH`, 则上述两条指令在 DEBUG 下变为: 329 | 330 | CALL 000A ; 直接转移至代码段 00A 处 331 | CALL [000A] ; 转移至代码段某处, 其偏移地址位于数据段的 00A 处 332 | 333 | 对于 `CALL BX` 指令,按照同样的道理,其转移至的代码段的偏移地址不是在 `BX` 中,而是在数据段的某个位置,这个位置由 `BX` 指出。`[BX]` 是普通的数据寄存器间接寻址,获取的数据单元的内容才是要转移至代码段内的偏移地址。所以如下两条指令虽然都是段内间接转移,但转移地址是不一样的: 334 | 335 | CALL BX ; 转移到的偏移地址位于 BX 中 336 | CALL [BX] ; 转移到的偏移地址在数据段某单元中, 此单元通过 BX 间接寻址得到 337 | 338 | ### 段间直接寻址 339 | 340 | 与段内直接寻址不同的是,段间直接寻址在指令中给出了要转移至(由 `JMP` 和 `CALL` 完成)的地址的代码段和偏移值内容。例如,如果 `p2` 为由 `FAR` 属性定义的过程,则如下指令为段间直接转移: 341 | 342 | CALL FAR PTR p2 343 | 344 | 要转移至的标号或过程名必须具备 `FAR` 属性。 345 | 346 | ### 段间间接寻址 347 | 348 | 类似于段内间接寻址,但间接寻址时不能将要转移至的地址直接放入寄存器,而必须放入内存单元中,且是一个**双字**。格式如下: 349 | 350 | JMP DWORD PTR [BX+INTERS] 351 | 352 | 其中 `[BX+INTERS]` 为数据的寄存器相对寻址方式,`DWORD PTR` 后可以是除立即寻址和寄存器寻址以外(也就是内存寻址)的任何一种方式。 353 | 354 | 内存单元中的转移地址是一个双字,低位在前高位在后。转移后,低位字变成 `IP`,高位字变成 `CS`。 355 | 356 | -------------------------------------------------------------------------------- /3-instruction.md: -------------------------------------------------------------------------------- 1 | # 指令系统 2 | 3 | ## 数据通路 4 | 5 | 8086 CPU 数据通路图示: 6 | 7 |  8 | 9 | 图中有 4 种实体,实体之间有 12 条 "可能" 的数据通路。其中 9 条实心箭头,对应 **9 条数据通路**。另有 3 条带 `X` 号的虚线箭头不是有效的数据通路。 10 | 11 | 根据数据通路定义,以下的九种 `MOV` 指令是合法的(其中 `ac` 为立即数, `reg` 为通用寄存器, `segreg` 为段寄存器, `mem` 为内存): 12 | 13 | MOV reg, ac 14 | MOV reg, reg 15 | MOV reg, segreg 16 | MOV reg, mem 17 | MOV mem, ac 18 | MOV mem, reg 19 | MOV mem, segreg 20 | MOV segreg, reg 21 | MOV segreg, mem 22 | 23 | 注意: 使用段寄存器作为目的操作数时,不允许用 `CS` (修改 `CS` 只能通过段间跳转指令)。 24 | 25 | ## 段超越 26 | 27 | 采用数据的寄存器间接寻址或跳转的寄存器寻址时,寄存器中存储的都是相对于某个段的偏移量。特定寻址场景、特定寄存器对应有默认的段(隐式,一般不需要写出): 28 | 29 | - 数据访问: 30 | - `BX`, `SI`, `DI` --> `DS` 31 | - `BP` --> `SS` 32 | - 转移指令: `CS` 33 | - 串指令的 `DI` 默认相对于 `ES` 34 | 35 | 如需改变默认相对的段,则使用段超越,即在 `[]` 前加 `段寄存器:` 以指定偏移量寄存器参考的段,例如 `SS:[BX]`, `CS:[DI]`。 36 | 37 | ## 类型转换 38 | 39 | 有时仅凭符号名/变量名很难准确知道内存操作数的类型。由于内存单元的基本单位是字节,所以无论变量如何定义,都可以进行类型转换。 40 | 41 | 通过 `BYTE PTR`, `WORD PTR`, `DWORD PTR` 三个操作符,明确地指定内存操作数的类型,或进行强制类型转换。 42 | 43 | 例如: 44 | 45 | MOV BYTE PTR x, AL ; 将 8 位的 AL 存入 x 对应的内存 46 | MOV WORD PTR [DI], AX ; 将 AX 中的一个 16 位的字存入 DI 寄存器所指向的内存地址 47 | 48 | ## 主要指令 49 | 50 | - 传送指令 51 | 52 | MOV, XCHG, PUSH, POP, PUSHF, POPF 53 | LEA, LDS, LES 54 | 55 | 操作符: 56 | 57 | OFFSET, SEG ; 获取变量符号的偏移量和段地址 58 | BYTE PTR, WORD PTR, DWORD PTR ; 类型转换 59 | 60 | - 算术运算指令 61 | 62 | ADD, ADC, SUB, SBB, INC, DEC, CMP, NEG 63 | MUL, IMUL, CBW 64 | DIV, IDIV, CWD 65 | 66 | - 逻辑运算指令 67 | 68 | AND, OR, XOR, NOT 69 | TEST 70 | SHL, SHR, SAL, SAR, ROL, ROR, RCL, RCR 71 | 72 | - 控制转移指令 73 | 74 | JMP (short, near, word, far, dword) 75 | JA/JB/JE 系列, JG/JL/JE 系列 76 | LOOP, LOOPZ, LOOPNZ 77 | CALL (near, word, far, dword) 78 | RET, RETF 79 | INT, IRET 80 | 81 | - 处理器控制指令 82 | 83 | CLC, STC, CLI, STI, CLD, STD, NOT, HLT 84 | 85 | - 其他指令 86 | 87 | LODS, STOS, MOVS, CMPS, SCAS, REP ; 串处理 88 | IN, OUT 89 | 90 | ### 传送指令 91 | 92 | MOV, XCHG, PUSH, POP, PUSHF, POPF 93 | 94 | 类型转换: 打破类型匹配约定, 按照希望的类型来寻址 95 | 96 | - `BYTE PTR`, `WORD PTR`, `DWORD PTR` 97 | 98 | 段超越: 打破操作数的段缺省约定, 转向指定的段来寻址 99 | 100 | - `CS:`, `DS:`, `ES:`, `SS:` 101 | 102 | #### `MOV` 指令 103 | 104 | 语法为 `MOV DST, SRC`, 必须遵守数据通路和以下规则: 105 | 106 | - 源和目的操作数必须类型匹配(8 位对 8 位,16 位对 16 位) 107 | - 目的操作数不能是立即数 108 | - 源和目的操作数不能同时为内存操作数(串指令除外) 109 | - 源和目的操作数不能通识为段寄存器 110 | 111 | 以下指令是错误的: 112 | 113 | MOV AX, BL ; 将 BL 赋值给 AX, 扩展成 16 位 -- 类型不匹配 114 | MOV ES, DS ; 将 ES 设成与 DS 相同 -- 源和目的不能同时为段寄存器 115 | MOV y x ; 赋值 y = x -- 不能内存到内存 116 | MOV [DI], [SI] ; 间接寻址, 内存变量传送 -- 不能内存到内存 117 | 118 | 类型转换: 119 | 120 | MOV BYTE PTR x, AL 121 | MOV WORD PTR [DI] AX 122 | 123 | `MOV` 指令的运用(也称 `MOV` 体操): 124 | 125 | 1. 遵循寻址方式规则: 在哪里,怎么存取,access 126 | 2. 遵循数据通路规则: 可以 / 不可以 127 | 3. 注意默认段 ( `DS`, `SS` ) 和段超越 128 | 4. 类型匹配和类型转换: `DB`, `DW`, `DD` 的范围, 相互如何 access 129 | 130 | `MOV` 指令不改标志位,只有运算指令改标志位。 131 | 132 | #### 交换指令 `XCHG` 133 | 134 | 格式为 `XCHG OPR1, OPR2`,使两个操作数互换。不允许任何一个操作数是立即数。 135 | 136 | 示例: 137 | 138 | XCHG BX, [BP+SI] ; 交换寄存器 BX 与堆栈段 SS:[BP+SI] 的操作数 139 | 140 | #### 堆栈指令 141 | 142 | 包括 `PUSH`, `POP`, `PUSHF`, `POPF`。示例: 143 | 144 | PUSH SRC ; SP=SP-2, SS:[SP]<-SRC 145 | PUSHF ; SP=SP-2, SS:[SP]<-PSW 146 | POP DST ; DST<-SS:[SP], SP=SP+2 147 | POPF ; PSW<-SS:[SP], SP=SP+2 148 | 149 | 其中 SRC 和 DST 都可以是寄存器及内存操作数。 150 | 151 | #### 其他传送指令 152 | 153 | LEA reg, src ; 将源操作数 src 的偏移地址送入 reg 中 154 | LDS reg, src ; 将 src 中的双字内容依次送入 reg 和 DS 155 | LES reg, src ; 将 src 中的双字内容依次送入 reg 和 ES 156 | 157 | 上述三条指令中的 reg 不能是段寄存器。 158 | 159 | - `LEA` 指令: 获取 src 的偏移地址。 160 | - `LDS` 和 `LES` 获取的是该单元处的双字内容,不是地址。 161 | - `LDS`: 将低字送入 src, 高字送入 `DS` 寄存器 162 | - `LES` 与 `LDS` 类似,高字使用 `ES` 寄存器 163 | - 使用 `LDS` 及 `LES` 时,src 处保存的双字通常是某个形如 `seg:offset` 的逻辑地址。 164 | 165 | #### 取地址还是取内容 166 | 167 | 当变量名(使用 `DW`, `DB`, `DD` 等伪指令定义的变量)直接位于 `MOV` 等指令中时,都是**直接寻址**,得到的是变量的内容。如果需要得到该变量地址,需要使用 `LEA` 指令。 168 | 169 | x DW ? ; 假定 x 在 DATA 段内,偏移地址为 000AH, 内容为 1234H 170 | 171 | MOV AX, x ; AX = x = 1234H 172 | LEA BX, x ; BX = &x = 000AH 173 | MOV AX, [BX] ; AX = *BX = 1234H 174 | 175 | `OFFSET` 和 `SEG` 操作符也可用于获取地址。 176 | 177 | MOV BX, OFFSET x ; 与 LEA BX, x 相同, 获取 x 的偏移地址 178 | MOV AX, SEG x ; 179 | 180 | #### 传送指令示例 181 | 182 | 数据段: 183 | 184 | X1 EQU 100 185 | X2 DW 1234h 186 | X3 DD 20005678h 187 | 188 | 内存图 ( `地址:数据` ): 189 | 190 | DS: 0000: 34 X2 191 | 0001: 12 192 | 0002: 78 X3 193 | 0003: 56 194 | 0004: 00 195 | 0005: 20 196 | 197 | 指令示例: 198 | 199 | MOV X2, X1 ; 源: 立即寻址, 目的: 直接寻址 200 | MOV X3, X2 ; 错误: 数据通路不对, 类型不匹配 201 | 202 | ; MOV X3, X2 的正确版 203 | MOV AX, X2 ; 源: 直接寻址, 目的: 寄存器寻址 204 | MOV WORD PTR X3, AX ; 源: 寄存器寻址, 目的: 直接寻址 205 | ; 执行后 DS:[0002] <- 34h, DS:[0003] <- 12h 206 | 207 | ; 以下两条指令等价 208 | LEA BX, X3 ; 源: 直接寻址, 目的: 寄存器寻址, BX = &X3 = 0002h 209 | MOV BX, OFFSET X3; OFFSET 得到的偏移地址是 16 位的立即数 210 | 211 | #### 另一组示例 212 | 213 | 数据段: 214 | 215 | X1 DW 2000h 216 | X2 EQU 100 217 | X3 DB '1' ; 31h 218 | X4 DD 12345678h 219 | X5 DD ? 220 | 221 | 内存图 ( `地址:数据` ): 222 | 223 | DS: 0000: 00 X1 224 | 0001: 20 225 | 0002: 31 X3 226 | 0003: 78 X4 227 | 0004: 56 228 | 0005: 34 229 | 0006: 12 230 | 0007: ?? X5 231 | 0008: ?? 232 | 0009: ?? 233 | 000A: ?? 234 | 235 | 指令示例 (将 `X4` 的值赋给 `X5`): 236 | 237 | LEA DI, X5 ; DI = &X5 238 | MOV AX, WORD PTR X4 ; 源: 直接寻址, 目的: 寄存器寻址 239 | MOV [DI], AX ; 源: 寄存器寻址, 目的:寄存器间接寻址 240 | MOV AX, WORD PTR X4+2 ; 源: 直接寻址(不是相对寻址) 241 | MOV [DI+2], AX ; 目的: 寄存器相对寻址 242 | ; 每次搬运一个字, 分两次完成双字的赋值 243 | 244 | 一些错误的指令: 245 | 246 | MOV X5, X4 ; 错误, 不能内存操作数直接赋值 247 | MOV [DI], WORD PTR X4 ; 错误, 内存-内存不能赋值 248 | MOV AX, X4 ; 错误, 类型不匹配 249 | 250 | 指令示例 (将 `X3` 的值赋给 `X5`, `X5` 高位置零): 251 | 252 | 注意 `X3` 是 `BYTE`, `X5` 是双字 `DD`。 253 | 254 | 采用直接寻址: 255 | 256 | ; 搬运最低位 257 | MOV AL, X3 258 | MOV BYTE PTR X5, AL ; 目的: 直接寻址 259 | ; 置零高位 260 | XOR AL, AL ; 清零 261 | MOV BYTE PTR X5+1, AL ; *(X5+1) = 00h 262 | MOV BYTE PTR X5+2, AL 263 | MOV BYTE PTR X5+3, AL 264 | 265 | 其中置零 `X5` 的高字也可以写成 266 | 267 | XOR AX, AX ; 将 AX 清零 268 | MOV WORD PTR X5+2, AX ; *(WORD*)(X5+2) = 0000h 269 | 270 | 仍然是 `X3` 赋给 `X5`, 采用间接寻址: 271 | 272 | MOV BX, OFFSET X5 ; BX=0007h, 取地址 273 | 274 | MOV AL, X3 ; AL = X3 275 | MOV [BX], AL ; *BX = AL 276 | XOR AL, AL ; 清零 AL 277 | INC BX ; BX = BX + 1 = &X3 + 1 278 | MOV [BX], AL ; *BX = 0 279 | INC BX ; BX = &X3 + 2 280 | MOV [BX], AL 281 | INC BX ; BX = &X3 + 3 282 | MOV [BX], AL 283 | 284 | 注意: 上述代码中的 `BX` 不能换成 `BP`, 因为 `BP` 默认相对 `SS` 寻址,破坏堆栈段且 `X5` 没有改变。 285 | 286 | ### 算术运算指令 287 | 288 | #### 加减法指令 289 | 290 | ADD dst, src ; dst += src 291 | ADC dst, src ; dst += src + CF (带进位加) 292 | INC opr ; opr++ 293 | SUB dst, src ; dst -= src 294 | SBB dst, src ; dst -= src - CF (带借位减) 295 | DEC opr ; opr-- 296 | 297 | 算术运算影响符号位: 298 | 299 | - `ZF`: 如果运算结果为零则 `ZF=1` 300 | - `SF`: 等于运算结果 dst 的最高位, 即符号位 301 | - `CF`: 加法有进位或减法有借位, 则 `CF=1` 302 | - `OF`: 若操作数符号相同且相加结果符号与操作数相反, 则 `OF=1` 303 | 304 | 加法举例 305 | 306 | ; 数据定义 307 | X DW ? 308 | Y DW ? 309 | Z DD ? 310 | 311 | ; 代码实现 Z = X + Y 312 | MOV DX, 0 ; 用 DX:AX 当被加数, 先清零 DX 313 | MOV AX, 0 314 | MOV AX, X ; AX 做被加数的低 16 位 315 | ADD AX, Y ; AX += Y, 可能产生进位 CF=1 316 | ADC DX, 0 ; DX += CF 317 | MOV WORD PTR Z, AX ; 储存和的低字 318 | MOV WORD PTR Z+2, DX ; 储存和的高字 319 | 320 | 减法举例 321 | 322 | ; 数据定义 323 | X DD ? 324 | Y DD ? 325 | Z DD ? 326 | 327 | ; 代码实现 Z = X - Y 328 | MOV DX, WORD PTR X+2 ; 用 DX:AX 作被减数, DX 作高字 329 | MOV AX, WORD PTR X ; AX 作低字 330 | SUB AX, WORD PTR Y ; 先进行低 16 位减法 331 | SBB DX, WORD PTR Y+2 ; 高 16 位借位减法 332 | MOV WORD PTR Z, AX ; 储存差的低字 333 | MOV WORD PTR Z+2, DX ; 储存差的高字 334 | 335 | #### 求补和比较 336 | 337 | NEG opr ; opr = -opr 338 | CMP opr1, opr2 ; opr1 - opr2, 结果不送回, 只影响标志位 339 | 340 | #### 乘除法 341 | 342 | 无符号乘 `MUL` 343 | 344 | - 字节操作数: 8 位 x 8 位, `AX = AL * src` 345 | - 字操作数: 16 位 x 16 位, `DX:AX = AX * src` 346 | 347 | 其中 `src` 为 8 位或 16 位的 reg 或 mem, **不能是立即数**。 348 | 349 | 无符号除法 `DIV` 350 | 351 | - 字节操作数: `AX / src`, 商在 `AL`, 余数在 `AH` 352 | - 字操作数: `DX:AX / src`, 商在 `AX`, 余数在 `DX` 353 | 354 | 同理,`src` 不能是立即数。 355 | 356 | 举例: 357 | 358 | MUL AL ; AX = AL * AL 359 | MUL 10 ; 错误, src 不能为立即数 360 | DIV 10 ; 错误, src 不能为立即数 361 | MUL X1 ; X1 为 DB 或 DW 变量 362 | MUL [SI] ; 错误, 虽然可用内存操作数但类型不明 363 | MUL BYTE PTR [SI] ; 正确, AX = AL * op8 364 | MUL WORD PTR [SI] ; 正确, DX:AX = AX * op16 365 | 366 | 举例 `Y=X*10` 367 | 368 | X DW ? 369 | Y DW ? 370 | 371 | MOV AX, X 372 | MOV BX, 10 373 | MUL BX ; DX:AX = AX * BX 374 | MOV Y, AX ; 只考虑低 16 位, 不考虑溢出 375 | 376 | 举例 `X=Y/10` 377 | 378 | X DW ? 379 | Y DW ? 380 | MOV AX, Y 381 | MOV BX, 10 382 | MOV DX, 0 ; 清零 DX, 被除数是 DX:AX 383 | DIV BX ; DX:AX / BX, 商在 AX 余数在 DX 384 | MOV X, AX ; 忽略余数 385 | 386 | 注意这里不能用 8 位除法,否则会溢出。 387 | 388 | #### 逻辑运算 389 | 390 | AND dst, src ; dst &= src 391 | OR dst, src ; dst |= src 392 | XOR dst, src ; dst ^= src 393 | NOT dst ; dst = ~dst 394 | 395 | 可以用这些指令实现组合/屏蔽/分离/置位,例如: 396 | 397 | AND AL, 0FH ; 清零高 4 位 398 | AND AL, F0H ; 清零低 4 位 399 | AND AL, FEH ; 清零最低位 400 | OR AL, 80H ; 最高位置 1 401 | XOR AL, AL ; 清零 AL, 等价于 MOV AL, 0 且效率更高 402 | 403 | OR AL, 30H ; 将 0~9 变为 '0'~'9' 404 | AND AL, 0FH ; 将 '0'~'9' 变为 0~9 405 | 406 | #### 移位指令 407 | 408 | SHL dst, count ; 逻辑左移 409 | SAL dst, count ; 算术左移 410 | SHR dst, count ; 逻辑右移 411 | SAR dst, count ; 算术右移 412 | ROL dst, count ; 循环左移 413 | ROR dst, count ; 循环右移 414 | RCL dst, count ; 进位循环左移 415 | RCR dst, count ; 进位循环右移 416 | 417 | 注意: `count` 只能为 `1` 或 `CL` 418 | 419 | 示例 `X = X * 10`, `X = (X << 3) + (X << 1)` 420 | 421 | X DW ? 422 | 423 | MOV BX, X 424 | SHL BX, 1 425 | PUSH BX ; X << 1 426 | SHL BX 1 427 | SHL BX 1 ; X << 3 428 | POP AX ; AX = X << 1 429 | ADD AX, BX ; AX = (X<<1) + (X<<3) 430 | MOV X, AX 431 | 432 | 示例 双字 `X`,`X << 4` 433 | 434 | X DD ? 435 | 436 | MOV AX, WORD PTR X 437 | MOV DX, WORD PTR X+2 438 | SHL AX, 1 ; 移出 CF 439 | RCL DX, 1 ; 移入 CF 440 | SHL AX, 1 441 | RCL DX, 1 442 | SHL AX, 1 443 | RCL DX, 1 444 | SHL AX, 1 445 | RCL DX, 1 446 | 447 | ; 也可以用循环实现但不如展开的效率高 448 | MOV CX, 4 449 | LP1: 450 | SHL AX, 1 451 | RCL DX, 1 452 | LOOP LP1 453 | 454 | 455 | ### 转移指令 456 | 457 | #### 条件转移 458 | 459 | - 无符号比较: `JA`/`JB`/`JE` 系列 (**A**bove / **B**elow / **E**qual) 460 | - 有符号比较: `JG`/`JL`/`JE` 系列 (**G**reater / **L**ess / **E**qual) 461 | 462 | 指令格式: `JX 标号` ,比较的依据是**标志位**(紧跟 `CMP` 指令)。标号位于指令前面,实质是一个段内偏移值。 463 | 464 | 无符号数的条件转移指令: 465 | 466 | - `JA` (`JNBE`): 无符号高于时转移 467 | - `JAE` (`JNB` / `JNC`): 无符号高于等于时转移 (`CF=0` 时转移) 468 | - `JE` (`JZ`) 等于时转移 (`ZF=1` 时转移) 469 | - `JBE` (`JNA`): 无符号低于等于时转移 470 | - `JB` (`JNAE` / `JC`): 无符号低于时转移 (`CF=1` 时转移) 471 | - `JNE` (`JNZ`): 不等于时转移 (`ZF=0` 时转移) 472 | 473 | 示例 求 `Z=|X-Y|`,`X`, `Y`, `Z` 都是无符号数。 474 | 475 | MOV AX, X 476 | CMP AX, Y ; if (AX < Y) swap AX, Y 477 | JAE L1 ; AX >= Y 则跳过交换 478 | XCHG AX, Y ; 交换 AX 和 Y 479 | L1: SUB AX, Y ; AX -= Y 480 | MOV Z, AX 481 | 482 | #### 循环指令 483 | 484 | 格式: `LOOP 标号`。 485 | 486 | `LOOP`: 先 `CX -= 1`, **然后**当 `CX` 不为零时转移。(与循环体无关,只是个转移指令) 487 | 488 | `JCXZ`: 当 `CX=0` 时转移(不执行 `CX -= 1`) 489 | 490 | 示例 491 | 492 | MOV CX, 4 493 | LP1: 494 | ...... 495 | LOOP LP1 ; loop body is executed 4 times 496 | 497 | MOV CX, 4 498 | LP2: 499 | DEC CX 500 | JCXZ LP2 501 | 502 | #### 无条件转移指令 503 | 504 | `JMP` 指令, 格式: `JMP 标号|寄存器操作数|内存操作数` 505 | 506 | 1. 段内直接短转移: `JMP SHORT PTR 标号`, EA 是 8 位 507 | 2. 段内直接转移: `JMP NEAR PTR 标号` 508 | 3. 段内间接转移: `JMP WORD PTR 寄存器或内存` 509 | 4. 段间直接转移: `JMP FAR PTR 标号` 510 | 5. 段间间接转移: `JMP DWORD PTR 寄存器或内存` 511 | 512 | #### 子程序调用 513 | 514 | - `CALL` 指令: 子程序调用 515 | - `RET` 指令: 从子程序中返回 516 | 517 | `CALL` 指令: 518 | 519 | 1. 段内直接调用: `CALL dst` 520 | - `SP=SP-2`, `SS:[SP]`: 返回地址偏移值 521 | - `IP=IP+dst` 522 | 2. 段内间接调用: `CALL dst` 523 | - `SP=SP-2`, `SS:[SP]`: 返回地址偏移值 524 | - `IP=*dst` 525 | 3. 段间直接调用: `CALL dst` 526 | - `SP=SP-2`, `SS:[SP]`: 返回地址段值 527 | - `SP=SP-2`, `SS:[SP]`: 返回地址偏移值 528 | - `IP=OFFSET dst` 529 | - `CS=SEG dst` 530 | 4. 段间间接调用: `CALL dst` 531 | - `SP=SP-2`, `SS:[SP]`: 返回地址段值 532 | - `SP=SP-2`, `SS:[SP]`: 返回地址偏移值 533 | - `IP` 为 `EA` 的低 16 位 534 | - `CS` 为 `EA` 的高 16 位 535 | 536 | 段内调用,`dst` 应为 `NEAR PTR`, 段间调用则为 `FAR PTR`。 537 | 538 | 示例 539 | 540 | CALL P1 ; 段内直接调用 P1, P1 为 NEAR 541 | CALL NEAR PTR P1 ; 同上 542 | CALL P2 ; 段间直接调用 P2, P2 为 FAR 543 | CALL FAR PTR P2 ; 同上 544 | CALL BX ; 段内间接寻址, 过程地址位于 BX 中 545 | CALL [BX] ; 段内间接地址, 过程地址位于数据段中 546 | CALL WORD PTR [BX] ; 同上 547 | 548 | `RET` 指令: 549 | 550 | 1. 段内返回: `RET` 551 | - `IP=[SP]`, `SP=SP+2` 552 | 2. 段内带立即数返回: `RET exp` 553 | - `IP=[SP]`, `SP=SP+2` 554 | - `SP=SP+exp` 555 | 3. 段间返回: `RET` 556 | - `IP=[SP]`, `SP=SP+2` 557 | - `CS=[SP]`, `SP=SP+2` 558 | 4. 段间带立即数返回: `RET exp` 559 | - `IP=[SP]`, `SP=SP+2` 560 | - `CS=[SP]`, `SP=SP+2` 561 | - `SP=SP+exp` 562 | 563 | 过程的定义 564 | 565 | 过程名 PROC [near | far] 566 | 过程体 567 | RET 568 | 过程名 ENDP 569 | 570 | ## 应用举例 571 | 572 | 将内存中的值 x 显示为 10 进制 573 | 574 | MOV AX, X ; 取 AX 575 | XOR DX, DX ; DX:AX 作为被除数 576 | MOV BX, 10000 ; 依次除以 10000, 1000, 100, 10 显示商 577 | DIV BX ; DX:AX / BX, 商在 AX, 余数在 DX 578 | PUSH DX ; 保存余数 579 | MOV DL, AL ; 显示的字符送 DL 580 | OR DL, 30H ; 0~9 -> '0'~'9' 581 | MOV AH, 2 ; DOS 2 号功能调用, 显示 DL 的 ASCII 字符 582 | INT 21H ; DOS 功能调用 583 | 584 | POP AX ; 上次的余数作为被除数 585 | XOR DX, DX 586 | MOV BX, 1000 ; 上次 10000, 这次 1000 587 | DIV BX 588 | PUSH DX 589 | MOV DL, AL 590 | OR DL, 30H 591 | MOV AH, 2 592 | INT 21H 593 | 594 | POP AX 595 | XOR DX, DX 596 | MOV BX, 100 ; 1000 -> 100 597 | DIV BX 598 | PUSH DX 599 | MOV DL, AL 600 | OR DL, 30H 601 | MOV AH, 2 602 | INT 21H 603 | 604 | POP AX 605 | XOR DX, DX 606 | MOV BX, 10 ; 100 -> 10 607 | DIV BX 608 | PUSH DX 609 | MOV DL, AL 610 | OR DL, 30H 611 | MOV AH, 2 612 | INT 21H 613 | 614 | POP AX 615 | MOV DL, AH ; 最后一个余数送给输出 616 | OR DL, 30H ; 转成 ASCII 码 617 | MOV AH, 2 618 | INT 21H 619 | 620 | 以上代码也可以写成循环(次数为 4),不如展开了效率高 621 | -------------------------------------------------------------------------------- /4-program.md: -------------------------------------------------------------------------------- 1 | # 程序结构 2 | 3 | ## 三段式程序结构 4 | 5 | 堆栈段, 数据段, 程序段。 6 | 7 | 定义堆栈段: 8 | 9 | STACK SEGMENT PARA STACK 10 | STACK_AREA DW 100h DUP(?) 11 | STACK_TOP EQU $-STACK_AREA 12 | STACK ENDS 13 | 14 | 定义数据段: 15 | 16 | DATA SEGMENT PARA 17 | TABLE_LEN DW 16 18 | TABLE DW 200, 300, 400, 10, 20, 0, 1, 8 19 | DW 41H, 40, 42H, 50, 60, 0FFFFH, 2, 3 20 | DATA ENDS 21 | 22 | ## 定义段 23 | 24 | 伪指令: `SEGMENT` 和 `ENDS` 25 | 26 | 格式: 27 | 28 | 段名 SEGMEMT [对齐类型] [组合类型] [类别名] 29 | ; 本段中的程序和数据定义语句 30 | 段名 ENDS 31 | 32 | 对齐类型: 定义了段在内存中分配时的起始边界设定 33 | 34 | - `PAGE`: 本段从页边界开始,一页为 256B 35 | - `PARA`: 本段从节边界开始,一节为 16B 36 | - `WORD`: 本段从字对齐地址(偶地址)开始,段间至多 1B 空隙 37 | - `BYTE`: 本段从字节地址开始,段间无空隙 38 | 39 | 组合类型: 确定段与段之间的关系 40 | 41 | - `STACK`: 该段为堆栈段的一部分。链接器链接时,将所有同名的具有 `STACK` 组合类型的段连成一个堆栈段,并将 `SS` 初始化为其首地址,`SP` 为段内最大偏移。(正确定义段的 `STACK` 属性可以在主程序中省略对 `SS` 和 `SP` 的初始化) 42 | 43 | ## 定义过程 44 | 45 | 伪指令: `PROC`, `ENDP`, `END 标号` 46 | 47 | 格式: 48 | 49 | 过程名 PROC [NEAR|FAR] 50 | ; 过程代码 51 | RET 52 | 过程名 ENDP 53 | 54 | - 如果过程为 `FAR` 则 `RET` 被编译为 `RETF`(段间返回) 55 | - `RET 2n`: 在 `RET` 或 `RETF` 后 `SP+=2n` 56 | 57 | `END 标号` 为程序总结束, 标号为入口(被设置为初始的 `CS:IP` 值) 58 | 59 | ## 定义数据 60 | 61 | 格式: 62 | 63 | 变量名 伪指令 值 64 | 65 | 用 `DB`, `DW`, `DD` 伪指令定义内存中的变量,变量名对应内存地址。 66 | 67 | 用 `EQU` 伪指令定义常量,不占内存,变量名被翻译成立即数。 68 | 69 | 值的表示: 70 | 71 | - 常数 72 | - `DUP` 重复操作符(用于定义数组,或定义堆栈空间) 73 | - `?` 不预置任何内容 74 | - 字符串表达式 75 | - 地址表达式 76 | - `$` 当前位置计数器 77 | 78 | ### `DUP` 表达式 79 | 80 | ARRAY1 DB 2 DUP (0, 1, 0FFH, ?) 81 | ARRAY2 DB 100 DUP (?) 82 | 83 | 其中 `ARRAY1` 定义了一个二维数组,等价于 84 | 85 | ARRAY1 DB 0, 1, 0FFH, ?, 0, 1, 0FFH, ? 86 | 87 | `ARRAY2` 定义了一段长度为 100 字节的连续空间,值未初始化。 88 | 89 | `DUP` 表达式可以嵌套,相当于定义多维数组。 90 | 91 | ARRAY3 DB 2 DUP(1, 2 DUP ('A', 'B'), 0) ; 'A', 'B' 为字符 ASCII 码 41H 和 42H 92 | 93 | 对应的内存图: 94 | 95 | ARRAY3+ 0000: 01H 96 | 0001: 41H 97 | 0002: 42H 98 | 0003: 41H 99 | 0004: 42H 100 | 0005: 00H 101 | 0006: 01H 102 | 0007: 41H 103 | 0008: 42H 104 | 0009: 41H 105 | 000A: 42H 106 | 000B: 00H 107 | 108 | ### 地址表达式 109 | 110 | 使用 `DW` 及 `DD` 伪指令后跟标号,表示变量的偏移地址或逻辑地址。使用 `DD` 存储逻辑地址时,低字为偏移地址,高字为段地址。 111 | 112 | 当前位置计数器: 用 `$` 表示,为当前标号所在的偏移地址。 113 | 114 | X1 DW ? ; X1 内容未定义 115 | X2 DW $ ; X2 单元存放当前 (X2 自身的) 偏移地址 116 | X3 DW X1 ; X3 单元存放 X1 偏移地址 117 | X4 DW L1 ; X4 单元存放 L1 标号的偏移地址 118 | X5 DW P1 ; X5 单元存放 P1 子程序的偏移地址 119 | X6 DD X1 ; X6 单元存放 X1 的逻辑地址 120 | X7 DD L1 ; X7 单元存放 L1 标号的逻辑地址 121 | X8 DD P1 ; X8 单元存放 P1 子程序的逻辑地址 122 | 123 | ### 字符串表达式 124 | 125 | STR1 DB 'ABCD', 0DH, 0AH, '$' 126 | STR2 DW 'AB', 'CD' 127 | 128 | 其中 `STR1` 分配了 7 个单元(字节),按顺序存放。其中 `$` 为字符串的结束(使用 DOS 9 号功能调用输出字符串,结果为 `ABCD\r\n` ) 129 | 130 | `STR2` 分配了两个字(4 个字节),按顺序其值分别为 `42H`, `41H`, `44H`, `43H`。对于 `DW` 和 `DD` 伪指令,不允许使用两个以上字符的字符串作为其参数。 131 | 132 | ## 初始化寄存器 133 | 134 | MOV AX, STACK 135 | MOV SS, AX 136 | MOV SP, STACK_TOP 137 | MOV AX, DATA 138 | MOV DS, AX 139 | 140 | `ASSUME` 伪指令: 告诉汇编器,将 `CS`, `DS`, `SS` 分别设定成相应段的首地址(仅仅是"设定",并没有真正将地址存入段寄存器,仍然需要使用上述几条指令来初始化段寄存器) 141 | 142 | ASSUME CS:CODE, DS:DATA, SS:STACK 143 | 144 | ## DOS 功能调用 145 | 146 | 调用指令: `INT 21H`,其中 `21H` 表示 DOS。 147 | 148 | 用寄存器 `AH` 指定功能调用号。 149 | 150 | ### 返回 DOS 151 | 152 | MOV AH, 4CH ; AH = 4C, DOS 4C 号功能调用: 返回 DOS 153 | MOV AL, 00H ; AL = 给 DOS 的返回值 154 | INT 21H ; 21H 表示 DOS 功能调用 155 | 156 | ### 输入输出 157 | 158 | 输入一个 ASCII 字符: 1 号功能调用 159 | 160 | MOV AH, 1 ; 1 号功能调用 161 | INT 21H 162 | MOV BYTE PTR X, AL ; 结果存储在 AL 中 163 | 164 | 输出一个 ASCII 字符: 2 号功能调用 165 | 166 | MOV DL, 'A' ; 输出的字符在 DL 中 167 | MOV AH, 2 ; 2 号功能调用 168 | INT 21H ; 输出的字符保存在 AL 中 (不确定) 169 | 170 | **注意**: `AL` 寄存器会改变(书上没找到,但是自己试出来了)! 171 | 172 | 输入字符串: `0AH` 号功能调用 173 | 174 | 使用时需要先构造一个特定格式的缓冲区,定义输入的最大允许长度。使用时键入一行字符串并按回车键。 175 | 176 | ; ---- DATA SEGMENT PARA ---- 177 | ; 构造一个特定格式的缓冲区 178 | LEN EQU 101 ; 允许输入字符个数(含回车) 179 | BUFFER DB LEN-1 ; 预设最大长度(不含回车) 180 | DB ? ; 实际输入长度(不含回车) 181 | DB LEN DUP(?) ; 输入的串 182 | ; ---- DATA ENDS ---- 183 | 184 | LEA DX, BUFFER ; 设定 DS:DX 为缓冲区首地址 185 | MOV AH, 0AH ; 0AH 号功能调用 186 | INT 21H 187 | 188 | 输出字符串: 9 号功能调用 189 | 190 | LEA DX, STR ; 输出的字符串首地址在 DX 中 191 | MOV AH, 9 ; 9 号功能调用 192 | INT 21H 193 | 194 | ## 一个完整的程序示例 195 | 196 | STACK SEGMENT PARA STACK 197 | STACK_AREA DW 100h DUP(?) 198 | STACK_TOP EQU $-STACK_AREA 199 | STACK ENDS 200 | 201 | DATA SEGMENT PARA 202 | MESSAGE DB 'Hello, World!', '$' 203 | DATA ENDS 204 | 205 | CODE SEGMENT 206 | ASSUME CS:CODE, DS:DATA, SS:STACK 207 | 208 | MAIN PROC 209 | ; initialize 210 | MOV AX, STACK 211 | MOV SS, AX 212 | MOV SP, STACK_TOP 213 | MOV AX, DATA 214 | MOV DS, AX 215 | ; display message 216 | MOV AH, 9 217 | LEA DX, MESSAGE 218 | INT 21H 219 | ; return to dos 220 | MOV AX, 4C00H 221 | INT 21H 222 | 223 | MAIN ENDP 224 | CODE ENDS 225 | END MAIN 226 | -------------------------------------------------------------------------------- /5-string.md: -------------------------------------------------------------------------------- 1 | # 字符串处理 2 | 3 | ## 串操作指令 4 | 5 | ### 使用方法 6 | 7 | 串操作隐含着间接寻址: `DS:[SI] --> ES:[DI]` 8 | 9 | 使用串指令的常见过程: 10 | 11 | - 设置 `DS`, `SI` (源串), `ES`, `DI` (目的串) 12 | - `DS` 和 `ES` 是段寄存器,不能直接用 `MOV ES, DS` 赋值 13 | - 借助其他寄存器 (`MOV AX, DS`, `MOV ES, AX`) 或堆栈 (`PUSH DS`, `POP ES`) 14 | - 设置 `DF` 标志 (Direction Flag) 15 | - 通过 `CLD` ( `DF=0` ) 和 `STD` ( `DF=1` ) 指令 16 | - `DF=0` 则每次 `SI/DI++` (或 `+=2`), `DF=1` 则每次 `SI/DI--` (或 `-=2`) 17 | - 选用重复执行指令 `REP*` 以及设置重复次数 `CX` 18 | - 重复指令包括无条件重复 `REP`, 有条件重复 `REPE/REPZ`, `REPNE/REPNZ` 19 | - `CX` 表示最大的重复次数 20 | - 重复的串指令每执行一次则 `CX--` 21 | 22 | ### 指令种类 23 | 24 | 串指令种类: 25 | 26 | - 取串指令 `LODSB` / `LODSW` 27 | - 将源串 `DS:[SI]` 的一个字节或字取到 `AL` 或 `AX`,同时按照 `DF` 修改 `SI` 的值。 28 | - `LODSB` 取字节, `LODSW` 取字 29 | - 存串指令 `STOSB` / `STOSW` 30 | - 将 `AL` 中的一个字节或 `AX` 的一个字存入目的串 `ES:[DI]`,并根据 `DF` 修改 `DI`。 31 | - `STOSB` 存字节,`STOSW` 存字 32 | - 串传送指令 `MOVSB` / `MOVSW` 33 | - 将源串 `DS:[SI]` 的字节或字传送到目的串 `ES:[DI]`,并根据 `DF` 修改 `SI` 及 `DI`。 34 | - `MOVSB` 传送字节,`MOVSW` 传送字 35 | - 串比较指令 `CMPSB` / `CMPSW` 36 | - 比较源串 `DS:[SI]` 与目的串 `ES:[DI]` 的一个字节或字。执行完后根据 `DF` 修改 `SI` 及 `DI`。 37 | - 用源串的字节或字减去目的串的字节或字,影响标志寄存器 `CF`, `SF`, `ZF` 。 38 | - 相当于 `CMP DS:[SI], ES:[DI]` 39 | - 后面往往跟着条件转移指令 40 | - 串扫描指令 `SCASB` / `SCASW` 41 | - 在目的串 `ES:[DI]` 中扫描是否有 `AL` 或 `AX` 指定的字节或字。执行完后根据 `DF` 修改 `SI` 及 `DI`。 42 | - 相当于 `CMP AL/AX, ES:[DI]` 43 | - 比较结果不保存,只影响标志寄存器。 44 | 45 | 重复前缀指令 `REP`: 46 | - 格式: `REP 串操作指令`,例如 `REP MOVSB` 47 | - 每执行一次串操作,`CX--`,直到 `CX` 为零时停止。 48 | 49 | 条件重复前缀指令 `REPE` / `REPZ`, `REPNE` / `REPNZ` 50 | - 格式与 `REP` 指令相似 51 | - 每执行一次,`CX--`,当 `CX` 等于零或 `ZF` 不满足条件时停止。 52 | 53 | - `REPE` / `REPZ`: 当 `CX != 0` 且 `ZF=1` 时执行串操作并 `CX--` 54 | - `REPNE` / `REPNZ`: 当 `CX != 0` 且 `ZF=0` 时执行串操作并 `CX--` 55 | 56 | 修改 `DF` 方向标志指令 57 | - `CLD`: `DF=0`,执行串操作后 `SI` 和 `DI` 增加(根据字节或字操作决定 `+1` 还是 `+2`) 58 | - `STD`: `DF=1`,执行串操作后 `SI` 和 `DI` 减少(根据字节或字操作决定 `-1` 还是 `-2`) 59 | 60 | ## 基本应用 61 | 62 | - 用 `REP STOSB` 指令将长度为 `LEN` 的缓冲区 `BUFF` 清零。 63 | 64 | PUSH ES ; 修改 ES 前先保存 65 | 66 | PUSH DS 67 | POP ES ; ES = DS 68 | 69 | MOV DI, OFFSET BUFF ; DI = BUFF 首地址 70 | ; 也可以用 LEA DI, BUFF 71 | 72 | MOV CX, LEN ; CX = BUFF 长度 73 | CLD ; 设置方向为自增 74 | MOV AL, 0 ; AX = 要存入的字节 75 | REP STOSB ; 重复 CX 次执行 STOSB 76 | 77 | POP ES ; 恢复 ES 78 | 79 | 对应 C 语言: 80 | 81 | ```c 82 | memset(BUFF, 0, LEN); 83 | ``` 84 | 85 | - 用 `REP MOVSB` 指令将缓冲区 `BUFF1` 内容传送到 `BUFF2`, 长度为 `LEN`。 86 | 87 | PUSH ES 88 | 89 | PUSH DS 90 | POP ES ; ES = DS 91 | 92 | MOV SI, OFFSET BUFF1 ; LEA SI, BUFF1 93 | MOV DI, OFFSET BUFF2 ; LEA DI, BUFF2 94 | 95 | MOV CX, LEN ; CX = 复制的长度 96 | CLD ; 方向自增 97 | REP MOVSB 98 | 99 | POP ES 100 | 101 | 对应 C 语言: 102 | 103 | ```c 104 | memcpy(BUFF2, BUFF1, LEN); 105 | ``` 106 | 107 | - 将长度为 `LEN` 的缓冲区 `BUFF1` 中的小写字母变成大写。 108 | 109 | PUSH ES 110 | 111 | PUSH DS 112 | POP ES ; ES = DS 113 | 114 | MOV SI, OFFSET BUFF1 ; LEA SI, BUFF1 115 | MOV DI, SI 116 | 117 | MOV CX, LEN 118 | CLD 119 | 120 | LP1: 121 | LODSB ; AL <- DS:[SI], SI++ 122 | CMP AL, 'a' 123 | JB CONTINUE 124 | CMP AL, 'z' 125 | JA CONTINUE 126 | SUB AL, 20H ; 小写变大写 127 | 128 | CONTINUE: 129 | STOSB ; ES:[DI] <- AL, SI-- 130 | LOOP LP1 131 | 132 | POP ES 133 | 134 | 对应 C 语言: 135 | 136 | ```c 137 | int CX = LEN; 138 | char *SI = BUFF1, *DI = *SI; 139 | while (cx--) { 140 | char AL = *SI; 141 | if (AL >= 'a' && AL <= 'z') 142 | AL -= 0x20; 143 | *DI = AL; 144 | SI++, DI++; 145 | } 146 | ``` 147 | 148 | - 比较 `STRING1` 与 `STRING2` 按字典序排序的大小,假定都是大写字母且长度都为 `LEN`。 149 | 150 | PUSH ES 151 | 152 | PUSH DS 153 | POP ES ; ES = DS 154 | 155 | MOV SI, OFFSET STRING1 156 | MOV DI, OFFSET STRING2 157 | 158 | MOV CX, LEN 159 | CLD 160 | REPZ CMPSB 161 | ; while (CX != 0 && DS:[SI] == ES:[DI]) SI++, DI++, CX--; 162 | 163 | JZ EQUAL ; 两字符串相等 164 | JA GREATER ; STRING1 > STRING2 165 | JB LESS ; STRING1 < STRING2 166 | 167 | ; 后续处理 168 | EQUAL: 169 | ; ...... 170 | 171 | GREATER: 172 | ; ...... 173 | 174 | LESS: 175 | ; ...... 176 | 177 | POP ES 178 | 179 | 对应 C 语言: 180 | 181 | ```c 182 | strncmp(STRING1, STRING2, LEN); 183 | ``` 184 | 185 | 上述代码还可以通过 `CX` 的值判断比较操作进行了多少次,如果 `CX=0` 则比较至最后了。 186 | 187 | - 扫描长度为 `LEN` 的串 `STRING1` 中是否含有字母 `A` 188 | 189 | PUSH ES 190 | 191 | PUSH DS 192 | POP ES ; ES = DS 193 | 194 | MOV DI, OFFSET STRING1 195 | 196 | MOV CX, LEN 197 | MOV AL, 'A' 198 | CLD 199 | REPNZ SCASB 200 | ; while (CX != 0 && AL != ES:[DI]) CMP AL, ES:[DI] / SI++,DI++ 201 | 202 | JZ FOUND ; 找到 203 | 204 | POP ES 205 | 206 | `REPNZ SCASB` 停下来后,如果 `ZF=1` 则 `AL=ES:[DI]`,通过 `CX` 的值可以看出 `A` 在 `STRING1` 中的位置。如果 `ZF=0` 则已经遍历到最后仍未找到。 207 | 208 | ## 综合应用 209 | 210 | - 在缓冲区中查找 `\r\n` (`0DH`, `0AH`) 并将其删掉(将若干行文本拼接成不换行的文本) 211 | - 在缓冲区查找换行符 `0AH` 并将其补成回车换行 `0DH, 0AH`。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # X86 汇编程序设计 2 | 3 | 课程笔记,内容全部基于熊桂喜老师的授课 PPT 及课程配套教材。 4 | 5 | 常用代码位于 `code` 分支。实验环境说明位于 `experiment.md` 文档中。 6 | 7 | PDF 版笔记已导出,可从 Releases 下载。 -------------------------------------------------------------------------------- /assets/8086.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhy2000/x86Course/a7577bf8ff4c0d509a0e89e18dbcef62454ea041/assets/8086.png -------------------------------------------------------------------------------- /assets/datapath.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 通用寄存器AX,BX,CX,DXSP,BP,SI,DI通用寄存器AX,BX,CX,DX...段寄存器CS,DS,SS,ES段寄存器 CS,DS,SS,ES内存内存XX立即数立即数XXXXText is not SVG - cannot display -------------------------------------------------------------------------------- /experiment.md: -------------------------------------------------------------------------------- 1 | # 实验环境说明 2 | 3 | 实验环境: dosbox + MASM 4 | 5 | ## dosbox 6 | 7 | 安装 (Arch Linux): 8 | 9 | yay -S dosbox 10 | 11 | 安装 (Ubuntu/Debian): 12 | 13 | sudo apt install dosbox 14 | 15 | 启动: 16 | 17 | dosbox 18 | 19 | 调整窗口大小: 20 | 21 | 配置文件位于 `~/.dosbox/dosbox-0.74-3.conf`,编辑配置文件 `[sdl] `分组下的 `windowresolution` 和 `output` 字段: 22 | - 将 `windowresolution` 的值由 `original` 调整成希望的尺寸,格式形如 `1280x800` 23 | - 将 `output` 字段的值由 `surface` 调整为 `opengl` 24 | 25 | 原始配置: 26 | 27 | ```configuration 28 | [sdl] 29 | 30 | windowresolution=original 31 | output=surface 32 | ``` 33 | 34 | 修改后配置: 35 | 36 | ```configuration 37 | [sdl] 38 | 39 | windowresolution=1280x800 40 | output=opengl 41 | ``` 42 | 43 | 注意: 以上解决方案适用于在 4K 显示器下窗口显示过小的问题,但调整大小后,窗口内的文字大小也随之变大。也就是暂时无法调整窗口内显示文字的行数。 44 | 45 | ## MASM 46 | 47 | 需要单独下载,套件内包括汇编器 `MASM.EXE`,链接器 `LINK.EXE`,调试器 `DEBUG.EXE`,代码编辑器 `EDIT.COM` 等。 48 | 49 | 将下载好的 MASM 工具放置在某个目录下,例如 `~/masm` (`~` 表示家目录) 。启动 dosbox 后使用 `mount` 命令挂载主机目录并切换盘符,即可在 dosbox 中使用 MASM 。以下命令表示将主机的 `~/masm` 目录映射到虚拟盘符 `c:` 并设置当前工作目录为虚拟 c 盘的根目录。 50 | 51 | mount c ~/masm 52 | c: 53 | 54 | 以上两条命令 ( `mount` 与切换盘符 ) 可以写入 dosbox 的配置文件末尾的 `[autoexec]` 分组,以便在下次启动 dosbox 时自动执行。 55 | 56 | 类似地,编写代码的目录也可通过上述方式挂在进 dosbox,例如固定为 `~/x86` 。将该目录通过 `mount` 命令挂载到虚拟盘符 `d:`。而后默认采用 `d:` 即代码目录作为 dosbox 的默认工作目录。(此时由于 MASM 可执行文件不在当前工作目录下,需要将其加入 `PATH` 环境变量以便调用。在 dosbox 中使用 `set` 命令设置环境变量) 57 | 58 | 完整的配置文件 `[autoexec]` 分组需添加内容如下: 59 | 60 | mount c ~/masm 61 | mount d ~/x86 62 | set PATH=%PATH%;C:\; 63 | d: 64 | 65 | ### 汇编器与链接器 66 | 67 | 以汇编程序 `HELLO.ASM` 为例: 68 | 69 | - 汇编: `MASM.EXE HELLO.ASM` ,生成目标文件 `HELLO.OBJ` 70 | 71 | 可选择是否生成 `.LST` 文件和 `.CRF` 文件,其中 `.LST` 文件为汇编和机器码的对应,体现了汇编语言如何翻译。 72 | 73 | - 链接: `LINK.EXE HELLO.OBJ` ,生成可执行文件 `HELLO.EXE` 74 | 75 | 可选择是否生成 `.MAP` 文件和 `.LIB` 文件,其中 `.MAP` 文件为内存图,含有每个段的起始地址,长度等信息。 76 | 77 | - 执行: `HELLO.EXE` 78 | 79 | 注: 在 dosbox 中可以使用 Tab 键补全命令,例如键入 `ma` 后按 Tab 即可自动补全为 `MASM.EXE`。 80 | 81 | ### 编辑器 82 | 83 | - 打开文件: `EDIT.COM HELLO.ASM` 84 | - 编辑文件: 与 `nano` 或 `vim` 的 `INSERT` 模式相似,使用键盘方向键移动光标。 85 | - 打开菜单栏: 按 `Alt` 键控制菜单栏,使用方向键移动光标选择栏目,按 `Enter` 键打开或执行;或直接按相应栏目上高亮的字母以打开或执行。 86 | 87 | ### 调试器 88 | 89 | 调试程序: `DEBUG.EXE HELLO.EXE` 90 | 91 | 常用调试命令: 92 | 93 | - `d` 显示内存单元内容 94 | - 无参数则默认从 `CS:IP` 开始,连续使用则地址一直向后移动。 95 | - 可指定起始地址, 地址格式形如 `DS:0000` 或 `078A:0000` 96 | - `e` 修改内存单元内容 97 | - 需指定起始地址,格式同 `d` 98 | - 每次修改一个字节,可以连续修改多个字节 99 | - `r` 查看和修改寄存器 100 | - 不加参数为查看 101 | - 指定寄存器名为修改 102 | - `u` 反汇编,查看机器码对应的汇编程序 103 | - 可以指定或不指定起始地址,规则同 `d` 104 | - 反汇编的结果不一定有意义(例如在数据段也可能反汇编出结果) 105 | - `a` 修改汇编指令 106 | - 不指定地址默认为 `CS:IP` 107 | - 可指定修改的地址 108 | - 可以连续修改多条指令 109 | - `t` 执行一条指令,同 Step In 110 | - `p` 执行完子程序/循环/功能调用,同 Step Over 111 | - `g` 执行到某个指令地址处,相当于断点 112 | - 不加参数则执行到结束 113 | - `l` 装入文件 114 | - `w` 写回文件 115 | - `q` 退出调试器 116 | --------------------------------------------------------------------------------