我正在查看汇编中的不同指令,我对如何确定不同操作数和操作码的长度感到困惑。
这是您应该从经验中了解的内容,还是有办法找出哪个操作数/运算符组合占用了多少字节?
例如:
push %ebp ; takes up one byte
mov %esp, %ebp ; takes up two bytes
所以问题是:
看到给定指令后,我如何推断其操作码需要多少字节?
我正在查看汇编中的不同指令,我对如何确定不同操作数和操作码的长度感到困惑。
这是您应该从经验中了解的内容,还是有办法找出哪个操作数/运算符组合占用了多少字节?
例如:
push %ebp ; takes up one byte
mov %esp, %ebp ; takes up two bytes
所以问题是:
看到给定指令后,我如何推断其操作码需要多少字节?
没有数据库的 x86 没有硬性规定,因为指令编码非常复杂(操作码本身可以在 1 到 3 个字节之间变化)。您可以查阅英特尔® 64 位和 IA-32 架构软件开发人员手册 2A文档(第 2 章:指令格式),了解指令及其操作数的编码方式:
所以,既然你似乎对这个话题感兴趣,让我给你一个概述。一条 x86 指令最多包含五个部分,最长为 15 个字节:
prefixes opcode operand displacement immediate
可以生成超过 15 个字节的编码,但 CPU 会拒绝它们。除操作码外的所有五个部分都是可选的。您可以按如下方式找到它们的长度:
f0
lock、f2
repne、f3
repe、2e
cs、36
ss、3e
ds、26
es、64
fs、65
gs、66
操作数大小覆盖和67
地址大小覆盖。但是,只有f0
, f2
,f3
和 只有26
, 2e
, 36
, 3e
, 64
, 和中的一个65
一次被识别。如果提供了来自每个组的多个前缀,则 CPU 的行为会有所不同。VEX 和 EVEX 编码指令可能只有段覆盖和地址大小覆盖旧前缀,因为其他前缀包含在 VEX 和 EVEX 前缀下。40
to之一4f
。在其他模式下,这些字节是指令,而不是前缀,您的解码器必须考虑到这一点。与传统前缀一样,VEX 或 EVEX 编码指令不能有 REX 前缀。c4
和c5
可以引入用于编码一些现代指令的VEX 前缀。在长模式下,它们总是这样做,但在其他模式下,您必须在之后检查字节:将其解释为 modr/m 字节,如果它编码一个r,r
操作数对,它是一个 VEX 前缀,否则它是les
or的操作码lds
。一个以 VEX 开头的前缀c4
是两个字节长,c5
它是三个字节。VEX 前缀还对VEX 编码指令中省略的0f
,0f 38
和opcode 前缀进行编码。0f 3a
请注意,通常,使用 VEX 前缀不是可选的。例如,pdep
被编码为VEX.NDS.LZ.F2.0F38.W0 F5 /r
(eg c4 e2 7b f5 c0
for pdep eax,eax,eax
) 但对应的遗留指令f2 0f 38 f5 r/m32
(egf2 0f 38 f5 c0
for pdep eax,eax
) 无效。请注意,相同的操作码可以带有 VEX 前缀和不带前缀,两者可能意味着不同的东西。例如,0f 77
is emms
but VEX.128.0F.WIG 77
(ie c5 f8 77
) is vzeroupper
。62
引入了用于编码 AVX512 指令的EVEX 前缀。与 VEX 前缀类似,需要检查接下来的几个字节以区分 EVEX 前缀和bound
指令。EVEX 前缀总是四个字节长,并且像 VEX 前缀一样对操作码的一部分进行编码。在前缀之后是操作码。最初,操作码总是一个字节,但后来空间不足,所以现在它要么是一个字节,要么是一个以 、 或 为前缀的0f
单0f 38
字节0f 3a
。如果指令是 VEX 编码的,则这些前缀不存在。请注意,某些前缀可能会更改编码的指令。例如,操作码0f b8
是jmpe
(Enter IA-64 mode) 但f3 0f b8
不是repe jmpe
,而是popcnt
.
操作码和前缀决定了对哪条指令进行编码。从这里开始,它基本上是一帆风顺的。根据指令,后面可能会出现一个modr/m字节。根据 modr/m 字节和地址覆盖前缀,sib 字节和一个、两个或四个位移字节可能会跟随。最后,根据指令、操作数大小覆盖前缀和 REX 前缀,后面可能会跟着1、2、4、6 或 8 个立即字节。
这就是我在 Stack Overflow 答案范围内所能给出的尽可能多的描述。所以TL;DR:这真的很复杂。
术语:“操作码”是指令中选择操作的部分,不包括操作数或修改操作的非强制性前缀(例如操作数大小)。使用“操作码”来指代整个指令是不正确的,尽管有些人经常谈论 shellcode。
是不是你应该从经验中知道的
有了查看机器代码的经验或特别是优化代码大小的经验,那么是的,您将开始记住您反复查找的内容,并学习如何查看 asm 行并知道指令的长度,无需记住字节数。
操作数编码规则不依赖于操作码,因此您只需要记住操作码长度,以及不使用 ModR/M 字节对操作数进行编码的特殊情况短格式。然后分别记住操作数编码规则。
就我个人而言,我喜欢用 x86 机器码回答像这样的代码高尔夫问题。(另请参阅使用 x86/x64 机器代码打高尔夫球的技巧)。我用 NASM 编写,计划/知道每条指令的长度,并让汇编器生成实际机器代码的 hexdump 作为列表。对于对代码高尔夫有用的简短指令,我不记得最近在指令长度方面有任何错误,但我很幸运能够很好地记住我觉得有趣的细节(如 x86 指令集)或大量使用。(我确实必须尝试rorx
看看它有多长时间。)
我自己不输入机器代码字节;要手动做到这一点,我必须查看手册中的每条说明。x86 没有用于 PC 相对寻址的短编码,因此在机器代码中查找/创建有用的常量(可以兼作数据)不是一件事,因此对于 code-golf 来说记忆任何数字通常没有用处指令编码的细节。
在优化性能时,当其他一切都相同时,通常越小越好,因此关心代码大小,尤其是对齐绝对是性能的一部分。
或者有没有办法找出哪个操作数/运算符组合占用了多少字节?
这在手册中有很好的记录。除了一些特殊情况的 1 字节指令之外,操作数编码对于(几乎)所有内容都是相同的。
大多数 x86 指令的机器代码编码遵循这种模式(@Mehrdad 的答案中来自英特尔的更好的图表版本):
[prefixes] opcode ModR/M [extra addressing-mode bytes] [immediate]
(没有显式操作数的指令没有 ModR/M 字节,只有操作码字节)。
对于大多数常见指令,x86 操作码是 1 个字节,尤其是自 8086 以来就存在的指令。后来添加的指令(例如386 中的bsf
和movsx
)通常使用带有0f
转义字节的 2 字节操作码。如果您在 SO 上闲逛,您会看到很多关于 8086 的问题(尤其是emu8086
);这就是我知道哪些指令在 8086 上不可用的主要原因。如果您只想直接记住哪些指令具有 2 字节操作码而没有历史细节,那很好。或者每次在手册中查找它:P
例如0f b6 c0 movzx eax,al
,所以 0F B6 是 的操作码mov r32, r/m8
,C0 是将 eax 编码为目标(/r
字段 = 0)、源寄存器直接模式(前 2 位 = 11)和al
源寄存器(/m
字段 = 0)。
我对所有示例 ( mnemonic dst, src1 [,src2, ...]
) 都使用 Intel 语法,因为这与您在 Intel 和 AMD 手册中找到的内容相匹配。AFAIK,没有任何使用 AT&T 语法的详细指令编码手册。即使在谈论 8086 的功能时,我也会使用 32 位或 64 位示例。当然 8086 只有 16 位实模式,但在 64 位模式下使用相同的操作码和编码(这是我们现在关心的)。
英特尔的指令集参考。手册(SDM vol.2)有 1、2、3 字节操作码的操作码映射(附录 A.3),因此您可以在操作码编码的选择中看到一些模式。或者对于任何给定的说明,请查看该手册中列出的编码以及完整说明。(还可以看到一些不错的在线摘录,每条指令只有一页,例如https://github.com/HJLebbink/asm-dude/wiki和http://felixcloutier.com/x86/。HJ Lebbink 的页面标记了每条指令的时间已引入,因此您可以看到 8086add
或 386 表示新形式的班次,以及movzx
)。
请注意,一些单操作数指令,如shl
or not
,使用/r
ModR/M 字节的字段作为额外的操作码位。此外,大多数带有立即数的指令仍然具有破坏性,因为它们将该/r
字段用作操作码位。 imul r32, r/m32, imm32
(386) 是该规则的一个例外,它具有立即数并为两个操作数使用完整的 ModR/M 字节。(注意,ModR/M 只能表示寄存器或内存操作数;for 的编码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 字节
and eax, imm32
(5 个字节)或and al,imm8
(2 个字节)。但是 ; 没有特殊的编码and eax, imm8
。仍然必须使用 3 字节and r/m32, imm8
编码。在处理 8 位数据时,使用al
对代码大小非常有利,特别是如果您避免或不担心部分寄存器停顿或错误依赖会导致性能问题。计数为 1 的移位/旋转: 8086 没有 imm8 旋转,只有cl
隐式 1 或隐式 1,所以有类似隐含shl r/m32,1
的操作码1
。
使用imm8
编码具有性能影响:P6 系列的潜在停顿,因为它在执行之前不检查 imm8 是否为零。但是rol r32,1
简短的形式是 2 微指令,而rol r32, imm8
在包括 Skylake 在内的 Sandybridge 家族中(即使 imm8 为 1)为 1。短rcl r32,1
格式比使用 imm8 快得多。(Skylake 上 3 微指令对 8 微指令)。
还有一些寄存器在指令字节的低 3 位中编码,有效地将 8 个字节的操作码编码空间用于使这些指令的寄存器操作数形式缩短 1 个字节。
mov r8, imm8
: 2 字节而不是一般mov r/m8, imm8
编码的 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
register , 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
:这是0x90 nop
来自:(xchg eax,eax
或在 16 位模式下,xchg ax,ax
)的缩写形式。在 x86-64 中, 90nop
也不是xchg eax,eax
,因为这会将 EAX 零扩展为 RAX。相反,它有自己的指令集手册条目。
xchg reg,reg
编译器从不使用,并且通常不快于 3 个mov
指令,因此如果我们将这 7 个操作码字节返回以用于更有用的未来扩展,那就太好了。(或者 8 如果nop
被移动到不同的操作码......)。当累加器“更特殊”时,它在 8086 中更有用,例如cbw
,将 AL 符号扩展到 AX 是唯一(好的)方法,因为movsx
不存在。并且只有 1 个操作数mul
/imul
可用。
xchg eax, r32
仍然非常适合代码高尔夫,例如8 字节 x86 32 位机器码的 GCD。另请参阅我的其他代码高尔夫球答案,了解各种代码大小技巧(主要以牺牲性能为代价;这就是代码高尔夫球的重点)。
r/m32
我认为这涵盖了所有也具有编码的单字节特殊情况的指令。
这个答案并不详尽。我没有过多地谈论最近的指令,并且对于稀有指令有很多特殊情况。何时需要 REX 前缀或操作数大小前缀的规则非常简单。以下是一些更一般的规则:
ABCps
指令有 2 字节操作码 (0F xx)如果 SSE 版本是 SSE3 或更早版本,并且第二个源寄存器不是“高”寄存器 (xmm/ymm8-15) ,则VEX 编码指令可以使用 2 字节 VEX 前缀。同一条指令的 XMM 和 YMM 版本的大小始终相同。(但是当您不关心或不希望高半部分归零时,更喜欢使用隐式零扩展而不是显式 ymm 的 xmm 。)
vpxor ymm8,ymm8,ymm5 ; 2-byte VEX
vpxor ymm7,ymm7,ymm8 ; 3-byte VEX
vpxor ymm7,ymm8,ymm7 ; 2-byte VEX
所以我们可以使用“高”寄存器作为目标或第一个源,而不需要一个 3 字节的 VEX,但不能作为第二个源(总体上是第三个操作数)。对于交换运算,您可以通过将低 8 位作为第二个源来节省大小。
请注意,对于 4 操作数指令(如vblendvps
),第 4 个操作数编码为imm8
. 所以它仍然是第三个操作数(第二个源),而不是最后一个操作数,这会影响需要什么大小的 VEX 前缀。但是blendvps
是SSE4.1,所以无论如何它总是需要一个3字节的VEX前缀来表示66.0F3A
前缀字段的编码。
操作码的长度是根据(至少)两个标准构建的
还,
除了另一个答案中提供的链接(特别列出了代码的大小),另请参阅处理器历史。
通常,在使用汇编语言进行编程时,您不需要从一条指令到另一条指令都知道这一点。如果它很重要(例如,如果您试图将某些特定代码放入受限空间),您可以查看汇编器的列表输出或反汇编列表。
从我的 6510 个装配日开始,答案通常与操作数地址和偏移量有关。6510 的操作码总是 1 个字节。地址总是两个字节。如果操作码需要一个地址,那么我知道总大小是三个字节。如果指定了两个地址,那么我知道总大小为 5 个字节。
至于偏移量,它们占用的空间取决于分支的长度。所以考虑一下:
bne FooBar
如果“Foobar”偏移量指向一个小于 128 字节的地址,那么操作数就是一个字节。如果偏移量指向超出该地址的地址,则需要完整地址。完整地址不再是偏移量,当然地址占用了两个字节。
所以在后一种情况下,可能不容易判断操作码 + 操作数是否需要两个或三个字节。
所以我想,有时你可以说出来,而其他时候则不是那么明显。