Основы отладки

b

История одного бага: как размер шрифта стоил релиза

Представьте: утро понедельника, вы заливаете финальную сборку на сервер клиента. Через час приходит паника — форма редактирования договоров перестала реагировать на ввод. Вы проверяете код вчерашнего коммита: всё чисто, логика не менялась. Но поле ввода ведёт себя как капризный подросток — то принимает символы, то игнорирует. Знакомо? Это реальный случай из практики поддержки одного медицинского ПО на Delphi 10.4.

Проблема оказалась не в бизнес-логике, а в остаточной точке останова (breakpoint). Кто-то из команды неделю назад отлаживал обработку шрифтов и оставил условие остановки на присвоении свойства Font.Size. Каждый раз, когда код менял размер шрифта (а это происходило при получении фокуса!), отладчик приостанавливал выполнение. На тестовом сервере это не проявлялось — там не стояла среда разработки. Заказчик же имел Delphi IDE на своём терминале для отчётов. Хитрый баг жил три недели.

Решение пришло через системный подход к настройке отладчика. Вы научитесь делать то же самое: не гадать на кофейной гуще, а использовать спецификации инструментов Delphi как скальпель. Ниже — техническая анатомия отладки с реальными примерами материалов, которые должны быть под рукой.

Железные правила: какие файлы и настройки вы обязаны проверить перед стартом

Прежде чем нажимать F9, отладчик использует несколько конфигурационных файлов. В отличие от мифических «универсальных решений», Delphi опирается на строгую иерархию спецификаций. Первый слой — файл проекта .dpr с секциями {$R *.res} и {$MINSTACKSIZE}, которые напрямую влияют на поведение отладчика при перехвате исключений. Если размер стека меньше 1024 КБ, отладчик может «падать» при просмотре вложенных вызовов.

Второй критический файл — .dproj (XML-представление проекта). Там прячется параметр EnableDebuggingDCUs. По умолчанию он равен False. Без него вы не увидите исходники Delphi RTL, когда среда выдаст сообщение «Project raised exception class EAccessViolation». Это различие между «чёрным ящиком» и прозрачным отладочным процессом. Включите его — и сможете шагать внутрь вызовов TList.Add или TStringBuilder.Append, видя реальные переходы по инструкциям.

Третий, часто игнорируемый материал — файл маппинга .map. Он генерируется компилятором только при включённой опции Debug Information в конфигурации. Без .map отладчик не построит стек вызовов с именами функций — только адреса в hex. Для расследования сложных утечек памяти или гонок потоков это равносильно работе вслепую. Сравните: с .map вы видите цепочку: TForm1.Button1Click → TDataModule1.QueryOpen → TDatabase1.Execute; без него — только шестнадцатеричные цифры.

Разница между Event Log и Watch List: когда одно не заменяет другое

Многие полагают, что Watch List (окно наблюдения за переменными) — это панацея. Но в 2026 году, с многопоточными приложениями, Watch List имеет критическое ограничение: он показывает значение только в момент остановки потока. Если у вас параллельная обработка запросов, а баг возникает в рандомном потоке, watch-выражение может показывать корректные данные, хотя реальный сбой — в другом экземпляре объекта. Это различие между статической и динамической отладкой.

Event Log, напротив, фиксирует последовательность событий отладчика: загрузка модулей, выгрузки DLL, исключения, сообщения OutputDebugString. Здесь вы увидите, что поток ThreadID 0x12A8 загрузил библиотеку с ошибкой до того, как произошёл сбой. Именно Event Log (меню View → Debug Windows → Event Log) позволяет воспроизвести хронологию, а не просто мгновенный снимок состояния. Качество отладки повышается, когда вы комбинируете оба инструмента.

Материалы, которые нужны для эффективной работы с Watch List: спецификация типов данных Delphi (размер Variant, динамических массивов) и список зарезервированных имён. Например, в watch нельзя использовать локальную переменную, объявленную внутри блока then...else, если отладчик не остановился внутри этого блока. Инструмент хорош только при точной настройке областей видимости.

  1. Откройте Event Log и очистите его (Ctrl+A, Delete) перед каждым сеансом — так вы не утонете в логах предыдущих запусков
  2. В Watch List всегда указывайте полный путь к свойству: Self.FDBNavigator1.Buttons[nbFirst].Enabled, а не просто Enabled — иначе отладчик запутается в перегрузках
  3. Используйте format-спецификаторы: добавьте ,m после имени переменной (например, MyArray,m) — отладчик покажет память как шестнадцатеричный дамп, а не абстрактный адрес
  4. Для строк AnsiString применяйте суффикс ,s — это форсирует отображение в ANSI-кодировке, а не Unicode
  5. При работе с COM-объектами включайте отображение интерфейсных ссылок через ,i — увидите счётчик AddRef/Release
  6. Никогда не ставьте watch на свойства, вызывающие побочные эффекты (например, на геттер, который меняет глобальную переменную) — отладчик вызовет его при каждом обновлении окна
  7. Настраивайте фильтр по типу исключения: в Event Log можно исключить EAbort, оставив только EAccessViolation и EInvalidPointer

Инспектор памяти: как заглянуть внутрь объекта, а не в его интерфейс

Когда обычный Watch List показывает значение nil, хотя вы уверены, что объект создан, приходит время открыть Memory Inspector. Этот инструмент показывает сырые байты по указанному адресу. Разница между ним и обычной проверкой через TObject.ToString — в уровне детализации. Вы увидите структуру VMT (Virtual Method Table), поля экземпляра и хвосты аллокаций. Без понимания этой спецификации вы будете верить «магии», а не фактам.

Реальный пример: клиент жаловался, что после вызова TObjectList.Clear оставались ссылки в памяти. Watch List показывал Count = 0, но инспектор по адресу Self.FList обнаружил, что внутренний массив указателей не обнулён — старые ссылки всё ещё там, просто Count обнулён. Это привело к утечке в 200 МБ за час работы. Memory Inspector выявил «мусорные» указатели, которые GC не трогал, пока не перезаписывался весь массив. Производственный баг был исправлен заменой Clear на Reset.

Качество вашего кода напрямую связано с умением читать память: адреса в hex, смещения полей, сигнатуры VMT. Используйте Ctrl+Alt+M в IDE для вызова инспектора. Введите адрес переменной (из Watch List, через &имяПеременной) и наблюдайте: если в первых четырёх байтах видите знакомый паттерн 00 00 00 00 — объект не инициализирован; если там адрес с кодом 0050xxxx — это указатель на VMT, объект жив.

Точки останова с условиями: как не сойти с ума в цикле из 100 000 итераций

Вы ставите точку останова на строке, где вызывается Insert в StringGrid. Программа останавливается через полчаса, но в этот момент счётчик слишком мал. Альтернатива — условная точка останова (Conditional Breakpoint). Разница от обычной в том, что отладчик проверяет логическое выражение и останавливается только когда оно истинно. Спецификация: условие пишется на Delphi, но с ограничениями — нельзя использовать функции с побочными эффектами.

Материалы для настройки: используйте переменную цикла и оператор Mod. Например, условие (i mod 1000 = 0) и (SomeList[i] = nil) — остановит программу только на каждой тысячной итерации, притом только если элемент равен nil. Время ожидания сокращается с часов до секунд. Важный нюанс: выражение вычисляется в контексте отладчика, поэтому тип Boolean должен быть явным — напишите = True или = False, иначе среда может интерпретировать Integer как Boolean с неожиданным результатом.

Ещё один инструмент — точка останова с действием (Breakpoint Action). Вы можете настроить её так, чтобы отладчик выводил сообщение в Event Log и продолжал выполнение (Log message and continue). Это незаменимо для фоновых потоков, где остановка нарушила бы тайминги. Качество такого подхода: вы получаете журнал состояний без вмешательства в работающий код, что критично для серверных решений.

Сравнение отладчиков: встроенный Delphi-дебаггер против внешнего WinDbg

Встроенный отладчик Delphi (EurekaLog, madExcept — надстройки) хорош для бизнес-логики. Но когда речь заходит об утечках дескрипторов или зависаниях на уровне ядра, WinDbg (или x64dbg) оказывается вне конкуренции. Разница: Delphi-дебаггер оперирует исходным кодом и видит символы DCU; WinDbg работает с ассемблерными инструкциями и дампами памяти. Для производства критично уметь использовать оба.

Кейс из практики: программа «зависала» при попытке открыть принтер через TPrintDialog. В Delphi-отладчике стек вызовов показывал вызов Printers.Printer(), но не указывал причину. WinDbg выявил ожидание мьютекса winspool.drv, который был заблокирован другим процессом. Разница в подходах: Delphi-дебаггер видит верхний слой, WinDbg — системные примитивы синхронизации. Решение — перейти на асинхронный вызов печати, обойдя мьютекс.

Материалы для настройки: обязательно сохраняйте .map-файл с полной информацией (Detailed). Экспортируйте PDB-файлы через утилиту map2dbg для совместимости с WinDbg. Спецификация: для 64-битных проектов используйте x64dbg, так как отладчик Delphi не полностью поддерживает SIMD-регистры. Качество отладки возрастёт, когда вы добавите в арсенал анализ объектов через !heap команду в WinDbg.

Технический чек-лист перед каждым отладочным сеансом

Чтобы не тратить часы на поиск бага, который лежит на поверхности, пройдитесь по этому списку. Он основан на анализе сотен ошибок в реальных проектах на Delphi.

Заключение: отладка как дисциплина, а не лотерея

Отладка перестаёт быть мистическим искусством, когда вы превращаете её в процедуру с чёткими спецификациями. Технические детали — от структуры файла .dproj до анализа дампов через WinDbg — дают вам контроль над кодом. Качество продукта определяется не тем, как часто вы нажимаете F8, а тем, как системно вы подходите к каждому багу: регистрируете симптомы, проверяете конфигурацию отладчика, используете все окна (Event Log, Memory Inspector, условные точки).

Вернёмся к истории из начала. Тот баг с точкой останова на Font.Size был найден за пять минут после включения Event Log: лог показал, что отладчик останавливается на каждом изменении шрифта, хотя разработчик думал, что он использует простой режим с одной остановкой. Решение — глобальный поиск всех точек останова через Ctrl+Alt+B и проверка их условий. Результат: релиз вышел на следующий день, клиент извинился за нервную атаку. Теперь этот чек-лист — стандарт для всей команды.

Добавлено: 27.04.2026