Вопрос или проблема
Я пытаюсь загрузить адрес ‘main’ в регистр (R10) в GNU ассемблере. У меня это не получается. Вот что у меня есть и сообщение об ошибке, которое я получаю.
main:
lea main, %r10
Я также пробовал следующий синтаксис (на этот раз используя mov)
main:
movq $main, %r10
С обоими из вышеуказанных вариантов я получаю следующую ошибку:
/usr/bin/ld: /tmp/ccxZ8pWr.o: relocation R_X86_64_32S against symbol `main' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: Nonrepresentable section on output
collect2: error: ld returned 1 exit status
Компиляция с -fPIC не решает проблему и просто дает мне ту же самую ошибку.
В x86-64 большинство немедленных значений и смещений все еще имеют 32 бита, потому что 64 бита будут занимать слишком много места в коде (потребление кэша и полоса пропускания для выборки/декодирования).
lea main, %reg
— это абсолютный disp32
режим адресации, который остановит рандомизацию адресов во время загрузки (ASLR), не позволяя выбрать случайный 64-битный (или 47-битный) адрес. Поэтому это не поддерживается на Linux, кроме как в позиционно-зависимых исполняемых файлах, или вообще на MacOS, где статический код/данные всегда загружаются за пределами низких 32 бит. (Смотрите вики тега x86 для ссылок на документы и руководства.) На Windows вы можете делать исполняемые файлы “осведомленными о больших адресах” или нет. Если вы выберете “нет”, адреса будут помещаться в 32 бита.
Стандартный эффективный способ поместить статический адрес в регистр — это LEA относительно RIP:
# LEA относительно RIP всегда работает. Синтаксис для различных ассемблеров:
lea main(%rip), %r10 # Синтаксис AT&T
lea r10, [rip+main] # GAS .intel_syntax noprefix эквивалент
lea r10, [rel main] ; эквивалент NASM, или используйте по умолчанию rel
lea r10, [main] ; FASM по умолчанию является относительно RIP. Может быть и MASM
Смотрите Как работают относительные переменные ссылок RIP, такие как “[RIP + _a]” в x86-64 GAS с Intel-синтаксисом? для объяснения трех синтаксисов, и Почему глобальные переменные в x86-64 доступны относительно указателя команды? (и это) с причинами, почему относительная адресация RIP является стандартным способом адресации статических данных.
Это использует 32-битное относительное смещение от конца текущей инструкции, как jmp
/call
. Это может достичь любых статических данных в .data
, .bss
, .rodata
или функции в .text
, предполагая обычный лимит на общий размер в 2 GiB для статического кода + данных.
В позиционно зависимом коде (собранном с помощью gcc -fno-pie -no-pie
, например) на Linux вы можете воспользоваться 32-битной абсолютной адресацией, чтобы сэкономить размер кода. Кроме того, mov r32, imm32
имеет немного лучшую пропускную способность, чем LEA относительно RIP на процессорах Intel/AMD, поэтому выполнение вне порядка может лучше перекрывать его с окружающим кодом. (Оптимизация для размера кода обычно менее важна, чем большинство других вещей, но когда все остальное одинаково, выбирайте более короткую инструкцию. В этом случае все остальное также, по крайней мере, равно или даже лучше с mov imm32
.)
Смотрите 32-битные абсолютные адреса больше не разрешены в x86-64 Linux? для получения дополнительной информации о том, как PIE исполняемые файлы являются по умолчанию. (Именно поэтому вы получили ошибку компоновщика о -fPIC
с использованием 32-битного абсолютного адреса.)
# в не-PIE исполняемом файле, mov imm32 в 32-битный регистр еще лучше
# то же самое, что вы бы использовали в 32-битном коде
## GAS синтаксис AT&T
mov $main, %r10d # 6 байт
mov $main, %edi # 5 байт: нет необходимости в префиксе REX для "наследуемого" регистра
## GAS .intel_syntax
mov edi, OFFSET main
;; mov edi, main ; синтаксис NASM и FASM
Обратите внимание, что запись любого 32-битного регистра всегда нулевым образом расширяет полный 64-битный регистр (R10 и RDI).
lea main, %edi
или lea main, %rdi
также будет работать в не-PIE исполняемом файле Linux, но никогда не используйте LEA с режимом адресации [disp32]
(даже в 32-битном коде, где это не требует байта SIB); mov
всегда по крайней мере так же хорош.
Суффикс размера операнда избыточен, когда у вас есть операнд-регистровый, который уникально его определяет; я предпочитаю просто писать mov
вместо movl
или movq
.
Тупой/плохой способ — это 10-байтовый 64-битный абсолютный адрес в качестве немедленного значения:
# Неэффективно, НЕ ИСПОЛЬЗУЙТЕ
movabs $main, %r10 # 10 байт, включая 64-битный абсолютный адрес
Это то, что вы получаете в NASM, если вы используете mov rdi, main
вместо mov edi, main
, и многие люди в итоге делают это. Динамическая компоновка Linux действительно поддерживает исправления времени выполнения для 64-битных абсолютных адресов. Но область применения этого — для таблиц переходов, а не для абсолютных адресов в качестве немедленных значений.
movq $sign_extended_imm32, %reg
(7 байт) все еще использует 32-битный абсолютный адрес, но тратит байты кода на знаковое расширение mov
в 64-битный регистр, вместо неявного нулевого расширения до 64 бит при записи 32-битного регистра.
Используя movq
, вы говорите GAS, что вы хотите переезд R_X86_64_32S
, а не R_X86_64_64
64-битный абсолютный переезд.
Единственная причина, по которой вам когда-либо может понадобиться это кодирование, — это код ядра, где статические адреса находятся в верхних 2GiB 64-битного адресного пространства, а не в нижних 2GiB. mov
имеет незначительные преимущества в производительности перед lea
на некоторых ЦП (например, работа на большем количестве портов), но обычно, если вы можете использовать 32-битный абсолютный адрес, он находится в нижних 2GiB адресного пространства, где работает mov r32, imm32
.
(Связано: Разница между movq и movabsq в x86-64)
PS: Я намеренно опустил любое обсуждение “больших” или “огромных” моделей памяти/кода, где адресация RIP-относительно +-2GiB не может достигнуть статических данных, или, возможно, не может даже достичь других адресов кода. Вышеизложенное относится к “малым” и/или “малым-PIC” моделям кода ABI System V x86-64. Вам может понадобиться movabs $imm64
для средних и больших моделей, но это очень редко.
Я не знаю, работает ли mov $imm32, %r32
в исполняемых файлах Windows x64 или DLL с исправлениями времени выполнения, но LEA относительно RIP определенно работает.
Полусвязано: Вызвать абсолютный указатель в машинном коде x86 — если вы JIT, попытайтесь разместить буфер JIT рядом с существующим кодом, чтобы вы могли call rel32
, в противном случае movabs
указатель в регистр.
Ответ или решение
Чтобы загружать адрес функции или метки в регистр в GNU ассемблере (GAS), важно учитывать особенности архитектуры x86-64 и используемого формата кода. Основные проблемы, с которыми вы столкнулись, связаны с особенностями позиционных и позиционно-независимых (PIE) исполняемых файлов.
Ошибки и причины
Ваша первая попытка с использованием команды lea
:
main:
lea main, %r10
вызывает ошибку, поскольку вы пытаетесь использовать абсолютное адресование с 32-битным смещением (disp32). В современных версиях Linux абсолютные адреса в 32 бита не допускаются для динамически загружаемых библиотек (shared objects) из-за механизма рандомизации адресов (ASLR).
Ваш второй пример с использованием команды mov
также приводит к похожей ошибке:
main:
movq $main, %r10
Этот способ также стремится загрузить 32-битный адрес, что неприменимо для систем, использующих ASLR.
Правильный способ загрузки адреса
Для адресации в архитектуре x86-64 принято использовать RIP-относительное адресование. Правильный синтаксис будет выглядеть так:
main:
lea main(%rip), %r10 # AT&T синтаксис
или в Intel-синтаксисе:
main:
lea %r10, [rip + main] # Intel-синтаксис
Почему это работает?
Использование RIP-относительного адресования позволяет программе правильно вычислять адреса во время выполнения, что является критически важным в контексте работы с динамически загружаемыми библиотеками и ASLR. Инструкция lea
в этом случае будет использовать относительное смещение от текущего указателя команд, что позволяет адресоваться к статическим функциям или переменным в пределах 2 ГБ.
Дополнительные рекомендации
-
Используйте
-fPIC
и другие флаги компиляции: Если вы компилируете динамическую библиотеку, убедитесь, что ваша компиляция действительно требует позиционно-независимого кода. -
Различные синтаксисы: Обратите внимание на различия между синтаксисами AT&T и Intel. Выбор синтаксиса должен соответствовать вашим предпочтениям и инструментам.
-
Избегайте использования абсолютных адресов: Использование абсолютных адресов (например, с
movabs
) может быть допустимо, но в общем случае это не рекомендуется для динамически настраиваемых программ. Предпочтите RIP-относительные адреса для обеспечения совместимости с ASLR. -
Тестируйте ваши сборки: При изменении инструкций и методов тестируйте программы, чтобы удостовериться, что все работает так, как задумано, особенно при работе с внешними библиотеками.
Заключение
Использование RIP-относительного адресования — оптимальный и безопасный способ загрузки адресов функций и меток в регистры в GNU ассемблере на архитектуре x86-64. Убедитесь, что ваш код компилируется с нужными флагами и использует правильную структуру для работы с динамически загружаемыми библиотеками и обработкой адресации.