Почему NASM в Linux меняет регистры в сборке x86_64
Я новичок в программировании сборки x86_64. Я писал простую программу "Hello World" в сборке x86_64. Ниже мой код, который прекрасно работает.
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 ведет себя таким образом? Я знаю, что это меняет инструкции на основе кода операции, но я не уверен в том же поведении с регистрами.
Также это поведение влияет на функциональность исполняемого файла?
Я использую Ubuntu 16.04 (64 бит), установленную в VMware на процессоре i5.
Заранее спасибо.
2 ответа
В 64-битном режиме mov eax, 1
очистит верхнюю часть rax
зарегистрироваться (см. здесь для объяснения) таким образом mov eax, 1
семантически эквивалентно mov rax, 1
,
Бывший однако запасной REX.W (48h
численно) префикс (байт, необходимый для указания регистров, введенных с x86-64), код операции одинаков для обеих инструкций (0b8h
сопровождаемый DWORD или QWORD).
Таким образом, ассемблер идет вперед и подбирает самую короткую форму.
Это типичное поведение NASM, см. Раздел 3.3 руководства NASM, где приведен пример [eax*2]
собран как [eax+eax]
пощадить disp32
поле после байта SIB 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
и тому подобное.
РЕДАКТИРОВАТЬ: ответ Питера Кордеса показывает, что, на самом деле, есть способ сказать 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-битном режиме.
Это совершенно безопасная и полезная оптимизация, очень похожая на использование 8-битного немедленного вместо 32-битного немедленного при написании add eax, 1
,
NASM оптимизирует только тогда, когда более короткая форма инструкции имеет идентичный архитектурный эффект, потому что mov eax,1
неявно обнуляет верхние 32 бита RAX.
Но обратите внимание, что YASM этого не делает, поэтому неплохо бы провести оптимизацию самостоятельно в исходном коде asm, если вы заботитесь о размере кода (даже косвенно по соображениям производительности).
Для инструкций, в которых 32- и 64-битный размер операнда не будет эквивалентен, если у вас очень большие (или отрицательные) числа, вам нужно явно использовать 32-битный размер операнда, даже если вы собираете с NASM вместо YASM, если вам нужно преимущество размера / производительности 32-битного размера операнда. Преимущества использования 32-битных регистров / инструкций в x86-64
Для 32-битных констант, для которых не установлен высокий бит, нулевое значение или знак, расширяющие их до 64 бит, дают идентичный результат. Таким образом, это чистая оптимизация для сборки mov rax, 1
до 5 байт mov r32, imm32
(с неявным расширением нуля до 64 бит) вместо 7 байт mov r/m64, sign_extended_imm32
,
На всех современных процессорах x86 единственной разницей в производительности между этим и 7-байтовым кодированием является размер кода, поэтому фактором являются только косвенные эффекты, такие как выравнивание и давление L1I$. Внутренне это просто mov-немедленное, так что эта оптимизация также не меняет микроархитектурного эффекта вашего кода (за исключением, конечно, размера кода / выравнивания / того, как он упаковывается в кэш uop).
10-байтовый mov r64, imm64
Кодировка еще хуже для размера кода. Если константе фактически задан какой-либо из ее старших битов, то она имеет дополнительную неэффективность в кэше UOP на процессорах семейства Intel Sandybridge (используя 2 записи в кэше UOP и, возможно, дополнительный цикл для чтения из кэша UOP). Но если константа находится в диапазоне -2^31 .. +2^31 (32-разрядный со знаком), она сохраняется так же эффективно внутри, используя только одну запись uop-cache, даже если она была закодирована на компьютере x86 код с использованием 64-битной немедленной. (См. Документ микроархива Agner Fog, таблица 9.1. Размер различных инструкций в кэше μop в разделе Sandybridge)
От сколько способов установить регистр на ноль? Вы можете принудительно использовать любую из трех кодировок с помощью 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&T movabs
и так же objdump
в режиме Intel-синтаксиса) для адреса, который является постоянной времени соединения, но неизвестен во время сборки.
YASM выбирает mov r64, imm32
т.е. он предполагает модель кода, где адреса меток 32-битные, если вы не используете mov rsi, strict qword msg
Поведение YASM обычно хорошее (хотя mov r32, imm32
для статических абсолютных адресов, таких как компиляторы C, было бы еще лучше). Модель кода по умолчанию, отличная от PIC, помещает весь статический код / данные в нижние 2 ГБ виртуального адресного пространства, поэтому 32-разрядные константы с нулевым или знаковым расширением могут содержать адреса.
Если вы хотите 64-битные адреса меток, вы должны обычно использовать lea r64, [rel address]
сделать RIP-родственник LEA. (В Linux, по крайней мере, позиционно-зависимый код может идти ниже 32, поэтому, если вы не используете модели большого / огромного кода, в любое время, когда вам нужно заботиться о 64-битных адресах меток, вы также создаете код PIC где вы должны использовать REA-относительную LEA, чтобы избежать необходимости перемещения текста абсолютных адресных констант).
т.е. gcc и другие компиляторы использовали бы mov esi, msg
, или же lea rsi, [rel msg]
, никогда mov rsi, msg
,