Перейти к основному содержимому

Чтение исполняемого файла и листинга

Разработчику

Контекст: исполняемый 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, 1mov 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 — BSS
  • U — неразрешённый (ожидает 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.


Типичные артефакты, которые путают новичков

  1. NOP-заполнение — выравнивание входа функции на 16 байт.
  2. Две метки на один адрес — оптимизация линкера / альтернативные имена символов.
  3. Адреса 0x7f... — позиционно-независимый код (PIE); при каждом запуске смещение (ASLR).
  4. Строка в .rodata — в листинге не видна; смотрят objdump -s -j .rodata или strings ./app.

Минимальный чек-лист разбора

  1. readelf -h — 64-bit, ELF, entry.
  2. readelf -S — есть .text, .rodata, размеры разумны.
  3. objdump -d -M intel — логика _start / main.
  4. nm — какие функции экспортированы, что тянется из libc.
  5. При расхождении с ожиданием — пересобрать с -g и сравнить в GDB по шагам.

Быстрый сценарий "нашёл баг в бинарнике"

  1. Зафиксируйте адрес падения из отладчика.
  2. Посмотрите участок вокруг него в objdump -d -M intel.
  3. Найдите, какие регистры и флаги должны были быть к этому моменту.
  4. Сравните с фактическим состоянием в отладчике.
  5. Откатитесь к последнему условному переходу/вызову, где состояние стало расходиться.

Такой подход быстрее, чем попытка сразу читать весь бинарник подряд.


Дальше

Практика отладки — справочник, раздел про GDB. Строковые циклы REPотдельная статья. SIMD и float в листинге — Числа с плавающей точкой и SIMD. Бинарник Windows (PE) — Windows x64, WinAPI и отличия от Linux.