Хабрахабр

[Из песочницы] Маскирующиеся баги в эмбедде

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

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


Выбор делителя частоты для настройки шины CAN

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

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

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

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

Анамнез

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

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

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

Диагноз

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

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

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

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

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

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

Лечение

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

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

Пролог

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

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

Глава 1

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

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

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

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

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

Глава 2

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

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

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

Глава 3

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

И, о, чудо!

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

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

Эпилог

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть