原则上仅允许电流作单方向传导,它在一个方向为低电阻,高电流,而在另一个方向为高电阻。计算机将高低电压定义为 0 和 1,借助二极管的特性完成运算。这就是为什么计算机以二进制形式运算。
门电路(Logic Gate Circuit)是数字电子电路中的基本构建块,用于实现逻辑运算和控制信号。逻辑门根据不同的逻辑运算规则来处理输入信号,并产生相应的输出信号。
常见的逻辑门包括与门(AND gate)、或门(OR gate)、非门(NOT gate)、异或门(XOR gate)等,上图是与门的实现。
所有的数学运算都可以由位运算组成。将常用运算符封装成一个器件,称之为单元。
运算单元通常由两个输入端,一个控制端和一个输出端组成。
可以控制硬件的二进制数据叫做机器码。
以上面的 ALU 为例,假设该 ALU 能进行 8 位二进制数的运算。
则我们可以将表达式 15 + 23
与上面的 ALU 的输入作如下对应:
00001111
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)。
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
。SF
,ZF
,OF
,CF
,AF
,PF
DF
,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 对齐。
一个物理地址可以由多个逻辑地址表示,但基于分段原则,一般编程中不会碰到。
8086 中,段基址都是存储在段寄存器中,段偏移可以用立即数或者通用寄存器指明。
DS
:数据段,默认使用 DX
。CS
:代码段,绑定 CS:IP
使用。SS
:堆栈段,用作函数栈,绑定 SS:SP
使用。ES
:扩展段,常用于串操作。debug
的一些命令也与段寄存器绑定:
a
,u
:代码段 CS
d
,e
:数据段 DS
当然我们也可以指定特定的段,例如 d ss:100
。
8086 有 20 跟地址线,16 根数据线,其中数据线与地址线的低 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
。 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
。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/popa
pushad/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
/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
+ 1sub
:减法
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
- 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
,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位 | 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 不变
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
(只能用于段内转移)指令 | 说明 | 示例 |
---|---|---|
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 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 |
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, 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
可以作用于段或者段内的变量,结果都是得到对应段的基址。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]:byte
uses 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
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