Skip to content

Files

Latest commit

c70125a · Jan 8, 2022

History

History
1247 lines (739 loc) · 53.7 KB

7.md

File metadata and controls

1247 lines (739 loc) · 53.7 KB

七、指令参考

以下指令参考旨在总结英特尔和 AMD 程序员手册中的一些信息,并对最常见和最有用的指令提供易于参考但详细的描述。所有说明的完整细节可以在英特尔和 AMD 手册中找到(参见推荐阅读部分,获取这些文档的链接)。

本参考资料仅涵盖应用程序编程指令集。不包括系统编程指令(或特权指令),也不包括现在已经过时并已从 x64 程序集中删除的指令。即使在兼容模式下仍然支持指令,指令也不包括在内(例如二进制编码十进制指令等)。).只有最常见和最有用的说明被包括在内,但还有数百个。

CISC 指令集

现代 x64 处理器是 CISC(复杂指令集计算),与精简指令集计算相反。这意味着有大量的专用指令,这些指令对于通用编程来说几乎是无用的,但是已经被添加到指令集中用于特定的目的,例如三维图形算法、加密等。

指令的命名几乎没有一致的逻辑,因为它们已经被添加了几十年,并且属于不同的指令集。

许多指令需要中央处理器的硬件支持,例如每个 SIMD 指令集和条件移动。有关如何检测硬件是否能够执行特定指令的详细信息,请参考CPUID指令。

参数格式

下表列出了我在本参考资料中使用的指令参数类型的简写:

表 9:速记指令参数

速记 意义
车辆注册号 x86 eax、ebx 等寄存器。
多媒体增强指令集(Multi Media Extension) 64 位 MMX 寄存器
xmm 128 位 SSE 寄存器
ymm 256 位 AVX 寄存器
mem 内存操作数,数据段变量
国际货币市场 直接值,文字或常量
标准时间 浮点单位寄存器

非常重要的一点是要注意,无论参数简写如何表述,都不能将两个内存操作数用作指令的参数。例如,MOV指令的一个参数可以是内存操作数,但两者都不能;一个必须是直系亲属或登记者。每个算术逻辑单元只有一个地址生成单元。当算术逻辑单元有指令要执行时,它最多只能产生一个内存地址。

除非另有明确说明,否则指令的所有参数必须大小匹配。不能将一个单词移入双字,也不能将双字移入一个单词。在程序集中没有隐式转换的概念。有一些指令(例如,移动和符号/零扩展指令)被设计成将一个数据大小转换成另一个数据大小,并且必须采用不同大小的操作数,但是几乎所有其他指令都遵循这个规则。

操作数的可能大小已经包含在指令的简写中。有些指令不适用于所有大小的操作数。例如,条件移动指令的助记符和参数可能如下所示:

CMOVcc [reg16/32/64], [reg16/32/64/mem16/32/64]

这意味着指令采用两个操作数,每个操作数都在方括号中,尽管在代码中它们没有被方括号包围,除非它们是指针。第一个可以是大小为 16 位、32 位或 64 位的 x86 寄存器,第二个可以是相同大小的另一个 x86 寄存器或内存操作数。

  CMOVE ax, bx; This would be fine, CMOVcc [reg16], [reg16]
  CMOVE al, bl; This will not work because AL and BL are 8
  bit registers

| | 注意:如前所述,原始 x86 寄存器的高字节形式不能与新 x64 寄存器的低字节形式一起使用。类似“MOV AH,R8B”的东西不会编译,因为它使用高字节形式 AH 和新的字节形式 R8B。高字节格式包含在 x64 中只是为了向后兼容。中央处理器不知道在一条指令中实现“MOV AH,R8B”的机器代码。 |

标志寄存器

许多 x86 指令会改变标志寄存器的状态,以便可以根据先前指令的结果使用后续的条件跳转或移动。与几乎所有其他来源相比,标志寄存器缩写在 Visual Studio 中的显示方式不同。本手册和 Visual Studio 的注册窗口中标志及其缩写的含义如下:

表 10:标志寄存器缩写

标志 Name 缩写 可视化工作室
进位标志 随军牧师 塞浦路斯
奇偶标记 脉波频率(Pulse Frequency 的缩写) 体育课
零标志 零频率(Zero Frequency) ZR
标志旗 芬兰 波兰
方向标志 DF 向上
溢出标志 属于…的 观测速度(observed velocity)

carry, zero的标志字段意味着进位标志和零标志都被指令以某种方式改变。这意味着所有其他标志要么没有改变,要么没有定义。当标志未定义时,相信指令不会修改标志是不安全的。如果不清楚指令将如何改变标志,请参见指令的详细描述。如果需要更多关于标志是否被修改或未定义的信息,请参阅您的 CPU 制造商的程序员手册。

如果指令不影响标志寄存器(如MOV指令),标志字段将显示为Flags: (None)。如果指令的标志字段是(None),那么指令根本不会改变标志寄存器。

几乎所有的 SIMD 指令都不修改 x86 标志寄存器,因此它们的描述中省略了标志字段。

前缀

有些指令允许改变指令工作方式的前缀。如果一个指令允许前缀,那么它的描述中将有一个前缀字段。

重复前缀

重复前缀用于字符串指令,以便搜索内存块。它们被设置为特定值或被复制。即使编译器允许,它们也不能与任何其他指令一起使用。当使用非字符串指令时,使用重复前缀的结果是未定义的。

  • REP :在 RCX 重复以下说明的次数。REP与存储串(STOS)和移动串(MOVS)指令结合使用。虽然这个前缀也可以和LODS一起使用,但是这样做没有意义。REP前缀后面的指令的每次重复都会递减 RCX。
  • REPE 雷普兹:零时重复或相等时重复是完全相同事物的两个不同前缀。这意味着当零标志设置为 1 且 RCX 不为零时,重复以下指令。如同在REP前缀中一样,该前缀也在跟随它的指令的每次重复时递减 RCX。该前缀用于扫描数组(SCAS指令)和比较数组(CMPS指令)
  • REPNZ,REPNE :不为零时重复或不相等时重复是REPZREPE的反义词。当零标志设置为 0 且 RCX 不为 0 时,该前缀将重复执行指令。像其他重复前缀一样,它在每次重复时减少 RCX。该前缀的用法与REPZREPE前缀相同。

锁定前缀

默认情况下,汇编指令不是原子的(它们不会在一次不间断的中央处理器移动中发生)。

  add dword ptr [rcx], 2

该示例代码将导致所谓的读-修改-写操作。RCX 指向的内存中的原始值将被读取,2 将被添加,结果将被写入。该操作有三个步骤(读-修改-写)。在多线程应用程序中,当一个线程正在进行这三步操作时,另一个线程可能会开始读取、写入或修改完全相同的地址。这可能导致第二个线程读取与第一个线程相同的值,并且只有一个线程成功写入值+2 的实际结果。

这就是所谓的种族条件;线程竞相读取-修改-写入相同的值。问题是,程序员不再控制哪些线程将成功完成指令,哪些线程将重叠并产生一些其他结果。如果多线程应用程序中存在竞争条件,那么根据定义,代码的输出无法确定,并且可能是许多场景中的任何一个。

LOCK前缀使以下指令原子化;它保证一次只有一个线程能够在内存中的某个特定点上运行。虽然仅对引用内存的指令有效,但它会阻止另一个线程访问内存,直到当前线程完成操作。这保证了没有比赛条件发生,但代价是速度。在每一行代码中添加LOCK前缀将使任何试图访问相同内存的线程按顺序工作,而不是并行工作,从而抵消了原本通过多线程可以获得的性能提升。

  lock add dword ptr [rcx], 2

在本例中,LOCK前缀已放在指令旁边。现在,无论有多少线程试图访问这个 dword,无论它们运行的是这个精确的代码还是引用内存中这个精确点的任何其他代码,它们都将被排队,并且它们的访问将变成顺序的。这个ADD指令是原子的;它保证不会被中断。

LOCK前缀对于创建多线程同步原语(如互斥体和信号量)非常有用。汇编没有这种固有的原语,程序员必须创建自己的或使用一个库。

x86 数据移动指令

移动

MOV [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32/64]

MOV指令将数据从第二个操作数复制到第一个操作数。两个操作数的大小必须相同。虽然它的名字暗示着数据会被移动,但数据实际上是被复制的;它将保留在指令之后的第二个操作数中。

MOV指令是标准的赋值运算符。

// C++ assignment

rax = 25

; Assembly equivalent

mov rax, 25

| | 注意:当第一个操作数是 32 位寄存器时,该指令将 64 位版本寄存器的前 32 位清零。这导致了 MOV 在 x64 中的特殊用途。当您希望清除 x86 寄存器(例如,RDX)的前 32 位时,您可以使用 32 位版本的寄存器作为两个操作数:mov edx,EDX;将 RDX 的前 32 位清零 |

标志:(无)

条件移动

CMOVcc [reg16/32/64], [reg16/32/64mem16/32/64]

这将数据从第二个操作数移动到第一个操作数,但前提是指定的条件为真。该指令读取标志寄存器以确定是否执行MOV。条件码放在cc所在的助记符中;下表列出了一些常见的状态代码。只需将cc替换为适当的条件代码,即可找到您需要的助记符。

表 11:CMOS VCC 的一些有用条件

条件码 意义
O 溢出,有符号溢出
没有溢出,没有签名溢出
零或等于,有符号和无符号
新西兰或东北 不为零或不等于,有符号和无符号
B 下面,无符号小于
A 上图,无符号大于
自动曝光装置 高于或等于,无符号
存在 低于或等于,无符号
L 小于,签名小于
G 大于,符号大于
通用电气公司 大于或等于,有符号
低爆速炸药(Low Explosive) 小于或等于,带符号
C 进位,无符号溢出
网络计算机 不进位,无无符号溢出
S 签,回答是否定的
纳秒 没有迹象,回答是肯定的
体育课 奇偶校验为偶数,低位字节中 1 的计数为偶数
邮局(post office) 奇偶校验为奇数,低位字节中 1 的计数为奇数

如果第二个是内存位置,则无论指令的条件是否为真,它都必须是可读的。这些指令不能用于 8 位操作数,只有 16 位及以上。

用条件移动代替条件跳跃通常更好。条件移动比分支(使用Jcc指令)快得多。现代的中央处理器提前读取它实际执行的代码,这样它就可以确保下一条指令在需要时已经从内存中取出。当它找到一个条件分支时,它会使用一个特定于制造商的算法(称为分支预测器)来猜测这两个分支中哪一个最有可能。如果它猜错了,就会有很大的性能损失。它已经读取并试图执行的所有机器代码都需要从中央处理器中清除,并且它必须从实际的分支中读取代码。正是因为这个原因,CMOVcc指令被发明,也是为什么它们经常比Jcc指令快得多。

  ; To move data from AX to CX only if the signed value in
  DX is
  ; equal to the value in R8W:
  cmp dx, r8w
  cmove cx, ax  ; Only moves if dx = rw8

  ; To move data from AX to CX only if the unsigned value in
  DX is
  ; above (unsigned greater than) the value in R8W:
  cmp dx, r8w
  cmova cx, ax  ; Only moves if dx > r8w?

| | 注意:与 MOV 指令的行为类似,当操作数是 32 位寄存器时,该指令清除第一个操作数的 64 位版本的前 32 位。即使条件为假,顶部将被清零,而低 32 位将保持不变。如果条件为真,顶部将被清除为 0,第二个操作数的值将移动到低 32 位。 |

标志:(无)

CPUID:函数 1;读取 EDX 的第 15 位,以确保中央处理器能够有条件地移动。

非时间移动

MOVNTI [mem32/64], [reg32/64]

非时间移动指令将一个值从寄存器移动到内存中,并让中央处理器知道缓存中不需要该值。不同的 CPU 会基于此做不同的事情。中央处理器可能会完全忽略非时间提示,不管您的指令如何,都将值放在缓存中。一些中央处理器将使用非时间提示来确保数据不被缓存,从而为不久的将来再次需要的数据留出更多的缓存空间。

标志:(无)

CPUID:函数 1;读取 EDX 的第 26 位(SSE2)以确保中央处理器能够执行 MOVNTI 指令。

移动和零延伸

MOVZX [reg16/32/64], [reg8/16/mem8/16]

这将值从第二个操作数移动到第一个操作数,但通过在左边添加零将其扩展到第二个操作数的大小。源操作数只能是 8 位或 16 位宽,并且可以扩展到 16 位、32 位或 64 位。

操作数之间的差异没有限制。这意味着您可以使用一个字节作为第二个字节,并将其扩展为 64 位 qword。

标志:(无)

移动并标记延伸

MOVSX [reg16/32/64], [reg8/16/mem8/16]

这通过将较小的源值复制到目标的下半部分,然后将源的符号复制到目标的上半部分,将较小的有符号整数转换为较大的类型。该指令不能从 32 位源符号扩展到 64 位目标,这需要使用MOVSXD指令来代替。

操作数之间的差异没有限制,这意味着可以使用一个字节作为第二个字节,并将其扩展到 64 位 qword。

标志:(无)

将 Dword 移至 Qword 并进行符号扩展

MOVSXD [reg64], [reg32/mem32]

将 32 位有符号整数转换为 64 位有符号整数。源移动到目标的下半部分,源的符号位复制到目标的所有位。

标志:(无)

交易所

XCHG [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64]

这将交换两个操作数的值。由于BSWAP不允许 8 位操作数,因此该指令可以代替 16 位寄存器的BSWAP;你可以用xchg al, ah代替bswap ax

如果使用内存操作数,该指令将自动成为原子指令(自动应用LOCK前缀)。

标志:(无)

前缀:LOCK

翻译表格

XLAT [mem8]

XLATB

该指令将 a1 中的值转换为 RBX 指向的表的值。将 RBX 指向最多包含 256 个不同值的字节数组,并将 a1 设置为要翻译的数组中的索引。

该指令不影响 RAX 的前 7 个字节;只有铝被改变。该指令完成了一些类似于将 RBX 加入到人工智能中的非法地址计算的事情。

  mov al, byte ptr [rbx+al]

内存操作数版本做完全一样的事情,内存操作数被完全忽略。内存操作数的唯一目的是记录 RBX 可能指向的位置。不要被误导;不管内存操作数是什么,表都是由 RBX 指定为指针的。

  XLAT myTable  ; myTable is completely ignored, [RBX] is
  always used!

标志:(无)

标记延伸 a1、AX 和 EAX

CBW

CWDE

CDQE

这些指示符号将 RAX 的各种版本扩展到更大的版本。操作数是隐含的,始终是CBW为 AL,CWDE为 AX,CDQE为 EAX。

CBW在 AH 上复制 AL 的符号,有效地使 AX 成为 AL 中的符号扩展版本。CWDE将 AX 的符号复制到 EAX 的上半部,实际上是从 AX 延伸到 EAX 的符号。CDQE通过在 RAX 上半部复制 EAX 的标志,标志延伸 EAX 到 RAX,标志延伸 EAX 到 RAX。

标志:(无)

复制 RDX 上的 RAX 标志

CWD

CDQ

CQO

这些指令创建了分部指令IDIVDIV使用的 RDX:RAX 的签名组合。他们在相同大小的 RDX 寄存器上复制 AX、EAX 或 RAX 的符号。

CWD在 DX 上复制 AX 的符号,如果 AX 为负,DX 为FFFFh,如果 AX 为正,则为0000h。这将创建 32 位复合寄存器 DX:AX。CDQ在 EDX 复制 EAX 的符号,如果 EAX 为负,EDX 变成FFFFFFFFh,如果 AX 为正,变成0。这就产生了 EDX:EAX 的复合注册。CQO在所有 RDX 位复制 RAX 符号。这创建了 128 位复合寄存器 RDX:RAX。

标志:(无)

将数据推送到栈

PUSH [reg16/32/64/mem16/32/64/imm16/32/64/seg16]

这将一个值推送到栈。这将导致栈指针按字节值的大小递减,并将该值移入内存。这用于在过程之间传递参数,也用于在调用过程之前保存返回地址。

除了作为传递参数的主干之外,栈还用于保存临时值,以释放寄存器用于其他用途。

8 位操作数不能推入栈,但可以推入段寄存器 FS 和 ES。推动奇数个 16 位值会导致栈指针错位(不在可被 32 整除的地址上)。您应该总是推送偶数个值,因为未对齐的栈指针会导致崩溃。

标志:(无)

从栈弹出数据

POP [reg16/32/64/mem16/32/64/seg16]

这将弹出以前推送到栈上的数据。这导致栈指针递增以指向下一个要弹出的数据,并且从内存中读取最后推送的数据项。

标志:(无)

推送标志寄存器

PUSHF

PUSHFQ

这将标志寄存器推入栈。您可以选择仅推低 16 位(PUSHF)或整个 64 位 rflags 寄存器(PUSHFQ)。该指令对于在调用过程之前保存标志寄存器的确切状态非常有用,因为过程很可能会改变其状态。该指令和 pop 标志寄存器指令也可用于设置标志寄存器的位:

  PUSHF         ; Push the state of the flags register
  POP AX        ; Pop the flags register into ax
  OR AX, 64     ; Set the bits using OR, BTS, BTR, or AND 
  PUSH AX       ; Push the altered flags
  POPF          ; Pop the altered flags back into the real
  flags register

有容易设置和清除进位和方向标志的说明。参见CLDCLCSTCSTD。推入和弹出标志寄存器不需要用来设置或清除这些特定的标志。

标志:(无)

弹出标志寄存器

POPF

POPFQ

这会将栈中的值弹出到标志寄存器中。标志寄存器不能像通用寄存器那样直接操作(除了CLDCLCSTC指令)。如果需要设置 rflags 的特定位,请遵循 push flags 寄存器指令中的示例。

标志:进位、奇偶校验、零、符号、方向、溢出

加载有效地址

LEA [reg16/32/64], [mem]

这会将内存位置的有效地址加载到源中。如果源是 16 位,则只加载地址的最低 16 位;如果源是 32 位,则只有地址的低 32 位被载入源。通常源为 64 位,LEA指令加载内存操作数的整个有效地址。

这条指令实际上计算了一个地址,并将其移动到源中。它类似于MOV指令,但LEA实际上并不读取内存。它只是计算一个地址。

  .data
  myVar dq 23          ; Define a variable and set it to 23

  .code
  MyProc proc
         mov rax, myVar; MOV will read the contents of myVar
  into RAX
         lea rax, myVar; LEA loads the address of myVar to
  RAX

         ; From the LEA instruction RAX has the address of
  myVar
         mov qword ptr [rax], 0     ; So we could set myVar
  to 0 like this
         ret
  MyProc endp
  End

| | 注意:由于该指令实际上只是计算一个地址和复杂的寻址模式(如[RBX+RCX*8]),因此可以使用该指令,但它不会尝试从该地址读取,并且可以用于执行快速算术运算。 |

例如,要将 RAX 设置为 5 * RCX,可以使用以下命令:

LEA RAX, [RCX+RCX*4]

要将 RBX 设置为 R9+12,您可以使用以下命令:

LEA RBX, [R9+12]

这种优化技术和更多的细节在迈克尔·阿布拉什的图形编程黑皮书中有详细描述(参见推荐阅读部分的链接)。

标志:(无)

字节交换

BSWAP [reg32/64]

这将颠倒源中字节的顺序。该指令旨在交换值的字符顺序。也就是说,它用于从小端字节到大端字节的变化,反之亦然。随着基于 x86 的 CPU 占据主导地位(x86 使用小字节序),改变字节序的愿望几乎消失了,因此该指令在反转字符串时也很有用。

| | 注意:如果你需要“BSWAP reg16”你应该使用 XCHG 指令。BSWAP 指令不允许 16 位参数,所以可以用“XCHG AL,AH”代替“BSWAP AX”。 |

标志:(无)

x86 算术指令

加减

ADD [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32]

SUB [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32]

这将第二个操作数与第一个操作数相加或相减,并将结果存储在第一个操作数中。对于加法,这并不重要,但是在使用SUB时,需要注意的是,第二个操作数是从第一个操作数中减去的,而不是相反。

这些指令可以用于有符号和无符号算术;知道如何读取标志很重要,因为标志反映了有符号和无符号的结果。

如果你在做无符号算术,你应该读进位标志。如果没有最终溢出,进位标志将为 0。如果存在最终溢出(表示操作最后一位的进位或借位),它将被设置为 1。

如果您正在进行有符号运算,或者在运算的倒数第二位没有最终进位或借位(因为最后一位是符号位),溢出标志将为 0。如果存在最终进位或借位,溢出标志将设置为 1。

如果加法或减法的结果正好为 0,则将设置零标志。

如果结果是负数(如果正在进行无符号运算,这可以忽略),那么将设置符号标志。

标志:进位、奇偶校验、零、符号、溢出

前缀:LOCK

带进位加,带借位减

ADC [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32]

SBB [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32]

这些指令与ADDSUB指令相同,除了它们还增加或减少进位标志(它们根据进位标志的状态增加或减少额外的 1 或 0)。当所处理的数字不适合 64 位寄存器,而是分解成多个 64 位数字时,它们对于执行任意大的整数加法或减法非常有用。

您也可以使用这些指令将寄存器设置为进位标志。要将 EAX 设置为进位标志,可以使用以下方法:

  MOV EAX, 0           ; Clear EAX to 0.
                       ; You can't use XOR here because that
  would clear
                       ; the carry flag to 0.
  ADC EAX, EAX  ; Sets EAX to 1 or 0 based on the carry flag

标志:进位、奇偶校验、零、符号、溢出

前缀:LOCK

增量和减量

INC [reg8/16/32/64/mem8/16/32/64]

DEC [reg8/16/32/64/mem8/16/32/64]

这些指令递增(加 1)或递减(减 1)寄存器或存储器变量。它们通常与寄存器结合使用来创建循环结构。常见的模式如下所示,循环 100 次:

  mov cx, 100   ; Number of times to loop

  LoopHead:     ; Start of the loop

                ; Body of the loop

         dec cx        ; Decrement counter
         jnz LoopHead  ; Loop if there's more, i.e. 100
  times

| | 注意:INC 和 DEC 不设置进位标志。如果需要执行 INC 或 DEC,但还需要设置进位标志,建议使用第二个操作数为 1 的 ADD 或 SUB。 |

标志:奇偶校验、零、符号、溢出

前缀:锁定

否定

NEG [reg8/16/32/64/mem8/16/32/64]

这就否定了一个有符号的数,因此负值变成了正值,反之亦然。这相当于翻转每一个位,并将结果加 1。这被称为一个数的二进制补码,与一进制补码相反,一进制补码可以通过NOT指令获得。

| | 注意:与许多位操作指令相比,x86 和 x64 CPUs 执行乘法的速度较慢;如果你需要将一个数字乘以-1,使用 NEG 总是比 IMUL 更快。 |

标志:进位、奇偶校验、零、符号、溢出

前缀:LOCK

比较

CMP [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32]

这将比较两个操作数,并设置标志寄存器以指示两个操作数之间的关系。

这个指令实际上和SUB指令做的完全一样,但是它不存储结果,它只是设置标志。从第一个操作数中减去第二个操作数,并相应地设置标志,但不改变目标操作数。通常比较指令之后是条件跳转或条件移动。

该指令用于设置标志,并随后根据结果执行一些条件操作。注意CMP指令如何比较操作数是非常重要的,因为像>>=<<=这样的比较对操作数的顺序很重要。

  cmp dx, ax
  jg SomeLabel  ; Jump if DX > AX

| | 注:CMP op1、op2 等于问“第一个操作数和第二个有什么关系”,而不是反过来问。从第一个操作数中减去第二个操作数。 |

标志:进位、奇偶校验、零、符号、溢出

相乘

MUL [reg8/16/32/64]

IMUL [reg8/16/32/64]

IMUL [reg8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm]

IMUL [reg8/16/32/64], [reg8/16/32/64/mem8/16/32/64], [imm8]

MUL执行无符号整数乘法,IMUL执行有符号整数乘法。

MUL只有单操作数版本,而IMUL有三个版本。在单操作数版本的IMULMUL中,第二个操作数是隐含的,答案存储在预定义的隐含寄存器中。隐含的第二个操作数是 RAX 寄存器的适当大小,所以如果操作数是 8 位,那么第二个隐含的操作数是 a1。如果源操作数是 64 位,那么隐含的第二个操作数是 RAX。

乘法的答案存储在 AX 中,用于 8 位乘法。对于其他数据大小(16 位、32 位和 64 位操作数),答案存储为上半部分存储在适当大小的 RDX 中,下半部分存储在适当大小的 RAX 中。这是因为最初的 16 位处理器没有足够大的寄存器来存储 16 位乘法可能产生的 32 位结果,所以使用了 DX:AX 的复合 32 位。当 32 位处理器出现时,也发生了完全相同的事情。32 位乘法的 64 位答案无法存储在 32 位寄存器中,因此使用了 EDX:EAX 的组合。现在有了我们的 64 位处理器,128 位的答案存储在 RDX:RAX 的组合中。

如果有任何结果出现在答案的上半部分(AH、DX、EDX 或 RDX),则进位和溢出标志设置为 1,否则为 0。

表 12

| 操作数 1 | 隐含操作数 2 | 回答 | | 8 位 | -艾尔 | 削减 | | 16 位 | 削减 | DX:AX | | 32 位 | EAX | EDX:EAX | | 64 位 | RAX | RDX:RAX |

IMUL的双操作数版本只是将第二个操作数乘以第一个操作数,并将结果存储在第一个操作数中。溢出(结果中不适合第一个操作数的任何位)丢失,进位和溢出标志设置为 1。如果没有溢出,整个答案适合第一个操作数,进位和溢出标志设置为 0。

IMUL的三操作数版本中,第二个操作数乘以第三个操作数(立即值),结果存储在第一个操作数中。同样,如果结果溢出,进位标志和溢出标志都被设置为 1,否则它们被清除。

| | 注意:这些指令非常慢,所以如果可能的话,用移位(SHL)替换乘法或使用 LEA 指令可能会更快。 |

Flags: Carry, Overflow

有符号和无符号除法

DIV [reg8/16/32/64/mem8/16/32/64]

IDIV [reg8/16/32/64/mem8/16/32/64]

IMUL不同,除法指令只有单操作数版本。DIV除无符号整数,IDIV除有符号整数。这些指令返回除法的商和余数。

给指令的单个操作数是除数(除法的 x/y 中的 y)。被除数(x/y 部分的 x)是隐含的。隐含红利的位置见表 13 中的例子。商以 RAX 的适当大小结束,剩余部分进入黑索今。

| | 注意:除法一直是最慢的指令之一(可能比加法慢 30-40 倍)。今天依然如此。如果可能的话,应该完全避免在紧密循环中分割。如果一个数要除以 2 的幂,用 SAR(算术右移)代替有符号除法,用 SHR 代替无符号除法。如果要执行的划分很多,可以考虑使用 SSE。 |

| | 注意:非常小心 RDX 中的内容。如果被划分的数字足够小,完全适合 RAX 的适当大小,你必须记住 RDX!要么使用无符号除法的异或来清除 RDX,要么使用 CWD、CDQ 或 CQO 来复制 RAX 符号。 |

例如,如果我们想使用带符号的 dwords 计算 100/43(该代码也适用于-100/43),请使用如下内容:

  mov eax, 100 ; Move implied dividend into EAX
  mov ecx, 43   ; Move divisor into ECX
  cdq           ; Copy sign of EAX across EDX
  idiv ecx      ; Perform division, EAX gets quotient, EDX
  gets remainder!

表 13:除法指令操作数和结果汇总

操作数 1(除数) 隐含股息 剩余物
8 位 削减 -艾尔
16 位 DX:AX 削减 高级的(deluxe 的简写)
32 位 EDX:EAX EAX EDX
64 位 RDX:RAX RAX 旋风炸药

标志:无(除后所有标志未定义!)

x86 布尔指令

布尔与、或、异或

AND [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32]

OR [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32]

XOR [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32]

这些指令ANDORXOR操作数,并将结果存储在第一个操作数中。每对位(一个来自源,另一个来自目标)都应用了操作,并且存储的答案与 C++ 布尔操作完全相同。

表 14:与真值表

位 1 第 2 位 结果
Zero Zero Zero
Zero one Zero
Zero Zero Zero
one one one

表 15:或真值表

位 1 第 2 位 结果
Zero Zero Zero
Zero one one
Zero Zero one
one one one

表 16:异或真值表

位 1 第 2 位 结果
Zero Zero Zero
Zero one one
Zero Zero one
one one Zero

溢出和进位标志被清除为 0,而符号和零标志指示结果。

| | 注意:传统上异或指令比 MOV 快,所以程序员通常使用异或将寄存器清零。如果异或运算的两个操作数具有完全相同的值,则异或运算返回 0,因此将 RAX 清零为 0“异或 RAX,RAX”比“异或 MOV RAX,0”更常见,尽管今天的中央处理器执行异或运算的速度可能不会更快。 |

标志:进位、奇偶校验、零、符号、溢出

前缀:LOCK

布尔非(每一位翻转)

NOT [reg8/16/32/64/mem8/16/32/64]

该指令翻转给定操作数中的每一位,使 0 变为 1,1 变为 0。它是按位或布尔NOT,有时被称为一的补码,与返回二的补码的NEG指令相反。

标志:(无)

前缀:LOCK

测试位

TEST [reg8/16/32/64/mem8/16/32/64], [reg8/16/32/64/mem8/16/32/64/imm8/16/32]

该指令用于位测试,如同CMP用于算术测试。它在源和目标之间执行布尔AND指令,但不在目标中设置结果。相反,它只是改变了旗帜。

进位标志总是被重置为 0,奇偶校验标志被设置,零和符号标志反映布尔AND的结果。

例如,如果您想知道 EAX 的第三个字节中是否有任何位被设置为 1,您可以使用TEST如下:

  test eax, 00ff0000h  ; 00ff0000h is only 1's in the 3rd
  byte
  jnz ThereAreOnes     ; If zero flag isn't set, EAX has
  something in 3rd byte
  jz ThirdByteIsClear  ; If zero flag is set then EAX has
  nothing in 3rd byte

如果要测试 RDX 是否包含偶数,可以使用TEST指令,如下所示:

  test rdx, 1    ; Is the first bit 1?
  jz EvenNumber  ; If it is not, the number is even
  jnz OddNumber  ; Otherwise the number in RDX is odd

标志:进位、奇偶校验、零、符号、溢出

左右移动

SHL [reg8/16/32/64/mem8/16/32/64], [CL/imm8]

SHR [reg8/16/32/64/mem8/16/32/64], [CL/imm8]

SAR [reg8/16/32/64/mem8/16/32/64], [CL/imm8]

这将第一个操作数中的位移动第二个操作数中指定的量。这些指令左移(SHL)、右移(SHR)或算术右移(SAR)。第二个操作数可以是 CL 寄存器或立即 8 位值(当该操作数是立即值 1 时,也有该指令的特殊版本)。

SHL可用于将有符号或无符号数乘以 2 的幂。SHR可以用来将无符号数除以 2 的幂。

  shl rax, 5    ; RAX = RAX * (2 ^ 5)

  shr rdx, 3    ; RDX = RDX / (2 ^ 3) where RDX is unsigned,
  use SAR for signed

使用SHLSHR指令,就像 C++ 中的移位操作一样,左侧和右侧空出的位被填充为 0。算术右移(SAR)将位右移,但用符号位填充左边的空白位,因此它可用于将有符号数除以 2 的幂。

如果第二个操作数为 0(无论是立即操作数还是 CL 操作数),则不会设置标志。

如果偏移不为零,则标志会受到影响。进位标志保存移出目的地的最后一位。

标志:进位、奇偶校验、零、符号、溢出

左右旋转

ROL [reg8/16/32/64/mem8/16/32/64], [CL/imm8]

ROR [reg8/16/32/64/mem8/16/32/64], [CL/imm8]

这将第一个操作数旋转源中指定的位数。旋转操作与位移位相同,只是当位在右侧移出时(ROR)它们在左侧重新输入,或者当位在左侧移出时(ROL)它们在右侧重新输入。

| | 注意:这些旋转和移动指令有特殊版本。如果使用立即操作数,并且它正好是 1,则设置溢出标志。这表明第一个操作数的符号是否已经改变。如果指令后溢出标志为 1,则目的操作数的符号已被改变,否则它保持不变。 |

标志:进位、溢出

通过进位标志左右旋转

RCL [reg8/16/32/64/mem8/16/32/64], [CL/imm8]

RCR [reg8/16/32/64/mem8/16/32/64], [CL/imm8]

这将通过进位标志向左(RCL)或向右(RCR)旋转目的地。这些指令与ROLROR旋转指令相同,只是它们也旋转标志寄存器中的进位标志,好像它是目的地的一部分。

对于RCL(通过进位标志向左旋转),被旋转的寄存器可以被认为具有进位标志作为其第九位(最高有效)。对于RCR(通过进位标志向右旋转),被旋转的寄存器将进位标志作为第一位(最低有效位)。

标志:进位、溢出

向左或向右移动两次

SHLD [reg/mem16/32/64], [reg16/32/64], [CL/imm8]

SHRD [reg/mem16/32/64], [reg16/32/64], [CL/imm8]

这会将第一个操作数向左(SHLD)或向右(SHRD)移动,并将第二个操作数的位从左(SHRD)或右(SHLD)移动。要移位的位数在第三个操作数中指定。该指令允许您将一个寄存器的内容转移到另一个寄存器或内存位置。该指令不改变第二个操作数。

| | 注意:这些指令没有接受 8 位操作数的版本;如果需要 8 位 SHLD 或 SHRD,则应使用 16 位 x86 寄存器之一。例如,您可以使用 AX 将位从 a1 移动到 AH 中的位以及从 AH 中的位移动。 |

标志:溢出、符号、零、奇偶校验、进位

位测试

BT [reg16/32/64/mem16/32/64], [reg16/32/64/imm8]

BTC [reg16/32/64/mem16/32/64], [reg16/32/64/imm8]

BTR [reg16/32/64/mem16/32/64], [reg16/32/64/imm8]

BTS [reg16/32/64/mem16/32/64], [reg16/32/64/imm8]

这将第二个操作数指定的从零开始的索引处的位从第一个操作数复制到进位标志中。

  bt eax, 4     ; Copy the 4th bit of EAX into the Carry
  Flag

当第一个操作数是内存,第二个操作数是寄存器时,使用这些指令的特殊版本。在这种情况下,整个内存变成了一个位数组,而不是常规的字节数组!传递的参数成为位数组的基(其最右边的零位是位数组的开始,其展开部分是内存的剩余部分)。访问内存的所有规则仍然适用,并且将生成分段错误。

  mov eax, 27873       ; We wish to know what the 27873th
  bit is.
  bt MyVariable, eax  ; Beginning from rightmost bit in
  MyVariable.

BT测试该位,并将其值复制到进位标志。BTC测试该位,然后在第一个操作数中对其进行补码。BTR测试该位,然后在第一个操作数中将其重置为 0。BTS测试该位,然后在第一个操作数中将其设置为 1。

标志:进位(除了方向以外的所有其他都是未定义的)

前缀:锁定(但不在蓝牙上,因为它不能写入内存)

正向和反向位扫描

BSF [reg16/32/64], [reg16/32/64/mem16/32/64]

BSR [reg16/32/64], [reg16/32/64/mem16/32/64]

这将从右到左(向前,BSF)或从左到右(向后,BSR)搜索第二个操作数,寻找设置为 1 的第一个位。如果发现某个位被设置为 1,则第一个操作数被设置为其索引,零标志被清除。如果第二个操作数中根本没有设置为 1 的位,零标志将设置为 1。

无论扫描方向如何,位索引都不会改变。如果操作数中只设置了一位,BSFBSR将返回完全相同的值。如果设置了多个位,它们将返回不同的值。

mov ax, 2

bsf bx, ax ; Places 1 into bx

bsr bx, ax ; Places 1 into bx

标志:零(除了方向以外,其余都是未定义的)

条件字节集

SETO [reg8/mem8] Overflow OF = 1

SETNO [reg8/mem8] Overflow OF = 0

SETB, SETC, SETNAE [reg8/mem8] Below, carry CF = 1

SETNB, SETNC, SETAE [reg8/mem8] Above or equal, carry CF = 0

SETZ, SETE [reg8/mem8] Equal, zero ZF = 1

SETNZ, SETNE [reg8/mem8] Not equal, zero ZF = 0

SETBE, SETNA [reg8/mem8] Below or equal, CF = 1 or ZF = 1

SETNBE, SETA [reg8/mem8] Above, CF = 0 and ZF = 0

SETS [reg8/mem8] Sign SF = 1

SETNS [reg8/mem8] Sign SF = 0

SETP, SETPE [reg8/mem8] Parity is even PF = 1

SETNP, SETPO [reg8/mem8] Parity is odd PF = 0

SETL, SETNGE [reg8/mem8] Less than SF <> OF

SETNL, SETGE [reg8/mem8] Not less than SF = OF

SETLE, SETNG [reg8/mem8] Less or equal ZF = 1 or SF <> OF

SETNLE, SETG [reg8/mem8] Greater than ZF = 0 and SF <> OF

这些指令根据标志是否满足指定条件,将操作数设置为 0 或 1。如果满足条件,目标变为 1,否则变为 0。条件都引用标志,因此该指令通常放在CMPTEST之后;它类似于CMOVcc指令,只是它移动 0 或 1,而不是像CMOVcc指令那样将第二个操作数移动到第一个操作数中。

标志:(无)

设置和清除进位或方向标志

STC Set carry flag CF = 1

CLC Clears the carry flag to 0

STD Set direction flag DF = 1

CLD Clears the direction flag

STC将进位标志设置为 1,而CLC将其清零。同样,STD将方向标志设置为 1,而CLD将其清除为 0。设置或清除方向标志对于设置字符串指令移动其自动指针的方向很有用。

旗帜:进位(STC 和 CLC),方向(STD 和 CLD)

跳跃

JMP Unconditionally jump

JO Jump on overflow

JNO Jump on no overflow

JB,JC,JNAE Jump if below, CF = 1, not above or equal

JNB,JNC,JAE Jump if not below, CF = 0, above or equal

JZ,JE ZF = 1, jump if equal

JNZ,JNE ZF = 0, jump if not equal

JBE,JNA Jump if below or equal, not above, CF or ZF = 1

JNBE,JA Jump if not below or equal, above, CF and ZF = 0

JS Jump on sign, SF = 1

JNS Jump on no sign, SF = 0

JP,JPE Jump on parity, parity even, PF = 1

JNP,JPO Jump on no parity, parity odd, PF = 0

JL,JNGE Jump if less, not greater or equal, SF != OF

JNL,JGE Jump if not less, greater, or equal, SF = OF

JLE,JNG Jump if less or equal, not greater than, ZF = 1 or SF != OF

JNLE,JG ">Jump if not less or equal, greater than, ZF = 0 and SF = OF

JCXZ Jump if CX = 0

JECXZ Jump if ECX = 0

JRCXZ Jump if RCX = 0

每个跳转指令采用一个操作数。这个操作数通常是代码中某处定义的标签,但实际上它相当灵活。Jxx指令可用的寻址模式如下:

JMP [reg/mem/imm]

Jcc [imm8/16/32]

JcCX [imm/8/16/32]

这些指令有时被称为分支;如果条件为真,RIP 寄存器将落到操作数,否则 RIP 将落到下一行代码。

通常操作数是一个标签。

         cmp edx, eax
         jg SomeLabel  ; Jump if greater

                ; Some code to skip

  SomeLabel:

标志:(无)

调用函数

CALL [reg16/32/64/mem16/32/64]

CALL [imm16/32]

这需要一个过程。该指令将下一条指令的偏移量推入栈,并将 RIP 寄存器跳转到作为第一个操作数给出的过程或标签。它本质上与跳转指令完全相同,只有返回地址被推送到栈,因此 RIP 可以使用子流程主体中的RET指令返回并恢复调用函数的执行。

| | 注:过去有远近通话之分。远程调用以另一个代码段结束。但是,由于 x64 使用的是平面内存模型,所以所有调用都是近调用。 |

标志:(无)

从功能返回

RET

该指令从用CALL指令调用的函数返回。这是通过将返回地址弹出到 RIP 中来实现的。

标志:(无)

x86 字符串指令

加载字符串

LODS [mem8/16/32/64]

LODSB Load byte

LODSW Load word

LODSD Load dword

LODSQ Load qword

这些指令将一个字节、字、dword 或 qword 加载到适当大小的 RAX 寄存器中,然后它们递增(或递减,取决于方向标志)RSI 以指向下一个字节、字、dword 或 qword。他们读取 RAX 的任何 RSI(源索引寄存器)指向的数据,然后移动 RSI 指向下一个相同大小的数据。

可以使用REP前缀,但是这是没有意义的,因为不能对存储在 RAX 的连续值执行任何操作;循环将简单地遍历字符串,只留下 RAX 的最终值。

| | 注意:即使是内存操作数只读的版本,无论 RSI 指向什么。内存操作数几乎完全被忽略。它的唯一目的是指示应该读取什么大小的数据,以及应该将数据放入哪个版本的 RAX。 |

| | 注意:如果方向标志 DF 是由标准指令设置的 1,字符串指令将减少 RDI 和 RSI,而不是增加。否则指令将递增。 |

标志:(无)

前缀:REP

存储字符串

STOS [mem8/16/32/64]

STOSB Store byte

STOSW Store word

STOSD Store dword

STOSQ Store qword

这会将 a1、AX、EAX 或 RAX 存储到 RDI 指向的内存中,并根据方向标志增加(或减少)RDI。该指令可用于快速将大量值设置为同一事物。RDI 随着每次重复的数据类型的大小而递增或递减。

要将 100 个单词设置为 56,请确保 RDI 指向内存中 100 个单词的开头。

  lea rdi, someArray ; Point RDI to the start of the array
  mov rcx, 100
  mov ax, 56
  rep stosw

| | 注意:即使有内存操作数的版本也只存储到 RDI。内存操作数几乎完全被忽略。内存操作数的唯一目的是指示应该存储 a1、AX、EAX 或 RAX 中的哪一个,以及 RDI 的增量。 |

| | 注意:如果方向标志 DF 是由标准指令设置的 1,字符串指令将减少 RDI 和 RSI,而不是增加。否则指令将递增。 |

标志:(无)

前缀:REP

移动弦

MOVS [mem8/16/32/64], [mem8/16/32/64]

MOVSB

MOVSW

MOVSD

MOVSQ

这会将 RSI 指向的字节、字、dword 或 qword 移动到 RDI 指向的字节、字、dword 或 qword,并同时递增 RSI 和 RDI 以指向下一个字节(或根据方向标志递减)。每次重复,RSI 和 RDI 都会随着数据类型的大小而递增。该指令可用于将数据从一个数组快速移动到另一个数组。将源阵列开始处的 RSI 和目标开始处的 RDI 设置为,并将要复制的元素数量放入 RCX。

  lea rsi, SomeArray
  lea rdi, SomeOtherArray
  mov rcx, 1000

  rep movsq     ; Copy 8000 byes

| | 注意:即使是有内存操作数的版本,也会将数据从 RSI 复制到 RDI 内存操作数的唯一用途是指定要复制的数据的大小。 |

| | 注意:如果方向标志 DF 为 1,如标准指令所设置的,字符串指令将减少 RDI 和 RSI,而不是增加。否则指令将递增。 |

前缀:REP

扫描字符串

SCAS [mem8/16/32/64], [mem8/16/32/64]

SCASB

SCASW

SCASD

SCASQ

这将 RDI 指向的字节、字、dword 或 qword 与 RAX 的适当大小进行比较,并相应地设置标志。然后,它递增(或递减,取决于方向标志)RDI,以指向内存中下一个相同大小的元素。该指令旨在与REPEZNENZ前缀一起使用,它扫描一个字符串以查找 RAX 的元素,或者直到 RCX 的计数达到 0。

要扫描最多 100 个字节的字节数组以找到字符“a”的第一个匹配项,请使用以下命令:

         lea rdi, arr     ; Point RDI to some array
         mov rcx, 100     ; Load max into RCX
         mov al, 'a'      ; Load value to seek into AL
  repnz scasb             ; Search for AL in *RDI
         jnz NotFound     ; If the zero flag is not set
  after the 
                           ; scan AL is not in arr
         lea rax, [arr+1] ; Otherwise we can find the index
  of the 
                           ; first occurrence of AL
         sub rdi, rax     ; By subtracting arr+1 from the
  address where we found AL

| | 注意:即使是带有内存操作数的版本,也只扫描 RDI 所指向的内容。内存操作数几乎完全被忽略。内存操作数的唯一目的是指示应该将 a1、AX、EAX 或 RAX 中的哪一个与 RDI 进行比较,以及 RDI 的增量。 |

| | 注意:如果方向标志 DF 是由标准指令设置的 1,字符串指令将减少 RDI 和 RSI,而不是增加。否则指令将递增。 |

标志:溢出、符号、零、奇偶校验、进位

字首:重复,重复,重复,重复

比较字符串

CMPS [mem8/16/32/64], [mem8/16/32/64]

CMPSB

CMPSW

CMPSD

CMPSQ

这些指令将 RSI 指向的数据与 RDI 指向的数据进行比较,并相应地设置标志。它们根据操作数大小增加(或减少,取决于方向标志)RSI 和 RDI。它们可用于扫描RSI 和RDI,以查找两个数组之间不同或相同的第一个字节、字、dword 或 qword。

| | 注意:即使是有内存操作数的版本,也只能比较 RSI 和 RDI。内存操作数几乎完全被忽略。内存操作数的唯一目的是指示每一轮应该增加或减少多少 RDI 和 RSI。 |

| | 注意:如果方向标志 DF 是由标准指令设置的 1,字符串指令将减少 RDI 和 RSI,而不是增加。否则指令将递增。 |

字首:重复,重复,重复,重复

x86 杂项说明

无操作

NOP

这条指令除了等待一个时钟周期之外什么也不做。但是,它对于优化管道使用和修补代码很有用。

标志:(无)

暂停

Pause

该指令与NOP类似,但它也向 CPU 指示线程处于旋转循环中,以便 CPU 可以使用其拥有的任何节能功能。

标志:(无)

读取时间戳计数器

RDTSC

该指令将时间戳计数器加载到 EAX EDX。时间戳计数器是自中央处理器复位以来经过的时钟周期数。这对于获得极其精细的定时读数非常有用。

下面是读取时间戳计数器的一个小函数:

  ReadTimeStamp proc
         rdtsc
         shl rdx, 32
         or rax, rdx
         ret
  ReadTimeStamp endp

很难获得单个时钟周期级别的性能读数,因为 Windows 一直在运行的应用程序和多任务之间切换。最好的办法是反复运行测试。您应该测试ReadTimeStamp程序需要多长时间,并从后续测试中减去该时间,然后以平均或最佳时钟周期读数为基准。

标志:(无)

循环

LOOP [Label]

LOOPE [Label]

LOOPNE [Label]

LOOPZ [Label]

LOOPNZ [Label]

如果满足一个条件,这些将减少 RCX 计数器并跳转到指定的标签。例如,LOOP指令从标签开始递减 RCX 并重复,直到它为 0。然后它不会分支,而是继续执行循环后面的代码。LOOP指令几乎不用,因为手动减量和跳转更快。

  dec rcx
  jnz LoopTop

除了速度更快,手动两行版本允许程序员指定哪个寄存器用作LOOPxx使用 RCX 的计数器。

LOOP指令很有趣,但是它们根本没有像手册DECJNZ那样设置标志寄存器。当 RCX 在LOOP中达到 0 时,RIP 寄存器将失效,但是零标志将不会从它在循环体中的最后设置改变。

如果循环的结构组件不改变标志寄存器很重要,那么使用LOOP指令代替手动两行循环可能值得研究。对于LOOPELOOPZ,如果零标志为 1,则循环失败。对于LOOPNELOOPNZ,如果零标志为 0,则循环失败。

循环可以通过 RCX 变成 0 或者在零标志的条件下被打破。这可能会导致一些混乱。如果在长循环的第一次迭代期间恰好设置了零标志,则LOOPE指令不会减少 RCX 并重复循环。在零标志的情况下,循环将中断。

如前所述,LOOP指令经常不被使用。它们比上一个示例中的两行手动循环尾慢,因为它们不仅仅是简单地DEC计数器和跳转。如果LOOP指令恰好执行您所需要的,它们可能会提供良好的性能提升,而不是检查零标志。然而,在绝大多数情况下,简单的DECJNZ会更快。

CPUID

MOV EAX, [function] ; Move the function number into EAX first

CPUID

该指令返回正在执行的中央处理器的信息,包括中央处理器供应商、高速缓存大小、内核数量和可用指令集的数据。

CPUID指令本身可能在较旧的 CPU 上不可用。测试CPUID指令是否可以执行的推荐方法是切换标志寄存器的第 21 位。如果该位可以被程序设置为 1,则中央处理器理解CPUID指令。否则,CPU 不理解CPUID指令。

以下是测试CPUID指令可用性的示例:

         pushfq        ; Save the flags register
         push 200000h ; Push nothing but bit 21
         popfq         ; Pop this into the flags
         pushfq        ; Push the flags again
         pop rax       ; This time popping it back into RAX
         popfq         ; Restore the original flag's state
         cmp rax, 0    ; Check if our bit 21 was changed or
  stuck
         je No_CPUID   ; If it reverted back to 0, there's
  no CPUID

不是所有的中央处理器都能执行所有的指令。现代的中央处理器通常比旧的能够执行更多的指令集。为了知道执行您代码的 CPU 是否知道任何特定的指令集,您可以调用特殊的CPUID指令。

CPUID指令不带操作数,但隐含了 EAX。当指令被调用时,EAX 中的值被中央处理器作为功能号读取。

有大量不同的功能,每个 CPU 制造商都能够定义自己的功能。例如,特定于制造商的函数通常将 EAX 的前 16 位设置为 8000。确定许多指令集的功能是英特尔、AMD 和 VIA 的标准功能。

要调用特定的函数,首先将函数号MOV输入 EAX,然后使用CPUID指令。

  mov eax, 1    ; Function number 1
  cpuid         ; No formal parameters but EAX is implied!

CPUID功能 1(当 EAX 设置为 1 时调用CPUID)列出特征标识符;特征标识符是 CPU 知道的指令集。它通过在 ECX 和 EDX 存储一系列位标志来列出可能的指令集。位设置为 1 表示中央处理器能够实现特定功能,0 表示不能。下表列出了一些最有用的特性,包括寄存器(ECX 或 EDX)和用于检查特性的位数。每一代新的中央处理器都有更多的附加功能。

表 17:简略特征标识符

功能号(EAX) 登记册(ECX/EDX) ECX/EDX 的比特指数 特征
one ECX Twenty-eight AVX
one ECX Twenty-five 俄歇电子能谱
one ECX Twenty-three 持久性有机污染物
one ECX Twenty SSE4.2
one ECX Nineteen SSE4.1
one ECX nine SSSE3
one ECX Zero SSE3
one EDX Twenty-six SSE2
one EDX Twenty-five 同SOUTH-SOUTH-EAST
one EDX Twenty-three 多媒体增强指令集(Multi Media Extension)
one EDX Fifteen 条件移动
one EDX four 刚果民主共和国
one EDX Zero x87 FPU

以下示例测试 MMX 和 SSE4.2。在汇编文件中,可以更改寄存器(ECX 或 EDX)和位号来测试任何功能。关于CPUID可以在 AMD 芯片上做什么的完整列表,请参考 AMD 的 CPUID 规范。有关CPUID在英特尔芯片上能做什么的完整列表,请参考英特尔处理器标识和英特尔发布的 CPUID 指令。手册和其他推荐阅读的链接可以在本书的末尾找到。

  // This is the C++ file
  #include <iostream>
  using namespace std;
  extern "C" bool
  MMXCapable();
  extern "C" bool
  SSE42Capable();

  int main()
  {
         if(MMXCapable())
  cout<<"This CPU is MMX capable!"<<endl;
         else
  cout<<"This CPU does not have the MMX instruction set
  :("<<endl;

         if(SSE42Capable())
  cout<<"This CPU is SSE4.2
  capable!"<<endl;
         else
  cout<<"This CPU does not have the SSE4.2 instruction set"<<endl;
         cin.get();
         return 0;
  }

  ; This is the assembly file
  .code
  ; bool MMXCapable()
  ; Returns true if the current CPU knows MMX
  ; else returns false
  MMXCapable proc 
         mov eax, 1           ; Move function 1 into EAX
         cpuid                ; Call CPUID
         shr edx, 23          ; Shift the MMX bit to
  position 0
         and edx, 1           ; Mask out all but this bit in
  EDX
         mov eax, edx         ; Move this answer, 1 or 0,
  into EAX
         ret                  ; And return it
  MMXCapable endp

  ; bool SSE42Capable()
  ; Returns true if the current CPU knows SSE4.2
  ; else returns false
  SSE42Capable proc
         mov eax, 1           ; Move function 1 into EAX
         cpuid                ; Call CPUID
         shr ecx, 20          ; Shift SSE4.2 bit to position
  0
         and ecx, 1           ; Mask out all but this bit
         mov eax, ecx         ; Move this bit into EAX
         ret                  ; And return it
  SSE42Capable endp
  end

| | 注意:在过去,如果指令集不受支持,简单地调用一条指令并让 CPU 抛出异常是很常见的。给定的机器代码在旧的 CPU 上执行而不抛出异常的可能性很小,但它实际上会执行一些其他指令。因此,CPUID 指令是测试指令集是否可用的推荐方法。 |