为什么要学习 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 & 23
:10 00001111 00010111
15 ^ 23
:11 00001111 00010111
助记符
机器码的二进制值难记,因此可以将每种功能的二进制控制码取一个容易记住的名字,这个名字叫做助记符,也称之为指令。
例如:
00
:add
01
:sub
10
:and
11
:xor
因此上面列举的机器码可以转换为如下汇编:
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 是必须的。
- 取指令
- 译码
- 取数据
- 执行
- 存储结果
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]
:从startaddr
到endaddr
。[startaddr l num]
:从startaddr
到startaddr + num
。
标志寄存器
标志寄存器反应 ALU 运算结果的状态。
- 条件标志位
SF
,ZF
,OF
,CF
,AF
,PF
- CPU 执行完一条指令后自动设置。
- 反映算术、逻辑运算等指令执行完毕后,运算结果的特征。
- 控制标志位
DF
,IF
,TF
- 控制 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(方向标志) | si 、di 递减? |
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
的一些命令也与段寄存器绑定:
a
,u
:代码段CS
d
,e
:数据段DS
当然我们也可以指定特定的段,例如 d ss:100
。
8086 有 20 跟地址线,16 根数据线,其中数据线与地址线的低 16 位复用。内部通过地址加法器计算地址。
寻址
- 指令中用于说明操作数所在的方式称为寻址方式。
- 16 位 CPU 的寻址方式有 7 种,32 位 CPU 还会多一种比例因子寻址。
立即寻址
- 操作数的值存在指令中的方式称作立即寻址。
- 汇编中整数常量称作立即数。
- 立即数可以是 8 位数,也可以是 16 位数。
寄存器寻址
- 操作数的值存储在寄存器的寻址方式称作寄存器寻址。
- 寄存器包括通用寄存器和段寄存器。
注意:
- 段寄存器之间不能赋值。
- 指令指针寄存器不能用作寻址(例如不存在
MOV AX, IP
汇编)。
直接寻址
操作数值在内存中,机器码中存储 16 位段内偏移的寻址方式称作直接寻址。
寄存器间接寻址
- 操作数值在内存中,段内偏移存储在寄存器中的寻址方式称作寄存器间接寻址。
- 间接寻址的寄存器有
BX
,BP
,SI
,DI
。
EA = [ ( BX ) ( BP ) ( SI ) ( DI ) ] \text{EA}=\begin{bmatrix} (\text{BX})\\ (\text{BP})\\ (\text{SI})\\ (\text{DI}) \end{bmatrix} EA= (BX)(BP)(SI)(DI)
寄存器相对寻址
- 操作数值在内存中,段内偏移存储由
[寄存器 + 立即数]
计算得来的的寻址方式称作寄存器相对寻址。 - 寄存器相对寻址的寄存器有
BX
,BP
,SI
,DI
。 - 寄存器相对寻址的立即数可以是 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]
基址变址寻址
- 操作数值在内存中,段内偏移由
[寄存器 + 寄存器]
计算得来的寻址方式称作基址变址寻址。 - 可用做基址的寄存器有
BX
,BP
。 BX
默认DS
段,BP
默认SS
段。- 可用作变址的寄存器有
SI
,DI
。
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)]
基址变址相对寻址
- 操作数值在内存中,段内偏移由
[基址寄存器+变址寄存器+偏移常量]
计算得来的寻址方式称作基址变址寻址。 - 可用做基址的寄存器有
BX
,BP
。 BX
默认DS
段,BP
默认SS
段。- 可用作变址的寄存器有
SI
,DI
。 - 可用作常量的数值可以是 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
- 16 位:
注意:
- 对于 8086 CPU,
push
指令的操作数只能是长度为 2 字节的寄存器(包括段寄存器)或内存。80286,80386 及以上的 CPU 的push
指令支持立即数和寄存器。 - 8086 不支持
pusha
指令,80286 才开始支持该指令。 pusha
指令会将 16 位通用寄存器AX
,CX
,DX
,BX
,SP
,BP
,SI
,DI
中的值依次压入栈中。
标志寄存器传送指令
标志寄存器传送指令用来传送标志寄存器 FLAGS
的内容,方便进行对各个标志位的直接操作。
- 低 8 位传送
LAHF
:AH
←FLAGS
的低字节LAHF
指令将标志寄存器的低字节传送给寄存器AH
。SF
/ZF
/AF
/PF
/CF
状态标志位分别送入AH
的第 7/6/4/2/0 位,而AH
的第 5/3/1 位任意。
SAHF
:FLAGS
的低字节 ←AH
SAHF
将AH
寄存器内容传送给FLAGS
的低字节。- 用
AH
的第 7/6/4/2/0 位相应设置SF
/ZF
/AF
/PF
/CF
标志位。
- 16 位传送
PUSHF
:PUSHF
指令将标志寄存器的内容压入堆栈,同时栈顶指针SP
减 2 。POPF
:POPF
指令将栈顶字单元内容传送标给志寄存器,同时栈顶指针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 端口 ←AL
(i8
直接寻址)OUT DX, AL
:字节输出,I/O 端口 ←AL
(DX
间接寻址)OUT i8, AX
:字输出,I/O 端口 ←AX
(i8
直接寻址)OUT DX, AX
:字输出,I/O 端口 ←AX
(DX
间接寻址)
这个指令的其中一个用途是检测虚拟机。在真机环境中由于输入输出指令为特权指令,在 3 环执行会触发异常。而在虚拟机中则不会。
算术运算类指令
加法
add
:加法ADD reg, imm/reg/mem
:reg
←reg
+imm
/reg
/mem
ADD mem, imm/reg
:mem
←mem
+imm/reg
adc
:带进位加法ADC reg, imm/reg/mem
:reg
←reg
+imm
/reg
/mem
+CF
ADC mem, imm/reg
:mem
←mem
+imm
/reg
+CF
inc
:加一,不影响CF
标志位。INC reg/mem
:reg/mem
←reg
/mem
+ 1
减法
sub
:减法SUB reg, imm/reg/mem
:reg
←reg
-imm
/reg
/mem
SUB mem, imm/reg
:mem
←mem
-imm
/reg
sbb
:带借位的减法SBB reg, imm/reg/mem
:reg
←reg
-imm
/reg
/mem
-CF
SBB mem, imm/reg
:mem
←mem
-imm
/reg
-CF
dec
:减一,不影响CF
标志位。DEC reg/mem
:reg/mem
←reg
/mem
- 1
求补指令 NEG(negative)
NEG
指令对操作数执行求补运算:用零减去操作数,然后结果返回操作数。求补运算也可以表达成:将操作数按位取反后加 1 。
NEG reg/mem
:reg
/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)
- 说明:目的操作数减去源操作数,然后根据结果设置标志位,但该结果不存入目的地址。
- 影响标志位:
AF
,CF
,OF
,PF
,SF
,ZF
- 作用:一般后面跟一条条件转移指令,根据比较结果跳转到不同的分支,用于处理
OPD
和OPS
大小比较不同的情况。
乘法指令
- 无符号乘法
位数 隐含的被乘数 乘积存放的位置 举例 8位 AL
AX
MUL BL
16位 AX
DX-AX
MUL BX
32位 EAX
EDX-EAX
MUL ECX
- 格式:
MUL reg/mem
- 功能:显式操作数*隐式操作数(看成无符号数)
- 影响标志位:
CF
和OF
,如果乘积的高一半位(AH
/DX
/EDX
)包含有乘积的有效位,则CF=1
,OF=1
;否则CF=0
,OF=0
。
- 格式:
- 有符号乘法
- 格式:
IMUL reg/mem
IMUL reg, imm
(80286+)IMUL reg, reg, imm
(80286+)IMUL reg, reg/mem
(80386+)
- 功能:有符号数相乘
- 影响标志位:
CF
和OF
,如果乘积的高一半位(AH
/DX
/EDX
)不是低位的纯符号扩展,则CF=1
,OF=1
;否则CF=0
,OF=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
- 影响标志位:
AF
,CF
,OF
,PF
,SF
,ZF
- 除法溢出:字节除时商不在
-128~127
范围内或者在字除时商不在-32768~32767
范围内。
- 格式:
符号扩展指令
CBW
(Convert Byte to Word):将AL
中的符号扩展至AH
中,操作数是隐含且固定的。XX04
→0004
XXFE
→FFFE
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)
,PF
,SF
,ZF
(AF
无定义)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)
,PF
,SF
,ZF
(AF
无定义)
- 格式:
- 逻辑非(按位取反):
NOT
- 格式:
NOT reg/mem
- 受影响的标志位:无
- 格式:
- 异或:
XOR
- 格式:
XOR reg/mem, reg/mem/imm
- 受影响的标志位:
CF(0)
,OF(0)
,PF
,SF
,ZF
(AF
无定义)
- 格式:
TEST
指令- 格式:
TEST reg/mem, reg/mem/imm
- 作用:执行
AND
,但不影响目标操作数。 - 受影响的标志位:
CF(0)
,OF(0)
,PF
,SF
,ZF
(AF
无定义)
- 格式:
以 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
- 影响标志:
OF
,ZF
,SF
,PF
,CF
- 指令
SAL
(Shift Arithmetic Left)/SHL
(Shift Logical Left):算术左移/逻辑左移
SAR
(Shift Arithmetic Right):算术右移
SHR
(Shift Logical Right):逻辑右移
- 格式:
- 循环移位
- 格式:
OP reg/mem, 1/cl
- 影响标志:
OF
,CF
,其他标志无定义。 - 指令
ROL
(Rotate Left):循环左移
ROR
(Rotate Right):循环右移
RCL
(Rotate through Carry Left):带进位循环左移
RCR
(Rotate through Carry Right):带进位循环右移
- 格式:
串操作类指令
串操作指令
- 源操作数使用
SI
,默认段为DS
,可段超越。 - 目的操作数使用
DI
,默认段为ES
,不可段超越。 DF
寄存器决定串操作方向。DF
值为0(UP)
则执行完指令之后SI
和DI
都加操作的数据长度。DF
值为1(DN)
则执行完指令之后DI
和DI
都减操作的数据长度。
段超越(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):串存储,把AL
或AX
数据传送至目的地址。STOSB
:字节串存储,ES:[DI] ← AL (DI ← DI ± 1)
。STOSW
:字串存储,ES:[DI] ← AX (DI ← DI ± 2)
。STOSD
:双字串存储,ES:[DI] ← EAX (DI ← DI ± 4)
。
LODS
(Load String):串读取,把指定主存单元的数据传送给AL
或AX
。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 = 0
或ZF = 0
则重复执行结束。- 含义:当数据串没有结束(
CX ≠ 0
)并且串相等(ZF = 1
)则继续比较。 - 举例:
REPE/REPZ SCAS/SCASB/SCASW/SCASD
REPE/REPZ CMPS/CMPSB/CMPSW/CMPSD
- 含义:当数据串没有结束(
REPNZ
:每执行一次串指令,CX
减 1 ,并判断ZF
是否为 1 。只要CX = 0
或ZF = 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 reg
(reg
为通用寄存器) - 功能:
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 zeroCX = 0
CX
为 0JS
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 || ZF = 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 || ZF = 1
小于等于/不大于 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] | 段内间接调用 | push 返回地址jmp 函数地址 |
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/TASM
和VSCode 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 endsend 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 segmentg_btVal db 55h
data_seg endsuninitdata_seg segmentg_btVal1 db ?
uninitdata_seg endscode_seg segment
START:assume ds:data_seg ; 告诉编译器当前使用的是哪个段mov ax, data_segmov ds, ax ; 给段寄存器设置正确的值mov al, g_btVal ; 使用变量
code_seg endsend 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
可以作用于段或者段内的变量,结果都是得到对应段的基址。length
和size
都是按定义的数组的第一个“,
”前面的部分来计算的。例如前面的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
关键字让程序在被加载的时候指定ss
,bp
和sp
。- 使用数组为栈设置大小。
stack_seg segment stackdb 256 dup(0cch)
stack_seg ends
调用 dos 功能号
- DOS 系统提供的功能(API),通过 21 号中断来调用。
- 每个功能都有一个编号,通过
AH
指定功能编号。 - 每个功能的参数查看手册。
例如:
AH
为 0x4c 时为退出程序,退出码为AL
。AH
为 0x09 时为输出$
结尾的字符,字符串地址存放在DX
中。
利用这两个功能号我们可以实现一个 Hello World 程序。
data_seg segmentg_szHello db "hello,word!$"
data_seg endsstack_seg segment stackdb 256 dup(0cch)
stack_seg endscode_seg segment
START:assume ds:data_segmov ax, data_segmov ds, axmov ah, 09mov dx, offset g_szHelloint 21hmov ax, 4c00hint 21h
code_seg endsend START
中断
- 中断是 CPU 提供的流程跳转指令,类似函数调用。
- 在
00:00
位置存储着一个双字数组,大小为 256 ,称作中断向量表。 - 数组元素为逻辑地址
段基址:段偏移
(段偏移在低 2 字节)。 int n
的意思是从第 n 个元素获取地址,然后跳转执行。
函数
函数结构
函数执行流程:
- 参数入栈
- 返回地址入栈,跳转到函数
- 保存栈帧
- 申请局部变量空间
- 保存寄存器环境
- 执行函数功能
- 恢复寄存器环境
- 恢复栈帧
- 弹出返回地址,返回[平栈]
- [平栈]
函数定义
函数名 proc [距离][调用约定] [uses reg1 reg2..] [参数:word, 参数名:word..]local 变量:wordlocal 变量:wordret
函数名 endp
示例:
TestProc PROC far stdcall uses bx dx si di arg1:wordlocal btVal:byteret
TestProc ENDP
- 距离:
距离关键字 说明 near
函数只能段内调用
函数使用ret
返回
调用时ip
入栈far
段内段间都可调用
函数使用retf
返回
调用时ip
和cs
入栈- 如果是用
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 8877hjmp @b ; 跳到第 3 行jmp @f ; 跳到第 8 行@@:mov ax, not 5566h@@:mov ax, 5566h xor 7788h
- 调整偏移量指令
ORG
- 指令:
ORG 偏移值
- 此指令后的下一个变量或指令从
偏移值
开始存放。 - 两个内容放在同一偏移处,后一个内容会覆盖前一个内容。
data_seg segmentg_buf dw 10h dup(0)org 20hg_w dw 65h ; 段偏移 20h 开始存放org 4g_w0 dw 6655h ; 会与 g_buf 的第四个字节开始的数据重复 data_seg ends
- 指令:
- 当前地址指令
$
$
伪指令代表当前指令或变量的地址(段内偏移)。- 常用于计算缓冲区长度和获取当前
IP
值。 - 可与
ORG
配合使用。
结构体
结构体名 struc; 这里定义结构体成员结构体名 ends
- 结构体使用
<>
来初始化。 - 结构体可以通过变量名和寄存器来访问成员。
Student strucm_sz db 64 dub(0)m_id dw 0
Student endsdata_seg segmentg_stu Students <"Hello", 5566h> ; 结构体全局变量
data_seg endsCODE segmentFunc1 PROClocal @stu:Students ; 结构体局部变量mov @stu.m_id, 6 ; 使用结构体局部变量assume bx:ptr Studentslea bx, @stumov [bx].m_id, 6 ; 使用结构体指针ret
Func1 ENDPCODE 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, op2push op2pop op1
endmshift macro n, reg, dmov cl, nro&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 1Func1 proto far stdcall arg1:word, arg2:word
extern g_dw:wordendif
函数使用
函数在源文件定义,在头文件声明即可。
全局变量
- 全局变量定义在文件中必须使用
public
指明此变量为全局public 变量名
。 - 全局变量在使用文件中必须使用
extern
指明此变量来自外部文件extern 变量:类型
。
; 文件1
public g_wValdata_seg segmentg_wVal dw 5566h
data_seg ends; 文件2
extern g_wVal:word