Рецепты отладки. Зависший 3D редактор.

Несмотря на то, что правильные пацаны советуют очень правильный тулсет, а другие правильные пацаны смотрят на это крайне позитивно, я продолжаю трахаться с разными неприкольными ситуациями, которые вряд ли отловит PREfast. Поэтому пользуясь случаем, отвлекусь от избитой темы ошибок в памяти и переключусь на ошибки другого рода – это нарушения последовательности вызова функций внешнего middleware. Уважающие себя пакеты предпочитают проверять состояние объекта или модуля перед вызовом функций и информировать програмистов о нарушениях логики. Неуважающие себя пакеты такими мелочами не занимаются, в крайнем случае пишут в документации что-то типа “никакие функции библиотеки не могут быть вызваны до вызова xxx() или после вызова yyy()).

Исходные данные: имеется мощный пакет моделирования под названием XSI (который кстати теперь тоже аутодесковский). Имеется плагин к нему. При выгрузке плагина XSI зависает вусмерть (а плагин загружается и выгружается довольно регулярно – на каждый “чих”).

В данном случае отладка оказалась не таким сложным делом, но чтобы исправить ошибку, потребовалось влезать во внутренности крупного пакета и смотреть, где конкретно там произошел зависон. Запустить XSI непосредственно под отладчиком не удалось – менеджер лицензий заявил, что имеет место быть ахтунг с попыткой взлома, поэтому пришлось пользоваться другой полезной функцией MSVC (несмотря на то, что в каментах мне уже предлагали иллюстрировать ситуации другими отладчиками, я продолжаю давать примеры “привычной” среды разработки для большинства программистов :) ). Эта полезная функция называется Attach to Process и позволяет перехватывать в отладчик большинство запущенных приложений. В том числе и таких, которые защищены разными системами защиты – как правило, проверки наличия отладчика проводятся на старте приложения + несколько контрольных точек в процессе выполнения. В нашем случае вполне хватало времени, чтобы посмотреть, куда же мы таки зависли, чтобы выловить проблему. Итак, к зависшему приложению коннектимся командой Attach to Process, после чего вызываем команду Break All. Несмотря на то, что отладочной информацией и не пахнет, можно попробовать проанализировать ситуацию по тому, что у нас есть. А есть у нас следующее: список загруженных модулей, список запущенных тредов и невнятная информация о call stack в каждом из них.
Быстро пробежался по по всем запущенным тредам с просмотром их callstack. Большая часть тредов висит в дебрях ntdll, что как правило означает, что мы в недрах синхронизационных функций или критических секций. Один из тредов меня озадачил – он висел где-то в недрах GDI+ функции. Попытка пройти функцию по шагам, поставить брекпоинт на ret и запустить программу далее не увенчалась успехом – мы висели в длительном бесконечном цикле. Зависание на GDI+ функции стало первой монетой в копилку.

Второй прием, который часто применяют программисты для отладки сложных ситуаций – это дихотомия. Или попросту – комментируем ключевые фрагменты кода и проверяем, как ведет себя программа (комментируем, естественно, так, чтобы не сломать программу вусмерть). Буквально за несколько запусков проблемное место было выяснено – оно находилось где-то внутри блока завершения работы плагина onProcessDetach(), в функции shutdown() для библиотеки обработки изображений. Ну и наконец, самой точной дитохомией стало выявление функции GdiplusShutdown(), которая и отвечала за зависание XSI после выгрузки плагина. Это была вторая монета в копилку, и, как водится, было самое время залезть в документацию, чтобы посмотреть, что к чему. Вот что было найдено в MSDN в описании GdiplusStartup():

Do not call GdiplusStartup or GdiplusShutdown in DllMain or in any function that is called by DllMain. If you want to create a dynamic-link library (DLL) that uses GDI+, you should use one of the following techniques to initialize GDI+: * Require your clients to call GdiplusStartup before they call the functions in your DLL and to call GdiplusShutdown when they have finished using your DLL.
* Export your own startup function that calls GdiplusStartup and your own shutdown function that calls GdiplusShutdown. Require your clients to call your startup function before they call other functions in your DLL and to call your shutdown function when they have finished using your DLL.
* Call GdiplusStartup and GdiplusShutdown in each of your functions that make GDI+ calls.

Резюме по отладке:

  1. Attach to Process / Break All является очень удобным средством для того, чтобы получить предварительную информацию о проблеме в каком то приложении, которое так просто отладчику не поддается. Это может быть какой внешний инструментальный пакет, ваше собственное приложение, которое защищено одной из систем защиты ну или даже просто запущенная игра, в которой что-то происходит не так. Кстати, Attach to Process для дебажной версии игры с большой долей вероятности подгрузит вам и отладочную информацию, и можно будет инспектировать код и переменные так же просто, как и при обычном запуске под отладчиком. Поэтому когда отдаете игру геймдизайнерам или тестерам на изнасилование, обязательно кладите рядом хотя бы свеженький PDB.
  2. Дихотомия тоже является хорошим помошником. Хотя в ряде случаев (например, который произошел с SQLFetchScroll() в одном из предыдущих блогов) при плавающем баге дихотомия является плохим союзником. Там комментирование узловых точек программы может привести к неправильному вычленению “сбойной” функции и как следствие, к совершенно бесполезным поискам ошибки в ней.
  3. В тех случаях, когда вы пишете библиотеку для использования сторонними программистами, вычленяйте возможные комбинации запрещенных последовательностей вызовов и ассертируйте их. Впрочем, более подробно про 3 типа ошибок в программах я напишу чуть попозже.
  4. Читайте, наконец, документацию )))).

Приятной вам отладки.

  • http://www.sdl.ru TSS

    2dDIMA:
    “Читайте, наконец, документацию ))))” — это я бы первым пунктом поставил =)