137 lines
14 KiB
Markdown
137 lines
14 KiB
Markdown
# Задача 09 — отладка: найти и исправить дефекты (gdb)
|
||
|
||
В `crash.c` лежит программа, которая падает на части входов и портит память.
|
||
Задача — не переписать её с нуля, а **найти дефекты отладчиком и починить минимально**.
|
||
|
||
## Ожидаемое поведение после починки
|
||
|
||
- `./crash` (без аргумента) печатает `len=5` и завершается с кодом 0;
|
||
- `./crash <слово>` печатает `len=<длина слова>` и завершается с кодом 0;
|
||
- `./crash ""` печатает `len=0` и завершается с кодом 0;
|
||
- сборка с `-fsanitize=address,undefined` не даёт ни одного сообщения об ошибке.
|
||
|
||
**Факты для карточек**
|
||
- base | Что печатает `./crash` без аргументов после починки? — `len=5`, код возврата 0
|
||
- base | Что печатает `./crash ""` после починки? — `len=0`, код возврата 0
|
||
- core | Какие два санитайзера должны не давать сообщений после починки? — ASan и UBSan (`-fsanitize=address,undefined`)
|
||
|
||
Почему дальше: чтобы вообще видеть номера строк и имена переменных в отладчике, а не голый
|
||
ассемблер, программу нужно собрать определённым образом — с этого и начинается разбор.
|
||
|
||
## Сборка с отладочной информацией и санитайзерами
|
||
|
||
```
|
||
gcc -g -O0 -fsanitize=address,undefined crash.c -o crash
|
||
```
|
||
|
||
`-g` встраивает в бинарник отладочную информацию в формате DWARF — таблицу соответствия
|
||
машинных адресов номерам строк исходника, именам переменных, их типам и границам функций.
|
||
Именно из DWARF gdb берёт всё, что показывает человеку: `bt` печатает имена функций и строки
|
||
вместо голых адресов, `print x` знает тип `x` и умеет напечатать его по правилам этого типа
|
||
(структуру — по полям, массив — по элементам), `list` показывает исходный код вокруг текущей
|
||
точки. Без `-g` DWARF-секций в бинарнике нет, и gdb видит только адреса и ассемблер.
|
||
|
||
`-O0` отключает оптимизации компилятора: оптимизатор переставляет и удаляет инструкции,
|
||
инлайнит функции и переиспользует один регистр под несколько переменных с непересекающимся
|
||
временем жизни — из-за этого отладочная информация от `-O2` часто не соответствует
|
||
однозначно исходному коду (строки «прыгают», переменная в `print` показывает не то значение,
|
||
потому что регистр уже переиспользован под другую переменную). Отсюда правило: для отладки
|
||
всегда пересобирают отдельно с `-g -O0`, а не отлаживают прод-сборку с оптимизациями.
|
||
|
||
`-fsanitize=address,undefined` — это ASan и UBSan вместе, инструментирование на этапе
|
||
компиляции, а не отдельный инструмент поверх готового бинарника:
|
||
- **ASan** окружает каждый выделенный блок памяти недоступными «красными зонами» и ведёт
|
||
теневую карту состояния памяти — ловит выход за границы буфера, use-after-free и двойное
|
||
освобождение прямо в момент обращения, с точным стеком, а не когда испорченные данные
|
||
позже вызовут крах в случайном другом месте;
|
||
- **UBSan** инструментирует места, где поведение по стандарту C не определено — знаковое
|
||
переполнение, сдвиг за пределы разрядности типа, разыменование `NULL` с невалидным типом —
|
||
и печатает конкретную строку кода нарушения.
|
||
|
||
**Ловушки**
|
||
- Собрать без `-g` и пытаться понять крах по голому `bt` с адресами вместо строк → отладка
|
||
вслепую по ассемблеру, не соответствует задаче «найти минимальными правками».
|
||
- Отлаживать сборку с `-O2` → строки в `bt`/`print` не совпадают с реальным местом бага
|
||
из-за инлайнинга и переиспользования регистров → ложный след при поиске дефекта.
|
||
- Пропустить пересборку с `-fsanitize=address,undefined` перед финальной проверкой → баг,
|
||
который не падает явным креша при обычном запуске (например, чтение за границей внутри
|
||
тем же выделенного блока), останется незамеченным до `grade.py`.
|
||
|
||
**Факты для карточек**
|
||
- base | Что даёт флаг `-g` при сборке для gdb? — отладочную информацию (DWARF): соответствие адресов строкам, именам и типам переменных
|
||
- core | Почему для отладки собирают с `-O0`, а не с `-O2`? — оптимизатор переставляет/удаляет инструкции и переиспользует регистры, из-за чего строки и значения в отладчике перестают однозначно соответствовать исходнику
|
||
- core | Что ловит ASan из перечисленного: выход за границы, use-after-free, двойное освобождение? — все три, в момент обращения к памяти, а не постфактум
|
||
- deep | Что именно ловит UBSan, в отличие от ASan? — неопределённое поведение по стандарту C (знаковое переполнение, сдвиг за пределы разрядности), а не ошибки работы с памятью
|
||
|
||
Почему дальше: раз отладочная информация уже в бинарнике, нужно знать конкретные команды
|
||
gdb, которыми по ней ходят при разборе краша.
|
||
|
||
## Команды gdb для разбора краша
|
||
|
||
Рабочий цикл: `gdb ./crash`, затем `run [аргумент]` — передаёт управление процессу; если
|
||
процесс получает сигнал (обычно `SIGSEGV` от порчи памяти или abort от санитайзера), gdb сам
|
||
останавливается на инструкции, вызвавшей сбой.
|
||
|
||
- `bt` — стек вызовов, цепочка кадров от текущей функции до `main`; первое, на что смотрят
|
||
после остановки — показывает, в какой функции и через какие вызовы дошли до краша.
|
||
- `frame N` — переключает контекст `print`/`list` на конкретный кадр стека из `bt`, чтобы
|
||
посмотреть локальные переменные вызывающей функции, а не только текущей.
|
||
- `info locals` — печатает все локальные переменные текущего кадра с их значениями по
|
||
информации из DWARF.
|
||
- `watch var` — аппаратная точка наблюдения: останавливает выполнение при каждом изменении
|
||
значения переменной. Нужна, когда переменная меняется как будто сама по себе из другого
|
||
места кода (типичный симптом переполнения буфера, которое затирает соседнюю память) — без
|
||
`watch` пришлось бы расставлять точки останова по подозрению вручную.
|
||
- `print x` — печатает текущее значение `x`, по типу из DWARF (структуру — по полям).
|
||
|
||
Санитайзер добавляет отдельный слой: при нарушении (ASan/UBSan) программа останавливается
|
||
сама, печатая стек и описание нарушения ещё до того, как повреждение памяти успело привести
|
||
к крашу в другом, не связанном с причиной месте — это часто быстрее, чем ловить последствия
|
||
вручную в gdb по голому `SIGSEGV`.
|
||
|
||
**Факты для карточек**
|
||
- base | Какая команда gdb показывает цепочку вызовов до краша? — `bt`
|
||
- core | Чем `watch var` отличается от обычного `break`? — останавливает выполнение при каждом изменении значения переменной, а не в заданной точке кода
|
||
- core | Зачем нужен `frame N` после `bt`? — переключает контекст `print`/`info locals` на конкретный кадр стека, чтобы смотреть переменные не только текущей функции
|
||
|
||
## Отчёт и проверка
|
||
|
||
Исправить `crash.c` (минимальные правки, стиль сохранить). Заполнить `answer.txt`: сколько
|
||
дефектов нашёл, какие именно, какими командами отладчика это подтвердил (по шагам), почему
|
||
падало именно так.
|
||
|
||
Проверка: `python3 grade.py 09` — компилирует `crash.c` (компилятор C, gcc) с ASAN/UBSAN и
|
||
прогоняет 5 входов. `answer.txt` проверяется ревью (это часть оценки: важен ход разбора, а
|
||
не только результат).
|
||
|
||
**Факты для карточек**
|
||
- base | Сколько входов прогоняет `grade.py 09`? — 5
|
||
- base | Каким компилятором собирается `crash.c` на проверке? — gcc (компилятор C)
|
||
|
||
<details>
|
||
<summary>Проверь себя</summary>
|
||
|
||
1. Почему для отладки нельзя использовать тот же бинарник, что уже собран с `-O2` для
|
||
финальной проверки производительности?
|
||
<details><summary>Ответ</summary>Оптимизатор переставляет и удаляет инструкции, инлайнит
|
||
вызовы и переиспользует регистры под разные переменные — отладочная информация от такой
|
||
сборки не соответствует однозначно исходным строкам, `bt`/`print` покажут «прыгающие» или
|
||
неверные значения; для отладки нужна отдельная сборка `-g -O0`.</details>
|
||
|
||
2. Программа падает с `SIGSEGV` без санитайзеров, но при сборке с ASan вместо краша печатается
|
||
сообщение о heap-buffer-overflow раньше, в другом месте кода. Почему это не противоречие?
|
||
<details><summary>Ответ</summary>ASan останавливает программу в момент фактического выхода
|
||
за границу буфера — в точке причины; без ASan повреждённая память могла не привести к
|
||
немедленному краху, а вызвать `SIGSEGV` значительно позже, когда испорченные данные
|
||
использовались уже в другом месте — это типичная картина «краш далеко от причины».</details>
|
||
|
||
3. Чем `watch var` полезнее обычной точки останова (`break`) для поиска места, где переменная
|
||
портится неожиданно?
|
||
<details><summary>Ответ</summary>`break` останавливает выполнение в заданной строке кода,
|
||
но не знает, где именно переменная меняется; `watch var` — аппаратная точка наблюдения,
|
||
которая остановит выполнение на любой инструкции, изменившей значение этой переменной, даже
|
||
если изменение происходит из неожиданного места (например, из-за записи за границей другого
|
||
буфера, затирающей соседнюю память).</details>
|
||
|
||
</details>
|