Уроки D2-D7 (14 файлов): деревья/куча, epoll, потоки, gdb, графы, L2-L3, ДП, TCP, bash, ядро, docker, мок-интервью + cards_src 613
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
# D3, часть 3. Отладка: gdb, core dump, санитайзеры (урок)
|
||||
|
||||
Это не проверка, а урок: сначала разбираем механизм, потом решаешь `tasks/09_gdb`. Задача
|
||||
дня — не переписать падающую программу с нуля, а найти дефекты отладчиком и починить
|
||||
минимально.
|
||||
|
||||
## 1. Что нужно для отладки: `-g -O0`, DWARF
|
||||
|
||||
Флаг `-g` встраивает в бинарник отладочную информацию в формате **DWARF** — соответствие
|
||||
машинных адресов номерам строк исходника, именам переменных и их типам. Без `-g` gdb видит
|
||||
только адреса и ассемблер: `bt` покажет голые адреса вместо имён функций и номеров строк.
|
||||
|
||||
`-O0` отключает оптимизации компилятора и обязателен вместе с `-g` для комфортной отладки:
|
||||
оптимизатор переставляет и удаляет инструкции, инлайнит функции и переиспользует регистры под
|
||||
разные переменные, из-за чего отладочная информация перестаёт однозначно соответствовать
|
||||
исходному коду — строки «прыгают», переменные показывают не то значение. Типичная ошибка —
|
||||
попытаться отладить прод-бинарник, собранный с `-O2`: получится рассинхронизация строк и
|
||||
пропущенные шаги.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Какие два флага компиляции нужны для комфортной отладки в gdb? — `-g -O0`
|
||||
- base | Как называется формат отладочной информации, который встраивает `-g`? — DWARF
|
||||
- core | Почему `-O2` мешает отладке даже при наличии `-g`? — оптимизатор переставляет/удаляет инструкции и переиспользует регистры, отладочная информация перестаёт однозначно совпадать с исходником
|
||||
|
||||
Почему дальше: раз бинарник собран правильно, следующий шаг — реально запустить его под
|
||||
отладчиком и разобраться с базовыми командами.
|
||||
|
||||
## 2. Запуск и базовые команды
|
||||
|
||||
`gdb ./prog` запускает отладчик с указанным бинарником, `run args` внутри gdb передаёт
|
||||
управление процессу с аргументами командной строки, останавливаясь на точках останова или
|
||||
при сигнале.
|
||||
|
||||
- `bt` — печатает стек вызовов (backtrace): цепочку кадров от текущей функции до `main`.
|
||||
- `frame N` — переключает контекст `print`/`list` на конкретный кадр этого стека.
|
||||
- `info locals` / `info args` — печатают локальные переменные и аргументы текущего кадра.
|
||||
- `print x` (или `p x`) — печатает текущее значение переменной по её типу из отладочной
|
||||
информации.
|
||||
- `x/16xb ptr` — команда examine memory: печатает 16 (`N`) единиц в формате hex (`x`),
|
||||
единица — байт (`b`), начиная с адреса `ptr`; формат `x/NFU addr`.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Какая команда печатает стек вызовов в gdb? — `bt`
|
||||
- base | Что означает `x/16xb ptr`? — 16 байт в hex начиная с адреса `ptr` (формат `x/NFU addr`)
|
||||
- core | Чем `frame N` отличается от `bt`? — `bt` печатает весь стек, `frame N` переключает текущий контекст `print`/`info locals` на конкретный кадр из этого стека
|
||||
|
||||
Почему дальше: чтобы остановиться в нужном месте, а не просто дойти до конца программы,
|
||||
нужны точки останова.
|
||||
|
||||
## 3. Точки останова
|
||||
|
||||
`break file:line` или `break func` ставит точку останова — адрес, на котором выполнение
|
||||
приостанавливается при каждом попадании. `tbreak` — то же самое, но точка автоматически
|
||||
удаляется после первого срабатывания, удобно для одноразовой остановки без ручной очистки.
|
||||
|
||||
`watch var` — точка наблюдения (watchpoint): останавливает выполнение при каждом изменении
|
||||
значения переменной, а не в конкретной строке кода. `rwatch var` — остановка при чтении,
|
||||
`awatch var` — при чтении или записи (access). Механизм — аппаратные регистры отладки
|
||||
процессора: на x86 их 4 (`DR0`–`DR3`), поэтому одновременно можно держать лишь ограниченное
|
||||
число аппаратных watchpoint; gdb использует их, если может, иначе откатывается на медленный
|
||||
программный watchpoint (построчное выполнение с проверкой значения после каждой инструкции).
|
||||
`watch` незаменим, когда переменная меняется как будто сама по себе из другого места кода —
|
||||
без него пришлось бы вручную расставлять точки останова по подозрению.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Чем `tbreak` отличается от `break`? — `tbreak` удаляется автоматически после первого срабатывания
|
||||
- core | Чем `rwatch` отличается от `watch`? — `watch` реагирует на изменение значения, `rwatch` — на чтение переменной
|
||||
- deep | Сколько аппаратных регистров отладки на x86 ограничивают число одновременных аппаратных watchpoint? — 4 (`DR0`–`DR3`)
|
||||
|
||||
Почему дальше: точки останова дают место остановки, но дальше нужно управлять именно ходом
|
||||
выполнения — построчно или до конца функции.
|
||||
|
||||
## 4. Степание: `next`/`step`/`finish`/`until`
|
||||
|
||||
- `next` — выполняет текущую строку целиком, включая вызовы функций внутри неё, не заходя
|
||||
внутрь них.
|
||||
- `step` — заходит внутрь вызываемой функции, если для неё есть отладочная информация.
|
||||
- `finish` — выполняет до возврата из текущей функции, печатает возвращаемое значение.
|
||||
- `until` (без аргумента) — продолжает выполнение до строки с номером больше текущей в
|
||||
текущем кадре, удобно чтобы выйти из цикла, не проходя его пошагово итерацию за итерацией.
|
||||
|
||||
Если бы `step` всегда заходил внутрь, отладка кода с вызовами библиотечных функций без
|
||||
отладочной информации была бы мучительной — `next` даёт способ пропустить неинтересную
|
||||
функцию, не теряя контроль над остальным ходом программы.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Чем `next` отличается от `step`? — `next` не заходит внутрь вызываемых функций, `step` заходит
|
||||
- core | Что делает `finish`? — выполняет до возврата из текущей функции и печатает возвращаемое значение
|
||||
- core | Зачем нужен `until` внутри цикла? — продолжить до строки с номером больше текущей, не проходя цикл пошагово
|
||||
|
||||
Почему дальше: все эти команды одинаково работают и при разборе уже случившегося падения —
|
||||
после срабатывания сигнала или по сохранённому снимку памяти (core dump).
|
||||
|
||||
## 5. Разбор падения: core dump, `ulimit -c`, `bt` по кадрам
|
||||
|
||||
Первый путь — запустить программу прямо под gdb и дождаться сигнала (обычно `SIGSEGV`, код
|
||||
139 = 128+11): gdb сам остановится на инструкции, вызвавшей сбой, `bt` покажет полный стек
|
||||
вызовов, а `print` значений указателей и переменных в нужных кадрах обычно сразу показывает,
|
||||
например, что указатель равен `nullptr` или мусорному значению.
|
||||
|
||||
Второй путь — по **core dump**: файл-снимок памяти процесса, который ядро ОС сохраняет в
|
||||
момент сигнала, приводящего к аварийному завершению, — все сегменты адресного пространства
|
||||
(стек, куча, регистры процессора, список загруженных библиотек). Открывают его отдельно:
|
||||
`gdb prog core` — и получают тот же `bt`/`print`, но постфактум, без необходимости
|
||||
воспроизводить падение заново. Core dump — единственный способ разобрать баг, который
|
||||
воспроизводится редко или только под нагрузкой в проде, куда заранее интерактивный отладчик
|
||||
не прицепишь.
|
||||
|
||||
По умолчанию система часто отключает сохранение core-файлов — перед тем как ждать падение,
|
||||
выставляют `ulimit -c unlimited`. Типичная ошибка — забыть это сделать и потерять
|
||||
единственный шанс поймать редкий баг; вторая типичная ошибка — пересобрать бинарник (даже
|
||||
без изменения логики) перед анализом core — адреса не совпадут с записанными в core, и gdb
|
||||
покажет несогласованный стек вместо реального.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Каким кодом завершается процесс при SIGSEGV? — 139 (128+11)
|
||||
- base | Какой командой разрешить сохранение core-файлов перед ожиданием падения? — `ulimit -c unlimited`
|
||||
- core | Как открыть core dump вместе с бинарником в gdb? — `gdb prog core`
|
||||
- core | Почему пересборка бинарника перед анализом core ломает разбор? — адреса в новом бинарнике не совпадают с адресами, записанными в core, gdb покажет несогласованный стек
|
||||
|
||||
Почему дальше: падение может быть не одиночным потоком — если программа многопоточная, нужно
|
||||
отдельно смотреть, какой именно поток и в каком состоянии упал или завис.
|
||||
|
||||
## 6. Отладка многопоточности
|
||||
|
||||
`info threads` показывает все потоки процесса и их текущее состояние (на какой строке/в
|
||||
какой функции остановлен каждый). `thread N` переключает текущий контекст отладчика (для
|
||||
`bt`, `frame`, `print`) на поток с номером `N`.
|
||||
|
||||
Это основной способ диагностировать зависший дедлок: программа не падает и не пишет ничего в
|
||||
лог, просто стоит — `info threads` и просмотр стека (`bt`) каждого потока по очереди
|
||||
показывают, какой поток на каком мьютексе застрял и кого он, в свою очередь, ждёт.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Какая команда gdb показывает все потоки процесса разом? — `info threads`
|
||||
- core | Как переключиться на конкретный поток по номеру для `bt`/`print`? — `thread N`
|
||||
- core | Как gdb помогает диагностировать дедлок, если программа просто зависла без вывода? — `info threads` + `bt` по каждому потоку показывают, кто на каком мьютексе застрял
|
||||
|
||||
Почему дальше: не каждый краш находится ровно там, где gdb его показывает, — иногда точка
|
||||
остановки и точка реальной ошибки в коде далеко друг от друга.
|
||||
|
||||
## 7. Почему краш «далеко от причины»: ASAN против gdb
|
||||
|
||||
Переполнение буфера или запись по неверному указателю портит чужую память, но крах может
|
||||
случиться значительно позже — например, когда программа попытается использовать уже
|
||||
испорченный указатель совсем в другом месте кода. gdb в момент самого краша покажет именно
|
||||
**точку симптома** (где программа реально упала), а не точку, где память была испорчена
|
||||
изначально.
|
||||
|
||||
Санитайзеры (ASAN — `-fsanitize=address`, UBSAN — `-fsanitize=undefined`) инструментируют
|
||||
каждое обращение к памяти на этапе компиляции и останавливают программу с диагностикой в
|
||||
момент **нарушения**, а не когда испорченные данные позже вызовут крах в неожиданном месте —
|
||||
то есть показывают точку причины напрямую. ASAN дополнительно ловит выход за границы,
|
||||
use-after-free и двойное освобождение через «красные зоны» вокруг выделенных блоков и
|
||||
теневую карту памяти; в связке с LeakSanitizer он же в конце работы программы репортит
|
||||
утечки. Санитайзеры — это перекомпиляция с проверками, а не отдельная программа поверх
|
||||
готового бинарника, и включаются одним флагом на этапе сборки.
|
||||
|
||||
**Факты для карточек**
|
||||
- core | В чём разница между точкой, которую покажет gdb при краше, и точкой, которую покажет ASAN? — gdb показывает точку симптома (где реально упало), ASAN — точку причины (момент нарушения)
|
||||
- base | Каким флагом компиляции включается ASAN? — `-fsanitize=address`
|
||||
- base | Каким флагом компиляции включается UBSAN? — `-fsanitize=undefined`
|
||||
|
||||
Почему дальше: санитайзеры требуют пересборки с флагами — если такой возможности нет
|
||||
(например, готовый чужой бинарник или библиотека), есть альтернатива без пересборки.
|
||||
|
||||
## 8. `valgrind --tool=memcheck` как альтернатива
|
||||
|
||||
Valgrind ищет ошибки работы с памятью и утечки без пересборки программы с особыми флагами.
|
||||
Модуль **memcheck** запускает бинарник внутри собственной виртуальной машины — эмулирует
|
||||
каждую машинную инструкцию на лету, отслеживая состояние памяти (инициализирована/не
|
||||
инициализирована, выделена/освобождена), и на каждое подозрительное обращение выводит
|
||||
диагностику со стеком вызовов.
|
||||
|
||||
Эмуляция каждой инструкции в софтверной VM принципиально тяжелее, чем компиляторная
|
||||
инструментация ASAN (которая добавляет проверки только вокруг реальных обращений к памяти
|
||||
уже в нативном коде) — valgrind ощутимо медленнее санитайзеров, счёт идёт на кратное
|
||||
замедление, а не на проценты накладных расходов. Главное преимущество — не нужен доступ к
|
||||
исходникам и пересборка, поэтому его применяют на готовых бинарниках или сторонних
|
||||
библиотеках, где ASAN не подключить; в собственном проекте с доступом к сборке обычно
|
||||
предпочитают санитайзеры именно из-за скорости на CI.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Какой модуль valgrind ищет ошибки памяти? — `memcheck` (`valgrind --tool=memcheck`)
|
||||
- core | Почему valgrind медленнее ASAN? — эмулирует каждую машинную инструкцию в софтверной VM, а не добавляет проверки только вокруг обращений к памяти в нативном коде
|
||||
- core | Когда valgrind предпочтительнее санитайзеров? — когда нет доступа к исходникам/пересборке (готовый бинарник, сторонняя библиотека)
|
||||
|
||||
## 9. Задача дня: `crash.c` (`tasks/09_gdb`)
|
||||
|
||||
`crash.c` — программа, которая падает на части входов и портит память; задача — найти
|
||||
дефекты отладчиком и починить минимально, не переписывая с нуля. Ожидаемое поведение после
|
||||
починки:
|
||||
|
||||
- `./crash` (без аргумента) печатает `len=5`, код возврата 0;
|
||||
- `./crash <слово>` печатает `len=<длина слова>`, код возврата 0;
|
||||
- `./crash ""` печатает `len=0`, код возврата 0;
|
||||
- сборка с `-fsanitize=address,undefined` не даёт ни одного сообщения об ошибке.
|
||||
|
||||
Порядок работы: собрать `gcc -g -O0 -fsanitize=address,undefined crash.c -o crash`; под
|
||||
`gdb ./crash` командами `run`, `bt`, `frame`, `info locals`, `watch` разобраться, что именно
|
||||
портит память, а что приводит к падению при выходе — это разные дефекты, и падение при
|
||||
выходе из программы обычно означает испорченный служебный указатель (например, порчу стека
|
||||
или метаданных кучи), который проявляется только в момент разрушения объекта или выхода из
|
||||
функции. Проверка — `python3 grade.py 09`: компилирует `crash.c` компилятором C (gcc) с
|
||||
ASAN/UBSAN и прогоняет 5 входов; `answer.txt` проверяется ревью — важен ход разбора
|
||||
командами отладчика, а не только итоговый результат.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Какой командой собирают `crash.c` для отладки с санитайзерами? — `gcc -g -O0 -fsanitize=address,undefined crash.c -o crash`
|
||||
- base | Что печатает `./crash` без аргументов после починки? — `len=5`, код возврата 0
|
||||
- core | Почему падение может произойти не в строке с самой ошибкой, а при выходе из программы? — испорченный служебный указатель (стек/метаданные кучи) проявляется только в момент разрушения объекта, а не в момент самой порчи
|
||||
|
||||
Ссылка на задачу этого дня: `tasks/09_gdb` — проверка `python3 grade.py 09`, 5 входов, ASAN/UBSAN
|
||||
чистые.
|
||||
|
||||
<details>
|
||||
<summary>Проверь себя</summary>
|
||||
|
||||
1. Собрал бинарник с `-g -O2` и заметил, что `next` в gdb «перепрыгивает» через строки не по
|
||||
порядку. В чём причина и что нужно изменить в сборке?
|
||||
<details><summary>Ответ</summary>Оптимизатор при `-O2` переставляет и инлайнит инструкции,
|
||||
отладочная информация перестаёт однозначно соответствовать исходнику; нужно пересобрать с
|
||||
`-O0` (оставив `-g`).</details>
|
||||
|
||||
2. Программа падает по `SIGSEGV` только раз в несколько дней под нагрузкой в проде. Какие две
|
||||
вещи нужно сделать заранее, чтобы разобрать этот краш постфактум?
|
||||
<details><summary>Ответ</summary>Выставить `ulimit -c unlimited`, чтобы ядро сохранило core
|
||||
dump при падении, и не пересобирать бинарник между падением и анализом — иначе адреса в
|
||||
core не совпадут с бинарником и `gdb prog core` покажет несогласованный стек.</details>
|
||||
|
||||
3. ASAN указывает на строку записи за границу массива, а обычный gdb без санитайзеров на этом
|
||||
же баге падал бы совсем в другом месте кода. Почему так?
|
||||
<details><summary>Ответ</summary>Порча памяти повреждает чужие данные, но видимый крash
|
||||
может случиться значительно позже, когда программа использует уже испорченные данные в
|
||||
другом месте — gdb без санитайзера показывает точку симптома (где реально упало), ASAN
|
||||
инструментирует каждое обращение и останавливает программу прямо в момент нарушения,
|
||||
то есть в точке причины.</details>
|
||||
|
||||
4. Нужно проверить на утечки готовую стороннюю библиотеку без исходников и без возможности
|
||||
пересобрать её с ASAN. Какой инструмент подходит и почему не санитайзер?
|
||||
<details><summary>Ответ</summary>`valgrind --tool=memcheck` — он эмулирует уже готовый
|
||||
бинарник в софтверной VM и не требует пересборки с флагами `-fsanitize=...`, в отличие от
|
||||
санитайзеров, которые требуют перекомпиляции исходного кода.</details>
|
||||
|
||||
</details>
|
||||
|
||||
## Материалы
|
||||
|
||||
- man 1 gdb — https://man7.org/linux/man-pages/man1/gdb.1.html
|
||||
- Документация GDB (sourceware) — https://sourceware.org/gdb/current/onlinedocs/gdb.html/
|
||||
- GCC: флаги инструментации (ASAN/UBSAN) — https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
|
||||
- Clang: документация AddressSanitizer — https://clang.llvm.org/docs/AddressSanitizer.html
|
||||
- man 2 setrlimit (ulimit -c / RLIMIT_CORE) — https://man7.org/linux/man-pages/man2/setrlimit.2.html
|
||||
- man 7 signal (SIGSEGV) — https://man7.org/linux/man-pages/man7/signal.7.html
|
||||
Reference in New Issue
Block a user