assembly 如何判断x86指令的长度?

0yycz8jy  于 2023-10-19  发布在  其他
关注(0)|答案(7)|浏览(115)

我在看汇编中的不同指令,我对不同操作数和操作码的长度是如何决定的感到困惑。
这是你应该从经验中了解的吗?或者有没有办法找出哪个操作数/操作符组合占用了多少字节?
例如:

push %ebp ; takes up one byte
mov %esp, %ebp ; takes up two bytes

所以问题是:
在看到一条给定的指令时,我如何推断出它的操作码需要多少字节?

hlswsv35

hlswsv351#

对于没有数据库的x86没有硬性规定,因为指令编码非常复杂(操作码本身可以从1到3个字节不等)。您可以参考Intel® 64 and IA-32 Architectures Software Developer’s Manual 2A文档(第2章:指令格式),查看指令及其操作数是如何编码的:

j5fpnvbx

j5fpnvbx2#

既然你对这个主题感兴趣,那我就给予你一个概述。一条x86指令最多由五个部分组成,最多15字节长:

prefixes opcode operand displacement immediate

可以生成超过15字节的编码,但CPU拒绝它们。除了操作码之外,所有五个部分都是可选的。你可以找到它们的长度如下:

  • 指令可以具有任何数量的“传统前缀”。它们是:f0lockf2repnef3repe2ecs36ss3eds26es64fs65gs66操作数大小覆盖和67地址大小覆盖。然而,一次只能识别f0f2f3中的一个,并且只能识别262e363e6465中的一个。如果每个组提供了多个前缀,CPU的行为就会不同。VEX和EVEX编码的指令可以仅具有段覆盖和地址大小覆盖传统前缀,因为其他前缀被包含在VEX和EVEX前缀下。
  • 在长模式下(并且仅在长模式下),指令可以在所有传统前缀之后立即具有 * 雷克斯前缀 *。雷克斯前缀是404f之一。在其他模式下,这些字节是指令,而不是前缀,解码器必须考虑到这一点。与传统前缀一样,VEX或EVEX编码的指令不能具有雷克斯前缀。
  • 字节c4c5可以引入一个 *VEX前缀 *,用于编码一些现代指令。在长模式下,它们总是这样做,但在其他模式下,你必须在之后检查字节:将其解释为modr/m字节,如果它编码r,r操作数对,则它是VEX前缀,否则它是leslds的操作码。以c4开头的VEX前缀是两个字节长,c5是三个字节长。VEX前缀还对VEX编码指令中省略的0f0f 380f 3a操作码前缀进行编码。请注意,通常,使用VEX前缀不是可选的。例如,pdep被编码为VEX.NDS.LZ.F2.0F38.W0 F5 /r(例如,pdep eax,eax,eaxc4 e2 7b f5 c0),但对应的传统指令f2 0f 38 f5 r/m32(例如,pdep eax,eaxf2 0f 38 f5 c0)无效。请注意,相同的操作码可以有VEX前缀,也可以没有VEX前缀,这两个前缀可能意味着不同的事情。例如,0f 77emms,但VEX.128.0F.WIG 77(即c5 f8 77)是vzeroupper
  • 字节62引入了一个 *EVEX前缀 *,用于编码AVX 512指令。与VEX前缀类似,需要检查接下来的几个字节,以将EVEX前缀与bound指令区分开。EVEX前缀始终为四个字节长,并像VEX前缀一样对部分操作码进行编码。

前缀之后是操作码。最初,操作码总是一个字节,但后来空间用完了,所以现在它要么是一个字节,要么是一个前缀为0f0f 380f 3a的字节。如果指令是VEX编码的,则不存在这些前缀。请注意,某些前缀可能会更改编码的指令。例如,操作码0f b8jmpe(进入IA-64模式),但f3 0f b8不是repe jmpe,而是popcnt
操作码和前缀决定编码哪个指令。从现在开始,一切都很顺利。根据指令的不同,后面可能会跟着一个 modr/m 字节。根据modr/m字节和地址覆盖前缀,后面可能会有一个 *sib字节 * 和一个、两个或四个 * 位移字节 *。最后,根据指令、操作数大小覆盖前缀和雷克斯前缀,可以跟随一个、两个、四个、六个或八个 * 立即字节 *。
这是我给予的关于堆栈溢出答案的描述。所以**TL;DR:**这真的很复杂。

9jyewag0

9jyewag03#

术语表:"opcode"是选择操作的指令的一部分,不包括操作数或修改操作的非强制性前缀(例如,操作数大小)。使用“opcode”来指代整个指令是不正确的,尽管有些人在谈论shellcode时经常这样做。
这是你应该从经验中了解的吗
有了看机器码的经验,或者特别是优化代码大小的经验,那么是的,你会开始记住你反复查找的东西,学会如何查看asm行,知道指令有多长,而不需要记住字节是什么。
操作数编码规则不依赖于操作码,所以你只需要记住操作码的长度,以及不使用ModR/M字节来编码操作数的特殊情况的短格式。然后分别记住操作数编码规则。
就我个人而言,我喜欢用x86机器码来回答像这样的代码高尔夫问题。(参见Tips for golfing in x86/x64 machine code)。我用NASM编写,计划/知道每条指令将有多长,并让汇编程序生成实际机器代码的十六进制转储作为列表。对于代码高尔夫有用的短指令,我不记得最近有任何时候在指令长度上出错,但我很幸运,我对我感兴趣或经常使用的细节(如x86指令集)有很好的记忆力。(我不得不尝试rorx,看看它有多长。
我不自己输入机器码字节;要用手做这个我得把每一条说明都查到手册里。x86没有用于PC相对寻址的短编码,因此在机器代码中查找/创建有用的常量(可以作为数据)并不是一件事,因此对于code-golf来说,记住指令编码的任何数字细节通常都没有用。
当优化性能时,在其他条件相同的情况下,通常越小越好,因此关注代码大小,特别是对齐绝对是性能的一部分。
或者有没有办法找出哪个操作数/运算符组合占用了多少字节?
这在手册中有很好的记录。除了一些特殊情况的1字节指令外,操作数编码(几乎)对所有指令都是相同的。
大多数x86指令的机器码编码遵循以下模式(英特尔在@Mehrdad的回答中给出了更好的图表版本):

[prefixes] opcode ModR/M [extra addressing-mode bytes] [immediate]

(没有显式操作数的指令没有ModR/M字节,只有操作码字节)。
x86操作码对于大多数普通指令是1字节,特别是自8086以来存在的指令。随后添加的说明(例如,如386中的bsfmovsx)经常使用带有0f转义字节的2字节操作码。如果你在SO上闲逛,你会看到很多关于8086的问题(特别是emu8086);这就是我知道8086上哪些指令不可用的主要原因。如果你宁愿直接记住哪些指令有2字节的操作码,而不想知道历史细节,那完全可以。或者每次都在手册中查找:P
例如0f b6 c0 movzx eax,al,因此0 F B6是mov r32, r/m8的操作码,C 0是ModR/M字节,将eax编码为目的地(/r字段= 0),将寄存器直接模式编码为源寄存器(前2位= 11),将al编码为源寄存器(/m字段= 0)。
我在所有示例中使用Intel语法(mnemonic dst, src1 [,src2, ...]),因为这与您在Intel和AMD手册中找到的语法相匹配。AFAIK,没有任何详细的使用AT&T语法的解释编码手册。我还使用了32位或64位的例子,甚至在谈论8086的时候。当然,8086只有16位真实的模式,但在64位模式中使用相同的操作码和编码(这是我们这些天关心的)。
Intel's instruction set ref. manual (SDM vol.2)具有1、2、3字节操作码的操作码Map(附录A.3),因此您可以看到操作码编码选择中的一些模式。或者对于任何给定的指令,请查看该手册中沿着的编码和完整描述。(也可以看到一些不错的在线摘录,每个指令一页,比如https://github.com/HJLebbink/asm-dude/wikihttp://felixcloutier.com/x86/。HJ Lebbink的页面标记了每个指令的引入时间,因此您可以看到8086用于add,或386用于新形式的移位,以及movzx)。

请注意,某些单操作数指令(如shlnot)使用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 /ADD,我们可以使用lea ecx, [rax + 1]作为复制和添加。

操作数编码:

大多数带有立即数操作数的指令的长度与寄存器/内存源版本相同,加上对立即数进行编码的字节。immediate是imm 8或imm 32,所以从-128到127的值更紧凑。(在16位模式下,它是imm 8或imm 16)。
ModR/M字节是寄存器直接寻址或最简单的无位移单寄存器寻址模式所需的全部内容。([esp]除外)。所以add eax, ecx是2个字节长,就像add eax, [ecx]一样。索引寻址模式(以及使用esp/rsp作为基址寄存器的模式)需要SIB(比例/索引/基址)字节。
寻址模式中的恒定位移需要在ModR/M +可选SIB之上额外增加1或4个字节(符号扩展disp 8或disp 32)。
带有disp 8的AVX 512 EVEX按向量宽度缩放disp 8,因此vaddps zmm31, zmm30, [rsi + 256]仅为7个字节(4字节EVX +操作码= 0x 58 + motion + disp 8),但vaddps zmm31, zmm30, [rsi + 16]为11个字节:它必须使用Disp 32来编码+16,因为它不是64的倍数。但是,具有xmm寄存器的相同指令可以使用disp8
请参阅英特尔手册了解完整详情。

最常见指令的特殊短格式

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

  • 添加/css/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可以很好地控制代码大小,特别是如果您已经避免或不担心partial-register stalls or false dependencies导致性能问题。
  • 移位/旋转,计数为1:8086没有imm 8旋转,只有cl或隐式1,所以有像shl r/m32,1这样的操作码,其中1是隐式的。

使用imm8编码会对性能产生影响:potential stalls on P6-family,因为它在执行之前不会检查imm 8是否为零。但是rol r32,1的缩写形式是2 uops,而对于rol r32, imm8为1(即使imm 8为1),适用于Sandybridge系列,包括Skylake。rcl r32,1短格式比imm 8快得多。(3 uops vs. 8 on Skylake)。
几个,其中寄存器被编码在指令字节的低3位中,有效地将8个字节的操作码编码空间专用于使这些指令的寄存器操作数形式缩短1个字节。

  • mov r8, imm8:2个字节,而不是一般mov r/m8, imm8编码的3个字节。
  • mov r32, imm32:5字节,而不是mov r/m32, imm32的6字节。有趣的事实:在x86-64中,短形式操作码的雷克斯.W=1版本是唯一可以使用64位立即数的指令。10字节mov r64, imm64。雷克斯.W=1版本的r/m32操作码仍然使用32位立即数(像往常一样进行符号扩展),因此mov rax, -1最好采用这种方式编码,占用7字节,而5字节mov eax,-1。(或者如果优化代码大小,请参阅Set all bits in CPU register to 1 efficiently
  • push / pop register,1字节与2字节用于pop r/m32编码。
  • push/pop段寄存器(FS/GS除外)。虽然没有r/m16编码。
  • inc r32/dec r32(仅限16/32位模式:0x 4X字节是x86-64中的雷克斯前缀,因此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。它有its own instruction-set manual entry

xchg reg,reg从来没有被编译器使用过,并且通常不会比3个mov指令快,所以如果我们能把这7个操作码字节拿回来,以便将来进行更有用的扩展,那就太好了。(如果nop被移动到不同的操作码,则为8)。在8086中,当累加器“更特殊”时,它更有用,例如。cbw将AL符号扩展到AX是唯一(好)的方法,因为movsx不存在。只有1个操作数mul/imul可用。
xchg eax, r32对于code-golf来说仍然很棒,例如:8字节x86 32位机器码的GCD。另请参阅我的其他代码高尔夫答案,了解各种代码大小技巧(主要是以牺牲性能为代价;这就是代码的要点-高尔夫)。

我认为这涵盖了所有具有r/m32编码的单字节指令的特殊情况。
这个答案并不意味着是详尽的。我没有过多地讨论最近的指令,对于罕见的指令,有很多特殊情况。何时需要雷克斯前缀或操作数大小前缀的规则非常简单。这里有一些更一般的规则:

  • SSE 1/SSE 3 ABCps指令具有2字节操作码(0 F xx)
  • SSE 2整数/双精度指令通常具有3字节操作码(66 0 F xx或类似)
  • SSSE 3/SSE4.x指令具有4字节操作码(3个强制前缀)
    VEX编码指令可以使用2字节VEX前缀,前提是SSE版本为SSE 3或更早版本,* 且 * 第二个源寄存器不是“高”寄存器(xmm/ymm 8 -15)。同一条指令的XMM和YMM版本的大小始终相同。(但是当你不关心或者想要高半部分归零时,prefer 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,但不作为第二个源(总的第三个操作数)。对于可交换操作,您可以通过将低8作为第二个源来保存大小。
注意,对于像vblendvps这样的4操作数指令,第4个操作数被编码为imm8。所以它仍然是第三个操作数(第二个源),* 不是 * 最后一个操作数,这会影响所需的VEX前缀大小。但是blendvps是SSE4.1,所以它总是需要一个3字节的VEX前缀来表示前缀字段的66.0F3A编码。

js81xvg6

js81xvg64#

操作码的长度是根据(至少)两个标准构建的

  • 操作码的频率(如果在程序中经常使用,则将其放在1字节上,如果可能的话)
  • 操作码运行所需的信息(如果您需要绝对地址,则代码不能编码在唯一字节上)

还有,

  • 从最初的8088到最新的Intel处理器(30年),已经创建了许多新的指令,并且有些指令虽然经常出现在程序中,但不能在单个字节上编码,因为全部256个值都被保留了。

除了在另一个答案中提供的链接(特别列出了代码的大小)之外,还可以参见processors history

lzfw57am

lzfw57am5#

通常,在用汇编语言编程时,这不是你需要从一条指令到下一条指令知道的东西。如果有关系(比如,如果您试图将某些特定的代码放入受约束的空间),您可以查看汇编器的清单输出或反汇编清单。

qlckcl4x

qlckcl4x6#

从我6510年的汇编生涯开始,答案通常与操作数地址和偏移量有关。6510的操作码始终为1字节。但它总是两个字节。如果操作码需要一个地址,那么我知道总大小是三个字节。如果指定了两个地址,那么我知道总大小是5个字节。
至于偏移,它们所占的空间取决于分支的长度。所以考虑一下:

bne FooBar

如果“Foobar”偏移量指向一个距离小于128字节的地址,则操作数是一个字节。如果偏移量指向一个超出此范围的地址,则需要完整的地址。一个完整的地址不再是一个偏移量,当然地址占用两个字节。
因此,在后一种情况下,可能不容易判断操作码+操作数是否需要两个或三个字节。
所以我想,有时候你可以告诉和其他时候它不是那么明显。

u1ehiz5o

u1ehiz5o7#

1.你可以参考英特尔开发手册来计算大小
1.如果你试图理解一个特定的可执行文件,可能会更容易:使用gdb或objdump -d
在gdb中,你可以disas /rs <location>,它将把源代码和汇编+机器代码交织在一起,

相关问题