0%

X86-64指令集概述

X86-64指令集的基本概述,不包括一些高级指令和限制模式指令

Overview

本文首先介绍操作数的表示方法,然后各个章节将按照以下的分类方式对X86-64指令集依次进行介绍:

  • 数据移动 Data Movement
  • 转换指令 Conversion Instructions
  • 算术指令 Arithmetic Instructions
  • 逻辑指令 Logical Instructions
  • 控制指令 Control Instructions

基本上是一些整数操作,文末附上本文所有用到的指令的列表。

1. 操作数的表示方法 Operands Notation

一般来说,一条指令由指令或操作本身(如加法、减法、乘法等)和操作数(operand)组成。

操作数:指要被操作的数据的来源与结果的位置

1
2
3
这里语言匮乏无法解释的更清楚,
不在此处纠结此概念继续学习在下一节会容易理解。
此概念很重要。
操作数的表示方法 含义
<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将表示内存地址本身,编译器也不会报错,也许是个潜在的坑。这部分可以扩展出寻址模式,但这里还是不去深挖了。

目标操作数和源操作数的大小必须相同。

目标操作数不能是立即数。

两个操作数不能同时为内存。

1
2
3
; 以下两条指令表示bAns = bNum, 因为两个操作数不能都是内存,所以需要分开写
mov al, byte [bNum] ; 把内存中的数据移动到al寄存器
mov byte [bAns], al ; 把al寄存器中的数据移动到内存

寄存器的高位如果没有用到,将会被赋值为0。

总结

  • 两个操作数不能同时为内存

  • 目标操作数不能是立即数

  • 如果寄存器中有高位,将会被重置为0

示例

1
2
3
4
mov     ax,              42
mov cl, byte [bvar]
mov dword [dVar], eax
mov qword [qVar], rdx

3. 转换指令 Conversion Instructions

有时需要将一种大小的数据转换成另一种大小。比如从2字节转换到4字节

3.1 缩小转换(Narrowing Conversions)

缩小转换不需要特殊指令。内存位置或寄存器的内存位置或寄存器的低部分可以直接访问。例如,如果将50(0x32)放在 rax 寄存器中,则可直接访问 al 寄存器以获取该值,具体操作如下:

1
2
mov   rax,          50
mov byte [bVal], al

因为50在一个字节中是可以表示出来的,所以并不会造成信息缺失。

1
2
mov   rax,          500
mov byte [bVal], al

因为$$500=111110100_2$$,al寄存器中仅含后八位。所以会造成信息丢失,并且编译器并不会报告这个错误,程序员应自己负责该问题的处理。

3.2 扩大转换(Widening Conversions)

扩大转换是从较小的类型转换到较大的类型。涉及到最高位是不是表示正负的符号位,所以必须知道有符号或无符号的数据类型,并使用适当的程序或指令。

3.2.1 无符号的转换

要转入的寄存器的上半部分必须手动设置为0,比如

1
2
3
4
5
6
7
8
9
10
; 以下代码示例为把1字节的al的内容放入8字节的rbx中

; 把50放入al
mov al, 50

; 先把rbx设置为0,这一步主要是确保扩大后被扩大的部分都是正确的
mov rbx, 0

; 把al的值放入bl
mov bl, al

对于扩大转换,有专门的指令来解决这个问题:

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寄存器中,则需要它为:

image-20240621201128146

它的具体规则为,把符号位(本例中为1,即负号)放入所有扩展的区域。

有一系列专用指令用于将 A 寄存器中的有符号值从较小的大小转换为较大的大小。这些指令仅作用于 A 寄存器,有时使用 D 寄存器来处理结果。例如,cwd 指令将 ax 寄存器中的带符号值转换为 dx(高阶部分)和 ax(低阶部分)寄存器中的带符号值。按照惯例写为 dx:axcwde 指令将 ax 寄存器中的带符号值转换为 eax 寄存器中的带符号值。

以下为一些相关指令:

  • cbw:al到ax

  • cwd:ax到dx:ax

  • cwde:ax到eax

  • cdq:eax到edx:eax

  • cdqe:eax到rax

  • cqo:rax到rdx:rax

  • movsx:通用转换,对于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
2
3
4
5
inc    rax                   ; rax = rax + 1
inc byte [bNum] ; bNum = bNum + 1
inc word [wNum] ; wNum = wNum + 1
inc dword [dNum] ; dNum = dNum + 1
inc qword [qNum] ; qNum = qNum + 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; 定义两个128bit的变量,在后面的指令中将它们相加
; 由于64bit的CPU中寄存器最多64位,所以该运算会用到更多的寄存器
dquad1 ddq 0x1A000000000000000
dquad2 ddq 0x2C000000000000000
dqSum ddq 0

; rax是一个64位寄存器,先把dquad1放入rax
mov rax, qword [dquad1]
; 然后把dquad1加上8个字节就会得到高位的地址(little-endian)
mov rdx, qword [dquad1+8]
; 这两部之后rdx和rax共同存储了dquad1

; 先相加低位,add指令也会更新CF
add rax, qword [dquad2]
; 再相加高位,使用adc可以判断前一步add中有没有产生进位
adc rdx, qword [dquad2+8]

; 把相加后的值写入dqSum中
mov qword [dqSum], rax
mov qword [dqSum+8], rdx
; 高位也有可能发生溢出,需要程序员自己来保证没有错误
指令 含义
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
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
; 定义变量
bNum1 db 73
bNum2 db 42
bAns db 0

wNum1 dw 1234
wNum2 dw 4321
wAns dw 0

dNum1 dd 73000
dNum2 dd 42000
dAns dd 0

qNum1 dq 73000000
qNum2 dq 73000000
qAns dd 0

; bAns = bNum1 - bNum2
mov al, byte [bNum1]
sub al, byte [bNum2]
mov byte [bAns], al

; wAns = wNum1 – wNum2
mov ax, word [wNum1]
sub ax, word [wNum2]
mov word [wAns], ax

; dAns = dNum1 – dNum2
mov eax, dword [dNum1]
sub eax, dword [dNum2]
mov dword [dAns], eax

; qAns = qNum1 - qNum2
mov rax, qword [qNum1]
sub rax, qword [qNum2]
mov qword [qAns], rax

同样地,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寄存器。下图给出了寄存器与各个大小的操作数相乘的情况:

image-20240624223619837

如图所示,A寄存器和D寄存器有时会混用,可能会造成混乱。

例如,当一个 rax(64 位)乘以另一个64位操作数时,乘法指令会提供一个quadword结果(128 位)。这在处理非常大的数字时非常有用。由于 64 位体系结构只有 64 位寄存器,128 位结果必须放在两个不同的四字(64 位)寄存器中,rdx 表示高阶结果,rax 表示低阶结果,通常写成 rdx:rax。

而ax(32位)乘以另一个32位操作数时,虽然有eax寄存器更方便使用,但是实际上还是会使用dx:ax来存储结果,这样做是为了兼容以前的程序。

举例:

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
bNumA  db   42
bNumB db 73
wAns dw 0
wAns1 dw 0

wNumA dw 4321
wNumB dw 1234
dAns2 dd 0

dNumA dd 42000
dNumB dd 73000
qAns3 dq 0

qNumA dq 420000
qNumB dq 730000
dqAns4 ddq 0

; wAns = bNumA^2
mov al, byte [bNumA]
mul al ; 结果放在ax
mov word [wAns], ax

; wAns1 = bNumA * bNumB
mov al, byte [bNumA]
mul byte [bNumB] ; 结果放在ax
mov word [wAns1], ax

; dAns2 = wNumA * wNumB
mov ax, byte [wNumA]
mul word [wNumB] ; 结果放在dx:ax
mov word [dAns2], ax
mov word [dAns2+2], dx

; qAns3 = dNumA * dNumB
mov eax, dword [dNumA]
mul dword [dNumB] ; 结果放在edx:eax
mov dword [qAns3], eax
mov dword [qAns3+4], edx

; dqAns4 = qNumA * qNumB
mov rax, qword [qNumA]
mul qword [qNumB] ; 结果放在rdx:rax
mov qword [dqAns4], rax
mov qword [dqAns4+8], rdx
指令 含义
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
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
wNumA  dw   1200
wNumB dw -2000
wAns1 dw 0
wAns2 dw 0

dNumA dd 42000
dNumB dd -13000
dAns1 dd 0
dAns2 dd 0

qNumA dq 120000
qNumB dq -230000
qAns1 dq 0
qAns2 dq 0

; wAns1 = wNumA * -13
mov ax, word [wNumA]
imul ax, -13 ; 结果放在ax
mov word [wAns1], ax

; wAns2 = wNumA * wNumB
mov ax, word [wNumA]
imul ax, word [bNumB] ; 结果放在ax
mov word [wAns2], ax

; dAns1 = dNumA * 113
mov eax, dword [dNumA]
imul eax, 113 ; 结果放在eax
mov dword [dAns1], eax

; dAns2 = dNumA * dNumB
mov eax, dword [dNumA]
imul eax, dword [dNumB] ; 结果放在eax
mov dword [dAns2], eax

; qAns1 = qNumA * 7096
mov rax, qword [qNumA]
imul rax, 7096 ; 结果放在rax
mov qword [qAns1], rax

; qAns2 = qNumA * qNumB
mov rax, qword [qNumA]
imul rax, qword [qNumB] ; 结果放在rax
mov qword [qAns2], rax

; qAns1 = qNumA * 7096 还有一种用多个寄存器的写法
mov rax, qword [qNumA]
imul rbx, rax, 7096 ; 3参数写法
mov qword [qAns1], rbx
指令 含义
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