С теорией критического программного обеспечения можно ознакомиться тут.
Ограничение на простые конструкции потока управления
Не используйте инструкции goto, конструкторы setjmp/longjmp и прямую/косвенную рекурсии.
Упрощенный поток управления преобразуется в более надежные возможности для проверки и часто приводит к улучшению четкости кода. Без рекурсии гарантируется наличие графика вызовов ациклических функций, который может помочь доказать, что ограничены все нужные исполнения кода.
Обратите внимание, что это правило не требует, чтобы все функции имели единственную точку возврата, хотя это тоже часто упрощает поток управления. Но есть немало случаев, когда раннее возвращение ошибки является более простым решением.
Конечно, плохо написанный код не станет неожиданно надежным, если вы просто исключите “сомнительные” блоки функционала из него. Но надежный код почти всегда записывается без них.
Goto впервые получил дурное имя, когда в 1968 году Эдсгер Дейкстра опубликовал свою статью «Утверждение Goto считается вредным». Эта статья вызвала очень жаркие дискуссии в области разработки программного обеспечения за последние несколько десятилетий. Сегодня мало кто может сказать, что он может изменить свой стиль программирования, а также эмпирически не доказано, правильно ли суждение Дейкстры.
Задайте циклам фиксированный верхний предел
Отсутствие рекурсии и наличие границ цикла помогает избежать проблем в поддержке критического программного обеспечения. Разумеется, это правило не применяется к итерациям, которые должны быть бесконечными (например, в планировщике процессов или задач), но данные случаи относительно редки.
Простой способ соблюдения правила в том, чтобы добавить явную верхнюю границу ко всем циклам, которые имеют переменное число итераций. Когда верхняя граница превышена, происходит прерывание, а функция возвращает ошибку. Верхняя граница служит для предотвращения бесконечного цикла, поэтому абсолютное значение данной границы менее важно. Ограничения по размеру предпочтительны, чтобы предотвратить сбои по достижении ошибки.
Чтобы помочь статическому анализатору определить, что цикл обязательно всегда завершается, иногда необходимо немного переписать код, чтобы сделать его более предсказуемым. Одно правило, которое анализатор может легко проверить – это то, что индекс цикла из цикла for в программе C не изменяется внутри тела цикла. Соблюдение этого подправила часто применяется в критическом ПО.
Не используйте динамическое распределение памяти после инициализации
Это правило является общим для критического программного обеспечения и рассматривается в большинстве книг по разработке. Причина проста: распределители памяти, такие как сборщики malloc и сборщики мусора, часто имеют непредсказуемое поведение, которое может значительно повлиять на производительность.
Разработка приложений для работы в фиксированной заранее распределенной области памяти может устранить многие проблемы. Обратите внимание, что единственный способ претендовать на динамическую память при отсутствии выделения памяти из кучи – использовать стек памяти.
В отсутствие рекурсии верхняя граница использования памяти стека может быть статической, что позволяет доказать, что приложение всегда будет жить в пределах своих ресурсов выделенной памяти.
Функции на не более чем N строк кода
Функция не должна быть длиннее, чем то, что может быть напечатано на одном листе бумаги в стандартном формате с одной строкой для каждого оператора и одной строкой для объявления. Как правило, это не более 60 строк кода для каждой функции. N может быть любым значением в диапазоне 50 – 100 строк.
Каждая функция должна быть логической, понятной и поддающейся проверке. Сложно понять логический блок, который растянут на несколько экранов или страниц при печати. Чрезмерно длинные функции часто являются признаком плохо структурированного кода.
Опишем несколько вариантов того, как можно “причесать” функцию:
- Перед подсчетом длины функции мы создаем копию каждого файла и форматируем его стандартным способом (например, стиль K&R). Это делается для того, чтобы избежать зависимости от определенных способов, которыми разные разработчики форматируют свой код, а также для предотвращения объединения нескольких операторов или объявлений в одну строку. Нормализация макета кода может быть выполнена с помощью инструмента Linux: “indent -kr file.c”.
- Затем мы удаляем все комментарии и пустые строки из нормализованного кода. Это можно сделать с помощью инструмента ncsl: “ncsl -s file.c> output”.
- Затем мы измеряем длину функции, считая от имени функции до закрывающей фигурной скобки.
Используйте минимум N assert-ов для каждой функции длиннее M строк
Assert-ы используются для нахождения аномальных условий, которые никогда не должны появиться в коде во время выполнения.
Когда assert сталкивается с ошибкой происходит возврат условия с ошибкой функции, выполняющей этот assert. Assert-ы, для которых статическая проверка может доказать, что они никогда не закончатся с ошибкой или никогда не вызовут прерывание, должны быть удалены и заменены комментариями.
Для безопасности критического ПО типичными значениями для N и M являются 2 и 20 соответственно. Для менее важных приложений значение M может быть увеличено, но оно всегда должно быть меньше максимальной длины функции.
Статистика промышленного кодирования показывает, что в unit-тестах часто обнаруживается по крайней мере один дефект на 10-100 строк написанного кода. Вероятность перехвата дефектов возрастает с плотностью assert-ов. Используйте assert-ы так часто, как рекомендуется в рамках стратегии защитного программирования. Assert-ы могут использоваться для проверки пред- и пост-условий функций, значений параметров, возвращаемых значений функций и циклов-инвариантов. Типичное использование assert-ов может быть таким:
В этом определении __FILE__ и __LINE__ предопределены для создания имени файла и номера строки неудачного assert-а. Синтаксис #e превращает условие утверждения “e” в строку, которая выводится, как часть сообщения об ошибке. Вызов tst_debugging превращается в no-op, и утверждение превращается в чистый логический тест, который позволяет восстанавливать ошибки возникшие в результате аномального поведения программного обеспечения.
Объявляйте объекты данных на минимально возможной области видимости
Это правило поддерживает основной принцип сокрытия данных. Очевидно, что если объект не находится в области видимости, его значение нельзя передать как ссылку или повредить. Аналогичным образом, если необходимо диагностировать ошибочное значение объекта: чем меньше количество операторов, в которых значение могло быть назначено, тем легче диагностировать проблему. Правило не рекомендует повторное использование переменных для нескольких несовместимых целей, что может затруднить диагностику неисправностей.
Данные всегда должны быть объявлены в начале области, в которой она используется: для области файла объявления отображаются вверху исходного файла. Для области функций декларация идет вверху тела функции, а для области блока – в начале блока. Это означает, что объявления не должны размещаться в произвольных местах в коде, например, в точке первого использования.
Объекты данных, используемые только в одном файле критичного программного обеспечения, должны быть объявлены статическими.
Проверяйте возвращаемое значение non-void функций и правильность параметров
Возвращаемые значения non-void функций и ее параметры должны проверяться для каждой вызываемой функции. Это, возможно, наиболее часто нарушаемое правило в разработке любого программного обеспечения и оно гласит, что необходимо проверить даже возвращаемое значение операторов printf и операторов закрытия файла.
Однако может быть ситуация, когда код ошибки будет похож на код успешного выполнения программы, тогда нет смысла проверять возвращаемое значение. Это часто случается с вызовами printf и закрытием. В подобных случаях можно явно передать возвращаемое значение функции в (void) – тем самым указывая на то, что программист не случайно решает игнорировать возвращаемое значение.
Исполнение этого правила гарантирует, что исключения всегда явно обоснованы. Гораздо легче соблюдать правило, чем объяснить, почему несоблюдение является приемлемым.
Ограничьте использование препроцессора для включения файлов и простых макросов
Использование препроцессора должно быть ограничено включением файлов заголовков и простых макроопределений. Вставка меток, списков переменных аргументов и рекурсивных макровызовов запрещены.
Все макросы должны расширяться до полных синтаксических единиц.
Использование директив условной компиляции должно быть ограничено предотвращением повторного включения файлов в файлы заголовков.
Препроцессор C представляет собой мощный инструмент для обфускации, который может разрушить ясность кода. В новой реализации препроцессора C разработчикам часто приходится прибегать к использованию более ранних реализаций в качестве рефери для интерпретации сложного определяющего языка в стандарте C.
Макросы должны отображаться только в заголовочных файлах и никогда в исходном коде. Директива #undef не должна использоваться. Макросы никогда не должны скрывать объявления, и они не должны скрывать операции разыменования указателей с кодом. Ограничение макроопределений на определение полных синтаксических единиц означает, что все макрообъекты должны быть заключены в круглые или фигурные скобки.
Ограничьте использование указателей. Не более двух уровней разыменования для каждого выражения
Операции разыменования указателей не могут быть скрыты в определениях макросов или внутри объявлений typedef. Использование указателей функций должно быть ограничено простыми случаями. Указателями легко злоупотребляют, даже опытные программисты. Они могут затруднить отслеживание или анализ потока данных в программе, особенно с помощью инструментальных анализаторов. Аналогично, указатели на функции могут серьезно ограничивать типы проверок, которые могут выполняться статическими анализаторами, и их следует использовать только в том случае, если их использование обоснованное.
Операции разблокировки. Предел двух операций разблокировки для выражения означает, что должно быть не более двух операторов слева или справа от оператора присваивания или в любом отдельном условном выражении.
Указатели функций. Статический анализатор должен во всех случаях определять, какая функция вызывается, если вызов выполняется с помощью указателя функции. Допустимо разрешение случаев, когда число возможных вызываемых функций больше одного, если это число не влияет на точность самого анализа кода. Это означает, что оно может зависеть от возможностей конкретного статического анализатора.
Компиляция с включенными предупреждениями и использование одного или нескольких анализаторов исходного кода
Весь код с самого начала разработки должен быть скомпилирован со всеми включенными предупреждения компилятора. Весь код должен компилироваться с этими настройками без предупреждений. Весь код должен быть проверен на каждой сборке, по крайней мере, с одним, а лучше несколькими, современными статическими анализаторами исходного кода и должен проходить анализы с нулевыми предупреждениями.
На рынке существует несколько очень эффективных статических анализаторов исходного кода, а также множество бесплатных программ. Нет никаких оправданий, чтобы не использовать эту технологию. Его следует рассматривать как рутинную практику, особенно для разработки критически важных программ. Предупреждение о правилах нуля применяется даже в тех случаях, когда компилятор или статический анализатор дают ошибочное предупреждение: компилятор “запутался”, код, вызывающий прерывание, должен быть переписан.
Статические анализаторы изначально имели плохую репутацию из-за ограниченных возможностей ранних версий (например, раннего инструментария Unix). Ранние инструменты генерировали в основном недопустимые сообщения, но это не относится к текущему поколению коммерческих инструментов. На сегодняшний день статические анализаторы производят выборочные и точные сообщения очень быстро.