为什么 Linux 上的 NASM 会更改 x86_64 程序集中的寄存器

Sha*_*avi 4 assembly x86-64 nasm micro-optimization shellcode

我是 x86_64 汇编编程的新手。我正在用 x86_64 程序集编写简单的“Hello World”程序。下面是我的代码,它运行得很好。

global _start

section .data

    msg: db "Hello to the world of SLAE64", 0x0a
    mlen equ $-msg

section .text
    _start:
            mov rax, 1
            mov rdi, 1
            mov rsi, msg
            mov rdx, mlen
            syscall

            mov rax, 60
            mov rdi, 4
            syscall 
Run Code Online (Sandbox Code Playgroud)

现在,当我在 gdb 中反汇编时,它会给出以下输出:

(gdb) disas
Dump of assembler code for function _start:
=> 0x00000000004000b0 <+0>:     mov    eax,0x1
   0x00000000004000b5 <+5>:     mov    edi,0x1
   0x00000000004000ba <+10>:    movabs rsi,0x6000d8
   0x00000000004000c4 <+20>:    mov    edx,0x1d
   0x00000000004000c9 <+25>:    syscall
   0x00000000004000cb <+27>:    mov    eax,0x3c
   0x00000000004000d0 <+32>:    mov    edi,0x4
   0x00000000004000d5 <+37>:    syscall
End of assembler dump.
Run Code Online (Sandbox Code Playgroud)

我的问题是为什么 NASM 会这样行事?我知道它会根据操作码更改指令,但我不确定寄存器的行为是否相同。

这种行为也会影响可执行文件的功能吗?

我在 i5 处理器上使用 VMware 中安装的 Ubuntu 16.04(64 位)。

先感谢您。

Mar*_*oom 6

在 64 位模式下mov eax, 1将清除rax寄存器的上半部分(参见此处的解释),因此mov eax, 1在语义上等同于mov rax, 1.

然而,前者保留REX.W48h数字)前缀(指定 x86-64 引入的寄存器所必需的字节),两条指令的操作码相同(0b8h后跟 DWORD 或 QWORD)。
所以汇编器继续并选择最短的形式。

这是NASM的典型行为,参见第3.3节的NASM手册,其中的例子[eax*2]被组装为[eax+eax]不伤害disp32后场SIB字节1[eax*2]仅可编码为[eax*2+disp32]其中汇编集disp32为0)。

我无法迫使NASM发出真正的mov rax, 1指令(即48 B8 01 00 00 00 00 00 00 00连的前缀来指示)o64
如果需要真正mov rax, 1的(这不是您的情况),则必须使用db类似的方法手动组装它。

编辑Peter Cordes 的回答表明,事实上,有一种方法可以告诉 NASM不要使用strict修饰符优化指令。
mov rax, STRICT 1产生指令的10字节版本(mov r64, imm64),而mov rax, STRICT DWORD 1产生一个7字节的版本(mov r64, imm32这里imm32符号扩展使用之前)。


旁注:最好使用RIP 相对寻址,这样可以避免 64 位立即数(从而减少代码大小),并且在 MacOS 中强制性的(以防万一)。
更改mov esi, msglea esi, [REL msg](RIP-relative 是一种寻址模式,因此它需要一个“寻址”,即方括号,以避免从我们使用的lea仅计算有效地址但不访问的地址中读取)。
您可以使用该指令DEFAULT REL来避免REL在每次内存访问中输入。

我的印象是Mach-O文件格式需要 PIC 代码,但情况可能并非如此


1个规模指数基地字节,用于编码所述新的寻址模式引回,然后用32位模式。

  • 我以前也是这么想的,所以也许你从我写的东西中得到了这样的印象。我可能将需要 64 位地址支持与需要 PIC 混为一谈,因为除了需要 PIC / ASLR 之外,为什么要放弃 32 位绝对地址的效率呢?但是,是的,Linux 对 PIC 代码进行了 64 位修复(这也让我感到惊讶),所以也许 OS X 也会做同样的事情。我不知道支持这一点的意义是什么。我猜它可以让你创建绝对跳转表,所以也许作为支持数据的副作用,它也适用于立即数。 (2认同)

Pet*_*des 6

TL:DR:您可以使用以下命令覆盖它

\n
    \n
  • mov eax, 1(明确使用最佳操作数大小)
    \nb8 01 00 00 00
  • \n
  • mov rax, strict dword 1(符号扩展 32 位立即数)
    \n48 c7 c0 01 00 00 00
  • \n
  • mov rax, strict qword 1(64 位立即数,如movabsAT&T 语法)
    \n 48 b8 01 00 00 00 00 00 00 00
    \n(也mov rax, strict 1相当于此,并且是禁用 NASM 优化后得到的结果。)
  • \n
\n
\n

这是一种完全安全且有用的优化,类似于在写入时使用 8 位立即数而不是 32 位立即数add eax, 1

\n

NASM 仅当指令的较短形式具有相同的架构效果时才进行优化,因为mov eax,1隐式地将 RAX 的高 32 位清零。请注意,这与NASM 无法优化它add rax, 0不同:只有像/或等不依赖于 32 与 64 位寄存器的旧值的指令才能通过这种方式进行优化。(NASM 不会优化或其他归零习惯;您应该始终手动使用 32 位操作数大小进行异或归零。)add eax, 0mov r32,...mov r64,...xor eax,eaxxor rax,rax

\n
\n

您可以使用nasm -O1(默认为-Oxmultipass)禁用它,但请注意,在这种情况下您将获得 10 字节mov rax, strict qword 1:显然 NASM 并不打算真正用于低于正常优化的情况。没有设置它将使用不会改变反汇编的最短编码(例如 7-byte mov rax, sign_extended_imm32= mov rax, strict dword 1)。

\n

-O0和之间的区别-O1在于 imm8 与 imm32,例如add rax, 1\
n 48 83 C0 01( add r/m64, sign_extended_imm8) 与-O1, vs.
\n 48 05 01000000( add rax, sign_extended_imm32) 与nasm -O0
\n有趣的是,它仍然通过选择暗示 RAX 目的地而不是采用 ModRM 字节的特殊情况操作码来进行优化。不幸的是-O1,并没有优化立即大小mov(其中sign_extended_imm8是不可能的。)

\n

strict如果您在某处需要特定的编码,请使用而不是禁用优化来请求。

\n
\n

其他汇编器

\n

请注意,YASM 不会执行此操作数大小优化,因此如果您关心代码中的代码大小(甚至是间接出于性能原因),最好在 asm 源代码中自行进行优化使用其他 NASM 兼容的汇编器进行组装。

\n

对于如果数字非常大(或负)则 32 位和 64 位操作数大小不相等的指令,即使您使用 NASM 而不是 YASM 进行汇编,也需要显式使用 32 位操作数大小,如果您想要尺寸/性能优势。\n在 x86-64 中使用 32 位寄存器/指令的优势

\n

GAS 将使用 as 进行此优化-Os,例如gcc -Wa,-Os -c foo.S,但不幸的是这不是默认设置。(gcc -O选项不会影响传递给 的选项as,即使显式输入是 a.s.Sgcc -O3 -Wa,-Os foo.c如果您有任何内联汇编,您不确定是否已手动优化,假设它不是手动优化的,那么使用是一个好主意出于对齐原因使用更长的指令。)

\n
\n

适合 32 位零或符号扩展的 64 位常量

\n

对于没有设置高位的 32 位常量,将它们扩展到 64 位的零或符号会给出相同的结果。因此,汇编mov rax, 1为 5 字节mov r32, imm32(隐式零扩展为 64 位)而不是 7 字节是纯粹的优化mov r/m64, sign_extended_imm32

\n

(有关x86-64 允许的形式的更多详细信息,请参阅x86-64 中 movq 和 movabsq 之间的区别mov;AT&T 语法对 10 字节立即数形式有一个特殊名称,但 NASM 没有。)

\n

表现

\n

在所有当前的 x86 CPU 上,它与 7 字节编码之间的唯一性能差异是代码大小,因此只有对齐和 L1I$ 压力等间接影响是一个因素。在内部,它只是一个 mov-immediate,因此这种优化也不会改变代码的微架构效果(当然除了代码大小/对齐/它在 uop 缓存中的打包方式)。

\n

10 字节mov r64, imm64编码的代码大小更差。如果该常量实际上设置了任何高位,那么它在 Intel Sandybridge 系列 CPU 上的 uop 缓存中的效率会特别低(在 uop 缓存中使用 2 个条目,并且可能需要一个额外的周期来从 uop 缓存中读取)。但是,如果常量在 -2^31 .. +2^31 范围(有符号 32 位)内,则它在内部存储的效率也一样,仅使用单个 uop 缓存条目,即使它是在使用 64 位立即数的 x86 机器代码。(参见Agner Fog 的 microarch 文档表 9.1. Sandybridge 部分的 \xce\xbcop 缓存中不同指令的大小

\n

有多少种方法将寄存器设置为零?您可以强制使用三种编码中的任何一种:

\n
mov    eax, 1                ; 5 bytes to encode (B8 imm32)\nmov    rax, strict dword 1   ; 7 bytes: REX mov r/m64, sign-extended-imm32.    NASM optimizes mov rax,1 to the 5B version, but dword or strict dword stops it for some reason\nmov    rax, strict qword 1   ; 10 bytes to encode (REX B8 imm64).  movabs mnemonic for AT&T.  Normally assemblers choose smaller encodings if the operand fits, but strict qword forces the imm64.\n
Run Code Online (Sandbox Code Playgroud)\n
\n

请注意,NASM 使用 10 字节编码(AT&T 语法称为movabs,因此objdump),该地址是链接时间常量,但在汇编时未知。

\n

YASM 选择mov r64, imm32,即它假定标签地址为 32 位的代码模型,除非您使用mov rsi, strict qword msg

\n

YASM\ 的行为通常很好(尽管mov r32, imm32像 C 编译器那样用于静态绝对地址会更好)。默认的非 PIC 代码模型将所有静态代码/数据放入低 2GiB 的虚拟地址空间中,因此零或符号扩展的 32 位常量可以保存地址。

\n

如果您需要 64 位标签地址,通常应该使用它lea r64, [rel address]来执行 RIP 相关的 LEA。(至少在 Linux 上,位置相关代码可以进入低 32 位,因此除非您使用大型/巨大代码模型,否则任何时候您需要关心 64 位标签地址,您也会您应该使用 RIP 相对 LEA 以避免需要绝对地址常量的文本重定位的 PIC 代码。

\n

gcc 和其他编译器会使用mov esi, msg, 或lea rsi, [rel msg], nevermov rsi, msg
\n请参阅如何将函数或标签的地址加载到寄存器中

\n


归档时间:

查看次数:

1274 次

最近记录:

5 年,7 月 前