就我个人而言,我喜欢用x86机器代码回答code-golf questions like this one . ( See also Tips for golfing in x86/x64 machine code ) . 我在NASM中编写,计划/知道每条指令的长度,并让汇编程序生成实际机器代码的hexdump作为列表 . 对于对代码高尔夫有用的简短说明,我很幸运能够获得有趣或使用很多的详细信息(如x86指令集) . (我确实必须尝试 rorx 看看它有多长 . )
对于大多数常见指令,x86操作码是1字节,尤其是自8086以来已存在的指令 . 稍后添加的指令(例如386中的 bsf 和 movsx )通常使用带有 0f 转义字节的2字节操作码 . 如果你在SO上闲逛,你会看到很多问题特别询问8086(特别是 emu8086 );那个's the main reason I know something about which instructions weren'吨可以在8086上找到 . 如果你'd rather just remember directly which instructions have 2-byte opcodes without the historical details, that'完全没问题 . 或者每次在手册中查找:P
例如 0f b6 c0 movzx eax,al ,所以0F B6是 mov r32, r/m8 的操作码,C0是将eax编码为目的地的ModR / M字节( /r field = 0),寄存器直接模式(前2位= 11), al 作为源寄存器( /m field = 0) .
add / adc / sub / cmp / test /和/或/ xor / etc. AL / AX / EAX,其大小与寄存器大小相同 . 例如 and eax, imm32 (5字节)或 and al,imm8 (2字节) . 但 and eax, imm8 没有特殊的编码;仍然必须使用3字节 and r/m32, imm8 编码 . 使用 al 在使用8位数据时非常适合代码大小,特别是如果您担心partial-register stalls or false dependencies导致性能问题 .
I think this covers all the single-byte special cases of instructions that also have r/m32 encodings.
This answer isn't meant to be exhaustive . 我没有't talked about more recent instructions much, and there are lots of special cases for rare instructions. The rules for when a REX prefix or operand-size prefix are required are pretty straightforward. Here'是一些更通用的规则:
SSE1 / SSE3 ABCps 指令有2字节操作码(0F xx)
SSE2整数/双精度指令通常有3字节操作码(66 0F xx或类似)
SSSE3 / SSE4.x指令有4字节操作码(3个强制性前缀)
VEX-coded instructions can use a 2-byte VEX prefix 如果SSE版本是SSE3或更早版本,并且第二个源寄存器不是"high"寄存器(xmm / ymm8-15) . 相同指令的XMM和YMM版本始终具有相同的大小 . (但是prefer xmm带有隐式零扩展而不是显式ymm,当你不关心或者想要将高半值归零时 . )
5 回答
没有数据库的x86没有严格的规则,因为指令编码非常复杂(操作码本身可以在1到3个字节之间变化) . 您可以参考Intel® 64 and IA-32 Architectures Software Developer’s Manual 2A文档(第2章:指令格式)来查看指令及其操作数的编码方式:
操作码的长度是考虑到(至少)两个标准
操作码的频率(如果经常在程序中使用,则将其置于1个字节,如果可能的话)
操作码所需的信息(如果需要绝对地址,则代码不能在唯一字节上编码)
也,
在最初的8088到最新的英特尔处理器(3年)之间的
除了另一个答案中提供的链接(具体列出代码的大小),另请参阅processors history .
术语:"opcode"是选择操作的指令的一部分,不包括操作数或修改操作的非强制性前缀(例如操作数大小) . 使用"opcode"来指代整个指令是不正确的,尽管有些人经常谈论shellcode .
有了查看机器代码的经验,或者特别是对代码大小进行优化的经验,是的,你反复查找,并学习如何查看asm行并知道指令将持续多长时间,而不记住字节将是什么 .
操作数编码规则不依赖于操作码,因此您只需记住操作码长度,以及不使用ModR / M字节编码操作数的特殊情况短格式 . 然后分别记住操作数编码规则 .
就我个人而言,我喜欢用x86机器代码回答code-golf questions like this one . ( See also Tips for golfing in x86/x64 machine code ) . 我在NASM中编写,计划/知道每条指令的长度,并让汇编程序生成实际机器代码的hexdump作为列表 . 对于对代码高尔夫有用的简短说明,我很幸运能够获得有趣或使用很多的详细信息(如x86指令集) . (我确实必须尝试
rorx
看看它有多长 . )我自己没有输入机器码字节;要手动完成,我必须在手册中查看每条指令 . x86没有用于PC相对寻址的短编码,因此在机器代码中查找/创建有用的常量(可能是数据的两倍)不是一件事,因此代码高尔夫通常不会记住任何数字指令编码的细节 .
在优化性能时,当其他条件相同时,通常更小,因此关心代码大小尤其是对齐肯定是性能的一部分 .
这在手册中有详细记载 . 除了一些特殊情况的1字节指令外,操作数编码对于(几乎)所有内容都是相同的 .
大多数x86指令的机器代码编码遵循这种模式(在@Mehrdad's answer中来自英特尔的更好的图表版本):
(没有显式操作数的指令没有ModR / M字节,只有操作码字节) .
对于大多数常见指令,x86操作码是1字节,尤其是自8086以来已存在的指令 . 稍后添加的指令(例如386中的
bsf
和movsx
)通常使用带有0f
转义字节的2字节操作码 . 如果你在SO上闲逛,你会看到很多问题特别询问8086(特别是emu8086
);那个's the main reason I know something about which instructions weren'吨可以在8086上找到 . 如果你'd rather just remember directly which instructions have 2-byte opcodes without the historical details, that'完全没问题 . 或者每次在手册中查找:P例如
0f b6 c0 movzx eax,al
,所以0F B6是mov r32, r/m8
的操作码,C0是将eax编码为目的地的ModR / M字节(/r
field = 0),寄存器直接模式(前2位= 11),al
作为源寄存器(/m
field = 0) .我正在为我的所有示例(
mnemonic dst, src1 [,src2, ...]
)使用英特尔语法,因为这与您的手册相匹配 . AFAIK,即使在谈论8086的内容时,也没有使用32或64位的例子 . 当然8086只有16位实模式,但在64位模式下使用相同的操作码和编码(这是我们最近关心的) .Intel's instruction set ref. manual (SDM vol.2)具有1,2,3字节操作码的操作码映射(附录A.3),因此您可以在操作码编码选择中看到一些模式 . 或者对于任何给定的指令,请查看列出的编码以及该手册中的完整说明 . (另请参阅一些不错的在线摘录,每条指令一页,如https://github.com/HJLebbink/asm-dude/wiki和http://felixcloutier.com/x86/ . HJ Lebbink的页面标记每条指令的引入时间,因此您可以看到8086表示
add
,或386表示新的表格形式,以及movzx
) .请注意,某些单操作数指令(如shl或
not
)使用ModR / M字节的/r
字段作为额外操作码位 . 此外,大多数带有立即数的指令仍具有破坏性,因为它们使用/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]
作为复制和添加 . )操作数编码:
大多数带有立即操作数的指令与寄存器/内存源版本的长度相同,加上用于编码立即数的字节 . Immediates是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操作码= 0x58 modrm disp8),但
vaddps zmm31, zmm30, [rsi + 16]
是11个字节:它必须使用disp32来编码+16
,因为它不是64的倍数 . 但xmm
寄存器的相同指令可以使用disp8
.有关完整详细信息,请参阅英特尔手册 .
最常见指令的特殊简短形式
为了节省代码大小,8086(以及后来的x86)为一些非常常见的指令提供了没有ModR / M字节的特殊编码 . 如果指令不是其中之一,则使用ModR / M字节
add / adc / sub / cmp / test /和/或/ xor / etc. AL / AX / EAX,其大小与寄存器大小相同 . 例如
and eax, imm32
(5字节)或and al,imm8
(2字节) . 但and eax, imm8
没有特殊的编码;仍然必须使用3字节and r/m32, imm8
编码 . 使用al
在使用8位数据时非常适合代码大小,特别是如果您担心partial-register stalls or false dependencies导致性能问题 .shift / rotate,计数为1:8086没有imm8旋转,只有
cl
或隐式1,所以有shl r/m32,1
等操作码,其中1
是隐式的 .使用
imm8
编码会影响性能:potential stalls on P6-family因为在执行之前它不会检查imm8是否为零 . 但是rol r32,1
的短形式是2 uops,而对于包括Skylake在内的Sandybridge家族的rol r32, imm8
(即使imm8是1)也是1 .rcl r32,1
缩写形式比使用imm8快得多 . (3 uops vs. 8 on Skylake) .并且 several where the register is encoded in the low 3 bits of the instruction byte ,有效地专用8个字节的操作码编码空间,使这些指令的寄存器操作数形式缩短1个字节 .
mov r8, imm8
:通用mov r/m8, imm8
编码为2个字节而不是3个字节 .mov r32, imm32:
mov r/m32, imm32
为5个字节而不是6个字节 . 有趣的事实:在x86-64中,短形式操作码的REX.W = 1版本是唯一可以使用64位立即数的指令 . 10字节mov r64, imm64
.r/m32
操作码的REX.W = 1版本仍然使用32位立即数(通常的符号扩展),因此mov rax, -1
最好以这种方式编码,占用7个字节而不是5个字节mov eax,-1
. (或者如果针对代码大小进行优化,请参阅Set all bits in CPU register to 1 efficiently . )push/pop register,
pop r/m32
编码的1字节与2字节 .push
/pop
段寄存器(FS / GS除外) . 虽然这些没有r / 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中 . 相反,它有its own instruction-set manual entry .xchg reg,reg
从未被编译器使用,并且是usually not faster than 3 mov instructions,所以如果我们将这7个操作码字节重新用于更有用的未来扩展,那将会很好 . (如果nop
被移动到另一个操作码,则为8 ...) . 当累加器为_2905991时,它在8086中更有用 .cbw
将AL签名扩展到AX是唯一(好)的方式,因为movsx
不存在 . 并且只有1操作数mul
/imul
可用 .xchg eax, r32
仍然适用于代码高尔夫,例如GCD in 8 bytes of x86 32-bit machine code . 另请参阅我的其他代码 - 高尔夫答案的各种代码大小的技巧(主要是以性能为代价;这是代码 - 高尔夫的重点) .I think this covers all the single-byte special cases of instructions that also have r/m32 encodings.
This answer isn't meant to be exhaustive . 我没有't talked about more recent instructions much, and there are lots of special cases for rare instructions. The rules for when a REX prefix or operand-size prefix are required are pretty straightforward. Here'是一些更通用的规则:
SSE1 / SSE3
ABCps
指令有2字节操作码(0F xx)SSE2整数/双精度指令通常有3字节操作码(66 0F xx或类似)
SSSE3 / SSE4.x指令有4字节操作码(3个强制性前缀)
VEX-coded instructions can use a 2-byte VEX prefix 如果SSE版本是SSE3或更早版本,并且第二个源寄存器不是"high"寄存器(xmm / ymm8-15) . 相同指令的XMM和YMM版本始终具有相同的大小 . (但是prefer xmm带有隐式零扩展而不是显式ymm,当你不关心或者想要将高半值归零时 . )
因此,我们可以使用“高”寄存器作为目标或第一个源,而不需要3字节VEX,但不能作为第二个源(整体第3个操作数) . 对于可交换操作,您可以通过将low8作为第二个源来保存大小 .
请注意,对于像vblendvps这样的4操作数指令,第4个操作数在
imm8
中编码 . 所以它仍然是第3个操作数(第二个源),而不是最后一个操作数,它影响所需的VEX前缀大小 . 但是blendvps
是SSE4.1,因此无论如何它总是需要一个3字节的VEX前缀来表示前缀字段的66.0F3A
编码 .通常,在使用汇编语言编程时,这不是从一条指令到下一条指令需要知道的 . 如果它很重要(例如,如果您尝试将某些特定代码放入受约束的空间中),则可以查看汇编程序的清单输出或反汇编列表 .
从我的6510汇编日开始,答案通常与操作数地址和偏移有关 . 对于6510,操作码总是1字节 . 地址总是两个字节 . 如果操作码需要一个地址,那么我知道总大小是三个字节 . 如果指定了两个地址,那么我知道总大小是5个字节 .
至于抵消,他们占用的空间取决于分支的长度 . 所以考虑一下:
如果“Foobar”偏移指向一个小于128字节的地址,那么操作数是一个字节 . 如果偏移指向超出该地址的地址,则需要完整地址 . 完整地址不再是偏移量,当然地址占用了两个字节 .
因此,在后一种情况下,可能不容易判断操作码操作数是否需要两个或三个字节 .
所以我想,有时候你可以分辨,有时则不那么明显 .