Маскирующиеся баги в эмбедде

в 7:28, , рубрики: баги, программирование микроконтроллеров, С++

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

Проще всего, когда не до конца понят мануал, стандарт или, скажем, порядок конфигурирования библиотеки под железо. Здесь ясно: не все ходы исчерпаны, терпение и труд, еще пяток-другой экспериментов и оно оживет. Осциллограф и научный тык в помощь.

Маскирующиеся баги в эмбедде - 1
Выбор делителя частоты для настройки шины CAN

Хуже, когда проблема — в опечатке или ошибке в логике, которую не видишь в упор, пока по этому месту двадцать раз не пройдешься и глазами, и в пошаговой отладке. Потом осеняет, звонкий удар по лбу, крик «Ну екарный ты ж бабай!», правка. Работает.

И мрачный третий вид: глюк, окопавшийся в чужой библиотеке и вылезающий на стыке с железом. Шекспировские страсти рождает под ровный свет монитора. «Да ведь не может, не мо-жет система так себя вести, потому что не может никогда! Ну правда же! А?!» Не-а. Получите, распишитесь.

В итоге реальность оказывается ширше ширее шире ожидаемого. Пара примеров:

История №1. MicroSD-флэшка и работа по DMA

Анамнез

Нужно сбрасывать данные в файл на SD-карту. Конечно, самостоятельно писать файловую систему и драйвер SDIO нет ни времени, ни желания, поэтому беру готовую библиотеку. Настраиваю под железо, и все отлично работает. Поначалу. А потом выясняется, что данные записаны диковато: объемы точные, но в самих файлах отдельные пары-тройки байт то дублируются, то пропадают, без какой-либо закономерности. Нехорошо!

Начинаются эксперименты. Пишу тестовые данные — все ок. Пишу боевые — какая-то чертовщина. Меняю размер буферов данных, периодичность их сброса, шаблоны данных — бесполезно. В самих буферах все всегда великолепно, данные в памяти везде те, что надо. И, тем не менее, глюки на флэшке — вот они.

На раскопку собаки ушла где-то пара дней.

Диагноз

Проблема оказалась во взаимодействии библиотеки с аппаратурой DMA.

У SD-карточек есть особенность: они пишутся только блоками по 512 байт. Для этого библиотека буферизует данные в 512-байтный массив, и по его заполнении сбрасывает оттуда через DMA на флэш. Но!

Если я передаю на запись фрагмент, больший по размеру, чем <512хN+пустое место в буфере библиотеки> байт, то библиотека (очевидно, чтобы не гонять лишний раз память туда-сюда), делает так: дозаполняет свой буфер, пишет его на флэш, и следующие следующие 512хN байт кидает в DMA прямиком из моего буфера! Ну и, если что-то осталось недописанное — снова копирует в свой, до следующего раза.

И все бы ничего, но контроллер DMA требует, чтобы данные были размещены в памяти с выравниванием по 4-байтной границе. Библиотечный-то буфер всегда так выровнен, язык это гарантирует. А вот с какого адреса, после копирования части данных, начинаются те оставшиеся 512xN с небольшим байт у меня — бог весть. И библиотека это никак не проверяет: адрес, как есть, передается контроллеру DMA.

«Корявое что-то прислали… Пес с ним.» Контроллер молча обнуляет младшие 2 бита переданного адреса. И запускает передачу.
Маскирующиеся баги в эмбедде - 2

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

Лечение

Пришлось добавить еще один буфер непосредственно перед вызовом аппаратной функции записи. Если передаваемый на запись адрес не кратен 4, данные сначала копируются в него. Заодно возросла средняя скорость за счет обоснованного выбора размера буфера. Конечно, на это ушла память, но что такое 4 килобайта на благое дело, когда у тебя в распоряжении — необозримые 192!

История №2. Рантайм и куча

Пролог

После очередного изменения программа начала падать, и падать как-то очень жестко, выкидывая процессор в обработчик Hard Fault-а. Причем выкидывало его туда сразу после старта, еще до того, как выполнение добиралось до main(), то есть ни строчки моего кода выполниться не успевало.

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

Глава 1

Полез в интернеты смотреть, как получить хоть какую-то дополнительную информацию. Нагуглилась процедура разбора последствий хардфолта: состояние регистров, дамп стека. Допилил. Воспользовался.

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

Продолжение анализа показало, что глюк — следствие попытки инициализации локальных статических переменных.

Лирическое отступление

Кстати, рассматривая дизассемблированный код, я попутно узнал ответ на вопрос, который иногда задавал себе, но был слишком ленив, чтобы сразу погуглить: как разруливается ситуация, когда такую переменную могут попытаться одновременно инициализировать 2 потока и более. Оказалось, что в этом случае компилятор обставляет инициализацию семафорами, гарантирующими, что только один поток за раз пройдет процедуру целиком, а остальные дождутся, пока закончит первый. Такое поведение стандартизировано, начиная с С++11. Вы знали? Я — нет.

Глава 2

Раз рантайм занимается конструированием переменных, ему же и вызывать деструкторы по завершении работы программы (даже если фактически программа никогда работу не завершит, что для микроконтроллеров абсолютная норма). Для этого ему надо где-то хранить информацию обо всех переменных, которые он успел наинициализировать.

Вот именно в месте, где такая информация сохраняется в какой-то внутренний список, рантайм и падал. Потому что функция malloc(), по стандарту выделяющая память, гарантированно выровненную как минимум по границе 8 байт, после н-ного количества удачных вызовов выдавала кусок, не выровненный по этой границе.

Маскирующиеся баги в эмбедде - 3

Полез в опции компилятора, искать какие-то ключевые слова, справки, но везде было сказано четко: malloc() гарантирует выдачу памяти, выровненной по фундаментальной границе. Либо null pointer в случае, если памяти недостаточно.

Глава 3

Долго я бессмысленно втыкал в код, ставил брекпойнты, страдал и ничего не понимал, пока в какой-то момент не торкнуло и я не поглядел на возвращаемые malloc-ом адреса внимательнее. До этого весь анализ состоял в том, чтобы посмотреть, кратна ли последняя цифра адреса 0x4. А теперь стал сравнивать целиком между собой адреса, выдаваемые последовательными вызовами malloc-а.

И, о, чудо!

Все успешные вызовы выдавали адреса из пространства оперативной памяти (0x20000000 и старше для этого камня), последовательно увеличивающиеся от вызова к вызову. А первый же неудачный — возвращал 0x00000036. То есть адрес мало того, что был не выровнен, так еще и находился вообще не в адресном пространстве оперативки! Процессор пытался скопировать что-нибудь оттуда и закономерно падал.

Причем, что удивительно, даже если бы malloc() действовал по стандарту и возвращал 0 при нехватке места — это ничего бы не изменило в смысле падения программы (разве что причина бага прояснилась бы раньше). Возвращаемое malloc-ом значение все равно никак не проверяется, а сразу идет в дело. Это в рантайме-то.

Эпилог

Увеличил в настроечном файле размер кучи, и все починилось.

А ведь до того момента я об ее объеме даже и задумывался. На черта ли сдалась мне эта куча, полагал я. Все равно у меня все переменные и объекты либо статические, либо лежат на стеке. Так, просто по инерции оставил под нее 0х300 байт, раз уж какой-то объем под кучу выделяется во всех шаблонных проектах. Ан нет вот, для рантайма С++ все равно нужна динамически выделяемая память, причем в достаточно заметных, по меркам контроллеров, количествах.

Век живи — век учись.

Автор: MicroCephalis

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js