Why NASM on Linux changes registers in x86_64 assembly

时间:2018-02-03 10:19:44

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

I am new to x86_64 assembly programming. I was writing simple "Hello World" program in x86_64 assembly. Below is my code, which runs perfectly fine.

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 

Now when I disassemble in gdb, it gives below output:

(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.

My question is why NASM behaves in such way? I know it changes instructions based on opcode, but I am not sure about same behaviour with registers.

Also does this behaviour affects functionality of executable?

I am using Ubuntu 16.04 (64 bit) installed in VMware on i5 processor.

Thank you in advance.

2 个答案:

答案 0 :(得分:3)

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

前者无需使用 REX.W mov rax, 1数字)前缀(指定x86-64引入的寄存器所需的字节),两个指令的操作码相同(48h后跟DWORD或QWORD) 所以汇编器继续前进并选择最短的形式。

这是NASM的典型行为,请参阅NASM手册的Section 3.3,其中0b8h的示例汇编为[eax*2]以免[eax+eax] SIB 字节 1 之后的字段(disp32仅可编码为[eax*2],其中汇编程序将[eax*2+disp32]设置为0)。

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

编辑Peter Cordes' answer表明实际上有一种方法可以告诉NASM 使用strict修饰符来优化指令。
db生成指令的10字节版本(mov rax, STRICT 1),而mov r64, imm64生成7字节版本(mov rax, STRICT DWORD 1,其中mov r64, imm32符号-extended 使用前)。

附注:使用RIP-relative addressing会更好,这可以避免64位立即常量(从而减少代码大小),并且是mandatory in MacOS(如果你关心的话)。 /> 将imm32更改为mov esi, msg(RIP相对是寻址模式,因此它需要&#34;寻址&#34;,方括号,以避免从中读取地址我们使用的lea esi, [REL msg]只计算有效地址但没有访问权限 您可以使用指令lea来避免在每次内存访问中键入DEFAULT REL

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

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

答案 1 :(得分:3)

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

NASM仅在较短形式的指令具有相同的架构效果时进行优化,因为mov eax,1 implicitly zeros the upper 32 bits of RAX

但是请注意YASM没有这样做,所以如果你关心代码大小(甚至间接是出于性能原因),最好自己在asm源中进行优化。

对于32和64位操作数大小不相等的指令,如果你有非常大(或负数)的数字,你需要明确使用32位操作数大小,即使你正在组装NASM而不是YASM,如果你想要32位操作数大小的大小/性能优势。  的 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是一种纯粹的优化。

在所有当前的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's microarch doc表9.1.Sandybridge部分中μop缓存中不同指令的大小

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

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&amp; T语法调用movabs,英特尔语法模式中objdump也是如此),地址是链路时间常数但是在集合时不知道。

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

YASM的行为通常是好的(尽管使用mov r32, imm32作为静态绝对地址,比如C编译器会更好)。默认的非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