Как избежать типичных багов встроенного ПО. ч.1
13/11/2011 - 16:05
Pavel Bobkov
Найэл Мерфи
Понимание ошибок программного и аппаратного обеспечения на примере других встраиваемых систем может помочь Вам идентифицировать, диагностировать и исправить ошибки в своей собственной системе.
Знаменитые ошибки – это своего рода военные истории разработчика ПО. В то время как остальные люди предпочли бы толочь воду в ступе, чем слушать историю, которая заканчивается словами "... и затем я понял, что эта переменная должна была быть 16-разрядным счетчиком, ха, ха, ха!", другим инженерам нравится слушать рассказы о злых шутках, которые сыграл с ними код. В этой статье мы исследуем наиболее коварные ошибки, с которыми я столкнулся за эти годы - иногда как автор, иногда как мастер по отладке, а иногда как заинтересованный наблюдатель.
Несмотря на то, что ни одна из этих ошибок не причинила серьёзного вреда, все они иллюстрируют любопытные особенности. По ходу повествования мы посмотрим на некоторые особенности работы препроцессора C, и я дам кое-какие предупреждения, связанные с использованием таймеров.
Побочные эффекты оптимизации
Первая группа ошибок вызывает изменения, которые никак не должны влиять на систему, однако непостижимым образом это происходит. Различные опции оптимизации никак не должны влиять на поведение системы с точки зрения функционирования, они влияют лишь на скорость выполнения кода и его размер. Конечно, когда система, работающая в реальном времени, начинает ускоренно выполнять определенные фрагменты кода, может возникнуть условие состязания сигналов, однако существуют и другие незаметные воздействия от оптимизации, из-за которых вы можете попасть впросак.
Во многих компиляторах предусмотрена опция, позволяющая Вам экономить место путём сохранения только одной копии идентичных строк. Например, если символьная строка "hello" появляется в исходном коде в трех местах, сохраняется только одна копия, а все другие места в коде, использующие данную строку, обратятся к этой копии. Поскольку этот метод применяется лишь к постоянным строкам, нет никакой опасности в том, что одна ссылка на строку изменит её.
В одном случае мы с коллегами нашли ошибку в коде, которой не было в предыдущей версии. Мы изучили историю изменений от предыдущей версии к текущей и сузили поиски до этой опции компилятора. Более детальное исследование кода показало, что из-за неаккуратно выполненной работы мы стали незащищенными перед влиянием оптимизации. В листинге 1 представлена упрощенная версия того, что мы делали.
x = "hello"; // hello1
// more code
if (leaving == TRUE)
{
y = "goodbye";
}
else
{
y = "hello"; // hello2
}
if (copying == TRUE)
{
y = x;
}
// more code
if (x == y)
{
identical();
}
Оператор if сравнивает два указателя. Перед нами две копии строки "hello". Мы воспользуемся hello1 и hello2, чтобы обратиться к их адресам памяти. После очень длительного использования строковых классов C++, где оператор == был перегружен, чтобы сравнивать содержание строкового класса, я применил ту же самую логику к указателям С. Оператор == не проверяет, являются ли строки идентичными. Вместо этого он проверяет, указывают ли указатели на одно и то же место памяти. При выключенной оптимизации hello1 и hello2 занимают различные расположения и, таким образом, даже если x указывает на "hello" и y указывает на копию "hello", находящуюся в другой позиции, конечное условие if будет ЛОЖНО.
Теперь включите оптимизацию, и поведение изменится. Поскольку строка в hello1 и hello2 идентична, компилятор хранит одну копию "hello", поэтому и hello1, и hello2 обратятся к этой позиции. В этом случае выполнение строк x = "hello" и y = "hello" приводит к тому, что результат сравнения x и y оказывается ИСТИННЫМ.
Суть в том, что if (x == y) не был правильным типом сравнения. Всегда должна использоваться функция strcmp () или ее эквивалент.
Граничные условия
В фильме «Парк юрского периода» есть сцена, в которой герой Джеффа Голдблюма обнаруживает, что их метод проверки того, все ли динозавры присутствуют, заключается в подсчёте общего числа животных, и как только оно достигает определенной отметки, счёт прекращается. То, что число динозавров увеличивалось, не было замечено потому, что герои фильма, достигая ожидаемого числа динозавров, никогда не продолжали счёт. Умные животные выяснили, как размножаться, несмотря на то, что, предположительно, все клонированные динозавры были женского пола.
Герой Голдблюма предлагает, чтобы компьютер искал большее число динозавров, и когда число найденных животных превышает исходное количество, группа понимает, что у них гораздо больше доисторических животных, чем предполагалось. Я называю эту ошибку «ошибкой Парка юрского периода». Это ошибка установки верхнего предела какого-нибудь значения, ложная уверенность в том, что большее число не встретится или не будет иметь значение при возникновении. Это рациональный метод, который часто упрощает программирование, но из-за него могут возникнуть неожиданные состояния, незаметные для системы.
Может ли такая ошибка возникнуть в Вашем программном обеспечении? Если Вы храните число в 8-ми разрядной переменной, Вы должны остановить счёт на 255. Это значение можно рассматривать как некое ошибочное состояние или как приближение максимально возможного значения переменной в системе. Какой из этих вариантов лучше, зависит от того, достигает ли счетчик 255 в ошибочном состоянии или в нормальном.
Когда мы используем арифметику с разбиением чисел на целые и дробные части, может оказаться полезным ограничивать значения этих частей так, чтобы расчеты с использованием слишком больших чисел не приводили к переполнению переменных.
В качестве иллюстрации «ошибки Парка юрского периода» приведу пример из своей практики. У нас была система, измеряющая поток газа. Обычно система контролировала потоки до 15 л/мин. Верхнюю границу измеряемого потока мы установили в 25 л/мин. Мы полагали, что даже если потоки газа превысят 25 л/мин, можно рассматривать их равными этой величине.
В некоторых случаях поток действительно превышал 25 л/мин, но мы полагали, что эти случаи можно не учитывать, поскольку система не находилась в состоянии контроля. Один такой случай был тестом, в котором сравнивались потоки, обнаруженные двумя идентичными датчиками. При условии, что датчики функционировали правильно и давали точные показания, потоки в каждом датчике должны были быть в пределах допуска друг друга. Когда внешнее давление в системе подачи газа было очень высоким, тестирование могло происходить и с превышением планки в 25 л/мин. Если предположить, что калибровка одного датчика была смещена, то скорость потока, измеренная этими двумя датчиками, равнялась, например, 26 л/мин и 29 л/мин. Обе эти величины впоследствии округлялись до 25 л/мин по описанным выше причинам. Когда пришло время сравнить оба показания, они были равны 25л/мин и соответственно равны друг другу. Обнаружив такую ситуацию, мы запретили использование сравнительного теста для потоков свыше 25л/мин.
Даже если Вы не ставите ограничение на считывание показаний в программном обеспечении, ограничения на Ваш проект могут наложить аппаратные средства. Например, выходное напряжение датчика будет иметь верхний предел, ограничивающий показание расхода. Я бы рекомендовал вам всегда использовать датчики с более широким диапазоном, чем предполагаемый диапазон Вашей системы. Однако имейте в виду, Вам придется разменять широкий диапазон датчика на разрешающую способность прибора. Датчик с меньшим диапазоном и большим разрешением может оказаться более точным, но вы можете оказаться в мёртвой точке, в которой не видно, что происходит.
Я периодически вижу «ошибку Парка юрского периода» в журналах событий. Рассмотрим устройство, которое записывает исключительные события в журнале. Имеется ограниченное пространство, предназначенное для хранения события, например, создаётся журнал с 30 записями. Каждая запись – это структура, содержащая детали типа события, время и, возможно, текущие настройки устройства. В полном журнале содержатся 30 событий. Однако если возникнет 50 событий, то в журнале их по-прежнему будет 30, таким образом, истинное количество событий оказывается скрытым. В этом случае проблему можно облегчить, используя одну заключительную строку, которая содержит количество незафиксированных событий. Хотя этот метод и не поможет узнать вам детали событий, по крайней мере, Вы будете знать, что что-то пропущено. Во время использования устройства этот счетчик укажет вам на то, была ли величина вложена в больший объём пространства памяти журнала.
Существует множество других разновидностей «ошибки Парка юрского периода». После того, как я неоднократно страдал из-за этой ошибки, я с меньшей охотой ставлю ограничение величины на некоторый максимум. Я предпочитаю ставить предел там, где могу утверждать, что система даст сбой при превышении предела.
Побочные эффекты комментариев
Один мой коллега работал над контрольной суммой, которая использовалась для подтверждения того, что обновленные копии нашего программного обеспечения загружаются без повреждений. В качестве одного из этапов тестирования он отмечал значения контрольной суммы для каждой исполняемой программы. Однажды он приехал ко мне и сообщил, что понял - то о чём он думал, вероятно, было ошибкой компилятора. Поскольку в этом компиляторе было очень много ошибок, это нас не особо удивило.
Этот коллега изменил комментарий, и контрольная сумма стала отличаться от своего предыдущего значения. Как правило, изменение комментария не должно оказывать влияние на сгенерированный код, и поэтому контрольная сумма должна была быть идентичной до и после изменения комментария. Я сразу предположил, что были изменены какие-то опции компилятора, вследствие чего мог измениться результат без изменения исходного кода. Однако никаких изменений не было. Я спросил, использовался ли другой компьютер для компилирования программы — у нас, возможно, была более старая версия компилятора. Но это также не было решением проблемы.
Мы сохраняли в коде номер версии. В некоторых наших проектах этот номер автоматически обновлялся с каждой сборкой, и поэтому две компиляции не были бы идентичны. В данном проекте номер менялся, только когда программист вручную редактировал файл. В этом случае он не был изменен и не мог быть причиной изменения контрольной суммы.
Я подумал, что в какой-то момент в эту программу могла быть вставлена отметка даты. Возможно, такова была функция, встроенная в компилятор. Если бы отметка даты была сохранена в программе, то она изменила бы контрольную сумму, даже если бы к отметке даты рабочий код никогда не получал доступ. Я внимательно просмотрел руководство к компилятору и не смог найти ссылку на такой метод. Мы, в конечном счете, устранили воздействие времени суток, просто проводя повторную компиляцию и не внося изменений. Время шло, но контрольная сумма оставалась той же самой, таким образом, проблема заключалась не сохраненной метке времени.
Используя утилиту, мы проверяли, где в коде возникли изменения. Если бы мы могли определить такое расположение в двоичном файле, можно было бы проследить его до исходного кода. Изменений было много, они были небольшими и разбросаны по всему модулю, который содержал изменение комментария. Это напоминало изменение флага оптимизации, поскольку это нечто такое, что могло оказывать воздействие во многих местах, однако мы уже устранили эту возможность. Размер кода совершенно не изменился, что также давало возможность предположить, что причина заключалась не в оптимизации.
Я еще раз взглянул на комментарий, который вносил изменения. К блоку текста была добавлена одна дополнительная строка. Это не должно менять суть программы, но при этом меняются номера строк. И тут я всё понял.
Мы много использовали макрос assert(). Этот макрос использует встроенный в компилятор макрос __ LINE __, который приравнивается к текущей строке скомпилированного файла. Если бы мы изменили номера строк, то значение __ LINE __ , используемое каждым assert(), отличалось бы, а эти значения сохранялись в исходной программе.
Таким образом, тайна изменения контрольной суммы была раскрыта. В конечном итоге оказалось, что, по сути, это явление не было реальной ошибкой, поскольку не представляло собой проблему при условии, что программисты знали, какое влияние могло оказать изменение комментариев.
Как избежать типичных багов встроенного ПО ч.2
Как избежать типичных багов встроенного ПО ч.2
Niall Murphy "How to Avoid Common Firmware Bugs"
Вольный перевод ChipEnable.Ru
Comments
т.е. например цикл где переменная A меняется от 0 до 10 и мы сравниваем условие A=10?. если вдруг по каким то причинам переменная A выйдет за предел 10 то мы выполним эту операцию непонятное число раз пока переменная не переполнится и отсчет не начнется занова с нуля. Если бы мы проверяли условие A=>10? то при таком сбое цикл был бы короче и сбой не такой критичный, но опять же зависит от конкретных условий...
2 igor727: Вас вообще кто программировать то учил? При рабое с циклами крайне нежелательно использовать жёсткие условия. Читайте ГОСТы, там много чего интересного есть.
Да и всем кстати не угодишь. Кто то в школе на голове стоял, а кто то этого и сейчас не умеет. Зато умеет что то другое.
RSS feed for comments to this post