Как Чинить Программные Ошибки?

#ретроспектива

4baa981beb031cfe7f22d8dfbee3e60d.png

Программисты микроконтроллеров регулярно занимаются починкой багов. Более того 60%-80% работы программиста — это как правило починка багов. Часто программистов нанимают как раз только для того чтобы чинить чужие баги.

Терминология

Баг (bug) — ошибка в компьютерной программе. Несоответствие между реальным поведением программы и ожидаемым поведением программы.

В чём проблема?

Проблема в том, что все баги они уникальные! Поиск причины каждого конкретного бага это скорее искусство. Починка багов сродни работы детектива. Ты следуешь по следам, мыслишь последовательно, фокусируешься, делаешь гипотезы, ставишь эксперименты, доказываешь или опровергаешь предположения.

В программировании микроконтроллеров часто надо уметь отличить программную ошибку от аппаратной ошибки. Обычно как железо так и прошивку делает одна и та же организация. Поэтому ошибки в электронной плате это вообще норма жизни. Однако в этом тексте я сосредоточусь именно на программных ошибках. Например у вас ошибка воспроизводится на отладочной плате от производителя.

При хорошем DevOps можно отловить много багов на стадии статического анализатора, или отработки скриптов сборки (Make, CMake). Потом можно отловить баги на фазе препроцессора (утилита cpp.exe), затем отловить баги на фазе компилятора (gcc.exe). Ошибки также показывает компоновщик (ld.exe). Часть ошибок могут отловить модульные тесты уже в run-time (е). Последний рубеж обороны от ошибок это интеграционные тесты.

Однако тем не менее ошибки просачиваются в релизные артефакты (*.elf, *.bin, *.hex) и обнаруживаются только на фазе исполнения программы (run-time (е)).

Какого рода программные осечки я подразумеваю. Вот несколько примеров из реальной разработки.

#

Пример программной ошибки

1

Прошивка свалилась в Hard Fault ISR

2

Прошивка постоянно непрерывно перезагружается

3

Не проходит инициализация какого-то программного компонента. Например FatFs.

4

По BLE приходит хрипящий звук

5

На выходе расшифровщика не понятный текст, а кракозябры

6

LoRa радио трансивер не отвечает на команды

7

Подали питание и Heart Beat LED не мигает

8

Не отвечает UART-CLI

9

Печатаешь текст и на экране ничего не происходит. Через 20сек разом вываливается большой текст.

Вот некоторые типичные причины программных багов.

Причина программной ошибок

1

Произошло переполнение типа данных

2

Неверно подобранные приоритеты потоков в RTOS

3

Переполнение стековой памяти

4

Попытка прописать read only память (.rodata)

5

Произошло деление на ноль

6

Произошло обращение к нулевому указателю

7

Сработало прерывание для которого нет обработчика

8

Код перетер чужую память за пределами массива

9

Неверный конфиг для программного компонента

10

Кто-то закомментировал кусок кода, который должен был исполняться

11

Программа застряла в бесконечном цикле while (1)

Есть ли универсальный способ, методичка на то, как чинить баги?

Да, я считаю, что можно в какой-то степени обобщить алгоритм поиска программной ошибки.

Фаза № 1. Подробно опишите программную ошибку

Подробно напишите, что именно не так. В чем несоответствие. Что должно быть, что происходит на самом деле. Добавьте скриншот. Добавьте кусок лога программы в этом месте. Заведите про эту ошибку текстовый файл или заметку в трекере задач. Это подобно тому как полиция делает фотографии на месте преступления. Составьте дефектную ведомость.

Фаза № 2. Научиться стабильно воспроизводить баг *

Баг надо воспроизвести. Надо составить подробный алгоритм, который приводит к воспроизведению программного бага. Если нет возможности локально воспроизвести баг, то дальше и говорить не о чем. Без этого вы будете просто ходить кругами и фантазировать.

Чтобы отлаживать код, его надо исполнять. Если в коде баг и нет возможности исполнить код, то баг не исправить. Это как писал философ Э.В. Ильенков, если найти в лесу отдельно лежащую человеческую ногу, то не ясно что это и зачем. Понятно только, когда эта нога видна в действии: в составе танцора на сцене или просто идущего по улице человека. Аналогично в понимании причины программных багов. Надо запускать код!

Чтобы лёд тронулся надо сперва научиться регулярно воспроизводить баг, желательно автоматически из какого-нибудь скрипта. Если Вам сказали, что обнаружен баг, то первый вопрос, который вы должны задать себе это: «Настолько стабильно баг воспроизводится?».

Фаза №3. Активировать более глубокий уровень логирования

Включите уровни логирования того компонента в котором возникла ошибка до уровня Debug или Paranoid. В хорошей прошивке, должен быть UART-CLI. CLI позволяет включать/отключать уровни логирования для каждого программного компонента. Вероятно баг вызван тем, что не выполняется какой-то важный код, который должен выполняться.

Фаза №4. Прогоните модульные тесты.

Покройте функционал модульными тестами. Это не формальность. Модульные тесты являются скрепами, которые сдерживают корректный функционал при перестройке программы. Очень вероятно, что отчет модульных тестов в UART-CLI сузит место поиска причины бага, либо явно укажет на причину бага. Модульные тесты это отличный способ отладить большой кусок кода, который невозможно или проблематично пройти пошаговым отладчиком.

Фаза №5. Пройдите код пошаговым GDB отладчиком

Это крайняя мера. Проходите код пошаговым отладчиком. Каждую строчку. Анализируйте по какой ветви происходит исполнение кода. Правильные ли значения лежат в локальных и глобальных переменных? Вероятно не выполняется нужная ветвь кода. Пошаговая отладка это весьма утомительное мероприятие. Особенно когда приходится делать её из CLI.

Фаза №6. Прогоните код через статический анализатор.

Это скорее профилактическая мера. Есть такие утилиты, которые анализируют код без его исполнения. Это например CppCheck.exe. Они часто находят нелепые опечатки, которые не замечает компилятор. Минимум раз в месяц надо прогонять код репозитория через статический анализатор. Сами вы всё не уследите.

Фаза №7. Задайте вопрос на профессиональных форумах и сообществах.

Это скорее фаза отчаяния. Эта мера абсолютно не гарантирует результата. Никто вам ничего там не должен. Но как попытка вполне легальное действие.

Итоги

Каждый программный баг он уникальный. Однако есть одно общее правило буравчика, которое справедливо для разбора любого программного сбоя.

Чтобы найти причину программной ошибки надо научиться стабильно воспроизводить ошибку на фазе исполнения.

По крайней мере с этим можно будет дальше работать. А просто разглядывать код с умным видом в надежде найти причину бага — это бессмысленное занятие.

Компилировать сорцы в уме, а потом фантазировать на тему как этот код будет исполняться — это не наши методы. Код с багом надо запускать на target устройстве и анализировать ситуацию по горячим следам.

Если есть, что добавить, то пишите в комментариях.

Links

16 Способов Отладки и Диагностики FirmWare https://habr.com/ru/articles/681280/

Пошаговая GDB отладка ARM процессора из консоли в Win10 https://habr.com/ru/articles/694708/

Модульное Тестирование в Embedded https://habr.com/ru/articles/698092/

Почему Нам Нужен UART-Shell? https://habr.com/ru/articles/694408/

11 Aтрибутов Хорошего Firmware https://habr.com/ru/articles/655641/

Вопросы

--Какой самый сложный программный баг Вам удавалось починить? В чем, собственно, было дело?

© Habrahabr.ru