为什么Linux上的NASM在x86_64汇编中会改变寄存器

4

我是一名新手,想学习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位)。
提前致谢。

1
这是大小优化。mov rax,1mov eax,1完全相同(因为在x86-64上,写入32位寄存器变体如eax将自动清除64位rax的上32位,这就是AMD设计x86-64的方式)。而且,eax变体是用于小立即数的1B短操作码(rax具有与REX前缀字节相同的操作码)。- 但我没有想到即使在这种情况下也会这样做,这让我有点惊讶(我只知道mov eax,1会自动选择imm8操作码变体,除非您编写mov eax,dword 1以强制使用imm32操作码变体)。 - Ped7g
这只是一个注释(不是答案),因为我太懒了,没有检查重复。 - Ped7g
@Ped7g:据我所知,这并不是严格意义上的重复;其他问题从不同的角度涉及了相同的问题,但我不记得有人问过为什么NASM这样做。 (虽然我没有搜索,因为这是一个足够好的问题,我不想关闭它) - Peter Cordes
2
@Ped7g:“我只知道'mov eax,1'会自动选择'imm8'操作码变体” - 实际上,mov 没有 8 位扩展立即形式,你错了。 - ecm
一个早期的重复问题,其中答案展示了一些反汇编示例以说明差异:从64位nasm代码接收32位寄存器 - Peter Cordes
2个回答

8
在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],以节省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的答案表明,实际上有一种方法可以告诉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相对寻址是一种寻址模式,因此需要一个“寻址”,即方括号,以避免从该地址读取,我们使用lea仅计算有效地址但不访问)。
您可以使用指令DEFAULT REL来避免在每个内存访问中键入REL

我曾经认为Mach-O文件格式需要PIC代码,但可能并非如此


1比例指数基地址字节,用于编码32位模式中引入的新寻址模式。


1
请查看我的答案:mov rax,严格 dword 1 - Peter Cordes
我看到过一些帖子说,在类似的hello-world可执行文件中,mov rsi,msg在OS X上可以工作。 OS X将可执行文件映射到4GiB以上,因此您需要64位常量来表示地址,但显然不需要 PIC可执行文件,或者它支持文本重定位以修复ASLR后的64位绝对地址。 - Peter Cordes
@PeterCordes,感谢您提供了strict修饰符,我一直缺少它。关于MacOS,我一直认为Mach-o需要PIC(不支持64位的fixups),但我从未在Mac上进行过实验,所以我会逐字引用您的评论 :) - Margaret Bloom
2
这是我以前的想法,也许你从我写的某些东西中得到了这种印象。我可能将需要64位地址支持与需要PIC混淆在一起,因为除了要求PIC / ASLR外,为什么要放弃32位绝对地址的效率呢?但是,是的,Linux会为PIC代码进行64位修复(这也让我感到惊讶),所以也许OS X也是如此。我不知道支持它的意义是什么。我猜它可以让你制作绝对跳转表,所以也许作为支持数据的副作用,它也适用于立即数。 - Peter Cordes

6

简述: 您可以使用以下方式覆盖此设置

  • 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隐式清零RAX的上32位。请注意,add rax,0add eax,0不同,因此NASM无法进行优化:只有像mov r32,... / mov r64,...xor eax,eax这样不依赖于32位vs. 64位寄存器旧值的指令才能以这种方式进行优化。(但NASM不会优化xor rax,rax或其他清零习惯用语; 您应始终手动使用32位操作数大小进行xor-zeroing。)
您可以使用nasm -O1来禁用它(默认为-Ox多次通行),但请注意,这种情况下您将获得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是使用-O1
48 83 C0 01 (add r/m64, sign_extended_imm8),而使用nasm -O0的则是
48 05 01000000 (add rax, sign_extended_imm32)。
有趣的是,它仍然通过选择暗示RAX目标的特殊情况操作码进行了优化,而不是采用ModRM字节。不幸的是,-O1没有针对mov(其中sign_extended_imm8不可能)优化立即数大小。

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


其他汇编器

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

对于那些如果有非常大的(或负数)数字,32位和64位操作数大小不等效的指令,即使您使用NASM而不是YASM进行汇编,如果您想要获得大小/性能优势,仍需要显式使用32位操作数大小。 在x86-64中使用32位寄存器/指令的优点

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


适合32位的常量,可以在64位中扩展为零或符号扩展

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

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

性能

在所有当前的x86 CPU上,7字节编码和这种编码之间唯一的性能差异是代码大小,因此只有间接的影响如对齐和L1I$压力才是一个因素。内部只是一个mov-immediate,所以这个优化不会改变你的代码的微架构效果(当然除了代码大小/对齐/它如何打包在uop缓存中)。
10字节的mov r64,imm64编码对于代码大小来说更糟糕。如果常量实际上设置了其高位中的任何一位,则在Intel Sandybridge系列CPU上它会在uop缓存中产生额外的低效率(使用2个uop缓存条目,并可能需要额外的周期从uop缓存中读取)。但如果该常量在-2^31..+2^31范围内(有符号32位),即使它使用64位立即数编码在x86机器代码中,也可以使用单个uop缓存条目进行内部存储,而且效率相同。(参见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,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
请参见如何将函数或标签的地址加载到寄存器中

1
有趣的事实:GAS将使用as -Os进行此优化,例如gcc -Wa,-Os,但不幸的是这不是默认设置。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接