ВПЕРЁД

НАЗАД



Манглирование имён функций

До сих пор (в примерах трансляции программ с языка 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
	ret

Godbolt

ASM (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

Godbolt

Примеры манглированных имён

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

Деманглированное имяМанглированное имяСигнатура (имя функции, типы входных параметров) и тип возвратного значения
hello()_Z5hellovvoid hello()
linus(int)_Z5linusivoid linus(int)
torvalds()_Z8torvaldsvint torvalds()
tux(int)_Z3tuxiint tux(int)
fox(int, char)_Z3foxicvoid fox(int, char)
mainmainint 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++Код манглирования
voidv
charc
signed chara
unsigned charh
shorts
unsigned shortt
inti
unsigned intj
longl
unsigned longm
long longx
floatf
doubled
boolb

Особый случай: 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 уникальны - перегрузка работает корректно.



ВПЕРЁД

НАЗАД