繁体   English   中英

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

[英]Why NASM on Linux changes registers in x86_64 assembly

我是 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 位)。

先感谢您。

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

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

这是 NASM 的典型行为,请参阅 NASM 手册的第 3.3 节,其中[eax*2]的示例组装为[eax+eax]disp32 SIB字节1之后的disp32字段( [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, msg更改为lea esi, [REL msg] (RIP-relative 是一种寻址模式,因此它需要一个“寻址”,即方括号,以避免从该地址读取,我们使用lea只计算有效地址,但不访问)。
您可以使用指令DEFAULT REL来避免在每次内存访问中键入REL

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


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

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

NASM 仅在指令的较短形式具有相同架构效果时进行优化,因为mov eax,1隐式将 RAX 的高 32 位清零 请注意, add rax, 0add eax, 0不同add eax, 0因此 NASM 无法对其进行优化:仅像mov r32,... / mov r64,...xor eax,eax类的指令不依赖于旧指令可以通过这种方式优化 32 位和 64 位寄存器的值。

您可以使用nasm -O1 (默认为-O2 )禁用它,但请注意,在这种情况下,您将获得 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 01add r/m64, sign_extended_imm8 )与-O1 ,与
48 05 01000000add rax, sign_extended_imm32 )与nasm -O0
有趣的是,它仍然通过选择暗示 RAX 目的地的特殊情况操作码而不是采用 ModRM 字节来优化。 不幸的是-O1不优化立即尺寸mov (其中sign_extended_imm8是不可能的。)

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


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

如果您有非常大(或负)的数字,那么对于 32 位和 64 位操作数大小不相等的指令,即使您使用 NASM 而不是 YASM 进行组装,您也需要明确使用 32 位操作数大小,如果您想要尺寸/性能优势。 在 x86-64 中使用 32 位寄存器/指令的优点


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

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

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

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

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

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 ,英特尔语法模式下的objdump也是如此)作为链接时间常量但在汇编时未知的地址。

mov r64, imm32选择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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM