assembly 为什么Linux上的NASM更改x86_64汇编中的寄存器

ubby3x7f  于 2023-02-12  发布在  Linux
关注(0)|答案(2)|浏览(176)

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

kzmpq1sx

kzmpq1sx1#

在64位模式下,mov eax, 1将清除rax寄存器的高位部分(有关说明,请参见here),因此mov eax, 1在语义上等同于mov rax, 1
然而,前者保留了一个 * 雷克斯.W*(48h数字)前缀(指定x86-64引入的寄存器所需的字节),两条指令的操作码相同(0b8h后跟一个DWORD或QWORD)。
所以汇编程序会选择最短的形式。
这是NASM的典型行为,参见NASM手册的第3.3节,其中[eax*2]的示例被汇编为[eax+eax],以备用 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等手动组装它。

EDITPeter Cordes' answer表明,事实上,有一种方法可以告诉NASM not 使用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代码,但事实可能并非如此。
1 Scale Index Base 字节,用于对当时32位模式引入的新寻址模式进行编码。

7cwmlq89

7cwmlq892#

    • 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
(Also mov rax, strict 1与此等效,并且是禁用NASM优化后得到的结果。)

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

由于mov eax,1 implicitly zeros the upper 32 bits of RAX,NASM仅在指令的较短形式具有相同的体系结构效果时进行优化。请注意,add rax, 0add eax, 0不同,因此NASM无法优化:只有像mov r32,.../mov r64,...xor eax,eax这样不依赖于32位寄存器和64位寄存器的旧值的指令才能用这种方法优化(不过NASM不会优化xor rax,rax或其他置零习惯用法;you should always use 32-bit operand-size manually for xor-zeroing.)

    • 您可以使用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 01add r/m64, sign_extended_imm8)与-O1的对比
x一米27纳米1 x(x一米28纳米1 x)与x一米29纳米1 x。
有趣的是,它仍然通过选择暗示RAX目的地的特殊情况操作码而不是ModRM字节进行了优化。不幸的是,-O1没有优化mov的立即数大小(其中sign_extended_imm8是不可能的)。
如果您在某个地方需要特定的编码,请使用strict请求它,而不是禁用优化。

其他装配工

请注意,YASM不进行这种操作数大小优化,因此,如果您关心可以用其他与NASM兼容的汇编器汇编的代码中的代码大小(即使是间接地出于性能原因),最好在asm源代码中自己进行优化。
对于32位与64位操作数大小不相等的指令(如果数字非常大(或为负数)),如果希望获得大小/性能优势,则需要显式使用32位操作数大小(即使使用NASM而不是YASM进行汇编)。The advantages of using 32bit registers/instructions in x86-64

    • GAS将使用-Os**进行此优化,例如gcc -Wa,-Os -c foo.S,但遗憾的是,这不是默认值。(gcc -O选项不影响传递给as的选项,即使显式输入是.s.S。如果您不确定是否手动优化了任何内联asm,则使用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是一种纯粹的优化。

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

性能

在当前所有的x86 CPU上,它和7字节编码之间唯一的性能差异是代码大小,因此只有对齐和L1I $压力等间接影响是一个因素。在内部,它只是一个mov-immediate,因此这种优化也不会改变代码的微架构效果(当然除了代码大小/对齐/它在uop缓存中的压缩方式)。
10字节的mov r64, imm64编码对于代码大小来说更糟糕,如果常量实际上设置了任何高位,那么它在IntelSandybridge系列CPU上的uop缓存中的效率会非常低(使用微指令缓存中的2个条目,并且可能需要一个额外的周期来从微指令缓存中读取)。但是,如果常数在-2^31 .. +2^31范围内(有符号32位),即使它是使用64位立即数在x86机器码中编码的,它也同样有效地存储在内部,只使用一个微操作缓存条目(参见Agner Fog's microarch doc,* 表9.1. Sandybridge部分中微操作缓存中不同指令的大小 *)
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相关莱亚。(至少在Linux上,位置相关的代码可以位于低32位,因此除非您使用大型/巨型代码模型,否则任何时候您需要关心64位标签地址时,您还在编写PIC代码,其中应使用RIP相对莱亚,以避免绝对地址常量的文本重定位)。
例如,gcc和其他编译器会使用mov esi, msglea rsi, [rel msg],而不会使用mov rsi, msg
参见How to load address of function or label into register

相关问题