[英]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.W ( 48h
数字)前缀(指定 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, 0
与add 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 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 位操作数大小不相等的指令,即使您使用 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, msg
或lea rsi, [rel msg]
,从不使用mov rsi, msg
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.