Программирование — это не только когда пишешь код, запускаешь его и удовлетворенно наблюдаешь за его безупречной работой, ведь зачастую он работает совсем не так, как мы рассчитываем! Возникает необходимость в эффективной отладке приложений, а это, оказывается, целое искусство! В данной статье я привожу свой собственный список советов, которые, как я надеюсь, помогут вам в отладке нативного кода.
Вспомогательные средства
Каждый программист должен знать, как запускать отладчик, устанавливать контрольные точки, возобновлять исполнение кода, выполнять вход и выход из функций (используя клавиатуру!). Вот несколько простейших советов по облегчению процесса отладки на основе базовых приемов.
1. Добавляйте информацию о местоположении ошибки к отчету отладчика (LinePos)
Независимо от своего уровня мастерства вы наверняка по-прежнему пользуетесь одним из основных методов отладки: отслеживаете те или иные значения с помощью функций и макросов printf, TRACE, outputDebugString и т.д. и сверяетесь с отчетом отладчика. В Visual Studio можно проделать один интересный трюк, который позволяет быстро переходить из окна вывода к конкретной строке кода.
Для этого нужно лишь задать следующий формат вывода:
"%s(%d): %s", file, line, message
Однако помните, что значения file и line нужно брать в соответствии с их действительным положением в исходном файле, а не в регистрирующей функции, так что вам, вероятно, понадобится макрос наподобие этого:
#define MY_TRACE(msg, ...)
MyTrace(__LINE__, __FILE__, msg, __VA_ARGS__)
// usage:
MY_TRACE("hello world %d", 5);
Обратите внимание, что __LINE__ и __FILE__ являются стандартными ANSI-совместимыми предопределенными макросами препроцессора, которые могут быть распознаны компилятором. См. Predefined Macros, MSDN.
Также не забывайте пользоваться функцией OutputDebugString, чтобы сообщение показывалось в окне вывода, а не в консоли.
Теперь при появлении некоторого сообщения в окне вывода VS можно, дважды щелкнув на нем мышью, перейти к указанному файлу и строке. Таким же образом можно работать с предупреждениями или сообщениями об ошибках во время компиляции. Однажды я получил сообщение об ошибке, но не мог определить точное местоположение ее в коде, отчего потерял уйму времени. В тот раз мне надо было найти строку, а это долгий и трудоемкий процесс. Описанный же способ перехода к коду по двойному щелчку позволяет сделать это за считанные миллисекунды.
Кстати, если у вас другая IDE (не Visual Studio), есть ли в ней такая возможность? Отпишитесь в комментариях, мне было бы интересно узнать.
А вот простой пример кода, на котором можно опробовать описанный прием: github.com/fenbf/DebuggingTipsSamples.
Обновление: как указал jgalowicz в своем комментарии, если вы хотите, чтобы имена файлов выводились в сокращенном виде, сделайте макрос __SHORT_FILE__: подробности реализации см. в заметке в его блоге. Впрочем, в Visual Studio опция компилятора /FC по умолчанию отключена, так что обычно выдаются как раз сокращенные имена файлов (относящиеся только к директории решения).
2. Заведите простую статическую переменную для управления той или иной опцией
// change while debugging if needed
static bool bEnableMyNewFeature = true;
Функция Edit And Continue в Visual studio — очень мощный инструмент, но можно обойтись и упрощенной, «ручной» ее версией. Она не настолько изящна, но свою задачу выполняет. Нужно всего лишь завести статическую переменную, с помощью которой вы будете управлять той или иной опцией программы. Для этого можно использовать обычный булев флаг или переменную целочисленного типа. Во время отладки значение этой переменной можно менять, что позволит вам исследовать работу опции без перезапуска или пересборки программы.
Как изменять значение во время отладки? Перейдите к окну слежения за переменными (Watch window) или просто поводите курсором над переменной — должно появиться поле редактирования, в котором можно задать нужное значение.
И не забудьте отключить/убрать эту ужасную переменную в финальных сборках и коммитах!
3. Условные контрольные точки
Надеюсь, вы и так пользуетесь условными контрольными точками, но я все же хочу вкратце рассказать об основных способах их применения. Как следует из названия, работа отладчика в этих точках приостанавливается при выполнении некоторого условия (достаточно простого).
Один маленький совет: напишите свою собственную контрольную точку, если вам требуется исследовать работу некоторого участка кода более подробно.
См. список выражений, применяемых в условиях: msdn: Expressions in the Debugger
Но это еще не все.
Как вы, должно быть, заметили на скриншоте, существует еще одно полезное условие для контрольной точки: «Hit count». С его помощью можно задать число событий, после которого контрольная точка будет активирована. Эта опция очень полезна, когда вам надо отследить какое-то динамическое событие или множество объектов.
4. Не заходите в функции, которые не хотите исполнять
Сколько раз вы были вынуждены заходить в конструктор строкового типа, чтобы затем быстро выйти из него? Или во множество мелких/библиотечных функций, прежде чем вы могли добраться до нужного метода? В большинстве случаев это пустая трата времени.
Посмотрите на этот пример:
void MyFunc(const string &one, const string &two)
{
auto res = one + two;
std::cout << res << "n";
}
....
MyFunc("Hello ", "World");
А теперь попробуйте зайти в вызов функции MyFunc(),нажав Ctrl+F11. Куда же перейдет отладчик? Вот что происходит у меня:
Более того, если выйти из этого конструктора и снова зайти в него… то попадешь в конструктор второго параметра. А теперь представьте, что будет, если таких параметров несколько. С ума сойдешь, пока доберешься до нужного метода!
Как правило, подобные нежелательные методы лучше отфильтровывать: вряд ли проблема кроется в конструкторе std::string :)
Как же отфильтровать эти базовые функции? Начиная с версии 2012, в VS достаточно отредактировать файл default.natstepfilter.
О способах фильтрации функций в более ранних версиях VS см. здесь: How to Not Step Into Functions using the Visual C++ Debugger. Там придется повозиться со значениями реестра.
В качестве дополнительного стимула сообщу, что в Visual Assist есть такая же опция и работать с ней гораздо проще. В режиме отладки отображается окно VA Step Filter: просто поставьте или уберите в нем галочку напротив нужного метода из списка найденных методов. Эти настройки можно применять как глобально, так и локально для данного проекта. Настройки фильтрации в VA являются пользовательскими и не могут быть добавлены в файл default.natstepfilter.
5. Добавляйте вспомогательные переменные для объектов в режиме отладки
Чем больше данных, тем лучше! Нежелательные сообщения всегда можно отфильтровать, а вот получить данные из ничего нельзя. В зависимости от конкретных целей может быть полезно завести несколько вспомогательных переменных в объектах: при отладке эти переменные могут сообщить очень ценную информацию или просто облегчить процесс работы.
Например, при работе с древовидными структурами вам, вероятно, часто потребуется проверять элементы pNext и pPrev. Эти указатели обычно помещаются в какой-нибудь базовый класс вроде TreeNode, и, если вас интересует MyTreeNode, который находится тремя уровнями ниже в иерархии классов, проверять каждый раз pNext может быть довольно утомительно. А что если обновлять MyTreeNode, добавляя в него данные из pNext? Тогда не надо будет проходить каждый раз по всем уровням иерархии, чтобы проверить этот элемент. У описанного метода, правда, есть один недостаток: нужно как-то фиксировать это обновленное состояние. Значение pNext может легко измениться, так что придется реализовать дополнительную логику, чтобы синхронизировать эти изменения. Впрочем, хоть это и справедливо для большинства случаев, для задач отладки сойдет и менее изящное и эффективное решение.
Приведу пример.
Мне часто приходится работать с древовидными структурами, представляющими текстовые объекты. Текстовый объект содержит строки, а строки — символы. Проверять каждый раз, в какой строке я в данный момент нахожусь (т.е. какой текст в ней содержится), было очень утомительно, поскольку приходилось возвращать первый символ строки, а затем смотреть значение по указателю pNext, чтобы узнать второй символ и на основании этих двух символов смотреть, что же это за строка. Как упростить этот процесс? Я просто завел переменную strLine и добавил ее к Line и периодически обновляю ее. Пусть это и несовершенный источник информации (данные теряются, если символ добавляется или удаляется за один фрейм, но в следующем фрейме они все равно появляются), но, по крайней мере, я могу быстро узнать, в какой строке я в данный момент нахожусь. Вот так просто! И времени можно сэкономить немало.
6. Пишите собственные отладочные визуализаторы
Это обширная тема, которую я лишь кратко представлю здесь:
Если вам не нравится, как отображаются объекты в отладчике, попробуйте написать собственные визуализаторы.
Debug Visualizers in Visual C++ 2015
В VS2015 даже появился новый встроенный шаблон, который можно найти в файле Project → Add New Item → Visual C++ → Utility → Debugger visualization file (.natvis).
Приемы отладки
На основе базовых приемов отладки можно построить более сложные.
7. Приходится проверять много объектов?
Если некоторый код вызывается для множества объектов, проходить через все эти объекты и построчно проверять их очень проблематично. А что если использовать уникальное значение какого-нибудь поля в качестве подсказки о том, где искать ошибку? Для этого можно создать условную контрольную точку, в которой это значение будет сравниваться с некоторым диапазоном. Чем этот диапазон меньше, тем лучше.
Пример: мне часто приходилось отлаживать код, перебирающий все символы в документе. С одним (специальным) символом возникли проблемы. Отлаживать все символы по отдельности невозможно, но я знал, что этот конкретный символ отличался от прочих размером ограничивающей рамки, так что я задал условную контрольную точку, в которой проверялось значение width, благодаря чему я смог получить доступ к этому символу (условие width > usual_char_width). Данному условию отвечали только два или три элемента, так что я быстро разобрался с ошибкой.
Вообще говоря, чем уже такие диапазоны, тем лучше: не придется проверять десятки или сотни мест.
8. События мыши
Отладка событий мыши особенно затруднительна, ведь большинство таких событий исчезают при остановке отладчика!
Отладка щелчков мыши обычно не представляет сложностей. Например, если необходимо проверить, какой код был вызван по щелчку мыши на некотором объекте, нужно просто создать контрольную точку на входе в метод OnClick/onMouseDown.
А как насчет перетаскивания объектов? При остановке отладчика состояние перетаскивания теряется. В таких ситуациях можно попробовать следующие приемы:
- Используйте старый добрый trace/printf. При перетаскивании выдается множество сообщений, которые помогут разобраться в происходящем без необходимости прерывать исполнение кода. Этот способ подходит для краткосрочных операций перетаскивания, потому что иначе на выходе будет слишком много данных. Имея такую информацию, можно локализовать проблему и вернуться к ней позже.
- Устанавливайте условные контрольные точки в тех местах, которые требуют проверки. Например, при вращении объекта он может неожиданно изменить положение, и вам надо разобраться, почему это происходит. Для этого вы создаете контрольные точки при входе в члены, отвечающие за положение объекта, что дает вам возможность изучить их. При остановке отладчика состояние вращения потеряется, но, прежде чем это произойдет, вы сможете определить потенциальное местоположение проблемы в коде. Еще один способ — использовать условие obj_rot > some_meaningful_value.
- Перетаскивание объектов часто происходит во время их копирования. В этом случае по завершении операции копирования оригинальным объектам присваивается соответствующий статус. Что если установить контрольную точку, в которой проверялись бы только оригинальные объекты? Или реализовать особое состояние, сигнализирующее об операции перетаскивания? В таком случае отладчик остановится по завершении этой операции.
9. Создавайте отладочные визуализаторы и прочие инструменты
Этот совет предполагает дальнейшее развитие приема с созданием простых вспомогательных переменных. Если вы работаете со сложными объектами, хорошо иметь средства, которые могли бы более эффективно отслеживать данные. Базовый набор подобных средств можно найти в Visual Studio и любой другой IDE или отладчике, но, поскольку все проекты разные, лучше разрабатывать собственные решения.
Такая ситуация, как мне кажется, типична для игр. Можно, например, создать дополнительный слой интерфейса и включать его во время игры, чтобы увидеть игровую статистику, сведения о производительности и потреблении памяти. Количество отображаемой информации ограничивается только вашими потребностями. Так что я настоятельно рекомендую уделить время созданию таких инструментов.
Прочее
10. Отладка в режиме Release
Release-сборки работают быстрее, поскольку практически все оптимизации кода уже включены. Однако ничто не мешает проводить отладку и в этом случае. Как именно это можно реализовать? Нужно сделать следующее (для VS 2013 или VS 2015):
- Установить опцию Debug Information Format в /Z7 (формат совместимости с C7) или /Zi (формат базы данных программы).
- Установить опцию Enable Incremental Linking в «нет»
- Установить опцию Generate Debug Info в «да»
- Установить опцию References в /OPT:REF, а Enable COMDAT Folding в /OPT:ICF
11. Ускоряем отладочную сборку!
- В случае медленной работы отладчика проверьте опцию Tools → Options → Debugging → General → «Require source files to exactly match the original version». Узнал про нее здесь.
- Отключите кучу отладки (Debug Heap) — актуально для версий младше VS 2015. О том, что это такое, можно прочитать в одной из моих предыдущих статей: Visual Studio slow debugging and _NO_DEBUG_HEAP. К счастью, в VS2015 куча отладки отключена по умолчанию, так что проблем с этим у вас не должно возникнуть.
- Ограничьте загрузку файлов отладочных символов. Этот прием позволяет сократить количество загруженных файлов символов, что ускорит запуск программы. Подробности см. здесь: Understanding symbol files and Visual Studio's symbol settings
Заключение
В этой статье я привел 11 советов, которые помогут вам ускорить процесс отладки программ. Какие из них наиболее ценны? Пожалуй, это пункты про условные контрольные точки, отладку множества объектов и ускорение отладочной версии. Тем не менее, остальные советы тоже важны, так что я затрудняюсь расположить их в каком-то определенном порядке. К тому же в зависимости от конкретных задач часто приходится отдавать предпочтение разным приемам.
Более того, этот список явно не полон — существует еще множество других приемов и практик. Возможно, вам тоже есть что добавить?
- Пользуетесь ли вы какими-то особыми приемами при отладке приложений?
- Прибегаете ли вы к помощи пользовательских инструментов?
Дополнительные ресурсы
- Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems
- Advanced Windows Debugging
- Debug It!: Find, Repair, and Prevent Bugs in Your Code
- Using Breakpoints, MSDN
- Navigating through Code with the Debugger, MSDN
- 10 More Visual Studio Debugging Tips for Native Development,
Marius Bancila, CodeProject - VA Step Filter, Visual Assist
- VC Team Blog: The Expression Evaluator
- VC Team Blog: Make Debugging Faster with Visual Studio
Автор: PVS-Studio