Files

137 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Задача 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>