Overview
本文首先介绍操作数的表示方法,然后各个章节将按照以下的分类方式对X86-64指令集依次进行介绍:
- 数据移动 Data Movement
- 转换指令 Conversion Instructions
- 算术指令 Arithmetic Instructions
- 逻辑指令 Logical Instructions
- 控制指令 Control Instructions
基本上是一些整数操作,文末附上本文所有用到的指令的列表。
1. 操作数的表示方法 Operands Notation
一般来说,一条指令由指令或操作本身(如加法、减法、乘法等)和操作数(operand)组成。
操作数:指要被操作的数据的来源与结果的位置。
1 | 这里语言匮乏无法解释的更清楚, |
操作数的表示方法 | 含义 |
---|---|
<reg> | 寄存器操作数,寄存器中的数据 |
<reg8>,<reg16>, <reg32>,<reg64> |
规定了具体大小的寄存器操作数,比如 reg8表示大小为一个字节的寄存器(al bl等) reg16表示大小为两个字节的寄存器(ax bx等) |
<dest> | 目标操作数(destination),寄存器或内存中的数据。 根据具体指令,其内容将会被新结果覆盖 |
<RXdest> | 浮点目标操作数,同上 |
<src> | 源操作数,该操作数的值不会被指令改变 |
<imm> | 立即数(immediate value),一个即刻生效的临时值 可指定进制,默认为十进制 |
<mem> | 内存操作数,可以是变量名或者内存地址 |
<op> 或 <operand> | 操作数 |
<op8>,<op16>, <op32>,<op64> |
规定了具体大小的操作数 |
<label> | 程序的标签 |
2. 数据移动指令 Data Movement
通常情况下,数据必须从 RAM 移入 CPU 寄存器才能进行运算。计算完成后,可将结果从寄存器赋值到变量中。这种基本的数据移动操作是通过移动指令(Data Movement)来完成的。移动指令的一般形式为:
mov <dest>, <src>
比如:
mov eax, dword [myVar]
- mov指该指令为数据移动指令
- eax指要把数据移动到eax寄存器中
- dword指的是源数据是一个dword类型的数据,中括号中的myVar指的是该数据在内存中的位置。
- 中括号表示该内存地址处的值,如果没有中括号且省略了dword来表示大小,即
mov eax, myVar
,则myVar将表示内存地址本身,编译器也不会报错,也许是个潜在的坑。这部分可以扩展出寻址模式,但这里还是不去深挖了。
- 中括号表示该内存地址处的值,如果没有中括号且省略了dword来表示大小,即
目标操作数和源操作数的大小必须相同。
目标操作数不能是立即数。
两个操作数不能同时为内存。
1 | ; 以下两条指令表示bAns = bNum, 因为两个操作数不能都是内存,所以需要分开写 |
寄存器的高位如果没有用到,将会被赋值为0。
总结:
两个操作数不能同时为内存
目标操作数不能是立即数
如果寄存器中有高位,将会被重置为0
示例:
1 | mov ax, 42 |
3. 转换指令 Conversion Instructions
有时需要将一种大小的数据转换成另一种大小。比如从2字节转换到4字节
3.1 缩小转换(Narrowing Conversions)
缩小转换不需要特殊指令。内存位置或寄存器的内存位置或寄存器的低部分可以直接访问。例如,如果将50(0x32)放在 rax 寄存器中,则可直接访问 al 寄存器以获取该值,具体操作如下:
1 | mov rax, 50 |
因为50在一个字节中是可以表示出来的,所以并不会造成信息缺失。
1 | mov rax, 500 |
因为$$500=111110100_2$$,al寄存器中仅含后八位。所以会造成信息丢失,并且编译器并不会报告这个错误,程序员应自己负责该问题的处理。
3.2 扩大转换(Widening Conversions)
扩大转换是从较小的类型转换到较大的类型。涉及到最高位是不是表示正负的符号位,所以必须知道有符号或无符号的数据类型,并使用适当的程序或指令。
3.2.1 无符号的转换
要转入的寄存器的上半部分必须手动设置为0,比如
1 | ; 以下代码示例为把1字节的al的内容放入8字节的rbx中 |
对于扩大转换,有专门的指令来解决这个问题:
movzx <dest>, <src>
该指令会直接把<dest>的上半部分设置为0
3.2.2 带符号的转换
对于带符号的加宽转换,高阶位必须设置为 0 或 1,取决于原始值是正值还是负值。
这是通过符号扩展操作实现的。
例如,如果 ax 寄存器的值设置为 -7 (0xfff9),则位数设置如下
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 1 |
如果要把它扩展到eax寄存器中,则需要它为:
它的具体规则为,把符号位(本例中为1,即负号)放入所有扩展的区域。
有一系列专用指令用于将 A 寄存器中的有符号值从较小的大小转换为较大的大小。这些指令仅作用于 A 寄存器,有时使用 D 寄存器来处理结果。例如,cwd
指令将 ax 寄存器中的带符号值转换为 dx(高阶部分)和 ax(低阶部分)寄存器中的带符号值。按照惯例写为 dx:ax
。cwde
指令将 ax 寄存器中的带符号值转换为 eax 寄存器中的带符号值。
以下为一些相关指令:
cbw
:al到axcwd
:ax到dx:axcwde
:ax到eaxcdq
:eax到edx:eaxcdqe
:eax到raxcqo
:rax到rdx:raxmovsx
:通用转换,对于32到64位转换有一个专用的movsxd
指令作为扩展。; 示例 movsx <dest>, <src> movsx <reg16>, <op8> movsx <reg32>, <op8> movsx <reg32>, <op16> movsx <reg64>, <op8> movsx <reg64>, <op16> movsxd <reg64>, <op32>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
### 4. 整数算术指令 Integer Arithmetic Instructions
整数算术指令是在整数值上执行加减乘除操作的指令。
本节将会很长。
#### 4.1 加法
通用加法指令为:
`add <dest>, <src>`
意义为:`dest = dest + src`,同样需要满足一些条件
- dest与src必须大小相同
- dest不能是imm
- 两个operands不能同时为内存
举例:
~~~assembly
; 定义以下变量
bNum1 db 42
bNum2 db 73
bAns db 0
wNum1 dw 4321
wNum2 dw 1234
wAns dw 0
dNum1 dd 42000
dNum2 dd 73000
dAns dd 0
qNum1 dq 42000000
qNum2 dq 73000000
qAns dq 0
; 我们进行以下的运算
; bAns = bNum1 + bNum2
; wAns = wNum1 + wNum2
; dAns = dNum1 + dNum2
; qAns = qNum1 + qNum2
; 使用指令应该以以下的方式进行
; 1. bAns = bNum1 + bNum2
mov al, byte [bNum1]
add al, byte [bNum2]
mov byte [bAns], al
; 2. wAns = wNum1 + wNum2
mov ax, word [wNum1]
add ax, word [wNum2]
mov word [wAns], ax
; 3. dAns = dNum1 + dNum2
mov eax, dword [dNum1]
add eax, dword [dNum2]
mov dword [dAns], eax
; 4. qAns = qNum1 + qNum2
mov rax, qword [qNum1]
add rax, qword [qNum2]
mov qword [qAns], rax
这里的qword、dword、word、byte可以省略,但是写进来是一个好的习惯。
此外还有一条递增指令:
inc <op>
意义为:op = op + 1
这条指令与add指令加1的时候起到同样的效果,需要注意,inc指令必须指定操作数的大小,即byte word dword qword等不能省略:
1 | inc rax ; rax = rax + 1 |
inc指令和add指令一样,程序员要自己确保操作合法,比如100000不能放入byte中。
指令 | 含义 |
---|---|
add <dest>, <src> | dest = dest + src - 两个操作数不能同时为内存 - dest不能为imm |
举例 | add cx, word [wVvar] add rax, 42 add dword [dVar], eax add qword [qVar], 300 |
inc <operand> | operand = operand + 1 |
举例 | inc word [wVvar] inc rax inc dword [dVar] inc qword [qVar] |
4.1.1 带进位的加法
带进位的加法(ADC, Addtion with Carry)是一个特殊的加法指令,是ADD指令的扩展,与ADD指令不同的是,它还会在进行加法运算的时候检查标志寄存器rFlags中的CF位,该位的作用为记录上一步计算中是否存在进位。在二进制中如果最高位都是1,相加后会的值会溢出,如果发生溢出就说明此次运算发生了进位,会被记录在CF位中。ADD与ADC都可能会更新CF的值。
指令的形式为:
adc <dest>, <src>
意义为:dest = dest + src + carryBit
举例:
1 | ; 定义两个128bit的变量,在后面的指令中将它们相加 |
指令 | 含义 |
---|---|
adc <dest>, <src> | dest = dest + src - 两个操作数不能同时为内存 - dest不能为imm |
举例 | adc rcx, qword [dVvar1] adc rax, 42 |
4.2 减法
通用减法指令为:
sub <dest>, <src>
意义为:dest = dest - src
一些注意事项:
- dest和src不能同时为内存
- dest不能是imm
- dest和src必须大小一致
举例:
1 | ; 定义变量 |
同样地,byte、word、dword、qword这些可以省略,但中括号不能省略。
同样地,有一个递减指令:
dec <operand>
意义为:operand = operand - 1
使用时不能省略大小定义。
总结:
指令 | 含义 |
---|---|
sub <dest>, <src> | dest = dest - src - 两个操作数不能同时为内存 - dest不能为imm |
举例 | sub cx, word [wVvar] sub rax, 42 sub dword [dVar], eax sub qword [qVar], 300 |
dec <operand> | operand = operand - 1 |
举例 | dec word [wVvar] dec rax dec dword [dVar] dec qword [qVar] |
4.3 乘法
在整数乘法中,整数的格式是否为带符号的整数有不同的运算规则。指令也分为无符号整数乘法指令mul,和有符号乘法指令imul。
乘法的结果通常会使占用的大小翻倍,比如1byte的整数乘以1byte的整数会变成2byte的整数,4byte的整数乘以4byte的整数会变成8byte的整数。
程序员需要自己来判断寄存器与内存是否发生溢出。
4.3.1 无符号乘法
通用无符号乘法指令为:
mul <src>
意义为:把A寄存器(al\ax\eax\rax,取决于src的大小)中的值与src相乘,结果放入A寄存器,根据结果的大小也有可能存入D寄存器。下图给出了寄存器与各个大小的操作数相乘的情况:
如图所示,A寄存器和D寄存器有时会混用,可能会造成混乱。
例如,当一个 rax(64 位)乘以另一个64位操作数时,乘法指令会提供一个quadword结果(128 位)。这在处理非常大的数字时非常有用。由于 64 位体系结构只有 64 位寄存器,128 位结果必须放在两个不同的四字(64 位)寄存器中,rdx 表示高阶结果,rax 表示低阶结果,通常写成 rdx:rax。
而ax(32位)乘以另一个32位操作数时,虽然有eax寄存器更方便使用,但是实际上还是会使用dx:ax来存储结果,这样做是为了兼容以前的程序。
举例:
1 | bNumA db 42 |
指令 | 含义 |
---|---|
mul <src> mul <op8> mul <op16> mul <op32> mul <op64> |
把A寄存器的值与src相乘,可能产生不同的结果 ax = al * src dx:ax = ax * src edx:eax = eax * src rdx:rax = rax * src src不能为imm |
举例 | mul word [wVvar] mul al mul dword [dVar] mul qword [qVar] |
4.3.2 带符号乘法
带符号乘法允许更多的操作数和操作数大小。有符号乘法的一般形式如下:
imul <source>
- 该指令与无符号乘法的规则相同
imul <dest>, <src/imm>
- 该指令意义为:
<dest> = <dest> * <src/imm>
imul <dest>, <src>, <imm>
- 该指令的意义为:
<dest> = <src> * <imm>
对于以上所有的指令,dest必须是寄存器。所有的操作数都必须大于byte。
举例:
1 | wNumA dw 1200 |
指令 | 含义 |
---|---|
imul <src> imul <dest>, <src/imm32> imul <dest>,<src>,<imm32> |
把A寄存器的值与src相乘,可能产生不同的结果 ax = al * src dx:ax = ax * src edx:eax = eax * src rdx:rax = rax * src src不能为imm 对于两个参数的指令: reg16 = reg16 * op16/imm reg32 = reg32 * op32/imm 对于3个参数的指令: reg16 = op16 * imm |
举例 | imul ax, 17 imul al imul ebx, dword [dVar] imul rcx, dword [dVar], 791 |