指令长度

5 x86 assembly instruction-set machine-code code-size

我正在查看汇编中的不同指令,我对如何决定不同操作数和操作码的长度感到困惑.

这是你应该从经验中得知的东西,还是有办法找出哪个操作数/运算符组合占用了多少字节?

例如:

push %ebp ; takes up one byte
mov %esp, %ebp ; takes up two bytes
Run Code Online (Sandbox Code Playgroud)

所以问题是:

在看到给定的指令后,如何推断出其操作码需要多少字节?

Pet*_*des 9

术语:“操作码”是指令中选择操作的部分,不包括操作数或修改操作的非强制前缀(例如操作数大小)。使用“操作码”来指代整个指令是不正确的,尽管一些谈论 shellcode 的人经常这样做。

这是你应该从经验中知道的事情吗

有了查看机器代码的经验,或者特别是优化代码大小的经验,那么是的,您将开始记住您反复查找的内容,并学习如何查看汇编行并知道指令的长度,无需记住字节是什么

操作数编码规则不依赖于操作码,因此您只需记住操作码长度以及不使用 ModR/M 字节对操作数进行编码的特殊情况短格式。然后分别记住操作数编码规则。

就我个人而言,我喜欢使用 x86 机器代码来回答像这样的代码高尔夫问题。另请参阅x86/x64 机器代码中打高尔夫球的提示)。我用 NASM 编写,计划/知道每条指令的长度,并让汇编器生成实际机器代码的十六进制转储作为列表。对于对代码高尔夫有用的简短指令,我不记得最近有过关于指令长度的错误,但我很幸运能够对我觉得有趣的细节(例如 x86 指令集)有很好的记忆力或使用很多。(我确实必须尝试rorx看看它有多长。)

我自己不会输入机器代码字节;要手动完成此操作,我必须查找手册中的每条说明。x86 没有用于 PC 相对寻址的短编码,因此在机器代码内查找/创建有用的常量(可以兼作数据)并不是一件事,因此对于代码高尔夫来说,记住任何数字通常没有用指令编码的详细信息。

在优化性能时,当其他条件相同时,通常越小越好,因此关心代码大小,尤其是对齐绝对是性能的一部分。

或者有没有办法找出哪个操作数/运算符组合占用了多少字节?

这在手册中有详细记录。除了一些特殊情况的 1 字节指令之外,(几乎)所有内容的操作数编码都是相同的。


大多数 x86 指令的机器代码编码都遵循此模式(英特尔在@Mehrdad 的回答中提供了更好的图表版本):

[prefixes] opcode ModR/M [extra addressing-mode bytes] [immediate]
Run Code Online (Sandbox Code Playgroud)

(没有显式操作数的指令没有 ModR/M 字节,只有操作码字节)。

对于大多数常见指令,x86 操作码是 1 字节,尤其是自 8086 以来就存在的指令。后来添加的指令(例如386 中bsf的 和)通常使用带有转义movsx字节的 2 字节操作码。0f如果你经常关注 SO,你会看到很多专门询问 8086 的问题(尤其是emu8086);这是我知道哪些指令在 8086 上不可用的主要原因。如果您只想直接记住哪些指令具有 2 字节操作码而不需要历史详细信息,那完全没问题。或者每次都在手册中查找它:P

例如0f b6 c0 movzx eax,al,所以 0F B6 是 的操作码mov r32, r/m8,C0 是 ModR/M 字节,它将 eax 编码为目标(/r字段 = 0)、源寄存器直接模式(前 2 位 = 11)以及al源寄存器(/m字段 = 0)。

我在所有示例中都使用 Intel 语法 ( mnemonic dst, src1 [,src2, ...]),因为它与您在 Intel 和 AMD 手册中找到的内容相匹配。AFAIK,没有任何使用 AT&T 语法的详细指令编码手册。即使在谈论 8086 的功能时,我也会使用 32 或 64 位的示例。当然8086只有16位实模式,但64位模式使用相同的操作码和编码(这就是我们现在关心的)。


Intel 的指令集参考。手册(SDM vol.2)有 1、2、3 字节操作码的操作码映射(附录 A.3),因此您可以在操作码编码的选择中看到一些模式。或者,对于任何给定的指令,请查看该手册中列出的编码以及完整说明。(另请参阅一些不错的在线摘录,每条指令一页,例如https://github.com/HJLebbink/asm-dude/wikihttp://felixcloutier.com/x86/。HJ Lebbink 的页面标记每条指令的时间被引入,因此您可以看到 8086 表示add,或 386 表示新形式的轮班,以及movzx)。

请注意,某些单操作数指令(例如shlnot)使用/rModR/M 字节的字段作为额外的操作码位。此外,大多数带有立即数的指令仍然具有破坏性,因为它们使用该/r字段作为操作码位。 imul r32, r/m32, imm32(386) 是此规则的例外,它具有立即数并为两个操作数使用完整的 ModR/M 字节。(请注意,ModR/M 只能向寄存器或内存操作数发出信号;编码add r/m32, imm8使用操作码来指示存在立即数。但是主操作码字节由多个指令共享,因此该/r字段用作操作码的一部分,这就是为什么我们没有add r/m32, r32, imm8。但是对于 ADD / SUB 我们可以用作lea ecx, [rax + 1]复制和添加。)


操作数编码:

大多数带有立即数操作数的指令的长度与寄存器/内存源版本相同,加上用于编码立即数的字节。立即数为 imm8 或 imm32,因此 -128..127 之间的值更加紧凑。(在 16 位模式下,它是 imm8 或 imm16)。

ModR/M 字节是寄存器直接寻址或最简单的无位移的单寄存器寻址模式所需的全部。(除了[esp])。所以add eax, ecx是 2 个字节长,就像add eax, [ecx]. 索引寻址模式(以及以esp/rsp作为基址寄存器的模式)需要 SIB(标度/索引/基址)字节。

寻址模式中的恒定位移需要在 ModR/M + 可选 SIB 顶部额外的 1 或 4 个字节(符号扩展 disp8 或 disp32)。

带有 disp8 的 AVX512 EVEX 按向量宽度缩放 disp8,因此vaddps zmm31, zmm30, [rsi + 256]只有 7 个字节(4 字节 EVX + opcode=0x58 + modrm + disp8),但vaddps zmm31, zmm30, [rsi + 16]有 11 个字节:它必须使用 disp32 进行编码+16,因为它不是64 的倍数。但是带有寄存器的相同指令xmm可以使用disp8.

有关完整详细信息,请参阅英特尔手册。


最常见指令的特殊简短形式

为了节省代码大小,8086(以及更高版本的 x86)为一些非常常见的指令提供了没有 ModR/M 字节的特殊编码。如果指令不是其中之一,则它使用 ModR/M 字节

  • 添加/adc/sub/cmp/test/和/或/xor/等。AL/AX/EAX 的立即数与寄存器大小相同。例如and eax, imm32(5 个字节)或and al,imm8(2 个字节)。但是 ; 没有特殊的编码and eax, imm8。仍然必须使用 3 字节and r/m32, imm8编码。在处理 8 位数据时,使用它al对于代码大小来说非常有好处,特别是如果您已经避免或不担心部分寄存器停顿或错误依赖关系导致性能问题。
  • 计数为 1 的移位/旋转:8086 没有 imm8 旋转,仅通过cl隐式 1 进行旋转,因此存在类似于shl r/m32,11式操作码的操作码。

    使用imm8编码会对性能产生影响:P6 系列上可能会出现停顿,因为它在执行之前不会检查 imm8 是否为零。但rol r32,1简写形式是 2 uops,而rol r32, imm8Sandybridge 系列(包括 Skylake)上的 uops 是 1(即使 imm8 是 1)。简短rcl r32,1形式比使用 imm8 快得多。(Skylake 上为 3 个微操作数,而 Skylake 上为 8 个微操作数)。

还有一些寄存器被编码在指令字节的低 3 位中,有效地专用了 8 个字节的操作码编码空间,使这些指令的寄存器操作数形式缩短了 1 个字节。

  • mov r8, imm8:一般mov r/m8, imm8编码用 2 个字节,而不是 3 个字节。
  • mov r32, imm32: 5 个字节而不是 6 个字节mov r/m32, imm32。有趣的事实:在 x86-64 中,短格式操作码的 REX.W=1 版本是唯一可以使用 64 位立即数的指令。10 字节mov r64, imm64。REX.W=1 版本的r/m32操作码仍然使用 32 位立即数(像平常一样进行符号扩展),因此mov rax, -1最好以这种方式进行编码,占用 7 个字节与 5 个字节mov eax,-1。(或者,如果针对代码大小进行优化,另请参阅有效地将 CPU 寄存器中的所有位设置为 1。
  • push/pop寄存器,1字节与2字节的pop r/m32编码。
  • push/pop段寄存器(FS/GS 除外)。虽然这些没有 ar/m16 编码。
  • inc r32/ dec r32(仅限 16/32 位模式:0x4X 字节是 x86-64 中的 REX 前缀,因此inc eax必须使用 2 字节inc r/m32编码)。
  • xchg eax, reg:这就是来自:(或在 16 位模式下, )0x90 nop的缩写形式。在 x86-64 中,90也不是,因为这会将 EAX 零扩展为 RAX。相反,它有自己的指令集手动输入xchg eax,eaxxchg ax,axnopxchg eax,eax

    xchg reg,reg编译器从未使用过,并且通常不会快于 3 个mov指令,因此如果我们能保留这 7 个操作码字节以用于将来更有用的扩展,那就太好了。nop(如果移动到不同的操作码,则为 8 ...)。当累加器“更特殊”时,它在 8086 中更有用,例如cbw将 AL 符号扩展为 AX 是唯一(好的)方法,因为movsx不存在。并且只有 1 个操作数mul/imul可用。

xchg eax, r32对于代码高尔夫来说仍然很棒,例如8 字节 x86 32 位机器代码中的 GCD。另请参阅我的其他代码高尔夫答案,了解各种代码大小技巧(主要以牺牲性能为代价;这就是代码高尔夫的要点)。

我认为这涵盖了也具有编码的指令的所有单字节特殊情况r/m32


这个答案并不意味着详尽无遗。我没有过多谈论最近的指令,对于罕见的指令有很多特殊情况。何时需要 REX 前缀或操作数大小前缀的规则非常简单。以下是一些更通用的规则:

  • SSE1/SSE3ABCps指令具有 2 字节操作码 (0F xx)
  • SSE2整数/双精度指令通常有3字节操作码(66 0F xx或类似)
  • SSSE3/SSE4.x 指令具有 4 字节操作码(3 个强制前缀)

如果 SSE 版本是 SSE3 或更早版本,并且第二个源寄存器不是“高”寄存器 (xmm/ymm8-15),则VEX 编码指令可以使用 2 字节 VEX 前缀。同一指令的 XMM 和 YMM 版本始终具有相同的大小。(但是当您不关心或希望高半为零时,更喜欢带有隐式零扩展的 xmm ,而不是显式 ymm。)

vpxor  ymm8,ymm8,ymm5    ; 2-byte VEX
vpxor  ymm7,ymm7,ymm8    ; 3-byte VEX
vpxor  ymm7,ymm8,ymm7    ; 2-byte VEX
Run Code Online (Sandbox Code Playgroud)

因此,我们可以使用“高”寄存器作为目标或第一个源,而不需要 3 字节 VEX,但不能作为第二个源(总共第三个操作数)。对于交换运算,您可以通过将 low8 运算作为第二个源来节省大小。

请注意,对于 4 操作数指令,如vblendvps,第 4 个操作数被编码为imm8. 所以它仍然是第三个操作数(第二个源),而不是最后一个操作数,这会影响所需的 VEX 前缀大小。但是blendvps是SSE4.1,所以无论如何它总是需要一个3字节的VEX前缀来表示66.0F3A前缀字段的编码。


Meh*_*ari 7

没有数据库的x86没有严格的规则,因为指令编码非常复杂(操作码本身可以在1到3个字节之间变化).您可以参考英特尔®64和IA-32架构软件开发人员手册2A文档(第2章:指令格式),了解如何编码指令及其操作数:

在此输入图像描述


归档时间:

查看次数:

3790 次

最近记录:

7 年,5 月 前