Caiwen的博客

X86指令集

2026-04-05 08:25

0. 数据格式

X86 中字长为 1616 位:

  • 字节,8位,汇编后缀为 b
  • 字(word)表示 16 位,2 字节,汇编后缀为 w
  • 双字(double words)表示 32 位,4 字节,汇编后缀为 l
  • 四字(quad words)表示 64 位,8 字节,汇编后缀为 q

上面的后缀是对于整数的指令的。浮点数使用单独的指令集,和上述有所不同:

  • 单精度(float),32 位,4 字节,汇编后缀为 s
  • 双精度(double),64 位,8 字节,汇编后缀为 l

1. 寄存器

1.1 通用寄存器

总的来说是 16 个寄存器,每个寄存器又分 4 个具体的寄存器,可以分别访问低 8、低 16、低 32、低 64 位的数据

1.2 浮点数寄存器

AVX 浮点体系架构还提供如下的寄存器:

这些寄存器主要用来存储向量,如每个 ymm 寄存器可以存放 8 个32 位值,或是 4 个64 位值,浮点数和整数都可以。不过如果单纯用来进行单个浮点数运算的话,只需要用到 xmm 寄存器的低 32 或 64 位

1.3 条件码

更多参考 https://www.caiwen.work/post/408-cs-data

条件码寄存器只存储一个位,对于一般的运算,有:

  • OF:溢出标志

    表示将最近的操作数解释为有符号数,该操作是否导致结果发生了溢出。

  • CF:进位标志。

    表示将最近的操作数解释为无符号数,该操作是否导致结果发生了溢出。

    对于浮点运算,如果运算结果是负数,或是有一个操作数是 NaN 的话就设置为 1。

  • ZF:零标志。

    表示最近的操作结果是否为 0。

    对于浮点运算,如果运算结果为 0 或是有一个操作数是 NaN 的话就设置为 1

  • SF:符号标志。

    表示最近的操作结果是否为负数,SF 为 1 表示负数,为 0 表示正数。

  • PF:奇偶标志位。

    对于整数操作,如果最近的操作结果是偶校验的(即含有偶数个二进制 1),则设置为 1。

    对于浮点运算,如果有一个操作数是 NaN 的话就设置为 1

对于移位,有:

  • CF:无论是何种移位,CF 都设置成最后一个被移出的位。

  • OF

    X86 仅对移 11 位有明确规定。大于 11 位时,OFOF 的设置是未定义行为。

    可视为移位后最高位是否发生变化。

    对于左移,移位后,OFOF 设置成结果的最高位与 CFCF(即被移出的位)的异或。在语义上等价于将操作数视为有符号整数之后,左移是否溢出。

    对于逻辑右移,OFOF 设置移位前的最高位(因为逻辑右移补上来的一定是 00,所以这里只需要看移动前的最高位)

    对于算数右移,OFOF 设置为 00(算数右移一定是保证最高位不变的)

乘法运算发生溢出后,CPU 会将 OFOFCFCF 同时置 11

除法运算后的 OFOFCFCFZFZFSFSF 的设置是未定义行为。

同时如下指令还有特殊行为:

  • leaq 指令不设置条件码
  • xor 指令不设置 CFOF
  • incdec 只设置 OFZF,不设置 CF

1.4 函数调用约定

被调用者保存寄存器 %rbx%rbp%r12 ~ %r15

调用者保存寄存器 上述以外的寄存器(%rsp 除外),包括 ymm 和 xmm 这些寄存器

返回值 返回值放在 %rax 寄存器中,如果返回值是浮点数,则会放在 %xmm0

参数

传递参数时,前 6 个参数通过寄存器传递,使用寄存器的规则如下:

多余 6 个的参数会直接压入栈中

对于浮点数参数,前 8 个浮点数参数会使用 %xmm0%xmm7 传递,剩余的会压入栈中

压入栈中的时候,无论是 8 位 16 位还是 32 位,都占用 64 位的栈空间

注意第七个函数位置是 %rsp+8,因为 %rsp 的位置上是 call 指令压入的返回值地址

上面这些内容是 SystemV AMB64 ABI 的函数调用约定,主要用在 Linux ,macOS 和其他大多数的类 Unix 系统上

而 Windows 系统上并不遵循这种约定。

同时上面这些还是针对于 64 位而言的。对于 32 位,还有 cdeclstdcallfastcall 等各种约定。

并不存在同一的函数调用约定。

1.5 其他

还有一些特殊的寄存器:

  • %rip 又称为 PC ,程序计数器,存放将要执行的下一条指令在内存中的地址

2. 操作数

  • 立即数,以 $ 开头随后接一个 C 语言中可以表示的整数字面量,如 $-577$0x1F
  • 寄存器,以 % 开头随后接寄存器的名称
  • 内存引用,完整的格式是 Imm(rb,ri,s),表示的地址为 Imm+rb+s*ri,我们其中 Imm 是一个数字,rbri 是寄存器,s 是一个只能为 1 2 4 8 的数字。我们把 rb 称为基址寄存器,Imm 称为偏移,ri 称为变址寄存器,s 称为比例因子

3. 指令集

3.1 数据传送

源和目的的数据大小一致

有几个注意点:

  • 如果选用寄存器,寄存器的大小要和指令后缀相符合
  • x86-64 要求传送指令的两个操作数不能够都是访问内存
  • 使用 movl 传送完双字之后,该寄存器所对应的 64 位寄存器的高 32 位部分置 0
  • 使用 movq 的话,立即数只能是 32 位的,传送时是将 32 位立即数进行符号扩展再传过去
  • movabsq 则可以直接使用 64 位的立即数,但也只能使用立即数,并且传送目标只能是寄存器

小数据传到大数据

有两种:一种进行零扩展一种进行符号扩展

  • 上面不存在 movzlq,是因为 movl 在传送后自动清空高 4 字节,达到了 movzlq 的目的
  • cltq 无需指定操作数,他和 movslq %eax,%rax 一致,但是编码更加紧凑

3.2 运算

3.2.1 加载有效地址

格式为 leaq S,D,这个指令的 S 部分必须是一个内存引用格式,其作用类似传送指令,把 S 表示的地址传送到 D 中。实际中这个指令和内存地址关系不大,只是单纯的计算数字,如:leaq 7(%rdx,%rdx,4),%rax 就相当于将 %rax 变为 7+5%rax

3.2.2 运算和逻辑

  • 上面的所有指令都需要添加后缀,比如 addq

  • 同样地,操作数不能同时是内存

  • 对于不符合交换律的运算,如 sub,一定是后面的操作数在前

3.2.3 移位

其中 k 为移位量,D 表示要作用的目标

  • k 必须是一个立即数,或是单字节寄存器 %cl 中,其他寄存器不可以

  • 上述的指令同样需要添加后缀。只要求 D 作为寄存器时大小和后缀相符即可

  • 如果 k 使用 %cl 的话,如果 D 的位长为 ww,那么位移量由 %cl 的低 mm 位决定,其中 2m=w2^m=w,高位部分会被忽略。

    比如 %cl0xFF 的话,salb 会移动 7 位,salw 会移动 15 位,sall 会移动 31 位,salq 会移动 63 位

3.2.4 乘除

Intel 把 16 字节的数称为八字(oct word)。为了表示 128 位的数据,我们规定 %rdx 存高 64 位,%rax 存低 64 位,然后我们有如下指令:

  • 注意 mulq 只有一个操作数,区别于上面有两个操作数的 mulq

  • divqidivq 指令执行后会分别把除数和余数放到两个寄存器中

  • 实际上对于非 128 位的数据进行除法或者取模运算的话也需要使用 idivqdivq 指令。这些数据在进行 div 指令前需要先通过 clto 指令来把自身符号扩展到 128 位

3.3 控制

3.3.1 仅设置条件码

cmptest 指令分别基于减法和按位与运算,但是只设置条件码

比较常见的是,cmp 指令用来判断两个数的大小,test 传递两个相同的操作数来判断这个数字的正负。

3.3.2 访问条件码

通常条件码寄存器不会被直接访问,而是通过如下的指令来访问

  • 其中注意 lb 的后缀不再表示数据的大小了,而是 less 和 below 的意思

  • 无需增加后缀,会自动根据目标寄存器的大小推断

  • 操作数 D 必须是一个单字节寄存器

3.3.3 跳转

间接跳转,即跳转目标的地址是从寄存器或者内存中读取的,如:jmp *%raxjmp *(%rax)

跳转指令要跳转到的地址在机器码层面一般有两种编码方式:

  • 一种是 PC 相对的,即目标编码为实际地址与当前 PC (当前 PC 是下一条指令的地址)的差值,或者说下一条指令的地址加上跳转指令的地址记为真实跳转到的目标地址;
  • 一种是绝对的,即直接写明要跳转到的地址

跳转指令可以用来实现 C 语言的 if 语句,如:

c
1
2
3
4
if (test-expr) then-statement else else-statement

一般会被翻译成如下的汇编(用 C 的形式表示)

c
1
2
3
4
5
6
7
8
t = test-expr if (!t) goto false; then-statement goto done; false: else-statement done:

有意思的是汇编的测试条件和 C 的正好相反

3.3.4 条件传送

上面的跳转可能会影响处理器性能,因为现代处理器是流水线化工作的,一条指令的执行被拆成了好几步,上一条指令还没执行完毕就同时执行下一条指令。如果程序是顺序结构的话,那么处理器在执行的时候会将指令填满流水线。而当遇到分支的时候,处理器无法得知后续要执行的指令,流水线则会空闲。处理器为了保持流水线充满,会对分支进行预测,预测后续执行的指令,但是如果预测失败,则需要很大的惩罚回退已经执行的操作。

部分分支情况可以使用条件传送进行优化。这种优化会把两个分支都进行计算,然后在最后根据条件选择要选用的值:

只有满足相应的条件,才会进行传送

  • S 只能是寄存器或者内存,R 只能为寄存器
  • 不支持单个字节的传送
  • 无需增加后缀,会自动根据目标寄存器的大小推断

C 语言中使用三目表达式可能会更容易让编译器使用条件传送指令

有些情况下,有可能把两个分支的值都计算完毕之后比分支预测失败带来的惩罚还高,那么编译器就不会选用条件传送

如果分支并非单纯的计算值,而是可能有副作用,那么也不会选择条件传送

如:

看起来可以用条件传送,但是如果 xp 真的为空指针的话,提前计算 *xp 会导致空指针异常

3.4 浮点相关

3.4.1 数据传送

其中 X 只能选择 XMM 寄存器,M 只能选择内存

如果浮点运算涉及到了立即数,那么需要由编译器先把立即数写到 rodata 段,然后从内存引用

3.4.2 转换操作

浮点转整数

截断会向 0 舍入

整数转浮点

一般源 2 和目的一致

单精度到双精度

如果想把单精度数据转成双精度数据的话,可以使用 vcvtss2sd 指令,如 vcvtss2sd %xmm0,%xmm0,%xmm0 即可把 %xmm0 的单精度变为双精度

不过 GCC 生成如下的代码

Unknown
1
2
vunpcklps %xmm0,%xmm0,%xmm0 vcvtps2pd %xmm0,%xmm0

vunpcklps 将两个 xmm 寄存器的值交叉放置,然后把结果放到第三个寄存器。比如第一个寄存器的内容为字 [s3,s2,s1,s0],另一个寄存器的内容为字 [d3,d2.d1,d0] ,那么目标寄存器的值会变为 [s1,d1,s0,d0]。那么对于上面这个指令,如果 %xmm0 的值为字 [x3,x2,x1,x0] 的话那么 %xmm0 会变为 [x1,x1,x0,x0]

vcvtps2pd 会将源寄存器的两个低位的单精度值扩展成 xmm 寄存器中的两个双精度值。对于上面这个例子,最终 %xmm0 变为 [dx0,dx0]

那么上述两个指令合起来的效果就是把 %xmm0 低 32 位表示的单精度浮点数转为两个一样的双精度

这样做不会有什么好处,GCC 这么做的原因也不明

双精度到单精度

可以使用 vcvtsd2ss %xmm0,%xmm0,%xmm0 指令,但是 GCC 产生如下的代码:

Unknown
1
2
vmovddup %xmm0,%xmm0 vcvtpd2psx %xmm0,%xmm0

vmovddup 会将 xmm 的低 64 位复制到高 64 位,于是如果 %xmm0[x1,x0] 的话会变成 [x0,x0]vcvtpd2psx 会将 xmm 的两个双精度数字变为两个单精度,并在高 64 位填充 0,即会变为 [0.0,0.0,x0,x0]

3.4.3 运算

  • 第一个操作数 S1 必须是 xmm 寄存器或者是内存
  • 第二个操作数和目的都必须是 xmm 寄存器

  • 源和目的都必须是 xmm 寄存器

3.4.4 比较

浮点运算主要会设置 ZFCFPF,设置条件如下:

3.5 函数调用相关

3.5.1 压栈与弹栈

%rsp 寄存器存放当前栈顶地址

3.5.2 调用与返回

其中 call 指令会首先把当前 PC (即下一条指令的地址)压入栈中,用来后续返回,然后将 PC 置为要调用的函数的地址

ret 指令会从栈中弹出一个值(编译器需要确保执行 ret 时已经弹栈到返回地址处了),然后将 PC 置为这个值

最后更新于:2026-04-07 11:34

Caiwen
本文作者
一只蒟蒻,爱好编程和算法