kr 第三阶段(一)16 位汇编

为什么要学习 16 位汇编?

  • 16 位汇编包含了大部分 32 位汇编的知识点。
  • 有助于在学习内核的两种模式。
    • 实模式:访问真实的物理内存
    • 保护模式:访问虚拟内存
  • 有助于提升调试能力,调试命令与 OllyDbg 和 WinDebug 通用。
  • 可以学习实现反汇编引擎(32 位的汇编引擎实现起来比较麻烦)

汇编基础

硬件运行机制

二极管

原则上仅允许电流作单方向传导,它在一个方向为低电阻,高电流,而在另一个方向为高电阻。计算机将高低电压定义为 0 和 1,借助二极管的特性完成运算。这就是为什么计算机以二进制形式运算。

门电路

门电路(Logic Gate Circuit)是数字电子电路中的基本构建块,用于实现逻辑运算和控制信号。逻辑门根据不同的逻辑运算规则来处理输入信号,并产生相应的输出信号。

常见的逻辑门包括与门(AND gate)、或门(OR gate)、非门(NOT gate)、异或门(XOR gate)等,上图是与门的实现。

算术/逻辑单元

所有的数学运算都可以由位运算组成。将常用运算符封装成一个器件,称之为单元。

运算单元通常由两个输入端,一个控制端和一个输出端组成。

  • 输入端用来传输操作数
  • 输出端用来输出运算结果
  • 控制端用于控制运算单元做对应的运算。

机器码

可以控制硬件的二进制数据叫做机器码。

以上面的 ALU 为例,假设该 ALU 能进行 8 位二进制数的运算。

则我们可以将表达式 15 + 23 与上面的 ALU 的输入作如下对应:

  • 输入1:00001111
  • 输入2:00010111
  • 控制:00

将上面的输入按照 输入1 输入2 控制 的格式可以写作 00 00001111 00010111(为了方便阅读用空格隔开),这就是机器码。

类似的还有如下机器码:

  • 15 & 2310 00001111 00010111
  • 15 ^ 2311 00001111 00010111

助记符

机器码的二进制值难记,因此可以将每种功能的二进制控制码取一个容易记住的名字,这个名字叫做助记符,也称之为指令。

例如:

  • 00add
  • 01sub
  • 10and
  • 11xor

因此上面列举的机器码可以转换为如下汇编:

  • 15 + 23 => 00 00001111 00010111 => add 0fh, 17h
  • 15 & 23 => 10 00001111 00010111 => and 0fh, 17h
  • 15 ^ 23 => 11 00001111 00010111 => xor 0fh, 17h

汇编

硬件不能识别助记符,因此需要将其转换成对应的机器码,这个过程叫做汇编。

关于汇编代码有几个关键名词,在查阅(反)汇编器文档时会经常遇到:

  • 助记符:Mnemonic
  • 操作数:Operand

微机系统硬件组成

一个典型的硬件系统组成

一个系统不可能由一个硬件单独完成,所以划分出多个硬件模块,然后由一个硬件模块居中调度,称作 CPU(Central Processing Unit)。

8086 CPU

  • 8086 是 16 位 CPU,有 20 根地址线(通过段寄存器寻址扩展了 4 位)和 16 根数据线。
  • 前缀为 AD 的引脚是用来寻址或存取数据的引脚。这种一个种引脚承担多种功能的特点称为引脚复用。
  • CLK 引脚是 CPU 的是时钟输入引脚,它接收来自外部的时钟信号,用于同步处理器内部的操作和各种电子元件的工作。时钟信号的频率决定了处理器的工作速度,即指令的执行速度。
  • 80286 是 8086 的后续型号,也是一款 16 位 CPU。它在 8086 的基础上引入了一些新的特性和改进,并提供了更高的性能。
  • 80386 是 Intel 推出的第一个 32 位 CPU,也被称为 386。

IO 桥

所有硬件模块连接到 I/O 桥,由 I/O 桥负责辅助 CPU 与哪一个硬件模块连接。

以上图为例,s 决定了 out 的输出是 a 还是 b

总线

以下图为例,CPU 有 8 位数据/地址总线,RAM 是一个 256 字节的存储器。

  • 控制线表示 CPU 的对 RAM 操作,例如寻址,读,写等。
  • 数据地址线表示 CPU 操作的 RAM 的地址。

计算机系统组成

计算机系统分层结构图

计算机程序的编译运行

以一个 hello.c 程序为例。

  • 编译
  • 加载可执行文件
  • 执行

8086 CPU 组织架构

8086 功能框图

执行单元(EU)

执行单元(Execution Unit,EU)是8086系列处理器中的一个重要组成部分,它负责执行指令并控制处理器的操作。

  • 算术逻辑单元(Arithmetic Logic Unit,ALU):ALU 是 EU 的核心部件,用于执行算术和逻辑运算。它可以执行加法、减法、乘法、除法等算术操作,以及逻辑操作(如与、或、非、异或等)和移位操作。
  • 寄存器组:EU 包含多个通用寄存器,用于存储数据和中间结果。这些寄存器包括累加器(Accumulator)、通用寄存器(General Purpose Registers)、标志寄存器(Flags Register)等。寄存器提供了快速的数据存储和访问,用于执行指令时的数据操作。
  • 控制单元:EU 的控制单元负责控制指令的执行流程。它根据指令的操作码和其他相关信息,生成相应的控制信号,以控制 ALU、寄存器和其他组件的操作。控制单元还负责处理分支指令、循环指令和异常情况等。
  • 数据通路:EU 内部的数据通路用于将指令和数据在不同的组件之间传递。它包括数据总线、地址总线和控制总线等,用于传输数据、地址和控制信号。

总线接口单元(BIU)

总线接口单元(Bus Interface Unit,BIU)是8086系列处理器中的一个重要组成部分,它负责处理处理器与系统总线之间的接口和通信。

  • 段管理:8086 处理器使用分段的内存管理方式,BIU 负责管理段寄存器和段选择子。它从段寄存器中获取段地址,并与偏移地址组合成物理地址,用于内存访问。
  • 指令预取:BIU 包含一个指令队列(Instruction Queue),用于预取和缓存指令。它从内存中获取指令并将其存储在队列中,以供执行单元(EU)使用。这样可以提高指令的获取速度,减少对内存的访问次数。
  • 总线控制:BIU 负责处理与系统总线的交互,包括地址传输、数据传输和控制信号的生成。它将处理器的地址、数据和控制信号发送到总线上,同时接收来自总线的响应和数据。
  • 内存访问和数据传输:BIU 负责处理对内存和外部设备的访问。它可以执行内存读取和写入操作,并处理与外部设备的数据传输。

寄存器

流水线

CPU 执行指令的过程可以分为一下 5 个步骤,其中 1,2,4 是必须的。

  1. 取指令
  2. 译码
  3. 取数据
  4. 执行
  5. 存储结果

8086 CPU 将指令的执行分成多个模块,这样可以多个模块同时工作,从而提高效率。

然而这种优化在程序中分支跳转较多的时候会导致程序运行变慢。因为提前取到的下一条指令是地址与当前指令地址相邻的指令。而当前指令如果为跳转指令则需要消除提前执行的下一条指令的痕迹。因此编译器优化的其中一个方向是尽量减少程序中的分支跳转数量。

debug 的使用

环境配置

获取 debug

在 Windows XP 的 C:\WINDOWS\system32 目录下有一个名为 debug.exe 的程序。debug 是一个命令行工具,它提供了一种简单的方式来执行低级别的调试和汇编操作。

不过这个程序只能在 Windows XP 系统下运行,高版本的 Windows 系统已经不支持该程序的运行。

安装 DOSBox

为了能够让 debug 在高版本的 Windows 系统下运行(方便后续编写汇编程序),需要安装 DOSBox 程序来模拟相应环境。(也可以使用 msdosplayer)

双击在 DOSBox 安装目录下的 DOSBox 0.74-3 Options.bat 可以打开 DOSBox 的配置文件。在文件末尾可以添加 DOSBox 启动时要执行的初始化命令。

这里我添加了如下命令:

mount c "C:\Program Files (x86)\DOSBox-0.74-3\C"
set path=C:
C:
  • DOSBox 需要手动挂载硬盘,即需要将电脑中的某个目录映射到 DOSBox 中作为一个硬盘。这里我将 C:\Program Files (x86)\DOSBox-0.74-3\C(手动创建的一个目录)挂载为 DOSBox 的 C 盘。
  • 将 DOSBox 中的 C: 盘添加为环境变量。
  • 切换当前目录为 C 盘。

另外我还将 Windows XP 中的 debug.exe 复制到 DOSBox 挂载的 C 盘中,这样就可以再 DOSBox 中运行 debug 进行调试了。

debug 常用命令

  • ?:显示 Debug 命令列表。

  • u [range]:反汇编。没有 range 默认从 CS:IP 或上一次反汇编结束位置开始反汇编。

    -u
    0AF1:0100 7419          JZ      011B
    0AF1:0102 8B0ED596      MOV     CX,[96D5]
    0AF1:0106 E313          JCXZ    011B
    0AF1:0108 B01A          MOV     AL,1A
    0AF1:010A 06            PUSH    ES
    0AF1:010B 33FF          XOR     DI,DI
    0AF1:010D 8E06B496      MOV     ES,[96B4]
    0AF1:0111 F2            REPNZ
    0AF1:0112 AE            SCASB
    0AF1:0113 07            POP     ES
    0AF1:0114 7505          JNZ     011B
    0AF1:0116 4F            DEC     DI
    0AF1:0117 893ED596      MOV     [96D5],DI
    0AF1:011B BB3400        MOV     BX,0034
    0AF1:011E E00A          LOOPNZ  012A
    
  • a [addr]:在指定地址写入汇编机器码。

    -a 110
    0AF1:0110 mov ax, ax
    0AF1:0112 mov dx, dx
    0AF1:0114 mov ax, dx
    0AF1:0116
    -u 110 l 6
    0AF1:0110 89C0          MOV     AX,AX
    0AF1:0112 89D2          MOV     DX,DX
    0AF1:0114 89D0          MOV     AX,DX
    
  • r [reg]:显示或改变一个或多个寄存器。

    -r ax
    AX 0000
    :1234
    -r ax
    AX 1234
    :
    
  • d [range]:显示部分内存的内容。

    -d 110
    0AF1:0110  96 F2 AE 07 75 05 4F 89-3E D5 96 BB 34 00 E0 0A   ....u.O.>...4...
    0AF1:0120  C7 96 00 74 03 BB 00 98-BE 77 97 8B 3E B9 98 B9   ...t.....w..>...
    0AF1:0130  08 00 E8 12 00 80 3C 20-74 09 B0 2E AA B9 03 00   ......< t.......
    0AF1:0140  E8 04 00 32 C0 AA C3 B4-00 8A F1 80 FC 01 74 09   ...2..........t.
    0AF1:0150  B4 00 8A 07 E8 DC E2 74-02 FE C4 AC 3C 3F 75 27   .......t....<?u'
    0AF1:0160  80 FC 00 74 20 80 FC 01-75 22 3A CE 75 05 80 3C   ...t ...u":.u..<
    0AF1:0170  20 74 0A 80 3C 3F 75 14-83 F9 01 76 0F 8A 07 AA    t..<?u....v....
    0AF1:0180  43 46 49 FE C4 8A 07 3C-20 74 01 AA 43 E2 BC C3   CFI....< t..C...
    
  • e:修改内存。

    • e addr

      -e 110
      0AF1:0110  96.11   F2.22   AE.33   07.44   75.55
      -d 110 l 5
      0AF1:0110  11 22 33 44 55                                    ."3DU
      
    • e addr val1[逗号|空格 val2 逗号|空格 val3...]

      -e 110 1,2,3,4 5 6 7 8
      -d 110 l 8
      0AF1:0110  01 02 03 04 05 06 07 08                           ........
      
    • e addr "字符串"

      -e 110 "sky123"
      -d 110 l 6
      0AF1:0110  73 6B 79 31 32 33                                 sky123
      
  • g:运行在内存中的可执行文件。

  • t:步入。

  • p:步过。

  • (n,cx,w):写入文件。

    -n text.txt
    -r cx
    CX 0000
    :100
    -w
    Writing 00100 bytes
    
    • n:要写入的文件的名称。
    • cx:要写入的数据的长度。(写完文件之后 cx 寄存器的值不会改变,还是写之前设置的写入长度。)
    • w:写文件命令。

其中 [range] 有下面两种种形式:

  • [startaddr] [endaddr]:从 startaddrendaddr
  • [startaddr l num]:从 startaddrstartaddr + num

标志寄存器

标志寄存器反应 ALU 运算结果的状态。

  • 条件标志位
    • SFZFOFCFAFPF
    • CPU 执行完一条指令后自动设置。
    • 反映算术、逻辑运算等指令执行完毕后,运算结果的特征。
  • 控制标志位
    • DFIFTF
    • 控制 CPU 的运行方式,工作状态。
标志 true false Name(名称) 命题
OF OV(Overflow) NV(Not Overflow) Overflow Flag(是否溢出) 存在溢出?
SF NG(Negative) PL(Plus) Sign Flag(结果的符号是正还是负) 是负数(正数看做无符号)?
ZF ZR(Zero) NZ(Not Zero) Zero Flag(运算结果是否为 0) 是 0 ?
PF PE(Event) PO(Odd) Parity Flag(结果中二进制位个数的奇偶性) 是偶数个 1 ?
CF CY(Carry yes) NC(Not carry) Carry Flag(进位标志) 有进位?
AF AC(Auxiliary Carry) NA(No Auxiliary Carry) Auxiliary Carry Flag(辅助进位标志) 发生辅助进位?
DF DN(Down) UP(Up) Direction Flag(方向标志) sidi 递减?
IF EI(Enable Interrupts) DI(Disable Interrupts) Interrupt Flag(中断标志) 允许中断?
TF ST(Single Step) NT(Non Trap) Trap Flag(陷阱标志) 单步调试模式?

进位标志CF(Carry Flag)

如果运算结果的最高位产生了一个进位或借位,那么,CF 的值为 1 ,否则为 0 。(无符号数溢出)

  • 由于 CF 是针对无符号数来说的,因此无论是加法还是减法参与运算的数都是无符号数(大于等于 0)。
  • 0 减去任何非零的数 CF 都会置 1 。

奇偶标志PF(Parity Flag)

奇偶标志 PF 用于反映运算结果中最低字节中 1 的个数的奇偶性。如果 1 的个数为偶数,则 PF 的值为 1 ,否则其值为 0 。

辅助进位标志AF(Auxiliary Carry Flag)

在发生下列情况时,辅助进位标志 AF 的值被置为 1 ,否则其值为 0 。

  • 在字操作时,发生低字节向高字节进位或借位时
  • 在字节操作时,发生低 4 位向高 4 位进位或借位时。

零标志ZF(Zero Flag)

如果运算结果为 0 ,则其值为 1 ,否则其值为 0 。在判断运算结果是否为 0 时,可使用此标志位。

符号标志SF(Sign Flag)

符号标志 SF 用来反映运算结果的符号位,它与运算结果的最高位相同。

溢出标志OF(Overflow Flag)

溢出标志 OF 用于反映有符号数加减运算所得结果是否溢出。(有符号数溢出)

分段与寻址

分段

分段寻址

8086 是 16 位 CPU ,但是能访问 1M 的内存,这是因为 8086 将内存划分成多段,通过段基址+段偏移的方式访问。

地址计算方式

地址计算方式:内存地址 = 段基址 * 10h + 段偏移

  • 段基址+段偏移的方式一般写作 段基址:段偏移 ,称为逻辑地址。
  • 偏移地址称为 EA(Effective Address,在很多库的文档中会出现这个名称)。
  • 通过逻辑地址计算出来的内存地址称为物理地址 PA(Physical Address)。

段的内存分布如下,不同的段之间可以重叠。由于段地址的计算方式,段的起始地址关于 0x10 对齐。

内存分布

划分段的原则

  • 段大小可以不是 64K
  • 段与段之间不能有重叠(8086 CPU 和 DOS 操作系统都不会管,内存分段靠自己,这样做只是避免程序出问题。)

逻辑地址与物理地址

一个物理地址可以由多个逻辑地址表示,但基于分段原则,一般编程中不会碰到。

可用内存

DOS 系统中,应用程序可用内存约 600K 。

段寄存器

8086 中,段基址都是存储在段寄存器中,段偏移可以用立即数或者通用寄存器指明。

  • DS:数据段,默认使用 DX
  • CS:代码段,绑定 CS:IP 使用。
  • SS:堆栈段,用作函数栈,绑定 SS:SP 使用。
  • ES:扩展段,常用于串操作。

debug 的一些命令也与段寄存器绑定:

  • au:代码段 CS
  • de:数据段 DS

当然我们也可以指定特定的段,例如 d ss:100

8086 有 20 跟地址线,16 根数据线,其中数据线与地址线的低 16 位复用。内部通过地址加法器计算地址。

寻址

  • 指令中用于说明操作数所在的方式称为寻址方式。
  • 16 位 CPU 的寻址方式有 7 种,32 位 CPU 还会多一种比例因子寻址。

立即寻址

  • 操作数的值存在指令中的方式称作立即寻址。
  • 汇编中整数常量称作立即数。
  • 立即数可以是 8 位数,也可以是 16 位数。

寄存器寻址

  • 操作数的值存储在寄存器的寻址方式称作寄存器寻址。
  • 寄存器包括通用寄存器和段寄存器。

注意:

  • 段寄存器之间不能赋值。
  • 指令指针寄存器不能用作寻址(例如不存在 MOV AX, IP 汇编)。

直接寻址

操作数值在内存中,机器码中存储 16 位段内偏移的寻址方式称作直接寻址。

寄存器间接寻址

  • 操作数值在内存中,段内偏移存储在寄存器中的寻址方式称作寄存器间接寻址。
  • 间接寻址的寄存器有 BXBPSIDI

EA = [ ( BX ) ( BP ) ( SI ) ( DI ) ] \text{EA}=\begin{bmatrix} (\text{BX})\\ (\text{BP})\\ (\text{SI})\\ (\text{DI}) \end{bmatrix} EA= (BX)(BP)(SI)(DI)

寄存器相对寻址

  • 操作数值在内存中,段内偏移存储由 [寄存器 + 立即数] 计算得来的的寻址方式称作寄存器相对寻址。
  • 寄存器相对寻址的寄存器有 BXBPSIDI
  • 寄存器相对寻址的立即数可以是 8 位,可以是 16 位的。

EA = [ ( BX ) ( BP ) ( SI ) ( DI ) ] + [ 8 位 disp 16 位 disp ] \text{EA}=\begin{bmatrix} (\text{BX})\\ (\text{BP})\\ (\text{SI})\\ (\text{DI}) \end{bmatrix}+\begin{bmatrix} \text{8 位 disp} \\ \text{16 位 disp} \end{bmatrix} EA= (BX)(BP)(SI)(DI) +[8 位 disp16 位 disp]

基址变址寻址

  • 操作数值在内存中,段内偏移由 [寄存器 + 寄存器] 计算得来的寻址方式称作基址变址寻址。
  • 可用做基址的寄存器有 BXBP
  • BX 默认 DS 段,BP 默认 SS 段。
  • 可用作变址的寄存器有 SIDI

EA = [ ( BX ) ( BP ) ] + [ ( SI ) ( DI ) ] \text{EA}=\begin{bmatrix} (\text{BX})\\ (\text{BP})\\ \end{bmatrix}+\begin{bmatrix} (\text{SI})\\ (\text{DI}) \end{bmatrix} EA=[(BX)(BP)]+[(SI)(DI)]

基址变址相对寻址

  • 操作数值在内存中,段内偏移由 [基址寄存器+变址寄存器+偏移常量] 计算得来的寻址方式称作基址变址寻址。
  • 可用做基址的寄存器有 BXBP
  • BX 默认 DS 段,BP 默认 SS 段。
  • 可用作变址的寄存器有 SIDI
  • 可用作常量的数值可以是 8 位,可以是 16 位。

指令

数据传送类指令

传送指令 MOV (move)

把一个字节或字的操作数从源地址传送至目的地址。

注意:

  • 不存在存储器向存储器的传送指令。
  • mov 指令源操作数和目的操作数指定的数据长度应一致。
  • 立即数到内存的数据传送指令需要指定数据长度,例如 mov byte ptr ds:[bx], 12h 。其他指令由于寄存器自带长度因此不需要指定数据长度。

交换指令 XCHG(exchange)

情形:

  • 寄存器与寄存器之间对换数据
  • 寄存器与存储器之间对换数据
  • 不能在存储器与存储器之间对换数据

效率:mov 优于 xchg ,因为 xchg 使用了内部暂存器。

换码指令 XLAT

作用:将 BX 指定的缓冲区中 AL 指定的位移处的一个字节取出赋给 AL ,即:al <-- ds:[bx + al] 。该指令无操作数。

用途:键盘的扫描码,需要转为 ASCII 码,可以将扫描码做成表,扫描码作下标可以查到对应的 ASCII 码。

堆栈操作指令

  • 进栈:push reg,相当于 sub sp, 2; mov [sp], reg;
  • 出栈:pop reg,相当于 mov reg, [sp]; add sp, 2;
  • 保存所有寄存器环境
    • 16 位:pusha/popa
    • 32 位:pushad/popad

注意:

  • 对于 8086 CPU,push 指令的操作数只能是长度为 2 字节的寄存器(包括段寄存器)或内存。80286,80386 及以上的 CPU 的 push 指令支持立即数和寄存器。
  • 8086 不支持 pusha 指令,80286 才开始支持该指令。
  • pusha 指令会将 16 位通用寄存器 AXCXDXBXSPBPSIDI 中的值依次压入栈中。

标志寄存器传送指令

标志寄存器传送指令用来传送标志寄存器 FLAGS 的内容,方便进行对各个标志位的直接操作。

  • 低 8 位传送
    • LAHFAHFLAGS 的低字节
      • LAHF 指令将标志寄存器的低字节传送给寄存器 AH
      • SF/ZF/AF/PF/CF 状态标志位分别送入 AH 的第 7/6/4/2/0 位,而 AH 的第 5/3/1 位任意。
    • SAHFFLAGS 的低字节 ← AH
      • SAHFAH 寄存器内容传送给 FLAGS 的低字节。
      • AH 的第 7/6/4/2/0 位相应设置 SF/ZF/AF/PF/CF 标志位。
  • 16 位传送
    • PUSHFPUSHF 指令将标志寄存器的内容压入堆栈,同时栈顶指针 SP 减 2 。
    • POPFPOPF 指令将栈顶字单元内容传送标给志寄存器,同时栈顶指针 SP 加 2 。
  • 32 位传送
    • PUSHFD:将 ELFAGS 压栈。
    • POPFD:将栈顶 32 字节出栈到 EFLAGS 中。

地址传送指令

地址传送指令将存储器单元的逻辑地址送至指定的寄存器

  • 有效地址传送指令 LEA(load EA):将存储器操作数的有效地址传送至指定的 16 位寄存器中。
  • LDS r16, mem:将主存中 mem 指定的字送至 r16 ,并将 mem 的下一字送 DS 寄存器。
  • LES r16, mem:将主存中 mem 指定的字送至 r16 ,并将 mem 的下一字送 ES 寄存器。

输入输出指令

8086 通过输入输出指令与外设进行数据交换;呈现给程序员的外设是端口(Port)即 I/O 地址。8086 用于寻址外设端口的地址线为 16 条,端口最多为 2 16 2^{16} 216=65536(64K)个,端口号为 0000H~FFFFH 。每个端口用于传送一个字节的外设数据。

8086 的端口有 64K 个,无需分段,设计有两种寻址方式:

  • 直接寻址:只用于寻址 00H~FFH 前 256个 端口,操作数 i8 表示端口号。
  • 间接寻址:可用于寻址全部 64K 个端口,DX 寄存器的值就是端口号。对大于 FFH 的端口只能采用间接寻址方式。

输入指令 IN:以将外设数据传送给 CPU 内的 AL/AX 为例

  • IN AL, i8:字节输入,AL ← I/O 端口(i8 直接寻址)
  • IN AL, DX:字节输入,AL ← I/O 端口(DX 间接寻址)
  • IN AX, i8:字输入,AX ← I/O 端口(i8 直接寻址)
  • IN AX, DX:字输入,AX ← I/O 端口(DX 间接寻址)

输出指令 OUT:以将 CPU 内的 AL/AX 数据传送给外设为例

  • OUT i8, AL:字节输出,I/O 端口 ← ALi8 直接寻址)
  • OUT DX, AL:字节输出,I/O 端口 ← ALDX 间接寻址)
  • OUT i8, AX:字输出,I/O 端口 ← AXi8 直接寻址)
  • OUT DX, AX:字输出,I/O 端口 ← AXDX 间接寻址)

这个指令的其中一个用途是检测虚拟机。在真机环境中由于输入输出指令为特权指令,在 3 环执行会触发异常。而在虚拟机中则不会。

算术运算类指令

加法

  • add:加法
    • ADD reg, imm/reg/memregreg + imm/reg/mem
    • ADD mem, imm/regmemmem + imm/reg
  • adc:带进位加法
    • ADC reg, imm/reg/memregreg + imm/reg/mem + CF
    • ADC mem, imm/regmemmem + imm/reg + CF
  • inc:加一,不影响 CF 标志位。
    • INC reg/memreg/memreg/mem + 1

减法

  • sub:减法
    • SUB reg, imm/reg/memregreg - imm/reg/mem
    • SUB mem, imm/regmemmem - imm/reg
  • sbb:带借位的减法
    • SBB reg, imm/reg/memregreg - imm/reg/mem - CF
    • SBB mem, imm/regmemmem - imm/reg - CF
  • dec:减一,不影响 CF 标志位。
    • DEC reg/memreg/memreg/mem - 1

求补指令 NEG(negative)

NEG 指令对操作数执行求补运算:用零减去操作数,然后结果返回操作数。求补运算也可以表达成:将操作数按位取反后加 1 。

NEG reg/memreg/mem ← 0 - reg/mem 。如果操作数为 0 则 CF = 0 ,否则 CF = 1

x == 0 ? 0 : -1 为例,我们可以通过 neg 指令将其优化为无分支程序:

mov ax, x
sub ax, 0		; CF 标志位清零
neg ax			; 如果 ax 非 0 则 CF 置位 
sbb ax, ax		; ax = ax - ax - CF = - CF 

对于其他类似的三目运算我们可以通过加减偏移和乘除系数转换为上述的三目运算,因此都可以把分支优化掉。

比较指令 CMP(compare)

  • 格式:CMP OPD, OPS
  • 功能:(OPD) - (OPS)
  • 说明:目的操作数减去源操作数,然后根据结果设置标志位,但该结果不存入目的地址。
  • 影响标志位:AFCFOFPFSFZF
  • 作用:一般后面跟一条条件转移指令,根据比较结果跳转到不同的分支,用于处理 OPDOPS 大小比较不同的情况。

乘法指令

  • 无符号乘法

    位数 隐含的被乘数 乘积存放的位置 举例
    8位 AL AX MUL BL
    16位 AX DX-AX MUL BX
    32位 EAX EDX-EAX MUL ECX
    • 格式:MUL reg/mem
    • 功能:显式操作数*隐式操作数(看成无符号数)
    • 影响标志位:CFOF ,如果乘积的高一半位(AH/DX/EDX)包含有乘积的有效位,则 CF=1OF=1 ;否则 CF=0OF=0
  • 有符号乘法

    • 格式:
      • IMUL reg/mem
      • IMUL reg, imm(80286+)
      • IMUL reg, reg, imm(80286+)
      • IMUL reg, reg/mem(80386+)
    • 功能:有符号数相乘
    • 影响标志位:CFOF ,如果乘积的高一半位(AH/DX/EDX)不是低位的 符号扩展,则 CF=1OF=1 ;否则 CF=0OF=0

除法指令

  • 无符号乘法

    位数 隐含的被除数 除数 余数
    8位 AX 8位ops AL AH
    16位 DX-AX 16位ops AX DX
    32位 EDX-EAX 32位ops EAX EDX
    • 格式:DIV reg/mem
    • 影响标志位:未定义,即指令执行后标志位是任意的,不可预测的。
    • 除法溢出:8 位除法运算结果大于 8 位,16 位除法运算结果大于 16 位。
  • 有符号除法

    位数 隐含的被除数 除数 余数
    8位 AX 8位ops AL AH
    16位 DX-AX 16位ops AX DX
    32位 EDX-EAX 32位ops EAX EDX
    • 格式:IDIV reg/mem
    • 影响标志位:AFCFOFPFSFZF
    • 除法溢出:字节除时商不在 -128~127 范围内或者在字除时商不在 -32768~32767 范围内。

符号扩展指令

  • CBW(Convert Byte to Word):将 AL 中的符号扩展至 AH 中,操作数是隐含且固定的。
    • XX040004
    • XXFEFFFE
  • CWD(Covert Word to Doubleword):将 AX 中的符号扩展至 DX 中,操作数是隐含且固定的。
  • CWDE(Covert Word to Extended Doubleworld,386+):将 AX 中的符号位扩展至 EAX 的高 16 位,操作数是隐含且固定的。
  • CDQ(Cover Doubleword to Quadword,386+):将 EAX 中的符号位扩展至 EDX 中,操作数是隐含且固定的。
  • CDQE(Convert Doubleword to Quadword Extended,x86-64)将 EAX 中的符号位扩展至 RAX 中,操作数是隐含且固定的。

位操作类指令

逻辑运算

  • 逻辑与:AND
    • 格式:AND reg/mem, reg/mem/imm
    • 受影响的标志位:CF(0)OF(0)PFSFZFAF 无定义)
      • CF(进位标志):AND 指令总是将 CF 标志设置为 0 ,即不会影响进位标志。
      • OF(溢出标志):AND 指令总是将 OF 标志设置为 0 ,即不会影响溢出标志。
      • PF(奇偶标志):AND 指令根据结果中的位数 1 的个数来设置奇偶标志。如果结果中的位数 1 是偶数个,则PF被设置为 1 ,否则设置为 0 。
      • SF(符号标志):AND 指令将结果的最高位(符号位)复制到 SF 标志位中。如果结果的最高位为 1 ,则 SF 被设置为 1 ,表示结果为负数;如果结果的最高位为 0 ,则 SF 被设置为 0 ,表示结果为非负数。
      • ZF(零标志):AND 指令将结果的所有位进行按位与操作,并将零标志设置为1,如果结果为零;否则,将零标志设置为 0 。
      • AF(辅助进位标志):AND 指令不会定义或影响辅助进位标志,因此对该标志位没有任何影响。
  • 逻辑或:OR
    • 格式:OR reg/mem, reg/mem/imm
    • 受影响的标志位:CF(0)OF(0)PFSFZFAF 无定义)
  • 逻辑非(按位取反):NOT
    • 格式:NOT reg/mem
    • 受影响的标志位:无
  • 异或:XOR
    • 格式:XOR reg/mem, reg/mem/imm
    • 受影响的标志位:CF(0)OF(0)PFSFZFAF 无定义)
  • TEST 指令
    • 格式:TEST reg/mem, reg/mem/imm
    • 作用:执行 AND ,但不影响目标操作数。
    • 受影响的标志位:CF(0)OF(0)PFSFZFAF 无定义)

x >= 0 ? x : -x 为例,我们可以通过 cwd 指令和逻辑运算指令将其优化为无分支程序:

mov ax, x
cwd				; 如果 x < 0 则 dx = -1 ,否则 dx = 0
xor ax, dx		; 如果 x < 0 则将 ax 取反,否则 ax 不变
sub ax, dx		; 如果 x < 0 则将 ax 加一,否则 ax 不变

移位指令

  • 算术移位和逻辑移位
    • 格式:OP reg/mem, 1/cl
    • 影响标志:OFZFSFPFCF
    • 指令
      • SAL(Shift Arithmetic Left)/SHL(Shift Logical Left):算术左移/逻辑左移
      • SAR(Shift Arithmetic Right):算术右移
      • SHR(Shift Logical Right):逻辑右移
  • 循环移位
    • 格式:OP reg/mem, 1/cl
    • 影响标志:OFCF,其他标志无定义。
    • 指令
      • ROL(Rotate Left):循环左移
      • ROR(Rotate Right):循环右移
      • RCL(Rotate through Carry Left):带进位循环左移
      • RCR(Rotate through Carry Right):带进位循环右移

串操作类指令

串操作指令

  • 源操作数使用 SI ,默认段为 DS ,可段超越。
  • 目的操作数使用 DI ,默认段为 ES ,不可段超越。
  • DF 寄存器决定串操作方向。
    • DF 值为 0(UP) 则执行完指令之后 SIDI 都加操作的数据长度。
    • DF 值为 1(DN) 则执行完指令之后 DIDI 都减操作的数据长度。

段超越(segment override)是指在指令中显式地指定要使用的段寄存器,而不是使用默认的段寄存器。

  • MOVS(Move String):串移动,把字节或字操作数从主存的源地址传送至目的地址。
    • MOVSB:字节串传送,ES:[DI] ← DS:[SI] (SI ← SI ± 1, DI ← DI ± 1)
    • MOVSW:字串传送, ES:[DI] ← DS:[SI] (SI ← SI ± 2, DI ← DI ± 2)
    • MOVSD:双字串传送, ES:[DI] ← DS:[SI] (SI ← SI ± 4, DI ← DI ± 4)
  • STOS(Store String):串存储,把 ALAX 数据传送至目的地址。
    • STOSB:字节串存储,ES:[DI] ← AL (DI ← DI ± 1)
    • STOSW:字串存储,ES:[DI] ← AX (DI ← DI ± 2)
    • STOSD:双字串存储,ES:[DI] ← EAX (DI ← DI ± 4)
  • LODS(Load String):串读取,把指定主存单元的数据传送给 ALAX
    • LODSB:字节读取,AL ← DS:[SI] (SI ← SI ± 1)
    • LODSW:字串读取,AX ← DS:[SI] (SI ← SI ± 2)
    • LODSD:双字串读取,EAX ← DS:[SI] (SI ← SI ± 4)
  • CMPS(Compare String):串比较,将主存中的源操作数减去至目的操作数,以便设置标志,进而比较两操作数之间的关系。
    • CMPSB:字节串比较,DS:[SI] - ES:[DI] (SI ← SI ± 1, DI ← DI ± 1)
    • CMPSW:字串比较,DS:[SI] - ES:[DI] (SI ← SI ± 2, DI ← DI ± 2)
    • CMPSD:双字串比较,DS:[SI] - ES:[DI] (SI ← SI ± 4, DI ← DI ± 4)
  • SCAS(Scan String):串扫描,将 AL/AX 减去至目的操作数,以便设置标志,进而比较 AL/AX 与操作数之间的关系。
    • SCASB:字节串扫描,AL - ES:[DI] (DI ← DI ± 1)
    • SCASW:字串扫描,AX - ES:[DI] (DI ← DI ± 2)
    • SCASD:双字串扫描,EAX - ES:[DI] (DI ← DI ± 4)

重复前缀指令

串操作指令执行一次,仅对数据串中的一个字节或字进行操作。

串操作指令前都可以加一个重复前缀,实现串操作的重复执行。重复次数隐含在 CX 寄存器中。

  • REP:每执行一次串指令,CX 减 1 ,直到 CX = 0 重复执行结束。
    • 含义:当数据串没有结束(CX ≠ 0),则继续传送。
    • 举例:
      • REP LODS/LODSB/LODSW/LODSD
      • REP STOS/STOSB/STOSW/STOSD
      • REP MOVS/MOVSB/MOVSW/MOVSD
  • REPZ:每执行一次串指令,CX 减 1 ,并判断 ZF 是否为 0 。只要 CX = 0ZF = 0 则重复执行结束。
    • 含义:当数据串没有结束(CX ≠ 0)并且串相等(ZF = 1)则继续比较。
    • 举例:
      • REPE/REPZ SCAS/SCASB/SCASW/SCASD
      • REPE/REPZ CMPS/CMPSB/CMPSW/CMPSD
  • REPNZ:每执行一次串指令,CX 减 1 ,并判断 ZF 是否为 1 。只要 CX = 0ZF = 1 则重复执行结束。
    • 含义:当数据串没有结束(CX ≠ 0)并且串不相等(ZF = 0)则继续比较。
    • 举例:
      • REPNE/REPNZ SCAS/SCASB/SCASW/SCASD
      • REPNE/REPNZ CMPS/CMPSB/CMPSW/CMPSD

流程转移类指令

无条件跳转

  • 直接转移

    名称 修饰关键字 格式 功能 指令长度 示例
    短跳 short jmp short 标号 ip ← 标号偏移 2 0005:EB0B jmp 0012
    近跳 near ptr jmp near 标号 ip ← 标号偏移 3 0007:E90A01 jmp 0114
    远跳 far ptr jmp far ptr 标号 jmp 段名:标号 ip ← 标号偏移 cs ← 段地址 5 0000:EA00007C07 jmp 0077C:0000
  • 使用寄存器间接转移

    • 格式:jmp regreg 为通用寄存器)
    • 功能:ip ← reg(只能用于段内转移)
  • 使用 EA 的间接转移

    指令 说明 示例
    jmp 变量名 jmp word ptr [EA] jmp near ptr [EA] 从内存中取出两字节的段偏移,然后 ip ← [EA] 000b:ff260000 jmp[0000] 000f:8d1e0000 lea bx, [0000] 0013:ff27 jmp [bx] 0000:cd 20
    jmp 变量名 jmp dword ptr [EA] jmp far ptr [EA] 从内存中取出两字节的段偏移和两字节段基址,然后 ip ← [EA]cs ← [EA + 2] 0021:ff260600 jmp[0002] 0025:8d1e0400 lea bx, [0002] 0029:ff2f jmp far [bx] 0002:00 00 7d 07

条件跳转

根据标志位判断,条件成立则跳转,条件不成立则不跳。

  • 单条件跳转

    指令 英文 标志 说明
    JZ/JE zero,equal ZF = 1 相等/等于零
    JNZ/JNE not zero,not equal ZF = 0 不相等/不等于零
    JCXZ CX is zero CX = 0 CX 为 0
    JS sign SF = 1 结果为负
    JNS not sign SF = 0 结果为正
    JP/JPE parity,parity even PF = 1 1 为偶数个
    JNP/JPO not parity,parity odd PF = 0 1 为奇数个
    JO overflow OF = 1 溢出
    JNO not overflow OF = 0 不溢出
    JC carry CF = 1 进位/小于
    JNC not carry CF = 0 不进位/大于等于
  • 无符号数判断

    指令 英文 标志 说明
    JB/JNAE below,not above or equal CF = 1 小于/不大于等于
    JAE/JNB above or equal,not below CF = 0 大于等于/不小于
    JBE/JNA below or equal,not above `CF = 1
    JA/JNBE above,not below or equal CF = 0 && ZF = 0 大于/不小于等于
  • 有符号判断

    指令 英文 标志 说明
    JL/JNGE less,not geater or equal SF != OF 小于/不大于等于
    JGE/JNL greater or equal,not less SF = OF 大于等于/不小于
    JLE/JNG less or equal,not greater `SF != OF
    JG/JNLE greater,not less or equal SF = OF && ZF = 0 大于/不小于等于

LOOP

格式:LOOP 标号 ,只能用于转移。

指令 重复条件
LOOP CX != 0
LOOPZ/LOOPE CX != 0 && ZF = 1
LOOPNZ/LOOPNE CX != 0 && ZF = 0

函数调用相关指令

指令 说明 功能
call (near ptr) 标号 段内直接调用 push 返回地址 jmp 标号
call REG `call near ptr word ptr [EA]` 段内间接调用
call far ptr 标号 call dword ptr [EA] 段间调用 push cs push 返回地址 jmp 标号
ret (n) 段内返回 pop ip add sp, n
retf (n) 段间返回 pop ip pop cs add sp, n

处理器控制类指令

masm 基础

VSCode 开发环境配置

  • 配置 DOSBox 环境变量
  • 安装 VSCode
  • 安装 MASM/TASMVSCode DOSBox 插件
  • 设置 → 扩展 → MASM/TASM 配置插件

完成上述配置后打开 asm 文件右键会出现"打开DOS环境"等选项,这里使用的是 DOS 自带的 MASM 开发环境和 DOSBox 配置文件,不需要配置直接可以编译运行 asm 文件。

编译命令:

ml /c asm文件.asm
link asm文件.obj

编译+调试脚本(VSCode自带这个功能):

ml /c %1.asm
link %1.obj
debug %1.exe

如果多个 asm 文件编译则将命令中分别添加参与编译的 asm 文件和生成的 obj 文件即可。

函数和变量声明可以统一放在一个 inc 文件中,在使用声明的 asm 文件开头添加 include xxx.inc 即可。

如果想要调试的时候再特定的位置断下来可以在程序中添加 int3 指令或者 db 0cch

入口和段

入口

  • 入口点指定使用关键字 end ,后跟标号名。

  • 如果未指定入口点则默认入口点是整个程序的起始位置。

    data_seg segment
    mov cx,cx
    mov cx,cx
    ENTRY:
    mov cx,cx
    mov cx,cx
    data_seg ends

    end ENTRY

  • 一个程序必须至少有一个段

  • 一个程序中可以定义多个段

  • 段不能嵌套

  • 段可以重名,重名的段会被编译到同一块内存中。

  • 段的起始地址关于 0x10 对齐。

    段名 segment

    段名 ends

注释

汇编中使用分号(;)来标注注释,汇编中只有行注释没有块注释。

; 这里是注释
mov ax, bx ; 这里是注释

常量

整数

  • 整数可以支持多个进制。
  • 数值必须以数字开头,如果非数字,前面必须加 0 。
  • 负数前面可以加负号(-)。
关键字 说明 示例
十进制 mov ax, 1234
D 十进制 mov ax, 1234d
B 二进制 mov ax, 1011b
O 八进制 mov ax, 76o
H 十六进制 mov ax, 76o mov ax, 0abh

字符

  • 字符可以用单引号(')或双引号("),例如 mov byte ptr [bx], '$'

变量

  • 变量可以支持多个类型

  • 变量可以有初始值,未初始化的值用问号(?)表示。

  • 变量一般定义在一个单独的段中。

    变量名 类型 初始值
    val dd 5566h

关键字 意义
db 字节
dw
dd 双字
dq 8 字节
dt 10 字节

变量使用前需要注意两点:

  • 首先要告诉编译器当前使用的是哪个段,这样编译器才能提供正确的段偏移。

  • 需要给段寄存器设置正确的值。

    data_seg segment
    g_btVal db 55h
    data_seg ends

    uninitdata_seg segment
    g_btVal1 db ?
    uninitdata_seg ends

    code_seg segment
    START:
    assume ds:data_seg ; 告诉编译器当前使用的是哪个段
    mov ax, data_seg
    mov ds, ax ; 给段寄存器设置正确的值
    mov al, g_btVal ; 使用变量
    code_seg ends

    end START

字符串

  • 字符串都可以用单引号(')或双引号(")。

  • 字符串一般以美元符($)结尾(在内存中 $ 是实际跟在字符串后面的,这么做是因为有些使用字符串的 API 有要求)。

    g_szHello db "hello,word!$"

数组

格式:

名字 类型 值1[,值2][,值3][,值4][,值5]
名字 类型 数量 dup(初值)[,数量 dup(初值)][,值]

示例:

g_dbArray1 db 78h, 96h, 43h					; 后面跟初始化的值
g_dbArray2 db 256 dup(0), 128 dup(11h)		; 重复 256 个 0 ,再跟重复 128 个 1 。
g_dbArray3 db 256 dup(0), 78h, 96h, 43h		; 重复 256 个 0 ,再跟 78h 96h 43h 。
g_dbArray4 db 256 dup(?)					; 开辟 256 字节的空间,不做初始化(初始化为 0)。

属性

masm 提供了很多伪指令,可以获取变量的大小和地址,称之为变量的属性。这些属性在编译过程中会计算成具体的常量值。

关键字 意义
seg 取段基址
offset 取段偏移
type 取元素类型大小
length 取元素个数
size 取数据大小(length * type

注意:

  • seg 可以作用于段或者段内的变量,结果都是得到对应段的基址。
  • lengthsize 都是按定义的数组的第一个","前面的部分来计算的。例如前面的 g_dbArray3 计算的 length 是 0x100,g_dbArray1 计算的 length 是 1 。
  • 区分以下四种用法:
    • lea di, g_dbArray:获取 g_dbArray 地址到 DI 寄存器中。
    • lea di, offset g_dbArray:获取 g_dbArray 地址到 DI 寄存器中。
    • mov dl, g_dbArray:获取 g_dbArray 前 1 个字节到 DI 寄存器中。(这里不能用 DX ,因为寄存器应该与数组元素大小匹配。)
    • mov di, offset g_dbArray:获取 g_dbArray 地址到 DI 寄存器中。

示例:

mov ax, seg g_dbArray1
mov ax, seg data_seg
mov ax, offset g_dbArray1
mov ax, type g_dbArray1
mov ax, length g_dbArray1
mov ax, size g_dbArray1

堆栈

  • stack 关键字让程序在被加载的时候指定 ssbpsp

  • 使用数组为栈设置大小。

    stack_seg segment stack
    db 256 dup(0cch)
    stack_seg ends

调用 dos 功能号

  • DOS 系统提供的功能(API),通过 21 号中断来调用。
  • 每个功能都有一个编号,通过 AH 指定功能编号。
  • 每个功能的参数查看手册。

例如:

  • AH 为 0x4c 时为退出程序,退出码为 AL
  • AH 为 0x09 时为输出 $ 结尾的字符,字符串地址存放在 DX 中。

利用这两个功能号我们可以实现一个 Hello World 程序。

data_seg segment
    g_szHello db "hello,word!$"
data_seg ends

stack_seg segment stack
    db 256 dup(0cch)
stack_seg ends

code_seg segment
START:
    assume ds:data_seg
    mov ax, data_seg
    mov ds, ax
    mov ah, 09
    mov dx, offset g_szHello
    int 21h
    mov ax, 4c00h
    int 21h
code_seg ends

end START

中断

  • 中断是 CPU 提供的流程跳转指令,类似函数调用。
  • 00:00 位置存储着一个双字数组,大小为 256 ,称作中断向量表。
  • 数组元素为逻辑地址段基址:段偏移(段偏移在低 2 字节)。
  • int n 的意思是从第 n 个元素获取地址,然后跳转执行。

函数

函数结构

函数执行流程:

  • 参数入栈
  • 返回地址入栈,跳转到函数
  • 保存栈帧
  • 申请局部变量空间
  • 保存寄存器环境
  • 执行函数功能
  • 恢复寄存器环境
  • 恢复栈帧
  • 弹出返回地址,返回[平栈]
  • [平栈]

函数定义

函数名 proc [距离][调用约定] [uses reg1 reg2..] [参数:word, 参数名:word..]
	local 变量:word
	local 变量:word
	
	ret
函数名 endp

示例:

TestProc PROC far stdcall uses bx dx si di arg1:word
	local btVal:byte

	ret
TestProc ENDP
  • 距离:

    距离关键字 说明
    near 函数只能段内调用 函数使用 ret 返回 调用时 ip 入栈
    far 段内段间都可调用 函数使用 retf 返回 调用时 ipcs 入栈
    • 如果是用 far 修饰且段内调用,汇编器也会手动压一个 cs 寄存器确保 retf 能正常返回。
  • 调用约定

    调用约定关键字 说明
    c 调用方平栈
    stdcall 被调用方平栈
  • 局部变量

    类型 局部变量类型 备注
    db byte 可以直接赋值使用
    dw word 可以直接赋值使用
    dd dword 不可以直接赋值使用
    dq qword 不可以直接赋值使用
    dt tword 不可以直接赋值使用
    • 一般习惯在局部变量前加 @ ,属于一种编程规范。
    • 数组局部变量定义:local @dwBuf[100h]:byte
  • 保存寄存器:uses reg1 reg2.. 表示函数中会使用相应的寄存器,因此在函数开始和结束位置会保存和恢复相应的寄存器。

invoke 伪指令

invoke 函数名, 参数1, 参数2, 参数3

说明:

  • 会生成参数入栈代码,参数可以是立即数,变量,寄存器等。注意立即数不能直接 push ,汇编器会使用 AX 寄存器中转一下,因此注意 AX 寄存器的使用。
  • 如果是 C 调用约定会生成平栈代码。
  • 如果是局部变量取地址需要使用 addr 伪指令。
  • 使用 addr 的时候会用 AX 临时存放指针值,因此注意 AX 的使用。
伪指令 说明
offset 获取段内偏移
addr 获取局部变量地址,使用 LEA 指令。专用于 invoke

函数声明

如果调用另一个文件中的函数或者在定义函数之前调用函数需要进行函数声明。masm 的函数声明的语法如下:

函数名 proto 距离 调用约定 参数列表

示例:

Fun1 proto fat c pAddr:word

宏汇编

表达式

表达式中的求值是在程序链接时完成的,所以表达式中的各值必须是在汇编或链接期就能确定,也就是说不能将寄存器或者变量运用于表达式。

  • 算术表达式

    运算符 意义 例子
    + 65 + 32
    - size val - 54
    * 23h * 65h
    / 98 / 45
    mod 取模 99 / 65
  • 逻辑运算
    逻辑运算即位运算,逻辑运算符与对应的指令助记符单词是相同的,当他们出现在操作码部分时是指令,出现在操作数时是逻辑运算符。

    运算符 意义
    and 位与
    or 位或
    not 按位取反
    xor 异或
  • 关系运算符
    关系运算符的结果,如果结果为真,则所有位都置为 1 ,即 0xFFFF;否则所有位都置为 0 ,即 0x0000 。

    运算符 英文 例子
    EQ equal 等于 ==
    NE not equal 不等于 !=
    GT greater than 大于 >
    LT less than 小于 <
    GE greater than or equal 大于等于 >=
    LE less than or equal 小于等于 <=

标号

  • 匿名标号

    • @@ 是匿名标号。

    • @b 向上查找最近的 @@b 是 back 。

    • @f 向下查找最近的 @@f 是 front 。

      @@:
      mov ax, 5566h and 6655h
      @@:
      mov ax, 7788h or 8877h
      jmp @b ; 跳到第 3 行
      jmp @f ; 跳到第 8 行

      @@:
      mov ax, not 5566h

      @@:
      mov ax, 5566h xor 7788h

  • 调整偏移量指令 ORG

    • 指令:ORG 偏移值

    • 此指令后的下一个变量或指令从 偏移值 开始存放。

    • 两个内容放在同一偏移处,后一个内容会覆盖前一个内容。

      data_seg segment
      g_buf dw 10h dup(0)
      org 20h
      g_w dw 65h ; 段偏移 20h 开始存放
      org 4
      g_w0 dw 6655h ; 会与 g_buf 的第四个字节开始的数据重复
      data_seg ends

  • 当前地址指令 $

    • $ 伪指令代表当前指令或变量的地址(段内偏移)。
    • 常用于计算缓冲区长度和获取当前 IP 值。
    • 可与 ORG 配合使用。

结构体

结构体名 struc
	
	; 这里定义结构体成员
	
结构体名 ends
  • 结构体使用 <> 来初始化。

  • 结构体可以通过变量名和寄存器来访问成员。

    Student struc
    m_sz db 64 dub(0)
    m_id dw 0
    Student ends

    data_seg segment
    g_stu Students <"Hello", 5566h> ; 结构体全局变量
    data_seg ends

    CODE segment

    Func1 PROC
    local @stu:Students ; 结构体局部变量

      mov @stu.m_id, 6			; 使用结构体局部变量
    
      assume bx:ptr Students
      lea bx, @stu
      mov [bx].m_id, 6			; 使用结构体指针
    
      ret
    

    Func1 ENDP

    CODE ends

equ 语句

  • 不可以重命名

  • 可用于常量和表达式

  • 可用于字符串

  • 可用于指令名,给指令取别名

  • 可用于类型,给类型取别名

  • 可用于操作数

    COUNT equ 100h ; 后跟数值
    SZHELLO equ "Hello,world!"
    MOVE equ mov ; 后跟助记符
    MYWORD equ dw ; 后跟类型
    BX_CONE equ byte ptr [bx] ; 后跟表达式

= 语句

  • 可以被修改

  • 只能用于常数

    COUNT2 = 100h ; 后跟数值
    COUNT2 = 100h ; 可以再次赋值
    mov ax, COUNT2

macro 语句

宏名 macro [参数1][,参数2]...
	宏体
endm
  • 宏会在使用的地方展开

  • 宏可以带参数

  • 字符串拼接使用 &

    movm macro op1, op2
    push op2
    pop op1
    endm

    shift macro n, reg, d
    mov cl, n
    ro&d reg, cl
    endm

分支

.IF condition
	; 条件成立时所执行的指令序列
.ENDIF

.IF condition
	; 条件成立时所执行的指令序列
.ELSE
	; 条件不成立时所执行的指令序列
.ENDIF

.IF condition1
	; condition1 成立时所执行的指令序列
.ELSEIF condition2
	; condition2 成立时所执行的指令序列
.ENDIF

其中条件表达式 condition 的书写方式与 C 语言中条件表达式的书写方式相似,也可用括号来组成复杂的条件表达式。

循环

.WHILE condition
  循环体的指令序列    ; 条件"condition"成立时所执行的指令序列
.ENDW

多文件编译

源文件

  • 源文件后缀名为 asm
  • 每个源文件末尾都需要有 end

头文件

  • 汇编头文件后缀名为 inc

  • 头文件包含 include xxx.inc

  • 头文件防重复包含

    ifndef SECOND_1
    SECOND_1 equ 1

    Func1 proto far stdcall arg1:word, arg2:word
    extern g_dw:word

    endif

函数使用

函数在源文件定义,在头文件声明即可。

全局变量

  • 全局变量定义在文件中必须使用 public 指明此变量为全局 public 变量名

  • 全局变量在使用文件中必须使用 extern 指明此变量来自外部文件 extern 变量:类型

    ; 文件1
    public g_wVal

    data_seg segment
    g_wVal dw 5566h
    data_seg ends

    ; 文件2
    extern g_wVal:word

相关推荐
Crossoads2 天前
【汇编语言】端口 —— 「从端口到时间:一文了解CMOS RAM与汇编指令的交汇」
android·java·汇编·深度学习·网络协议·机器学习·汇编语言
雪碧透心凉_5 天前
8086汇编(16位汇编)学习笔记00.DEBUG命令使用解析及范例大全
汇编
C66668888 天前
C#多线程
开发语言·汇编·c#
傻童:CPU8 天前
汇编源程序的理解
汇编
木槿718 天前
软件包git没有可安装候选
汇编·git
ok0609 天前
各种开源汇编、反汇编引擎的非专业比较
汇编·开源
roboko_9 天前
MIPS指令集(一)基本操作
汇编
Crossoads9 天前
【汇编语言】内中断(三) —— 中断探险:从do0到特殊响应的奇妙旅程
android·开发语言·javascript·网络·汇编·单片机·机器学习
染指111010 天前
49.第二阶段x86游戏实战2-鼠标点击call深追二叉树
汇编·c++·windows·游戏安全·反游戏外挂·游戏逆向
程序leo源12 天前
深入理解指针
android·c语言·开发语言·汇编·c++·青少年编程·c#