# Задача 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)
Проверь себя 1. Почему для отладки нельзя использовать тот же бинарник, что уже собран с `-O2` для финальной проверки производительности?
ОтветОптимизатор переставляет и удаляет инструкции, инлайнит вызовы и переиспользует регистры под разные переменные — отладочная информация от такой сборки не соответствует однозначно исходным строкам, `bt`/`print` покажут «прыгающие» или неверные значения; для отладки нужна отдельная сборка `-g -O0`.
2. Программа падает с `SIGSEGV` без санитайзеров, но при сборке с ASan вместо краша печатается сообщение о heap-buffer-overflow раньше, в другом месте кода. Почему это не противоречие?
ОтветASan останавливает программу в момент фактического выхода за границу буфера — в точке причины; без ASan повреждённая память могла не привести к немедленному краху, а вызвать `SIGSEGV` значительно позже, когда испорченные данные использовались уже в другом месте — это типичная картина «краш далеко от причины».
3. Чем `watch var` полезнее обычной точки останова (`break`) для поиска места, где переменная портится неожиданно?
Ответ`break` останавливает выполнение в заданной строке кода, но не знает, где именно переменная меняется; `watch var` — аппаратная точка наблюдения, которая остановит выполнение на любой инструкции, изменившей значение этой переменной, даже если изменение происходит из неожиданного места (например, из-за записи за границей другого буфера, затирающей соседнюю память).