Как загрузить адрес функции или метки в регистр

Вопрос или проблема

Я пытаюсь загрузить адрес ‘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 ГБ.

Дополнительные рекомендации

  1. Используйте -fPIC и другие флаги компиляции: Если вы компилируете динамическую библиотеку, убедитесь, что ваша компиляция действительно требует позиционно-независимого кода.

  2. Различные синтаксисы: Обратите внимание на различия между синтаксисами AT&T и Intel. Выбор синтаксиса должен соответствовать вашим предпочтениям и инструментам.

  3. Избегайте использования абсолютных адресов: Использование абсолютных адресов (например, с movabs) может быть допустимо, но в общем случае это не рекомендуется для динамически настраиваемых программ. Предпочтите RIP-относительные адреса для обеспечения совместимости с ASLR.

  4. Тестируйте ваши сборки: При изменении инструкций и методов тестируйте программы, чтобы удостовериться, что все работает так, как задумано, особенно при работе с внешними библиотеками.

Заключение

Использование RIP-относительного адресования — оптимальный и безопасный способ загрузки адресов функций и меток в регистры в GNU ассемблере на архитектуре x86-64. Убедитесь, что ваш код компилируется с нужными флагами и использует правильную структуру для работы с динамически загружаемыми библиотеками и обработкой адресации.

Оцените материал
Добавить комментарий

Капча загружается...