ВПЕРЁД ⇒
⇐ НАЗАД
Просмотр манглированных имён функций в объектном файле
Исходный код программы:
name_mangling.cpp
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);
}Соберём объектный файл программы и просмотрим в нём таблицу символов, содержащую интересующие нас манглированные имена функций. В этот раз эксперименты будем проводить в Linux-системе, для сборки объектного файла будем использовать GNU-компилятор g++ (GNU C++ Compiler).
Почему
g++, а неgcc?Оба компилятора входят в набор GNU Compiler Collection и позволяют собирать программы, написанные на языке C++, но между этими компиляторами имеется ряд различий.
Компилируемый язык по-умолчанию:
gccпо умолчанию компилирует.cфайлы как C, а.cppфайлы как C++.g++обрабатывает все файлы как C++ вне зависимости от расширения.Компоновка (линковка):
g++автоматически подключает стандартную библиотеку C++ (libstdc++).gccподключает стандартную библиотеку C++ (libstdc++), только если при вызове указать-lstdc++. Это наиболее важное практическое отличие: компиляция C++ черезgccзачастую проходит успешно, но линковка завершится ошибкой без явного указания флагов.Предопределённые макросы:
g++определяет макрос__cplusplus.gccне определяет макрос__cplusplus(кроме случаев компиляции.cppфайлов).Рекомендации по использованию компиляторов:
gccлучше использовать для проектов на C.- Для проектов на C++ лучше использовать
g++, так как это избавляет от необходимости вручную подключать стандартную библиотеку и обеспечивает правильные настройки по-умолчанию.Поскольку нас интересуют только программы на языке C++, то есть смысл упростить себе задачу: не заниматься ручной настройкой параметров, требуемых компилятором
gcc, а использовать более удобную для нас альтернативу в лице компилятораg++.
$ g++ -c name_mangling.cpp -o name_mangling.o # компиляция без линковки
$ nm name_mangling.o # просмотр таблицы символов объектного файла
000000000000004b T main
0000000000000038 T _Z3foxic
0000000000000028 T _Z3tuxi
0000000000000000 T _Z5hellov
000000000000000b T _Z5linusi
0000000000000019 T _Z8torvaldsvКомпиляция без линковки (создание объектного файла)
$ g++ -c name_mangling.cpp -o name_mangling.oФлаг -c указывает компилятору на необходимость выполнить только компиляцию (compile), но не линковку (link). Результат - объектный файл .o (object file), который содержит:
- Машинный код функций в секции
.text - Таблицу символов с именами функций и их адресами
- Релокации - метки мест, куда нужно подставлять адреса при линковке
- Метаданные для отладчика (если компилировали с опцией
-g)
Объектный файл не является исполняемым. Это промежуточное представление перед финальной сборкой программы.
Просмотр таблицы символов (nm - name list)
$ nm name_mangling.o
000000000000004b T main
0000000000000038 T _Z3foxic
0000000000000028 T _Z3tuxi
0000000000000000 T _Z5hellov
000000000000000b T _Z5linusi
0000000000000019 T _Z8torvaldsvАнализ адресов
Адрес Символ Функция исходника
0x00 _Z5hellov void hello() {}
0x0b _Z5linusi void linus(int arg) {}
0x19 _Z8torvaldsv int torvalds() { return 0; }
0x28 _Z3tuxi int tux(int arg) { return arg; }
0x38 _Z3foxic void fox(int arg1, char arg2) {}
0x4b main int main() { ... }
Расчёт размеров функций
(Размер текущей функции) = (адрес следующей функции) - (адрес текущей функции):
| Функция | Начало | Конец | Размер (байт) | Размер (hex) |
|---|---|---|---|---|
_Z5hellov | 0x00 | 0x0b | 11 | 0x0b |
_Z5linusi | 0x0b | 0x19 | 14 | 0x0e |
_Z8torvaldsv | 0x19 | 0x28 | 15 | 0x0f |
_Z3tuxi | 0x28 | 0x38 | 16 | 0x10 |
_Z3foxic | 0x38 | 0x4b | 19 | 0x13 |
main | 0x4b | --- | --- | --- |
Для последней функции (main) размер можно узнать через:
$ nm -S name_mangling.o | grep main
000000000000004b 000000000000003c T mainФлаг -S добавляет размер: 0x3c = 60 байт.
Как выполнять деманглирование для повышения уровня читаемости?
Деманглированное представление таблицы символов ускоряет процесс понимания разработчиком сигнатур функций, используемых в объектном файле.
$ nm -C name_mangling.o
000000000000004b T main
0000000000000038 T fox(int, char)
0000000000000028 T tux(int)
0000000000000000 T hello()
000000000000000b T linus(int)
0000000000000019 T torvalds()То же самое, но с использованием c++filt (утилиты деманглирования, то есть утилиты преобразования манглированных C++ имён в человекочитаемый вид):
$ nm name_mangling.o | c++filt
000000000000004b T main
0000000000000038 T fox(int, char)
0000000000000028 T tux(int)
0000000000000000 T hello()
000000000000000b T linus(int)
0000000000000019 T torvalds()Также в вызове c++filt можно сразу же вручную указать (для выполнения деманглирования) интересующее нас манглированное имя, не привязывая вызов утилиты к конкретному объектному файлу:
$ c++filt _Z3foxic
fox(int, char)Почему размеры функций различаются?
Размер машинного кода функции зависит от:
- Количества инструкций (больше логики = больше байт)
- Сохранения параметров на стек (
mov [rbp-4], ediи т.д.) - Пролога/эпилога (
push rbp,mov rbp, rsp,pop rbp,ret) - Выравнивания (компилятор может добавить
nopдля кратности 16 байт)
Для выполнения подробного анализа, из чего складывается размер функций в объектном файле, воспользуемся утилитой просмотра машинного кода objdump.
Просмотр машинного кода при помощи утилиты objdump
Вызов утилиты objdump для вывода реального ассемблерного кода внутри .o файла:
$ objdump -d --disassembler-options=intel name_mangling.o
name_mangling.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z5hellov>:
0: f3 0f 1e fa endbr64
4: 55 push rbp
5: 48 89 e5 mov rbp,rsp
8: 90 nop
9: 5d pop rbp
a: c3 ret
000000000000000b <_Z5linusi>:
b: f3 0f 1e fa endbr64
f: 55 push rbp
10: 48 89 e5 mov rbp,rsp
13: 89 7d fc mov DWORD PTR [rbp-0x4],edi
16: 90 nop
17: 5d pop rbp
18: c3 ret
0000000000000019 <_Z8torvaldsv>:
19: f3 0f 1e fa endbr64
1d: 55 push rbp
1e: 48 89 e5 mov rbp,rsp
21: b8 00 00 00 00 mov eax,0x0
26: 5d pop rbp
27: c3 ret
0000000000000028 <_Z3tuxi>:
28: f3 0f 1e fa endbr64
2c: 55 push rbp
2d: 48 89 e5 mov rbp,rsp
30: 89 7d fc mov DWORD PTR [rbp-0x4],edi
33: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
36: 5d pop rbp
37: c3 ret
0000000000000038 <_Z3foxic>:
38: f3 0f 1e fa endbr64
3c: 55 push rbp
3d: 48 89 e5 mov rbp,rsp
40: 89 7d fc mov DWORD PTR [rbp-0x4],edi
43: 89 f0 mov eax,esi
45: 88 45 f8 mov BYTE PTR [rbp-0x8],al
48: 90 nop
49: 5d pop rbp
4a: c3 ret
000000000000004b <main>:
4b: f3 0f 1e fa endbr64
4f: 55 push rbp
50: 48 89 e5 mov rbp,rsp
53: e8 00 00 00 00 call 58 <main+0xd>
58: bf 01 00 00 00 mov edi,0x1
5d: e8 00 00 00 00 call 62 <main+0x17>
62: e8 00 00 00 00 call 67 <main+0x1c>
67: bf 02 00 00 00 mov edi,0x2
6c: e8 00 00 00 00 call 71 <main+0x26>
71: be 04 00 00 00 mov esi,0x4
76: bf 03 00 00 00 mov edi,0x3
7b: e8 00 00 00 00 call 80 <main+0x35>
80: b8 00 00 00 00 mov eax,0x0
85: 5d pop rbp
86: c3 retАнализ размеров функций по результатам objdump
Сравнение размеров из nm и реального кода
Вспомним таблицу размеров из nm:
| Функция | Адрес начала | Адрес конца | Размер (hex, байт) | Размер (dec, байт) |
|---|---|---|---|---|
_Z5hellov | 0x00 | 0x0b | 0x0b | 11 |
_Z5linusi | 0x0b | 0x19 | 0x0e | 14 |
_Z8torvaldsv | 0x19 | 0x28 | 0x0f | 15 |
_Z3tuxi | 0x28 | 0x38 | 0x10 | 16 |
_Z3foxic | 0x38 | 0x4b | 0x13 | 19 |
main | 0x4b | 0x87 | 0x3c | 60 |
Теперь разберём реальный машинный код каждой функции из вывода objdump.
_Z5hellov - void hello() {}
Содержимое объектного кода
0000000000000000 <_Z5hellov>:
0: f3 0f 1e fa endbr64 # 4 байта
4: 55 push rbp # 1 байт
5: 48 89 e5 mov rbp,rsp # 3 байта
8: 90 nop # 1 байт
9: 5d pop rbp # 1 байт
a: c3 ret # 1 байт
# Следующая функция начинается с адреса 0x0bПодсчёт размера функции
endbr64: 4 байта (адреса 0x0–0x3)push rbp: 1 байт (адрес 0x4)mov rbp, rsp: 3 байта (адреса 0x5–0x7)nop: 1 байт (адрес 0x8)pop rbp: 1 байт (адрес 0x9)ret: 1 байт (адрес 0xa)
Итого: 11 байт (от 0x0 до 0xa включительно, следующая функция с 0xb)
Объяснение инструкций
endbr64- инструкция End Branch 64-bit, принадлежащая аппаратной технологии безопасности Indirect Branch Tracking (IBT), предназначенной для защиты от атак ROP (Return-Oriented Programming) и JOP (Jump-Oriented Programming). Добавляется компилятором автоматически при включённом флаге-fcf-protection. Это часть Intel CET (Control-flow Enforcement Technology), поддерживаемой современными процессорами Intel и AMD. Инструкция размещается в начале каждой функции в качестве защитного элемента и усложняет злоумышленникам задачу использования существующего кода программы не по назначению. В атаках ROP/JOP злоумышленник не добавляет свой код, а переиспользует короткие фрагменты легитимных инструкций (gadget’ы) в неправильной последовательности. Обычно такая подмена выполняется за счёт уязвимостей (например, переполнения буфера), приводящих к повреждению стека, что позволяет перезаписывать адреса возврата и указатели на функции.endbr64выступает маркером легального места входа в функцию. При косвенном переходе (черезcall,jmpилиret) процессор проверяет, что по целевому адресу первой инструкцией идёт именноendbr64. Если она отсутствует, выполнение прерывается с исключением. Это не гарантирует безопасность самой функции - злоумышленник может перенаправить выполнение на легитимную, но опасную функцию, которая тоже содержитendbr64. Однако инструкция резко сокращает количество доступных точек входа: злоумышленник больше не может произвольно прыгать в середину функций или между инструкциями, где расположены полезные для атаки gadget’ы. Таким образом, инструкцияendbr64не гарантирует полную защиту: IBT не останавливает инъекцию исполняемого кода и эффективна только в комбинации с другими технологиями (DEP/NX, ASLR, Shadow Stack). Однако она значительно усложняет атаку, сокращая число возможных точек входа с миллионов произвольных адресов до ограниченного набора начал функций.nop- инструкция “холостого хода” (nop - no operation). Инструкцияnopв данном случае нужна не для выравнивания, а как маркер конца тела функции (перед эпилогом). Подобный артефакт кодогенерации GNU-компилятором на уровне-O0(без оптимизации) всегда добавляется в конце тела любой функции типаvoid(то есть не возвращающей значение), независимо от того, пустое тело или нет.
endbr64 # защита IBT
push rbp # \
mov rbp, rsp # / пролог (сохранение фрейм-указателя)
nop # ← тело функции (пустое!)
pop rbp # \
ret # / эпилог (восстановление и возврат)_Z5linusi - void linus(int arg) {}
Содержимое объектного кода
000000000000000b <_Z5linusi>:
b: f3 0f 1e fa endbr64 # 4 байта
f: 55 push rbp # 1 байт
10: 48 89 e5 mov rbp,rsp # 3 байта
13: 89 7d fc mov DWORD PTR [rbp-0x4],edi # 3 байта
16: 90 nop # 1 байт
17: 5d pop rbp # 1 байт
18: c3 ret # 1 байт
# Следующая функция начинается с адреса 0x19Подсчёт размера функции
endbr64: 4 байта (0xb–0xe)push rbp: 1 байт (0xf)mov rbp, rsp: 3 байта (0x10–0x12)mov [rbp-0x4], edi: 3 байта (0x13–0x15)nop: 1 байт (0x16)pop rbp: 1 байт (0x17)ret: 1 байт (0x18)
Итого: 14 байт (от 0xb до 0x18 включительно)
Объяснение инструкций
mov DWORD PTR [rbp-0x4], edi- сохранение параметраint argиз сигнатуры функции на стек (параметр на момент вызова подпрограммы_Z5linusiрасположен вedi).nop- в данном случае эта инструкция служит маркером конца тела функции на адресе0x16(перед эпилогом). Подобный артефакт кодогенерации GNU-компилятором на уровне-O0(без оптимизации) всегда добавляется в конце тела любой функции типаvoid(то есть не возвращающей значение), независимо от того, пустое тело или нет.
_Z8torvaldsv - int torvalds() { return 0; }
Содержимое объектного кода
0000000000000019 <_Z8torvaldsv>:
19: f3 0f 1e fa endbr64 # 4 байта
1d: 55 push rbp # 1 байт
1e: 48 89 e5 mov rbp,rsp # 3 байта
21: b8 00 00 00 00 mov eax,0x0 # 5 байт
26: 5d pop rbp # 1 байт
27: c3 ret # 1 байт
# Следующая функция начинается с адреса 0x28Подсчёт размера функции
endbr64: 4 байта (0x19–0x1c)push rbp: 1 байт (0x1d)mov rbp, rsp: 3 байта (0x1e–0x20)mov eax, 0x0: 5 байт (0x21–0x25)pop rbp: 1 байт (0x26)ret: 1 байт (0x27)
Итого: 15 байт (от 0x19 до 0x27 включительно)
Объяснение инструкций
- Обратите внимание:
mov eax, 0x0занимает 5 байт, хотя обнуление через XOR (xor eax, eax) заняло бы всего 2 байта. Это следствие компиляции без оптимизации (-O0). При-O2компилятор заменит инструкциюmovнаxor. nopотсутствует, поскольку функцияtorvalds()возвращает значение типаint. Инструкцияmov eax, 0x0явно завершает тело и служит естественной границей перед эпилогом. Поэтому дополнительный маркерnopне требуется.
_Z3tuxi - int tux(int arg) { return arg; }
Содержимое объектного кода
0000000000000028 <_Z3tuxi>:
28: f3 0f 1e fa endbr64 # 4 байта
2c: 55 push rbp # 1 байт
2d: 48 89 e5 mov rbp,rsp # 3 байта
30: 89 7d fc mov DWORD PTR [rbp-0x4],edi # 3 байта
33: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] # 3 байта
36: 5d pop rbp # 1 байт
37: c3 ret # 1 байт
# Следующая функция начинается с адреса 0x38Подсчёт размера функции
endbr64: 4 байта (0x28–0x2b)push rbp: 1 байт (0x2c)mov rbp, rsp: 3 байта (0x2d–0x2f)mov [rbp-0x4], edi: 3 байта (0x30–0x32) - сохранитьargна стекmov eax, [rbp-0x4]: 3 байта (0x33–0x35) - прочитатьargобратно вeaxдля возвратаpop rbp: 1 байт (0x36)ret: 1 байт (0x37)
Итого: 16 байт (от 0x28 до 0x37 включительно)
Объяснение инструкций
Функция сначала сохраняет параметр на стек, затем читает его из стека. Это выглядит избыточно (можно было просто вызвать mov eax, edi), но так работает уровень без оптимизации -O0 - буквальное следование исходному коду
_Z3foxic - void fox(int arg1, char arg2) {}
Содержимое объектного кода
0000000000000038 <_Z3foxic>:
38: f3 0f 1e fa endbr64 # 4 байта
3c: 55 push rbp # 1 байт
3d: 48 89 e5 mov rbp,rsp # 3 байта
40: 89 7d fc mov DWORD PTR [rbp-0x4],edi # 3 байта
43: 89 f0 mov eax,esi # 2 байта
45: 88 45 f8 mov BYTE PTR [rbp-0x8],al # 3 байта
48: 90 nop # 1 байт
49: 5d pop rbp # 1 байт
4a: c3 ret # 1 байт
# Следующая функция (main) начинается с адреса с 0x4bПодсчёт размера функции
endbr64: 4 байта (0x38–0x3b)push rbp: 1 байт (0x3c)mov rbp, rsp: 3 байта (0x3d–0x3f)mov [rbp-0x4], edi: 3 байта (0x40–0x42) - сохранитьint arg1mov eax, esi: 2 байта (0x43–0x44) - скопироватьchar arg2изesiвeaxmov [rbp-0x8], al: 3 байта (0x45–0x47) - сохранить младший байтal(это и естьchar)nop: 1 байт (0x48)pop rbp: 1 байт (0x49)ret: 1 байт (0x4a)
Итого: 19 байт (от 0x38 до 0x4a включительно)
Объяснение инструкций
По ABI, второй аргумент передаётся через esi (32 бита), даже если это char (1 байт). Компилятор выполняет следующие действия:
- Копирует весь
esi→eax(промежуточный регистр). - Извлекает младший байт
al(8 бит) и сохраняет на стек.
Это можно было сделать напрямую через инструкцию movzx (MOV with Zero-eXtend, которая копирует значение меньшего размера в регистр большего размера с заполнением старших битов нулями), или через использование sil (младший байт rsi), но уровень -O0 (без оптимизации) делает буквальные операции.
nop на адресе 0x48 появляется по той же причине, что и в предыдущих примерах - это маркер конца тела функции типа void при компиляции с опцией -O0 (без оптимизации).
main - полная функция
Содержимое объектного кода
000000000000004b <main>:
4b: f3 0f 1e fa endbr64 # 4 байта
4f: 55 push rbp # 1 байт
50: 48 89 e5 mov rbp,rsp # 3 байта
53: e8 00 00 00 00 call 58 <main+0xd> # 5 байт (hello)
58: bf 01 00 00 00 mov edi,0x1 # 5 байт
5d: e8 00 00 00 00 call 62 <main+0x17> # 5 байт (linus)
62: e8 00 00 00 00 call 67 <main+0x1c> # 5 байт (torvalds)
67: bf 02 00 00 00 mov edi,0x2 # 5 байт
6c: e8 00 00 00 00 call 71 <main+0x26> # 5 байт (tux)
71: be 04 00 00 00 mov esi,0x4 # 5 байт
76: bf 03 00 00 00 mov edi,0x3 # 5 байт
7b: e8 00 00 00 00 call 80 <main+0x35> # 5 байт (fox)
80: b8 00 00 00 00 mov eax,0x0 # 5 байт
85: 5d pop rbp # 1 байт
86: c3 ret # 1 байт
# Всего 60 байтОбъяснение инструкций
Здесь наибольший интерес представляют инструкции call. Рассмотрим для примера инструкцию вызова функции torvalds:
62: e8 00 00 00 00 call 67 <main+0x1c> # 5 байт (torvalds)
67: bf 02 00 00 00 mov edi,0x2 # 5 байт
Структура инструкции call (байтовое представление):
Адрес: 0x62
Байты: e8 00 00 00 00
^^ ^^^^^^^^^^^
│ └─ операнд (4 байта)
└─ опкод
e8 - код операции для инструкции call с относительным 32-битным смещением.
00 00 00 00 - плейсхолдер для относительного смещения, которое будет вычислено линковщиком. На этапе компиляции (создания объектного файла) относительное смещение до функции torvalds ещё не известно, так как её финальный адрес в памяти будет определён только при линковке. Поэтому компилятор оставляет плейсхолдер 00 00 00 00, который линковщик заменит на реальное смещение.
В аннотации objdump для инструкции call показывается адрес возврата - адрес следующей инструкции, куда вернётся выполнение после завершения вызова.
Что означает 67 <main+0x1c>?
0x67 - это адрес следующей инструкции после call, то есть адрес возврата.
Адрес Байт Описание
----- ----------- ----------------------------------
0x62: e8 опкод call
0x63: 00 байт 0 смещения (младший)
0x64: 00 байт 1 смещения
0x65: 00 байт 2 смещения
0x66: 00 байт 3 смещения (старший)
0x67: bf начало следующей инструкции (mov)
Адрес 0x67 = 0x62 (адрес call) + 0x5 (размер инструкции)
<main+0x1c> - это символическое смещение относительно начала функции main.
main начинается с адреса: 0x4b
текущий адрес возврата: 0x67
смещение = 0x67 - 0x4b = 0x1c
Читается как: “адрес возврата находится на смещении 0x1c (28 байт) от начала main”
objdump показывает оба варианта, чтобы было понятно:
- Куда вернётся выполнение (абсолютный адрес
0x67) - Где это относительно функции (смещение
+0x1cот началаmain)
call 67 <main+0x1c>
│ └─────────┘
│ │
│ └─ символическое представление:
│ "начало main + 28 байт"
│
└─ абсолютный адрес возврата: 0x67
ВПЕРЁД ⇒
⇐ НАЗАД
Источники
- Здесь будет список источников
Категория
Теги
- 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
- Релокация Релокация
- Компилятор Компиляторы
- Компиляция Компиляция
- Линковщик Линковщик
- Линковка Линковка
- Компоновщик Компоновщик
- Компоновка Компоновка
- GNU GNU