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

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

常见的逻辑门包括与门(AND gate)、或门(OR gate)、非门(NOT gate)、异或门(XOR gate)等,上图是与门的实现。
所有的数学运算都可以由位运算组成。将常用运算符封装成一个器件,称之为单元。

运算单元通常由两个输入端,一个控制端和一个输出端组成。
可以控制硬件的二进制数据叫做机器码。
以上面的 ALU 为例,假设该 ALU 能进行 8 位二进制数的运算。

则我们可以将表达式 15 + 23 与上面的 ALU 的输入作如下对应:
000011110001011100将上面的输入按照 输入1 输入2 控制 的格式可以写作 00 00001111 00010111(为了方便阅读用空格隔开),这就是机器码。
类似的还有如下机器码:
15 & 23:10 00001111 0001011115 ^ 23:11 00001111 00010111机器码的二进制值难记,因此可以将每种功能的二进制控制码取一个容易记住的名字,这个名字叫做助记符,也称之为指令。
例如:
00:add01:sub10:and11:xor因此上面列举的机器码可以转换为如下汇编:
15 + 23 => 00 00001111 00010111 => add 0fh, 17h15 & 23 => 10 00001111 00010111 => and 0fh, 17h15 ^ 23 => 11 00001111 00010111 => xor 0fh, 17h硬件不能识别助记符,因此需要将其转换成对应的机器码,这个过程叫做汇编。

关于汇编代码有几个关键名词,在查阅(反)汇编器文档时会经常遇到:
MnemonicOperand一个系统不可能由一个硬件单独完成,所以划分出多个硬件模块,然后由一个硬件模块居中调度,称作 CPU(Central Processing Unit)。


AD 的引脚是用来寻址或存取数据的引脚。这种一个种引脚承担多种功能的特点称为引脚复用。CLK 引脚是 CPU 的是时钟输入引脚,它接收来自外部的时钟信号,用于同步处理器内部的操作和各种电子元件的工作。时钟信号的频率决定了处理器的工作速度,即指令的执行速度。
- 80286 是 8086 的后续型号,也是一款 16 位 CPU。它在 8086 的基础上引入了一些新的特性和改进,并提供了更高的性能。
- 80386 是 Intel 推出的第一个 32 位 CPU,也被称为 386。
所有硬件模块连接到 I/O 桥,由 I/O 桥负责辅助 CPU 与哪一个硬件模块连接。

以上图为例,s 决定了 out 的输出是 a 还是 b 。
以下图为例,CPU 有 8 位数据/地址总线,RAM 是一个 256 字节的存储器。


以一个 hello.c 程序为例。




执行单元(Execution Unit,EU)是8086系列处理器中的一个重要组成部分,它负责执行指令并控制处理器的操作。
总线接口单元(Bus Interface Unit,BIU)是8086系列处理器中的一个重要组成部分,它负责处理处理器与系统总线之间的接口和通信。

CPU 执行指令的过程可以分为一下 5 个步骤,其中 1,2,4 是必须的。
8086 CPU 将指令的执行分成多个模块,这样可以多个模块同时工作,从而提高效率。

然而这种优化在程序中分支跳转较多的时候会导致程序运行变慢。因为提前取到的下一条指令是地址与当前指令地址相邻的指令。而当前指令如果为跳转指令则需要消除提前执行的下一条指令的痕迹。因此编译器优化的其中一个方向是尽量减少程序中的分支跳转数量。
在 Windows XP 的 C:\WINDOWS\system32 目录下有一个名为 debug.exe 的程序。debug 是一个命令行工具,它提供了一种简单的方式来执行低级别的调试和汇编操作。
不过这个程序只能在 Windows XP 系统下运行,高版本的 Windows 系统已经不支持该程序的运行。
为了能够让 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:
C:\Program Files (x86)\DOSBox-0.74-3\C(手动创建的一个目录)挂载为 DOSBox 的 C 盘。C: 盘添加为环境变量。另外我还将 Windows XP 中的 debug.exe 复制到 DOSBox 挂载的 C 盘中,这样就可以再 DOSBox 中运行 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....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,PFDF,IF,TF| 标志 | 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 的值为 1 ,否则为 0 。(无符号数溢出)

CF 是针对无符号数来说的,因此无论是加法还是减法参与运算的数都是无符号数(大于等于 0)。CF 都会置 1 。奇偶标志 PF 用于反映运算结果中最低字节中 1 的个数的奇偶性。如果 1 的个数为偶数,则 PF 的值为 1 ,否则其值为 0 。
在发生下列情况时,辅助进位标志 AF 的值被置为 1 ,否则其值为 0 。
如果运算结果为 0 ,则其值为 1 ,否则其值为 0 。在判断运算结果是否为 0 时,可使用此标志位。
符号标志 SF 用来反映运算结果的符号位,它与运算结果的最高位相同。
溢出标志 OF 用于反映有符号数加减运算所得结果是否溢出。(有符号数溢出)

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

地址计算方式:内存地址 = 段基址 * 10h + 段偏移
段基址:段偏移 ,称为逻辑地址。EA(Effective Address,在很多库的文档中会出现这个名称)。PA(Physical Address)。段的内存分布如下,不同的段之间可以重叠。由于段地址的计算方式,段的起始地址关于 0x10 对齐。


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

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

8086 中,段基址都是存储在段寄存器中,段偏移可以用立即数或者通用寄存器指明。
DS:数据段,默认使用 DX 。CS:代码段,绑定 CS:IP 使用。SS:堆栈段,用作函数栈,绑定 SS:SP 使用。ES:扩展段,常用于串操作。debug 的一些命令也与段寄存器绑定:
a,u:代码段 CSd,e:数据段 DS当然我们也可以指定特定的段,例如 d ss:100 。
8086 有 20 跟地址线,16 根数据线,其中数据线与地址线的低 16 位复用。内部通过地址加法器计算地址。



注意:
MOV AX, IP 汇编)。操作数值在内存中,机器码中存储 16 位段内偏移的寻址方式称作直接寻址。

BX,BP,SI,DI 。
EA
=
[
(
BX
)
(
BP
)
(
SI
)
(
DI
)
]
\text{EA}=

[寄存器 + 立即数] 计算得来的的寻址方式称作寄存器相对寻址。BX ,BP ,SI ,DI 。
EA
=
[
(
BX
)
(
BP
)
(
SI
)
(
DI
)
]
+
[
8 位 disp
16 位 disp
]
\text{EA}=

[寄存器 + 寄存器] 计算得来的寻址方式称作基址变址寻址。BX ,BP 。BX 默认 DS 段,BP 默认 SS 段。SI ,DI 。
EA
=
[
(
BX
)
(
BP
)
]
+
[
(
SI
)
(
DI
)
]
\text{EA}=

[基址寄存器+变址寄存器+偏移常量] 计算得来的寻址方式称作基址变址寻址。BX ,BP 。BX 默认 DS 段,BP 默认 SS 段。SI ,DI 。
把一个字节或字的操作数从源地址传送至目的地址。

注意:
mov 指令源操作数和目的操作数指定的数据长度应一致。mov byte ptr ds:[bx], 12h 。其他指令由于寄存器自带长度因此不需要指定数据长度。情形:
效率:mov 优于 xchg ,因为 xchg 使用了内部暂存器。
作用:将 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; 。pusha/popapushad/popad注意:
push 指令的操作数只能是长度为 2 字节的寄存器(包括段寄存器)或内存。80286,80386 及以上的 CPU 的 push 指令支持立即数和寄存器。pusha 指令,80286 才开始支持该指令。pusha 指令会将 16 位通用寄存器 AX,CX,DX,BX,SP,BP,SI,DI 中的值依次压入栈中。标志寄存器传送指令用来传送标志寄存器 FLAGS 的内容,方便进行对各个标志位的直接操作。

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 标志位。PUSHF:PUSHF 指令将标志寄存器的内容压入堆栈,同时栈顶指针 SP 减 2 。POPF:POPF 指令将栈顶字单元内容传送标给志寄存器,同时栈顶指针 SP 加 2 。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 个,无需分段,设计有两种寻址方式:
i8 表示端口号。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/memADD mem, imm/reg:mem ← mem + imm/regadc:带进位加法
ADC reg, imm/reg/mem:reg ← reg + imm/reg/mem + CFADC mem, imm/reg:mem ← mem + imm/reg + CFinc:加一,不影响 CF 标志位。
INC reg/mem:reg/mem ← reg/mem + 1sub:减法
SUB reg, imm/reg/mem:reg ← reg - imm/reg/memSUB mem, imm/reg:mem ← mem - imm/regsbb:带借位的减法
SBB reg, imm/reg/mem:reg ← reg - imm/reg/mem - CFSBB mem, imm/reg:mem ← mem - imm/reg - CFdec:减一,不影响 CF 标志位。
DEC reg/mem:reg/mem ← reg/mem - 1NEG 指令对操作数执行求补运算:用零减去操作数,然后结果返回操作数。求补运算也可以表达成:将操作数按位取反后加 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 OPD, OPS(OPD) - (OPS)AF,CF,OF,PF,SF,ZFOPD 和 OPS 大小比较不同的情况。| 位数 | 隐含的被乘数 | 乘积存放的位置 | 举例 |
|---|---|---|---|
| 8位 | AL | AX | MUL BL |
| 16位 | AX | DX-AX | MUL BX |
| 32位 | EAX | EDX-EAX | MUL ECX |
MUL reg/memCF 和 OF ,如果乘积的高一半位(AH/DX/EDX)包含有乘积的有效位,则 CF=1 ,OF=1 ;否则 CF=0 ,OF=0 。IMUL reg/memIMUL 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位 | AX | 8位ops | AL | AH |
| 16位 | DX-AX | 16位ops | AX | DX |
| 32位 | EDX-EAX | 32位ops | EAX | EDX |
IDIV reg/memAF,CF,OF,PF,SF,ZF-128~127 范围内或者在字除时商不在 -32768~32767 范围内。CBW(Convert Byte to Word):将 AL 中的符号扩展至 AH 中,操作数是隐含且固定的。
XX04 → 0004XXFE → FFFECWD(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/immCF(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/immCF(0),OF(0),PF,SF,ZF(AF 无定义)NOT
NOT reg/memXOR
XOR reg/mem, reg/mem/immCF(0),OF(0),PF,SF,ZF(AF 无定义)TEST 指令
TEST reg/mem, reg/mem/immAND ,但不影响目标操作数。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/clOF,ZF,SF,PF,CFSAL(Shift Arithmetic Left)/SHL(Shift Logical Left):算术左移/逻辑左移
SAR(Shift Arithmetic Right):算术右移
SHR(Shift Logical Right):逻辑右移
OP reg/mem, 1/clOF,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/LODSDREP STOS/STOSB/STOSW/STOSDREP MOVS/MOVSB/MOVSW/MOVSDREPZ:每执行一次串指令,CX 减 1 ,并判断 ZF 是否为 0 。只要 CX = 0 或 ZF = 0 则重复执行结束。
CX ≠ 0)并且串相等(ZF = 1)则继续比较。REPE/REPZ SCAS/SCASB/SCASW/SCASDREPE/REPZ CMPS/CMPSB/CMPSW/CMPSDREPNZ:每执行一次串指令,CX 减 1 ,并判断 ZF 是否为 1 。只要 CX = 0 或 ZF = 1 则重复执行结束。
CX ≠ 0)并且串不相等(ZF = 0)则继续比较。REPNE/REPNZ SCAS/SCASB/SCASW/SCASDREPNE/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(只能用于段内转移)| 指令 | 说明 | 示例 |
|---|---|---|
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 || 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 | CX != 0 |
LOOPZ/LOOPE | CX != 0 && ZF = 1 |
LOOPNZ/LOOPNE | CX != 0 && ZF = 0 |
| 指令 | 说明 | 功能 |
|---|---|---|
call (near ptr) 标号 | 段内直接调用 | push 返回地址jmp 标号 |
call REGcall near ptr|word ptr [EA] | 段内间接调用 | push 返回地址jmp 函数地址 |
call far ptr 标号call dword ptr [EA] | 段间调用 | push cspush 返回地址jmp 标号 |
ret (n) | 段内返回 | pop ipadd sp, n |
retf (n) | 段间返回 | pop ippop csadd sp, n |
DOSBox 环境变量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 ends
end ENTRY
段名 segment
段名 ends
汇编中使用分号(;)来标注注释,汇编中只有行注释没有块注释。
; 这里是注释
mov ax, bx ; 这里是注释
-)。| 关键字 | 说明 | 示例 |
|---|---|---|
| 无 | 十进制 | mov ax, 1234 |
D | 十进制 | mov ax, 1234d |
B | 二进制 | mov ax, 1011b |
O | 八进制 | mov ax, 76o |
H | 十六进制 | mov ax, 76omov ax, 0abh |
')或双引号("),例如 mov byte ptr [bx], '$' 。?)表示。变量名 类型 初始值
val dd 5566h
| 关键字 | 意义 |
|---|---|
db | 字节 |
dw | 字 |
dd | 双字 |
dq | 8 字节 |
dt | 16 字节 |
变量使用前需要注意两点:
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 可以作用于段或者段内的变量,结果都是得到对应段的基址。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 stack
db 256 dup(0cch)
stack_seg ends
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
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 返回调用时 ip 和 cs 入栈 |
far 修饰且段内调用,汇编器也会手动压一个 cs 寄存器确保 retf 能正常返回。| 调用约定关键字 | 说明 |
|---|---|
c | 调用方平栈 |
stdcall | 被调用方平栈 |
| 类型 | 局部变量类型 | 备注 |
|---|---|---|
db | byte | 可以直接赋值使用 |
dw | word | 可以直接赋值使用 |
dd | dword | 不可以直接赋值使用 |
dq | qword | 不可以直接赋值使用 |
dt | tword | 不可以直接赋值使用 |
@ ,属于一种编程规范。local @dwBuf[100h]:byteuses reg1 reg2.. 表示函数中会使用相应的寄存器,因此在函数开始和结束位置会保存和恢复相应的寄存器。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 | 异或 |
| 运算符 | 英文 | 例子 |
|---|---|---|
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
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 [参数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
asmendincinclude xxx.incifndef 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