Чтение исполняемого файла и листинга
Контекст: исполняемый ELF под Linux x86-64. Инструменты —
readelf,objdump, отладчик. Обзор RE — в истории ассемблера.
Дизассемблирование ELF - чтение бинарника
Исходника нет — остаётся дизассемблирование: восстановление мнемоник из байт .text. Даже со своим кодом листинг полезен — увидеть, что NASM и линкер реально положили в файл, где PLT, выравнивание, RIP-relative адреса. На Linux objdump -d по умолчанию печатает AT&T; для сопоставления с NASM — objdump -d -M intel (см. Intel/AT&T).
Для новичка это очень важный шаг: листинг убирает иллюзию, что "компилятор сделал что-то магическое". Вы видите точные инструкции, реальные адреса и фактический поток выполнения. После нескольких таких разборов заметно легче писать и отлаживать собственный asm-код.
Из чего состоит ELF
| Секция | Содержимое |
|---|---|
.text | машинные инструкции |
.rodata | константы (строки, таблицы) |
.data | инициализированные глобальные данные |
.bss | нулевые данные при загрузке (в файле только размер) |
.symtab / .dynsym | имена функций и переменных |
.rela.* | как переписать адреса при загрузке (PIC, линковка с libc) |
Заголовок ELF описывает архитектуру (x86-64), точку входа (e_entry) и таблицу секций.
readelf -h ./app # заголовок
readelf -S ./app # секции
readelf -s ./app # символы
Дизассемблирование
objdump -d -M intel ./app
-M intel — синтаксис как в NASM (dest, src), а не AT&T.
Фрагмент может выглядеть так:
0000000000401000 <_start>:
401000: b8 01 00 00 00 mov eax,0x1
401005: bf 01 00 00 00 mov edi,0x1
...
Слева — виртуальный адрес после загрузки, справа — шестнадцатеричные байты и мнемоники.
С исходником рядом:
objdump -d -M intel -S ./app # если есть отладочные символы (-g при сборке)
Сопоставление с NASM-исходником
| В исходнике | В листинге |
|---|---|
mov rax, 1 | mov rax,0x1 — может быть другая длина инструкции |
lea rdi, [rel msg] | RIP-relative: lea rdi,[rip+0x...] |
call printf | часто call *...@plt — прыжок через таблицу процедур линковки |
section .bss | в файле секция пустая, размер в заголовке |
локальная метка .loop | может отсутствовать в символах; только адреса |
Не каждая метка попадает в символьную таблицу — локальные и static в C скрыты.
Таблица символов
nm ./app
Буквы состояния:
T/t— код в.text(глобальный / локальный)D/d— инициализированные данныеB/b— BSSU— неразрешённый (ожидает libc при линковке)
Имя _start или main — точка входа. Несколько U на printf — программа слинкована с динамической libc.
Отладочная информация
Сборка с символами:
gcc -g main.c add.o -o app
В GDB — disassemble /s my_add, info registers, x/s msg.
Без -g остаются только публичные символы и догадки по вызовам API.
Типичные артефакты, которые путают новичков
- NOP-заполнение — выравнивание входа функции на 16 байт.
- Две метки на один адрес — оптимизация линкера / альтернативные имена символов.
- Адреса 0x7f... — позиционно-независимый код (PIE); при каждом запуске смещение (ASLR).
- Строка в .rodata — в листинге не видна; смотрят
objdump -s -j .rodataилиstrings ./app.
Минимальный чек-лист разбора
readelf -h— 64-bit, ELF, entry.readelf -S— есть.text,.rodata, размеры разумны.objdump -d -M intel— логика_start/main.nm— какие функции экспортированы, что тянется из libc.- При расхождении с ожиданием — пересобрать с
-gи сравнить в GDB по шагам.
Быстрый сценарий "нашёл баг в бинарнике"
- Зафиксируйте адрес падения из отладчика.
- Посмотрите участок вокруг него в
objdump -d -M intel. - Найдите, какие регистры и флаги должны были быть к этому моменту.
- Сравните с фактическим состоянием в отладчике.
- Откатитесь к последнему условному переходу/вызову, где состояние стало расходиться.
Такой подход быстрее, чем попытка сразу читать весь бинарник подряд.
Дальше
Практика отладки — справочник, раздел про GDB. Строковые циклы REP — отдельная статья. SIMD и float в листинге — Числа с плавающей точкой и SIMD. Бинарник Windows (PE) — Windows x64, WinAPI и отличия от Linux.