我在研究汇编语言中的不同指令,但对于如何确定不同操作数和操作码的长度感到困惑。
这是一些需要通过经验来了解的东西吗?还是有办法找出哪种操作数/运算符组合占用了多少字节?
例如:
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个字节不等)。 您可以查阅Intel® 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
到4f
中的一个。在其他模式下,这些字节是指令,而不是前缀,您的解码器必须考虑到这一点。与传统前缀一样,VEX或EVEX编码指令不能具有REX前缀。c4
和c5
可以引入用于编码某些现代指令的VEX前缀。在长模式下,它们始终这样做,但在其他模式下,您必须检查其后面的字节:将其解释为modr/m字节,如果它编码了一个r,r
操作数对,则它是一个VEX前缀,否则它是les
或lds
的操作码。以c4
开头的VEX前缀为两个字节长,以c5
开头的VEX前缀为三个字节长。 VEX前缀还编码省略了VEX编码指令中的0f
,0f 38
和0f 3a
操作码前缀。请注意,通常使用VEX前缀不是可选的。例如,pdep
被编码为VEX.NDS.LZ.F2.0F38.W0 F5 /r
(例如c4 e2 7b f5 c0
表示pdep eax,eax,eax
),但相应的传统指令f2 0f 38 f5 r/m32
(例如f2 0f 38 f5 c0
表示pdep eax,eax
)无效。请注意,相同的操作码可以存在具有VEX前缀和不带VEX前缀的情况,两者可能意味着不同的事情。例如,0f 77
是emms
,但VEX.128.0F.WIG 77
(即c5 f8 77
)是vzer
在前缀后面,接下来出现的是操作码。原本,操作码总是一个字节,但当它们用尽了空间,现在它可以是单个字节或由0f
、0f 38
或0f 3a
前缀修饰的单个字节。如果指令被VEX编码,则这些前缀将不存在。请注意,某些前缀可能会更改所编码的指令。例如,操作码0f b8
是jmpe
(进入IA-64模式),但f3 0f b8
不是repe jmpe
,而是popcnt
。
操作码和前缀决定了所编码的指令。从这里开始,大多数情况都很顺利。根据指令的不同,可能会跟随一个modr/m字节。根据modr/m字节和地址覆盖前缀,可能会跟随一个sib byte和一个、两个或四个位移字节。最后,根据指令、操作数大小覆盖前缀和REX前缀,可能会跟随一个、两个、四个、六个或八个立即字节。
在Stack Overflow答案的范围内,这就是我能给出的描述。因此TL;DR:它真的很复杂。
通过查看机器代码或特别是优化代码大小的经验,您将开始记住重复查找的内容,并学习如何查看汇编行并知道指令的长度,而无需记忆字节是什么。这是你应该从经验中了解的东西吗?
rorx
以查看它的长度。)[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 是 ModR/M 字节,将 eax 编码为目标寄存器(/r
字段 = 0),将源设置为寄存器直接模式(前两位为 11)并将其设置为 al
(/m
字段 = 0)。我在所有示例中都使用英特尔语法(mnemonic dst,src1 [,src2,...]
),因为这与英特尔和AMD的手册匹配。据我所知,没有任何使用AT&T语法的详细指令编码手册。即使是在谈论8086存在的内容时,我也使用32或64位示例。当然,8086只有16位真实模式,但相同的操作码和编码在64位模式下使用(这是我们现在关心的)。
Intel的指令集参考手册(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]
作为复制并添加的替代方法。)
大多数带立即数操作数的指令与寄存器/内存源版本长度相同,加上编码立即数所需的字节。 立即数可以是imm8或imm32,因此-128..127范围内的值更紧凑。 (在16位模式下,它可以是imm8或imm16)。
对于直接寄存器或没有位移的最简单的单寄存器寻址模式(除了[esp]
),只需要ModR/M字节。 因此,add eax,ecx
与add eax,[ecx]
一样长,均为2个字节。 需要使用SIB(比例/索引/基础)字节来进行索引寻址模式(以及以esp
/ rsp
为基本寄存器的模式)。
寻址模式中的常量位移需要额外的1或4个字节(扩展符号的disp8或disp32),加上ModR/M +可选SIB。
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字节。
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,1
这样隐含着 1
的操作。
使用 imm8
编码会对性能产生影响:P6 家族可能导致的延迟, 因为它直到执行时才检查 imm8 是否为零。但是在 Sandybridge-family 和 Skylake 等处理器上,rol r32,1
短格式需要 2 个微操作,而 rol r32, imm8
(即使 imm8 为 1)需要 1 个微操作。使用 rcl r32,1
的短格式远比使用 imm8 更快(在 Skylake 上为 3 个微操作与 8 个微操作)。
mov r8, imm8
: 通用的mov r/m8, imm8
编码需要3个字节,而使用mov r8, imm8
只需要2个字节。mov r32, imm32
: 使用mov r/m32, imm32
编码需要6个字节,而使用mov r32, imm32
只需要5个字节。有趣的是,在x86-64中,短格式操作码的REX.W=1版本是唯一可以使用64位立即数的指令。10个字节的mov r64, imm64
。使用r/m32
操作码的REX.W=1版本仍然使用32位立即数(像往常一样进行符号扩展),因此最好以这种方式对其进行编码mov rax, -1
,占用7个字节,而不是5个字节的mov eax,-1
。(或者如果优化代码大小,请参见高效地将CPU寄存器中的所有位设置为1。)push
/pop
register:使用pop r/m32
编码需要2个字节,而使用push
/pop
register只需要1个字节。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中,90 nop
不再是xchg eax,eax
,因为这会将EAX零扩展为RAX。相反,它有自己的指令集手册条目。
xchg reg,reg
从未被编译器使用,并且通常不比3个mov
指令更快,因此如果我们可以将这7个操作码字节用于更有用的未来扩展,那将是很好的(或者如果nop
移动到不同的操作码,则为8个字节...)。在8086中,当累加器“更特殊”时,它更有用,例如cbw
将AL符号扩展为AX是唯一(好的)方法,因为movsx
不存在。只有1个操作数的mul
/ imul
可用。
xchg eax, r32
在编程竞赛中仍然很出色,例如 在x86 32位机器代码中用8个字节求最大公约数。此外,还可以查看我的其他编程竞赛答案,其中包含各种代码大小技巧(大多以性能为代价;这是编程竞赛的目的)。
我认为这涵盖了所有单字节特殊情况的指令,同时还具有r/m32
编码。
ABCps
指令具有2字节的操作码(0F xx)VEX编码指令可以使用2字节的VEX前缀,如果SSE版本是SSE3或更早,并且第二个源寄存器不是“高”寄存器(xmm/ymm8-15)。相同指令的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
因此,我们可以使用“高”寄存器作为目标或第一个源,而无需使用3字节的VEX,但不能将其用作第2个源(总共第3个操作数)。对于可交换的操作,通过将low8作为第2个源,可以节省大小。
请注意,对于像{{link1:vblendvps
}}这样的4操作数指令,第4个操作数编码在imm8
中。 因此,它仍然是第3个操作数(第2个源),而不是最后一个操作数,影响所需的VEX前缀大小。 但是,blendvps
是SSE4.1,因此它始终需要一个3字节的VEX前缀来表示前缀字段的66.0F3A
编码。
从我的6510汇编时代开始,答案通常涉及操作数地址和偏移量。 6510的操作码始终为1个字节。 地址始终为两个字节。 如果操作码需要一个地址,则我知道总大小为三个字节。 如果指定了两个地址,则我知道总大小为5个字节。
至于偏移量,它们占用的空间取决于分支的长度。 所以考虑这个:
bne FooBar
objdump -d
。disas /rs <location>
命令,它会将源代码与汇编代码和机器码交叉显示。nasm -f elf64 foo.asm && objdump -drwC -Mintel foo.o
。我有一个shell脚本(在一个SO答案中发布的asm-link
),它可以执行这个操作;它实际上还使用ld
将其链接到一个静态可执行文件中,这对于构建单文件程序作为实验非常方便,并且即使我没有包含代码来正常退出,也可以在GDB中逐步执行它们。所以使用asm-link -dn foo.asm
来使用NASM进行汇编,并使用objdump进行反汇编。 - undefinedllvm-objdump -d
是多架构的,如果你需要的话,这与 GNU Binutils 不同,后者只支持在配置时间选择的一个架构。GDB 是交互式的,这对于将指令放入文本文件中进行汇编+反汇编,或者查看你正在处理的函数中指令长度来说是一个不足之处。 - undefinedcat > foo.asm
/ 输入一些内容并按下控制-D / asm-link -dn foo.asm
总共只需要大约10秒钟,并且不会占用太多终端历史记录空间。(特别是因为我可以使用alt+.在下一个命令行中调用foo.asm。) - undefinedecho "disas schedule" | gdb vmlinux
。但是没错,当gdb需要加载源代码时,速度会慢很多。 - undefined