open atlas
↑ К треку
Инженерная практика ENG · 08 · 01

Брейкпоинты и watchpoint-ы: как на самом деле работают INT3, четыре debug-регистра и условный eval

Брейкпоинт — это байт 0xCC, вшитый в код, или один из четырёх аппаратных debug-регистров. Условный брейкпоинт вычисляется на каждом срабатывании и бывает в 1000 раз медленнее. Watchpoint ловит запись по адресу. Оптимизированная сборка ломает пошаговую отладку.

ENG Senior ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Баг проявлялся только на клиенте #4 815 162 ночной сверки и только иногда. Джун поставил брейкпоинт внутри цикла по строкам и запустил под отладчиком; задача, что обычно заканчивалась за 90 секунд, шла уже 40 минут — потому что отладчик два миллиона раз в секунду проваливался в ядро, чтобы проверить условие, которое инженер вписал в брейкпоинт. Сеньор поменял одну вещь: вместо условного брейкпоинта на цикле она поставила аппаратный watchpoint на единственное поле баланса счёта, сказала отладчику продолжить — и процесс шёл почти на полной скорости до той самой инструкции, что записала отрицательный баланс, а затем замер с живым стеком виновника. Условие, которое не удавалось воспроизвести чтением кода, стало одним trap-ом по адресу памяти. Разница между 40 минутами и 3 секундами — в знании, что один из этих брейкпоинтов это программный trap в отладчике, а другой — кремний, следящий за шиной.

Программные брейкпоинты: вшивание байта 0xCC

Обычный брейкпоинт — не магия: отладчик переписывает первый байт целевой инструкции на 0xCC, опкод INT3 в x86, и запоминает исходный байт. Когда исполнение доходит до этого адреса, CPU поднимает breakpoint-trap, ядро доставляет SIGTRAP, и отладчик (он подключён через ptrace) просыпается, восстанавливает исходный байт, даёт осмотреть состояние, а чтобы продолжить — делает один шаг по восстановленной инструкции и снова вшивает 0xCC. Поэтому программных брейкпоинтов неограниченно много — каждый стоит один пропатченный байт памяти, — но они работают только на записываемых страницах кода: нельзя вшить 0xCC в read-only флеш или страницу, на запись в которую нет прав, и ровно тогда приходишь к аппаратным брейкпоинтам.

У модели патча есть резкое следствие: брейкпоинт на адресе, который цель позже перезаписывает или перекомпилирует (JIT, самомодифицирующийся код, hot-patch функции), тихо перестаёт срабатывать — твой 0xCC затёрли. Delve и gdb это прячут, но механизм проступает, как только отлаживаешь JIT-рантайм.

Аппаратные брейкпоинты и watchpoint-ы: четыре регистра, без патча

x86-64 выставляет четыре debug-регистра, DR0DR3, каждый хранит адрес, плюс DR7 задаёт режим каждого: брейк на исполнение, на запись или на чтение/запись, длиной 1, 2, 4 или 8 байт. Поскольку CPU сравнивает эти адреса на шине аппаратно, аппаратный брейкпоинт ничего не патчит и работает на read-only страницах — но их всего четыре, на поток, и точка. Watchpoint — тот же механизм, наведённый на данные: ставишь DR0 следить за 8-байтным полем на запись, и CPU делает trap в тот же миг, как любая инструкция в него пишет, неважно каким путём. Это и есть приём сеньора из Hook: вместо «какая из тысячи строк портит это поле?» дать кремнию ответить, следя за самим полем.

Числа, что решают стратегию: аппаратный watchpoint стоит почти ноль рантайм-оверхеда, ведь сравнение в конвейере бесплатно, тогда как программный watchpoint — то, на что отладчик откатывается, когда все четыре регистра заняты, — делает по шагу через всю программу и сравнивает значение после каждой инструкции, замедляя цель в 100–1000 раз. gdb молча переключится с аппаратного на программный watchpoint, если попросить пятый; info watchpoints и сообщение Hardware watchpoint против Watchpoint — то, как узнаёшь, что получил.

Викторина

Ты просишь gdb следить за 5-й областью памяти, уже поставив четыре аппаратных watchpoint-а. Программа, прежде быстрая под отладчиком, теперь еле ползёт. Что произошло?

Почему это работает

Почему условный брейкпоинт стоит куда дороже watchpoint-а на той же логике? Условный брейкпоинт всё равно срабатывает аппаратно или через 0xCC каждый раз, как достигнут адрес, а затем отладчик вычисляет выражение условия в своём интерпретаторе и решает, останавливаться ли. В плотном цикле это полный цикл trap-в-ядро, переключение-на-отладчик, вычисление, продолжение на каждой итерации — легко в 1000 раз дороже тела цикла. Аппаратный watchpoint на данных переворачивает вопрос: CPU остаётся в горячем цикле на полной скорости и делает trap только на той одной записи, что нужна. Подгоняй trap под редкое событие, а не под частую строку.

Почему оптимизированная сборка ломает пошаговую отладку

Поставь брейкпоинт на строку в -O2-сборке — и можешь встать на строку, что «уже отработала», или шагнуть в строку, исполняющуюся трижды на один исходный оператор, или услышать, что локальная переменная <optimized out>. Это не баг отладчика. Оптимизатор переставляет инструкции через границы строк, инлайнит вызовы (нет кадра, в который шагать), держит переменные лишь в регистрах, что переиспользуются, и сливает одинаковые хвосты ветвей — так что взаимно-однозначного отображения исходной строки в диапазон инструкций, на которое опирается пошаговая отладка, больше нет. DWARF (стандарт отладочной информации, встроенной компилятором в бинарь) записывает best-effort таблицу строк, но для инлайненной, переставленной, размещённой по регистрам программы эта таблица lossy по построению. Сеньорский поток: воспроизвести под -O0 -g, когда можешь себе позволить, а когда нет (баг лезет только в оптимизации — частый Heisenbug) — спуститься к пошаговой отладке по инструкциям и читать регистры напрямую, а не верить номерам строк и локалам.

Викторина

Отлаживая -O2-сборку, ты ставишь брейкпоинт на строку 40, но он встаёт, локал показывает <optimized out>, а шаги пропускают строки. Тот же код на -O0 шагает чисто. Лучшее прочтение?

Вспомните перед уходом
  1. 01
    Сравни программные брейкпоинты, аппаратные брейкпоинты и watchpoint-ы по механизму и лимитам.
  2. 02
    Почему условные брейкпоинты в горячих циклах убивают производительность и почему -O2 ломает пошаговую отладку?
Итог

Брейкпоинт — одна из двух физических вещей. Программный брейкпоинт вшивает байт 0xCC INT3 поверх первого байта инструкции; на срабатывании CPU делает trap, ядро доставляет SIGTRAP ptrace-подключённому отладчику, тот восстанавливает байт, даёт посмотреть, затем шагает и снова вшивает — их неограниченно, но только на записываемом коде, и они тихо побеждаются JIT-ом, что перезаписывает пропатченную страницу. Аппаратный брейкпоинт использует один из всего четырёх debug-регистров DR0-DR3, под управлением DR7 для режимов исполнение/запись/чтение и длин 1-8 байт, сравнивая адреса на шине бесплатно и работая на read-only страницах. Watchpoint наводит тот же регистровый механизм на данные и делает trap на любой записи по адресу, превращая «какая из тысячи строк портит это поле?» в один trap — разница между 40-минутным циклом с условным брейкпоинтом и 3-секундным ответом. Условные брейкпоинты дороги, ведь они делают trap и переоценку на каждом срабатывании, до 1000x тела горячего цикла, тогда как аппаратный watchpoint остаётся вне цикла и срабатывает лишь на редком событии; попроси пятое слежение — и отладчик деградирует до программной эмуляции по шагу в 100-1000 раз медленнее. Оптимизированные -O2-сборки переставляют, инлайнят, размещают по регистрам и сливают хвосты, поэтому таблица строк lossy по построению — шаги пропускают, строки идут не по порядку, локалы читаются <optimized out>. Воспроизводи под -O0 -g, когда можешь, а когда баг лезет только в оптимизации — шагай по инструкциям и доверяй регистрам, а не номерам строк. Теперь, когда встретишь условный брейкпоинт, ползущий по плотному циклу, или watchpoint, тихо промахивающийся по записям, — ты знаешь, какой механизм поменять и почему это ничего не стоит.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.