【问题标题】:Why NASM on Linux changes registers in x86_64 assembly为什么 Linux 上的 NASM 会更改 x86_64 程序集中的寄存器
【发布时间】:2018-07-13 18:07:48
【问题描述】:

我是 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 

现在当我在 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.

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

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

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

提前谢谢你。

【问题讨论】:

  • 它的尺寸优化。 mov rax,1mov eax,1 具有完全相同的效果(因为在 x86-64 上写入像 eax 这样的 32 位寄存器变体会自动清除 64 位 rax 的高 32 位,这就是 AMD 设计 x86-64 的方式)。而eax 变体是微小立即数的 1B 短操作码(rax 具有完全相同的操作码,前面有 REX 前缀字节)。 - 但即使在这种情况下我也不认为它会这样做,让我有点惊讶(我只知道mov eax,1自动选择imm8操作码变体,除非你写mov eax, dword 1来强制它使用@ 987654332@一)。
  • 只是评论(不回答),因为我懒得检查是否有欺骗性。
  • @Ped7g:严格来说,这不是重复的 AFAIK;其他问题从不同的方向出现在同一个问题上,但我不记得看到有人问为什么 NASM 这样做。 (不过,我也没有搜索,因为这是一个足够好的问题,我并没有寻找关闭它的方法)
  • @Ped7g:“我只知道mov eax,1 自动选择imm8 操作码变体” - 实际上没有mov 的8 位扩展立即数形式,你错了。

标签: assembly nasm x86-64 micro-optimization shellcode


【解决方案1】:

TL:DR:你可以用

覆盖它
  • mov eax, 1(显式使用最佳操作数大小)
    b8 01 00 00 00
  • mov rax, strict dword 1(符号扩展的 32 位立即数)
    48 c7 c0 01 00 00 00
  • mov rax, strict qword 1(64 位立即数,如 AT&T 语法中的 movabs
    48 b8 01 00 00 00 00 00 00 00
    mov rax, strict 1 也与此等价,如果禁用 NASM 优化,您会得到。)

这是一个非常安全且有用的优化,类似于在编写 add eax, 1 时使用 8 位立即数而不是 32 位立即数。

NASM 仅在指令的较短形式具有相同的架构效果时进行优化,因为mov eax,1 implicitly zeros the upper 32 bits of RAX。请注意,add rax, 0add eax, 0 不同,因此 NASM 无法对其进行优化:仅像 mov r32,... / mov r64,...xor eax,eax 这样的指令不依赖于 32 位与 64 位的旧值寄存器可以这样优化。

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

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

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


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

如果您有非常大(或负)的数字,则对于 32 位和 64 位操作数大小不相等的指令,您需要明确使用 32 位操作数大小,即使您使用 NASM 而不是YASM,如果您想要尺寸/性能优势。 The advantages of using 32bit registers/instructions in x86-64


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

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

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

10 字节的mov r64, imm64 编码对于代码大小来说更加糟糕。如果该常量实际上设置了任何高位,则它在英特尔 Sandybridge 系列 CPU 上的 uop 缓存中效率低下(使用 uop 缓存中的 2 个条目,并且可能需要一个额外的周期来从 uop 缓存中读取)。但是,如果常量在 -2^31 .. +2^31 范围内(有符号 32 位),则它在内部存储的效率同样高,仅使用单个 uop-cache 条目,即使它是在 x86 机器中编码的使用 64 位立即数的代码。 (参见Agner Fog's microarch doc表 9.1。Sandybridge 部分中 μop 缓存中不同指令的大小

来自How many ways to set a register to zero?您可以强制使用三种编码中的任何一种:

mov    eax, 1                ; 5 bytes to encode (B8 imm32)
mov    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
mov    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.

请注意,NASM 使用 10 字节编码(AT&T 语法称为 movabs,在 Intel 语法模式下也是 objdump)作为链接时间常数但在汇编时未知的地址。

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

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

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

gcc 和其他编译器会使用 mov esi, msglea rsi, [rel msg],而不是 mov rsi, msg
How to load address of function or label into register

【讨论】:

  • 有趣的事实:GAS 将使用 as -Os 进行此优化,例如gcc -Wa,-Os,但不幸的是,这不是默认设置。
【解决方案2】:

在 64 位模式下,mov eax, 1 将清除rax 寄存器的上半部分(请参阅here 以获得解释),因此mov eax, 1 在语义上等同于mov rax, 1

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

这是 NASM 的典型行为,请参阅 NASM 手册的Section 3.3,其中[eax*2] 的示例组装为[eax+eax],以节省SIB 字节后的disp32 字段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' answer 表明事实上,有一种方法可以告诉 NASM 使用 strict 修饰符优化指令。
mov rax, STRICT 1 产生 10 字节版本的指令 (mov r64, imm64),而 mov rax, STRICT DWORD 1 产生 7 字节版本(mov r64, imm32 其中imm32 在使用前是符号扩展)。


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

我的印象是 Mach-O 文件格式需要 PIC 代码,但 this may not be the case


1Scale Index Base 字节,用于编码当时采用 32 位模式引入的新寻址模式。

【讨论】:

  • mov rax, strict dword 1,看我的回答。
  • 我看到帖子说mov rsi, msg 在 OS X 上为类似的 hello-world 可执行文件工作。 OS X 将可执行文件映射到 4GiB 以上,因此您需要 64 位常量作为地址,但它显然 不需要 需要 PIC 可执行文件,或者它支持文本重定位以在之后修复 64 位绝对地址ASLR。
  • @PeterCordes,感谢strict 修饰符,我错过了。关于 MacOS,我的印象是 Mach-o 需要 PIC(并且不支持 64 位修复),但我从未尝试过 Mac,所以我逐字引用您的评论 :)
  • 我以前也是这么想的,所以也许你是从我写的东西中得到的印象。我可能将需要 64 位地址支持与需要 PIC 混为一谈,因为除了需要 PIC / ASLR 之外,您为什么要放弃 32 位绝对地址的效率?但是,是的,Linux 对 PIC 代码进行了 64 位修复(这也让我感到惊讶),所以也许 OS X 也这样做了。 IDK 支持这一点有什么意义。我猜它可以让你制作绝对跳转表,所以也许作为支持它的数据的副作用,它也适用于立即数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-06-27
  • 2022-10-14
  • 1970-01-01
  • 2018-06-21
相关资源
最近更新 更多