Генерация релокаций является одной из фундаментальных составляющих процесса создания исполняемых и объектных файлов в ассемблерах. Релокации выступают в роли маркеров или плейсхолдеров в бинарном коде, которые указывают на участки данных или инструкций, значения которых не определены на этапе ассемблирования и зависят от окончательного расположения программы в памяти. Понимание механизмов генерации и обработки релокаций критично для разработчиков компиляторов, инструментов для анализа бинарей и всех, кто работает с низкоуровневым кодом и системным программированием. Современные популярные ассемблеры, такие как GNU Assembler (gas) и LLVM Integrated Assembler, реализуют этот процесс с учётом множества архитектурных и форматных особенностей, что делает изучение их подходов как технически увлекательным, так и практически полезным. Основой для формирования релокаций служат ссылки на символы.
Такие символы могут представлять функции, данные или другие ресурсы, адреса которых в момент ассемблирования либо неизвестны, либо зависят от компоновщика (линкера). Например, в архитектуре x86-64 инструкция movl sym(%rip), %eax содержит смещение относительно счётчика команд (PC), рассчитываемое с учётом конечного адреса символа sym. Ассемблер не может однозначно определить это смещение во время преобразования исходного текста в объектный код, поэтому он создаёт запись релокации с типом R_X86_64_PC32, которая позже применяется линкером, когда известна полная карта памяти. Процесс генерации релокаций в ассемблерах происходит в несколько этапов, включая анализ исходного текста, формирование секций и их фрагментов, а также разрешение и делегирование релационных выражений к конкретным записям релокаций. На начальном этапе парсинга инструкция и операнды разбираются на составляющие, где идентифицируются регистры, константы и символические ссылки.
Неопределённые в момент парсинга выражения превращаются в специальные объекты fixup, предоставляющие информацию о том, как и где будет применяться дальнейшая корректировка. Важной задачей становится разделение локальных и глобальных символов, поскольку в процессе компоновки разные типы символов имеют различные правила разрешения и связывания. Локальные символы, определённые в пределах одной секции, зачастую позволяют ассемблеру завершать вычисления смещений на своем уровне, без необходимости создания релокаций. Тогда как глобальные и внешние символы, особенно слабые (weak) и заранее неопределённые, обычно приводят к появлению соответствующих записей в таблице релокаций. Секция layout — это этап, на котором ассемблер рассчитывает точные оффсеты для фрагментов кода и данных, учитывая выравнивания и размеры.
Символы, связанные с этими фрагментами, получают свои точные адреса в рамках объектов. Однако символы, которые определены вне текущего ассемблируемого модуля, остаются неразрешёнными и передаются линкеру. После определения точных позиций фиксапов и символов, начинается фаза анализа выражений для выявления, требуют ли они создания релокаций. Концепция релоктабельных выражений, часто представляемых в форме relocation_specifier(sym_a - sym_b + offset), описывает как сложные, так и простые случаи вычислений. Многие архитектуры реализуют поддержку выражений с двумя символами, например, RISC-V и AVR, где операции вычитания символов отражаются в дополнительных типах релокаций.
Попытка использования неподдерживаемых форм выражений приводит к ошибкам на этапе ассемблирования. Рассматривая архитектуры с поддержкой PC-relative вычислений, релокации часто выражаются как разница между адресом символа (S) и текущей позицией инструкции (P), с добавочным константным оффсетом (A). Если символ локален и находится в пределах той же секции, многие из таких ссылок разрешаются сразу на этапе ассемблирования, без создания релокаций. Однако для глобальных символов создаются релокации, чтобы позволить компоновщику и загрузчику корректно изменить адреса при загрузке программы. Типы разрешений фиксапов делятся на три категории: ошибки в случае неподдерживаемых выражений, успешное прямое разрешение, и невозможность разрешения, ведущая к генерации записей релокаций.
Особенностей требует обращение с TLS (Thread-Local Storage) символами, GOT (Global Offset Table) и PLT (Procedure Linkage Table), которым сопутствуют свои специфические модификаторы релокаций и связанные с ними типы переходов и загрузок. В ассемблерах широко применяются различные синтаксические способы указания модификаторов релокаций — так называемых релокационных спецификаторов. Они различаются по архитектурам и историческим традициям. Например, «expr@specifier» является распространённым и используется в ряде архитектур с расширением binutils, тогда как «%specifier(expr)» предпочтителен в MIPS, RISC-V и SPARC за счет повышения читаемости и однозначности. Архитектуры семейства ARM используют удобочитаемую нотацию вида «:specifier:expr», что снижает неоднозначность при парсинге.
Релокационные спецификаторы уточняют, какую часть адреса символа следует использовать, являются ли адреса абсолютными или PC-относительными, и направляют ассемблер к правильному формированию кодирования инструкции. Генерация комплексных (составных) релокаций востребована для сложных инструкций и механизмов оптимизации, особенно в архитектурах PPC64, RISC-V и XCOFF, где одна виртуальная операнда может требовать сразу нескольких связанных релокаций, которые должны применяться совместно. В этом случае релокации формируют цепочку с особым порядком применения, что обеспечивает точность и оптимальность конечного результата. Внутреннее устройство GNU Assembler опирается на структуру fixup, в которой хранятся ссылки на добавляемые и вычитаемые символы, а также сдвиг. Несмотря на то, что спецификатор релокаций не входит в эту структуру напрямую, он интегрирован в логику формирования инструкций, что позволяет таргетно обрабатывать различия в типах и форматах адресации.
LLVM Integrated Assembler подходит к проблеме более «объектно-ориентированно», разделяя понятия fixup (позиция в коде и тип) и relocatable expression (MCValue), хранящий символы, константы и, при необходимости, спецификатор. Такая архитектура облегчает универсальное представление релокаций, но влечёт за собой дополнительные сложности при реализации некоторых таргетных выражений и усложняет раскладку выражений с релейтивными спецификаторами. Парсинг конструкций с релокационными спецификаторами в LLVM соединяет два мира: гибкость и сложность. Особенности поведения, например, при использовании символа @ после символа или выражения требуют специальных механизмов для корректного извлечения и формирования MCExpr или MCValue с релокационными модификаторами. Использование TLS-спецификаторов и работа с исключениями, особыми режимами запуска, такими как динамическая компоновка и загрузка, накладывают высокие требования на корректность обработки и генерации релокаций.
Ассемблеры и компоновщики должны обеспечить, чтобы в создаваемых объектных файлах были все необходимые записи и корректно установленный тип TLS-символов (STT_TLS), что гарантирует правильную работу потоково-локализованных данных в рантайме. Корректная генерация и оформление релокаций оказывает прямое влияние на переносимость, производительность и безопасность программного обеспечения. Ошибки на этом уровне могут привести к сбоям при загрузке, неправильной адресации вызовов функций и данных, что крайне сложно выявить и отладить. В связи с этим развитие и поддержка механизмов образования релокаций в современных ассемблерах – активная область исследований и инженерной работы, учитывающая новые архитектурные возможности и требования рынка. Подводя итог, можно отметить, что генерация релокаций — это сложный, многоуровневый процесс, основанный на точном анализе команд, выражений и символов, с учётом архитектурных, форматных и семантических особенностей целевой платформы.
Как GNU Assembler, так и LLVM Integrated Assembler продемонстрировали гибкость и глубину реализации данной функции, поддерживая широкий спектр архитектур и случаев использования. Знание устройства и принципов их работы может значительно облегчить понимание внутренней работы компилятора, линковщика и загрузчика, а также помочь в разработке собственных инструментов и оптимизации сборочных цепочек.