首页 文章

指令长度

提问于
浏览
4

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

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

例如:

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

所以问题是:

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

5 回答

  • 2

    没有数据库的x86没有严格的规则,因为指令编码非常复杂(操作码本身可以在1到3个字节之间变化) . 您可以参考Intel® 64 and IA-32 Architectures Software Developer’s Manual 2A文档(第2章:指令格式)来查看指令及其操作数的编码方式:

    enter image description here

  • 1

    操作码的长度是考虑到(至少)两个标准

    • 操作码的频率(如果经常在程序中使用,则将其置于1个字节,如果可能的话)

    • 操作码所需的信息(如果需要绝对地址,则代码不能在唯一字节上编码)

    也,

    在最初的8088到最新的英特尔处理器(3年)之间的

    • 已经创建了许多新指令,并且一些虽然经常出现在程序中,但是不能在一个字节上编码,因为整个256个值是保留的 .

    除了另一个答案中提供的链接(具体列出代码的大小),另请参阅processors history .

  • 2

    术语:"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中来自英特尔的更好的图表版本):

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

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

    对于大多数常见指令,x86操作码是1字节,尤其是自8086以来已存在的指令 . 稍后添加的指令(例如386中的 bsfmovsx )通常使用带有 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/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 / 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, imm32mov 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 registerpop 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中,90 nop 也不是 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,当你不关心或者想要将高半值归零时 . )

    vpxor  ymm8,ymm8,ymm5    ; 2-byte VEX
    vpxor  ymm7,ymm7,ymm8    ; 3-byte VEX
    vpxor  ymm7,ymm8,ymm7    ; 2-byte VEX
    

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

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

  • 6

    通常,在使用汇编语言编程时,这不是从一条指令到下一条指令需要知道的 . 如果它很重要(例如,如果您尝试将某些特定代码放入受约束的空间中),则可以查看汇编程序的清单输出或反汇编列表 .

  • 1

    从我的6510汇编日开始,答案通常与操作数地址和偏移有关 . 对于6510,操作码总是1字节 . 地址总是两个字节 . 如果操作码需要一个地址,那么我知道总大小是三个字节 . 如果指定了两个地址,那么我知道总大小是5个字节 .

    至于抵消,他们占用的空间取决于分支的长度 . 所以考虑一下:

    bne FooBar
    

    如果“Foobar”偏移指向一个小于128字节的地址,那么操作数是一个字节 . 如果偏移指向超出该地址的地址,则需要完整地址 . 完整地址不再是偏移量,当然地址占用了两个字节 .

    因此,在后一种情况下,可能不容易判断操作码操作数是否需要两个或三个字节 .

    所以我想,有时候你可以分辨,有时则不那么明显 .

相关问题