Перейти к содержанию

Символы и манглирование

Символы в терминах линковщика

Символ — именованная сущность, участвующая в линковке. Это могут быть функции, глобальные и статические переменные, а иногда и специальные объекты (секции, метки).

Каждый символ обладает набором атрибутов:

  • имя — строка в таблице символов;
  • тип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) { ... }

Или глобально для всего файла через флаг компилятора:

g++ -fvisibility=hidden lib.cpp -fPIC -shared -o libfoo.so

После этого все символы скрыты по умолчанию, и только явно помеченные 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", чтобы компилятор не манглировал имена:

extern "C" {
    void c_function(int x);
}

Деманглирование

Чтобы получить исходное имя из манглированного, используется утилита 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 с нулём (заглушка)
 │   …                      │
 └──────────────────────────┘
readelf -r a.out      # посмотреть таблицу релокаций объектного файла

Пример записи для 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 — утилита nm
  • man strip — утилита strip
  • man c++filt — утилита деманглирования
  • man readelf — утилита readelf
  • Itanium C++ ABI — правила манглирования