Символы и манглирование¶
Символы в терминах линковщика¶
Символ — именованная сущность, участвующая в линковке. Это могут быть функции, глобальные и статические переменные, а иногда и специальные объекты (секции, метки).
Каждый символ обладает набором атрибутов:
- имя — строка в таблице символов;
- тип —
STT_FUNC(функция),STT_OBJECT(переменная),STT_NOTYPEи т.д.; - адрес или
UND— если символ определён в данном файле, хранится его адрес; если определение ожидается из другого модуля — запись помечаетсяUND(undefined); - binding (связывание) — local, global или weak;
- visibility (видимость) — default, hidden или protected.
Линковщик сопоставляет использования символов (UND) с их определениями во всех объектных файлах и библиотеках.
Если определение не найдено — ошибка «undefined reference».
Связывание символов (binding)¶
| Тип | Описание |
|---|---|
STB_LOCAL |
Виден только внутри данного объектного файла; не экспортируется |
STB_GLOBAL |
Глобальный; может использоваться и экспортироваться из других файлов |
STB_WEAK |
«Слабое» определение; перекрывается глобальным с тем же именем |
STB_LOCAL соответствует функциям и переменным, объявленным со спецификатором static в C/C++. Они не участвуют в
разрешении внешних ссылок.
STB_WEAK применяется для реализаций по умолчанию: библиотека предоставляет слабое определение, которое пользователь
может заменить своим глобальным. Если глобальное определение отсутствует, слабый символ может иметь значение 0/
nullptr.
Видимость символов (visibility)¶
| Значение | Описание |
|---|---|
STV_DEFAULT |
Стандартная видимость; символ экспортируется и может быть перехвачен другой .so |
STV_HIDDEN |
Символ не экспортируется за пределы данного объекта |
STV_PROTECTED |
Символ экспортируется, но не может быть переопределён другой библиотекой |
Задать видимость можно в исходном коде через атрибут GCC:
__attribute__((visibility("hidden"))) void internal_func(void) { ... }
__attribute__((visibility("default"))) void public_api(void) { ... }
Или глобально для всего файла через флаг компилятора:
После этого все символы скрыты по умолчанию, и только явно помеченные visibility("default") будут экспортированы. Это
уменьшает размер .dynsym и ускоряет динамическую линковку.
Просмотр символов¶
nm a.out # все символы
nm -D a.out # только динамические символы (.dynsym)
nm -C a.out # с демангированием C++-имён
readelf -s a.out # полная таблица символов
readelf -sD a.out # динамические символы
objdump -t a.out # таблица символов
objdump -C -t a.out # с демангированием
Манглирование (name mangling)¶
В C++ одно и то же имя функции может быть перегружено разными сигнатурами, а функции могут находиться в пространствах имён или классах. На уровне линковщика все имена должны быть уникальными строками — для этого компилятор **манглирует ** (кодирует) имя функции вместе с её типами аргументов и контекстом.
Правила кодирования определены в Itanium C++ ABI (используется GCC и Clang). Примеры:
| Исходное имя | Манглированное имя |
|---|---|
int foo(int) |
_Z3fooi |
void Ns::Bar::baz(double) |
_ZN2Ns3Bar3bazEd |
int foo(int, char*) |
_Z3fooiPc |
В языке C манглирования нет: функция foo остаётся foo. Поэтому при использовании C-библиотек из C++ нужно объявлять
их через extern "C", чтобы компилятор не манглировал имена:
Деманглирование¶
Чтобы получить исходное имя из манглированного, используется утилита c++filt:
echo _Z3fooi | c++filt
# int foo(int)
echo _ZN2Ns3Bar3bazEd | c++filt
# void Ns::Bar::baz(double)
nm a.out | c++filt # деманглировать все символы из nm
objdump -C -t a.out # флаг -C включает деманглирование
Удаление символов: strip¶
strip удаляет из ELF отладочную информацию и таблицу символов .symtab. Это уменьшает размер бинаря и усложняет
реверс-инжиниринг, но не влияет на работоспособность динамически слинкованных программ (.dynsym остаётся).
strip a.out
strip --strip-debug a.out # только отладочную информацию, символы оставить
strip --strip-unneeded a.out # удалить только ненужные для динамической линковки символы
После strip без флагов nm a.out покажет только то, что осталось в .dynsym. Отладка через gdb без символов
существенно затруднена (см. статью Отладка (GDB)).
Релокации¶
При компиляции объектного файла адреса внешних символов неизвестны — вместо них компилятор ставит нули и создаёт запись
в таблице релокаций (.rela.text, .rela.data и т.д.). Каждая запись содержит:
- смещение в секции, где нужно вписать адрес;
- номер символа (индекс в
.symtab); - тип релокации (способ вычисления значения).
Линковщик читает таблицы релокаций и подставляет реальные адреса. Типы релокаций специфичны для архитектуры:
main.o printf.o (из libc.a)
┌──────────────────────────┐ ┌──────────────────────────┐
│ .symtab: │ │ .symtab: │
│ main GLOBAL FUNC │ │ printf GLOBAL FUNC │
│ printf GLOBAL UND ◀╌╌╌│╌╌╌╌│╌╌╌╌╌╌╌╌╌╌╌╌ определён │
├──────────────────────────┤ └──────────────────────────┘
│ .rela.text: │
│ offset=0x10 │ ld объединяет .o файлы,
│ sym=printf │ находит определение printf,
│ type=R_X86_64_PLT32 │ вписывает адрес по offset
├──────────────────────────┤ (или адрес PLT-заглушки
│ .text: │ при динамической линковке)
│ … │
│ e8 00 00 00 00 ◀╌╌╌╌╌╌│╌╌╌ call с нулём (заглушка)
│ … │
└──────────────────────────┘
Пример записи для x86-64:
Offset Info Type Sym. Value Sym. Name + Addend
000000000010 000500000004 R_X86_64_PLT32 0000000000000000 printf - 4
Для динамически линкованных бинарей часть релокаций остаётся в .rela.plt и .rela.dyn — их заполняет динамический
линковщик при загрузке.
Связанные темы¶
- Формат ELF — структура
.symtab,.dynsym,.rela.*в ELF-файле - Стадии сборки — когда создаются объектные файлы и выполняются релокации
- Отладка (GDB) — как символы используются отладчиком
Источники¶
man nm— утилита nmman strip— утилита stripman c++filt— утилита деманглированияman readelf— утилита readelf- Itanium C++ ABI — правила манглирования