Что-то пошло не так: основы пошаговой отладки
Программы и библиотеки не всегда работают правильно. И, чаще всего, при разработке, ваш код будет работать неправильно. В практике программирование поиск причин неработоспособности программы или библиотеки называют отладкой.
Ошибки в программах делятся на три типа
- синтаксические
- логические
- ошибки времени выполнения
Синтаксические ошибки связаны с с нарушением синтаксиса языка, чаще всего вылезают при сборке проекта и характерны в основном для начинающих разработчиков. QtCreator оснащен всеми средствами для поиска и нахождения таких ошибок. С опытом количество синтаксических ошибок, допускаемых разработчиком очень быстро падает практически до нуля. Поэтому мы и не будем рассматривать их в данной главе.
Логические ошибки не приводят к ошибкам компиляции, а связаны они с неправильной работой реализуемого алгоритма. Такие ошибки отлавливаются как просмотром кода, с целью неверно реализованных мест, так и пошаговым прогоном проблемного кода в отладчике.
Ошибки времени выполнения — самый сложный вид ошибок. Они могут совершенно не проявлять себя при определенном сочетании параметров программы, и вылезти в самый неподходящий момент, при произвольном изменении параметров работы программы, после перезагрузки системы и т.п. Обычно такие ошибки связаны с наличием в коде не инициализированных переменных, отсутствием проверок на выход за пределы выделенной памяти, отсутствием проверок деления на ноль — список можно продолжать долго.
Ловим логические ошибки: прогоняем алгоритм по шагам
Для отладки DLL, в отличие от программы, требуется дополнительно настроить её проект, а именно указать, какое приложение использует эту DLL и задать параметры его запуска. В нашум случаем модуль ПЕ используется процессом simulator.exe (код ПЕ работает в контексте этого процесса).
Во-первых, нам необходимо собрать отладочную версию нашей DLL, поэтому выбираем внизу панели инструментов конфигурацию сборки «Отладка». Собираем DLL, а затем идем (через ту же левую панель) по пути «Проекты» -> «Запуск». Заполняем поля «Программа», «Параметры командной строки» и «Рабочий каталог», так, как показано на скриншоте ниже
В поле «Программа» указывается путь к файлу simulator.exe, в поле командной строки указываются ключи, с которыми запускается simulator.exe, а именно
- —route — путь к каталогу с маршрутом. В вашем случае может быть указан другой маршрут, в зависимости от того, какой установлен у вас;
- —train-config — имя файла конфига поезда. Нам называется simple-loco.
Лаунчер запускает процесс simulator.exe именно так: определяет путь к выбранному в нем маршруту, определяет имя конфига поезда и подставляет эти значения в ключи процесса simulator.exe.
Теперь предположим, мы хотим пройти по алгоритму, загружающему из конфига пользовательские параметры, то есть по методу loadConfig(). Переходим обратно в редактор кода и ставим точку останова (F9, либо клик по полю с номерами строк) на нужной строке
Красный кружок на 97-й строке означает, что в этом месте программа будет остановлена и управление будет передано в отладчик.
Теперь жмем кнопку «Запуск в отладке» (слева внизу зеленый треугольничек с изображением жучка), дожидаемся запуска процесса (стартует довольно не быстро), и бац — мы остановились в нужном нам месте
Подробнее разберем те возможности, которые предоставляет нам режим отладки в QtCreator
Та строка кода, на которой стоит отладчик обозначается маркером в виде желтой стрелки. Права от окна редактора кода находятся две области, позволяющие просматривать значение видимых в данный момент локальных переменных, а так же окно для просмотра вычисляемых выражений, куда можно добавить значение любой интересующей нас в данный момент переменной.
Снизу, под окном редактора располагается окно так называемого стека вызовов, в котором, снизу вверх, показана последовательность с которой в процессе выполнения, до момента остановки, происходил вызов функций и методов программы и отлаживаемой DLL. Здесь, самой первой строкой (на вершине стека) мы видим метод SimpleLoco::loadConfig() на котором произошел останов. Однако ниже можно увидеть методы, вызов которых предшествовал данному вызову. Абракадабра вида
vehicle!_ZN7Vehicle17loadConfigurationE7QString
несет в себе массу полезной информации. Я специально выделил ее полужирным шрифтом
- vehicle — имя библиотеки, из которой произошел вызов, в данном случае это vehicle.dll, входящая в состав симулятора;
- Vehicle — имя класса, метод которого был вызван: в данном случае это класс Vehicle, базовый класс для SimpleLoco;
- loadConfiguration — имя самого метода класса Vehicle
- QString — тип параметра, переданного в вызванный метод.
Видите, сколько информации мы получили о симуляторе, даже не глядя в его исходный текст? В частности мы знаем теперь, что метод loadConfig() вызывается из метода loadConfiguration() класса Vehicle. А глядя еще ниже по стеку вызовов, мы видим что этот метод, в свою очередь вызывается из метода Train::loadTrain(), из класса Train, расположенного в библиотеке train.dll.
Таким образом, если в отлаживаемый нами метод попали бы неверные данные, мы могли бы с уверенностью отследить путь, по которому эти данные были сформированы и переданы в наш локомотив. Стек вызовов крайне полезная вещь, в чем мы еще многократно убедимся.
В окне просмотра локальных переменных мы видим значение переменной cfg_path, содержащую путь к конфигу нашего локомотива. И так и есть, этот путь C:\RRS\cfg\vehicles\simple-loco\simple-loco.xml.
Нажимая F10 мы можем продолжить выполнение нашего кода построчно, попутно просматривая как изменяются значения переменных, обрабатываемых в методе
В частности, на скиншоте выше мы видим, что после выполнения чтения параметра F_max, в соответствующей ему переменной оказалось то значение, которое перед запуском мы поместили в конфиг. Таким образом мы убеждаемся, что параметры читаются из конфига корректно. Измененные значения переменных подсвечиваются в окнах просмотра красным шрифтом.
То есть в наших руках есть инструмент, позволяющий пройтись по коду DLL и выяснить, почему наш локомотив работает неправильно.
Ловим ошибки времени выполнения
Очень часто бывает так, что библиотека работала себе работала, а потом, после внесения в неё ряда изменений перестала работать, да и ещё и обрушивает программу, вызывающую её. Как с этим бороться, как найти причину?
Давайте внесем критическую ошибку в код нашего локомотива, прямо в конструкторе его класса напишем такой код
SimpleLoco::SimpleLoco(QObject *parent) : Vehicle (parent)
, ref_traction_level(0.0)
, F_max(450.0)
, F_nom(350.0)
, V_nom(80.0)
{
double *a;
a[0] = 10.0;
}
Тот кто хорошо знаком с C++ поймет подвох — мы объявили указатель на массив вещественных чисел, и не выделив память для этого массива пытаемся записать туда число. Соберем DLL в режиме «Выпуск» и запустим симулятор. С вероятность 99% у нас ничего не будет работать, программа вылетит с критической ошибкой, или просто не будет работать. И весь трагизм ситуации заключается в том, что возможно и будет нормально работать — например на разных машинах, в одном случае сим падал у меня сразу же, а в другом нормально работал. Во втором случае этот массив видимо указывает на какую-то доступную программе область памяти, и исключение не происходит. Если в вашем случае все работает, немного измените этот код
double *a = 0;
Ага! Вот мы и приплыли!
Зелена кнопка «Старт» в окне лаунчера означает что упал процесс simulator.exe. Вьювер что-то показывает, но стоим мы явно не на станции Ростов Главный, а в начале маршрута, так как симулятор не передал во вьювер данные о положении поезда. Что же случилось, как выяснить это в случае, когда мы не знаем об ошибке (в данном случае-то понятно)
Запускаем режим отладки DLL, как мы делали в предыдущем параграфе, но не ставим никаких точек останова. Бац!
Segmetation fault — ошибка означающая попытку доступа к недоступной области памяти. Что же, жмем переходим в окно отладчика
Отладчик показывает нам желтой стрелкой ту операцию, которая вызвала падение программы. Ну так и есть — обращение к не выделенной памяти!
Причина падения не всегда может быть такой очевидной — иногда приходится просмотреть стек вызовов, возможна причина сбоя лежит где-то выше того места, где произошел останов. В любом случае, режим запуска отладчике позволяет выяснить причину неработоспособности кода в разы быстрее, чем гадание на кофейной гуще.
Запуск вьювера при отладке
При всех вышеперечисленных возможностях, существует одна проблема — при запуске отладки у нас запускается только процесс simulator.exe. Но в штатном режиме работы существует еще процесс viewer.exe, который, например, обрабатывает ввод команд с клавиатуры. Как быть? Запустить вьювер вручную!
Для начала запускаем DLL в режиме отладки без задания точек останова. При этом у нас стартует процесс simulator.exe.
Переведем вьювер в оконный режим, для чего в файле cfg/stttings.xml выставляем параметр FullScreen в 0
Жмем Win + R, и в появившемся окне вводим
Появляется окно командной строки Windows
Переходим в каталог bin симулятора, введя команду
cd C:\RRS\bin
Теперь запускаем viewer, передав ему в качестве параметров путь к маршруту и имя конфигурации поезда
viewer.exe --route ..\routes\rostov-goryachiy_kluch --train simple-loco
Вьювер должен успешно запуститься и получить положение поезда на участке
Теперь весь симулятор работает в отладочном режиме. Например мы без проблем можем привести локомотив в движение, а затем, перейдя в QtCreator, постaвить точку останова внутри метода step()
Симулятор остановится, мы можем пройти метод step() по шагам, при этом изображение во вьювере замрет. Сняв точку останова и нажав F5 можно продолжить выполнение.
Допустим нас интересует как происходит обработка нажатия клавиш. Ставим точку останова, например, на действия, выполняемые по нажатию клавиши A
Переходим в окно вьювера, жмем A — вуаля! Симуляция замерла на обработке этого нажатия
Таким образом, мы имеем возможность управляемого выполнения кода, который пишем. Это дает в наши руки мощнейший инструмент отладки самых сложных алгоритмов. Можно сказать, что теперь мы вооружены до зубов, и скоро сможем приступить к созданию настоящих железнодорожных экипажей.