ВПЕРЁД ⇒
⇐ НАЗАД
Манглирование имён функций
До сих пор (в примерах трансляции программ с языка C++ на язык ассемблера) имена функций в ассемблерных программах имели представление, наиболее комфортное для чтения разработчиком. Такое отображение имён функций обеспечивалось настроечной опцией деманглирования имён с машиночитаемого вида в человекочитаемый вид.

Отключение опции Demangle identifiers приведёт к тому, что имена функций (подпрограмм) в ассемблерной программе будут манглированными, то есть будут иметь машиночитаемый вид, отображающий сигнатуры вызываемых функций. Сразу обратим внимание: в языке C++ сигнатура включает в себя имя функции и типы входных параметров данной функции, но не учитывает возвращаемый функцией тип. Это важно учитывать при выполнении перегрузок функций в программах на языке C++. Перегрузка функций основана именно на различии сигнатур. Например, следующий код на языке C++ не скомпилируется, поскольку у перегружаемых функций совпадают сигнатуры:
int foo(int x) { return x; }
char foo(int x) { return 'A'; } // ОШИБКА: redefinition of 'foo'Обе функции имеют одинаковую сигнатуру: foo(int). Различие только в типе возврата не делает их разными функциями для компилятора.
А вот этот код корректен:
int foo(int x) { return x; }
int foo(char x) { return x; } // OK: разные сигнатурыВ данном случае сигнатуры, очевидно, различаются: foo(int) vs foo(char).
Если быть более точным, то (по стандарту C++) сигнатура функции включает:
- Имя функции (для обычных функций)
- Типы параметров (их количество, типы и порядок)
- CV-квалификаторы (для функций-членов класса:
const,volatile) - Ref-квалификаторы (для функций-членов:
&,&&) - Принадлежность к пространству имён или классу
Вернёмся к теме манглирования. Манглирование имён функций в C++ играет важную роль. В языке C отсутствует возможность выполнять перегрузку функций (имена функций в языке C должны быть уникальными). Это объясняется тем, что в самой ассемблерной программе имена символов (меток) должны быть уникальными в пределах программы, причём трансляция программы на языке C в программу на языке ассемблера приводит к тому, что одинаковые имена функций в программе на C превращаются в одинаковые имена символов в программе на языке ассемблера (на итоговое имя символа/метки никак не влияют ни типы аргументов функции, ни количество этих аргументов, ни их порядок на входе функции). По этой причине, в программах на языке C, разработчикам приходится изощряться для получения псевдо-перегруженных версий одной функции:
void setDogAge(void); // установка возраста значением по-умолчанию
void setDogAgeDays(int days); // установка возраста в днях
void setDogAgeMonths(int months); // установка возраста в месяцах
void setDogAgeYears(char years); // установка возраста в годахЯзык C++ под капотом схож с языком C: имена функций (после их трансляции в ассемблерное представление) так же, как и в языке C, должны иметь уникальные строковые представления символов в ассемблерной программе. Манглирование имён функций в языке C++ позволяет компилятору генерировать разные имена символов на языке ассемблера для функций с совпадающими именами, но различающимися аргументами (по их типу, количеству и/или порядку). Во время манглирования, к базовым именам меток/символов (то есть к непосредственным именам функций, ассоциированным с этими метками) дополнительно добавляется информация о входных параметрах функций.
Манглирование имён функций - это одновременно и преимущество, и недостаток языка C++. Преимущество заключается в том, что благодаря манглированию, языком C++ поддерживается статический полиморфизм в виде перегрузки функций различными версиями сигнатур этих функций. Недостаток заключается в том, что схема манглирования имён не стандартизирована и различается между компиляторами (GCC/Clang используют Itanium C++ ABI, MSVC - свою схему). Это создаёт проблемы бинарной совместимости: библиотека, собранная одним компилятором, может быть несовместима с кодом, собранным другим компилятором, даже если оба используют одинаковый ABI для передачи данных. Если сначала выполнить сборку всех единиц трансляции при помощи одного компилятора, а потом пересобрать какую-либо единицу трансляции при помощи другого компилятора, то не будет никакой гарантии, что пересобранная единица трансляции сможет быть прилинкована к остальным собранным единицам трансляции, и всё по причине того, что при пересборке строковые представления манглированных имен могут измениться, и линковщик просто не сможет связать старые манглированные имена (на которые ссылаются остальные непересобранные единицы трансляции) с изменёнными версиями этих же имён. Именно отсутствие механизма манглирования в языке C обеспечивает бинарную совместимость между библиотеками, написанными на языке C и собранными под одну платформу при помощи различающихся компиляторов (GCC, Clang, MSVC), поскольку все компиляторы на одной платформе генерируют идентичные имена символов для одних и тех же функций.
Код
C++
void hello() {}
void linus(int arg) {}
int torvalds() {
return 0;
}
int tux(int arg) {
return arg;
}
void fox(int arg1, char arg2) {}
int main() {
hello();
linus(1);
torvalds();
tux(2);
fox(3, 4);
}ASM (x86-64 clang) с деманглированными именами
hello():
push rbp
mov rbp, rsp
pop rbp
ret
linus(int):
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], edi
pop rbp
ret
torvalds():
push rbp
mov rbp, rsp
xor eax, eax
pop rbp
ret
tux(int):
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], edi
mov eax, dword ptr [rbp - 4]
pop rbp
ret
fox(int, char):
push rbp
mov rbp, rsp
mov al, sil
mov dword ptr [rbp - 4], edi
mov byte ptr [rbp - 5], al
pop rbp
ret
main:
push rbp
mov rbp, rsp
call hello()
mov edi, 1
call linus(int)
call torvalds()
mov edi, 2
call tux(int)
mov edi, 3
mov esi, 4
call fox(int, char)
xor eax, eax
pop rbp
retASM (x86-64 clang) с манглированными именами
_Z5hellov:
push rbp
mov rbp, rsp
pop rbp
ret
_Z5linusi:
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], edi
pop rbp
ret
_Z8torvaldsv:
push rbp
mov rbp, rsp
xor eax, eax
pop rbp
ret
_Z3tuxi:
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], edi
mov eax, dword ptr [rbp - 4]
pop rbp
ret
_Z3foxic:
push rbp
mov rbp, rsp
mov al, sil
mov dword ptr [rbp - 4], edi
mov byte ptr [rbp - 5], al
pop rbp
ret
main:
push rbp
mov rbp, rsp
call _Z5hellov
mov edi, 1
call _Z5linusi
call _Z8torvaldsv
mov edi, 2
call _Z3tuxi
mov edi, 3
mov esi, 4
call _Z3foxic
xor eax, eax
pop rbp
retПримеры манглированных имён
Сравнение деманглированных и манглированных имён
| Деманглированное имя | Манглированное имя | Сигнатура (имя функции, типы входных параметров) и тип возвратного значения |
|---|---|---|
hello() | _Z5hellov | void hello() |
linus(int) | _Z5linusi | void linus(int) |
torvalds() | _Z8torvaldsv | int torvalds() |
tux(int) | _Z3tuxi | int tux(int) |
fox(int, char) | _Z3foxic | void fox(int, char) |
main | main | int main() |
Структура манглированного имени (Itanium C++ ABI)
Манглированные имена следуют схеме Itanium C++ ABI, которую используют GCC и Clang. Разберём структуру на примерах:
_Z5hellov
_Z 5 hello v
│ │ │ │
│ │ │ └─ тип аргумента: v = void (нет аргументов)
│ │ └────── имя функции (5 символов)
│ └─────────── длина имени: 5
└─────────────── префикс манглирования C++
_Z5linusi
_Z 5 linus i
│ │ │ │
│ │ │ └─ тип аргумента: i = int
│ │ └────── имя функции (5 символов)
│ └─────────── длина имени: 5
└─────────────── префикс манглирования C++
_Z8torvaldsv
_Z 8 torvalds v
│ │ │ │
│ │ │ └─ тип аргумента: v = void (нет аргументов)
│ │ └─────── имя функции (8 символов)
│ └────────────── длина имени: 8
└────────────────── префикс манглирования C++
_Z3foxic
_Z 3 fox i c
│ │ │ │ │
│ │ │ │ └─ тип второго аргумента: c = char
│ │ │ └──── тип первого аргумента: i = int
│ │ └──────── имя функции (3 символа)
│ └──────────── длина имени: 3
└──────────────── префикс манглирования C++
Кодировки базовых типов
| Тип C++ | Код манглирования |
|---|---|
void | v |
char | c |
signed char | a |
unsigned char | h |
short | s |
unsigned short | t |
int | i |
unsigned int | j |
long | l |
unsigned long | m |
long long | x |
float | f |
double | d |
bool | b |
Особый случай: main
Обратите внимание: функция main не манглируется. Это исключение необходимо, потому что стартовый код C runtime (_start из libc) должен иметь возможность вызвать точку входа программы по фиксированному имени символа main. Если бы main манглировалась, линковщик не смог бы найти точку входа.
По стандарту C++, main всегда имеет C linkage для совместимости с системой запуска программы.
Анализ вызовов в main
main:
push rbp
mov rbp, rsp
call _Z5hellov # hello()
mov edi, 1
call _Z5linusi # linus(1)
call _Z8torvaldsv # torvalds()
mov edi, 2
call _Z3tuxi # tux(2)
mov edi, 3
mov esi, 4
call _Z3foxic # fox(3, 4)
xor eax, eax
pop rbp
retКомпилятор заменяет человекочитаемые имена на манглированные символы. Без деманглирования в Godbolt код выглядит именно так, как будет храниться в объектном файле (.o) и исполняемом файле (ELF).
Новые детали передачи аргументов
fox(int, char) - два аргумента разных типов
# В main:
mov edi, 3 # первый аргумент int (4 байта) → edi
mov esi, 4 # второй аргумент char (1 байт) → esi
# В fox:
mov al, sil # извлечь младший байт из esi → al
mov dword ptr [rbp - 4], edi # сохранить int на стек
mov byte ptr [rbp - 5], al # сохранить char на стек (1 байт)mov esi, 4 - второй аргумент передаётся через esi (32-битная часть rsi). По ABI, второй целочисленный аргумент идёт в rsi.
mov al, sil - внутри fox значение извлекается из младшего байта rsi (sil - это младшие 8 бит регистра rsi). Поскольку char занимает 1 байт, используется только эта часть.
mov byte ptr [rbp - 5], al - сохранение char на стек занимает ровно 1 байт по адресу [rbp - 5]. Обратите внимание: int лежит в [rbp - 4], а char в [rbp - 5] - они расположены последовательно.
Верхние адреса памяти (внутри fox)
--------------------------------------------------------------------
...
< ret addr в main >
rsp → (rbp fox ) → < старый rbp main >
(rbp_fox - 4) → < 0x00000003 > ← arg1 (int, 4 байта)
(rbp_fox - 5) → < 0x04 > ← arg2 (char, 1 байт)
--------------------------------------------------------------------
Нижние адреса памяти
Почему манглирование критично для перегрузки?
Представим гипотетическую ситуацию с перегрузкой:
void foo(int x) { /* версия 1 */ }
void foo(char x) { /* версия 2 */ }
int main() {
foo(42); // вызов версии 1
foo('A'); // вызов версии 2
}Без манглирования обе функции превратились бы в символ foo - линковщик не смог бы их различить. С манглированием:
_Z3fooi: # void foo(int)
...
_Z3fooc: # void foo(char)
...
main:
mov edi, 42
call _Z3fooi # вызов foo(int)
mov edi, 65 # ASCII 'A'
call _Z3fooc # вызов foo(char)Символы _Z3fooi и _Z3fooc уникальны - перегрузка работает корректно.
ВПЕРЁД ⇒
⇐ НАЗАД
Источники
- Здесь будет список источников
Категория
Теги
- Cpp Cpp
- Asm Asm
- Assembly Assembly
- Intel-asm-syntax Intel-asm-syntax
- x86-64 x86-64
- Стек Стек
- Stack Stack
- Стек-вызовов Стек-вызовов
- Call-stack Call-stack
- Стек-фрейм Стек-фрейм
- Stack-frame Stack-frame
- Указатель-фрейма Указатель-фрейма
- Frame-pointer Frame-pointer
- Указатель-стека Указатель-стека
- Stack-pointer Stack-pointer
- clang clang
- clang-fomit-frame-pointer clang-fomit-frame-pointer
- clang-O2 clang-O2
- Эпилог Эпилог
- Пролог Пролог
- Манглирование Манглирование
- Mangling Mangling
- Деманглирование Деманглирование
- Demangling Demangling
- Сигнатура Сигнатура
- Signature Signature