Язык спецификаций поведения программных компонентов

Вычислительные машины и программное обеспечение

УДК 004.432, 004.052.42

В.М. Ицыксон, М.И. Глухих ЯЗЫК СПЕЦИФИКАЦИЙ ПОВЕДЕНИЯ ПРОГРАММНЫХ КОМПОНЕНТОВ

Вопросы проектирования программных систем становятся все более актуальными в современном мире. Так как сложность программного кода постоянно возрастает, качество программных систем должно соответствовать требованиям. Одной из ключевых характеристик качества программного обеспечения (ПО) является надежность.

Немаловажную роль в повышении надежности ПО играет своевременное обнаружение программных ошибок (дефектов). Особенно важна задача обнаружения дефектов в программах на языках С и С++ в связи с большим объемом существующего С/С++ кода и особенностями языковых средств: отсутствием контроля программных ошибок, наличием операций с указателями, макроопределений и т. п. При большом объеме исходного кода проекта поиск дефектов путем тестирования может быть трудоемкой задачей. Кроме того, тестирование не гарантирует обнаружение всех дефектов в программе. Поэтому все большее распространение получают автоматизированные методы поиска дефектов, основанные на статическом анализе исходного кода программы. Эти методы используют в качестве входных данных только исходные тексты анализируемого проекта, не требуют запуска программы и при этом гарантируют анализ всех возможных трасс выполнения.

Статический анализ предполагает проверку всего исходного кода программного проекта, который может состоять из множества модулей. При этом часто возникает ситуация, когда поведение исследуемой программы определяется не только ее исходным текстом, но и внешними компонентами. Такими компонентами могут быть стандартные библиотеки (например, НЬс или pthread) или используемые в составе проекта проприетарные модули. Сложность анализа в таком случае за-

ключается в отсутствии части исходных текстов проекта, что не позволяет полностью провести построение модели программы [1] и полноценно проанализировать программный проект.

Решением проблемы может служить использование механизмов задания семантики компонента с отсутствующим исходным кодом. Задача полностью специфицировать поведение внешнего компонента не ставится, достаточно описать аспекты поведения, влияющие на анализируемый проект. Такого рода описания будем называть «частичными спецификациями компонентов» или «аннотациями». Подобные спецификации позволяют задавать для системы анализа поведение компонента с требуемой степенью детализации.

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

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

выделенные блоки в динамической памяти;

файлы и файловые дескрипторы;

сокеты (socket);

потоки ввода-вывода (stream);

семафоры;

критические секции;

мьютексы и т. п.

Будем в дальнейшем называть такие объекты «ресурсами». Каждый ресурс обладает своим жизненным циклом, специфичным для каждого типа ресурсов. За время жизни ресурс меняет свое состояние в соответствии с определенными правилами. Причиной смены состояния может служить вызов функции API или событие операционной системы. Ресурс может иметь один или несколько атрибутов. Примером атрибута может являться размер выделенного участка динамической памяти. Атрибуты также могут изменяться в течение жизни ресурса. Основные операции с ресурсами: создание ресурса; освобождение; изменение состояния и атрибутов ресурса.

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

Рассмотрим существующие подходы к описанию поведения компонентов с отсутствующим исходным кодом.

Подходы к заданию спецификаций поведения компонентов. В настоящее время используются три подхода к описанию поведения компонентов:

расширение целевого языка (С или С++) с помощью специализированных аннотирующих комментариев;

расширение целевого языка с использованием его внутренних средств;

разработка внешних средств для описания поведения компонентов.

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

Яркими представителями этого подхода являются язык ACSL, используемый средством Frama-C, и язык аннотирующих комментариев, используемый средством Splint.

Язык ACSL (ANSI/ISO C Specification Language) [2] позволяет задать дополнительную информацию о поведении функций и конструкций, используемую при анализе программы. Ин-

формация представляется комментариями особого вида (/*@ ... */ или //@ ...).

Язык ACSL позволяет программисту описывать контракты, которые должны быть выполнены функцией. Контракт задается описанием предусловий, постусловий и поведения функции. Предусловия (requires) должны выполняться непосредственно до вызова функции, постусловия (ensures) должны гарантированно выполняться после вызова функции. В языке имеются развитые средства задания сложного поведения и описания изменяемых функцией переменных (behavior, assumes, assigns). ACSL также позволяет задавать контракт для отдельных конструкций языка.

Специальная группа средств предназначена для описания инвариантов поведения, т. е. условий, которые должны всегда выполняться в определенной части программы. К ним относятся:

инварианты условий (assert) - справедливы в конкретной точке программы;

инварианты циклов (invariant) - справедливы внутри определенного цикла;

глобальные инварианты (global) - справедливы на всем протяжении программы.

Язык аннотирующих комментариев, используемый средством Splint [3], является менее функциональным. Он также использует комментарии особого рода /*@ ... @*/,с помощью которых можно задавать следующие свойства:

способность функции влиять на параметры и глобальные переменные;

вид возвращаемого указателя (указатель на вновь выделенный объект, на статическое хранилище и т. п.);

ограничения на значения параметра-указателя функции (например, может ли параметр-указатель функции равняться null);

направление передачи информации через параметр-указатель функции (т. е. является ли параметр-указатель функции входным или выходным параметром).

Интеграция в целевой язык - это одновременно и достоинство, и недостаток данного подхода. С одной стороны, интеграция позволяет привязать аннотирующие комментарии к определенным конструкциям программы, что облегчает интерпретацию этих комментариев и позволяет в удобной форме задать свойства любых объектов и конструкций программы.

С другой стороны, интеграция требует моди-

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

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

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

Одним из представителей этого подхода является пакет CLANG [4]. Данный пакет использует

атрибуты компилятора GCC _attribute_,

вызовы assert, а также настройки #pragma для расширения программы дополнительной семантикой. С помощью перечисленных средств можно, например, указать, является ли функция завершающей в программе, задать дополнительную информацию о значениях параметров функции или помечать вызовами assert гарантированно выполняющиеся условия.

Аналогичный подход используется в среде Microsoft Visual Studio (начиная с версии 2005). Язык аннотаций SAL [5] (Source Annotation Language), поддерживаемый средой, обладает следующими возможностями:

для параметра-буфера можно задать вариант использования (только чтение, только запись, чтение/запись), а также размер буфера;

для параметра-указателя или возвращаемого значения-указателя можно задать признак «указывает на динамическую память, созданную в функции»;

для возвращаемого значения можно задать признак «не может быть игнорировано»;

для указателя на строку можно задать признак

«является форматной строкой»;

для точек выхода из функции можно задать признак «завершилась с ошибкой».

Похожий подход используется в средстве PC Lint [6], где вызовами assert задаются гарантированно выполняющиеся условия.

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

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

Среди специализированных языков можно отметить язык Metal, используемый системой статического анализа xgcc, и язык CASL, используемый для проведения дедуктивной верификации.

Язык Metal [7] предназначен для создания расширений компиляции, так называемых «checker-ов». При их написании в программе выделяется один или несколько конечных автоматов, каждый из которых привязывается к одной или нескольким переменным, существующим в программе. Например, указателю может соответствовать автомат с состояниями «указывает на освобожденную память», «указывает на выделенную память». Дополнительно могут выделяться глобальные автоматы, состояние которых не связано с конкретными переменными.

Для каждого автомата определяются правила переходов из состояния в состояние, а также дефекты, связанные с действиями в данном состоянии. При этом используются ссылки на конкретные функции или операции. В листинге 1 приведен пример описания на языке Metal дефектов «разыменование освобожденного указателя» и «двойное освобождение указателя».

CASL (Common Algebraic Specification Language) [8] - язык алгебраических спецификаций, основанный на логике первого порядка. Основные его возможности связаны с описанием свойств отношений, типов и операций над ними,

state decl any pointer v; start: { kfree(v) } ==> v.freed; v.freed: { *v } ==> v.stop,

{ егг(«Использование %s после освобождения!», mc identifier(v)); | { kfree(v) } ==> v.stop,

{ eгг(«Двойное освобождение %s!», mc identifier(v)); };

Листинг 1: Пример описания на языке Metal

функций. Синтаксис языка использует форму Бэкуса-Наура, логические операции и операции над множествами, предикаты. С помощью спецификаций на этом языке можно задавать и проверять определенные утверждения, касающиеся анализируемой программы. Язык используется в некоторых средствах дедуктивной верификации.

К классу средств, использующих специализированные языки, относится также средство Fortify [9]. В нем применяются XML-описания, предназначенные для задания новых правил поиска дефектов.

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

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

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

Требования к специализированному языку. Основной целью разрабатываемого язы-

ка является возможность описания поведения используемых в программе функций, исходный код которых отсутствует либо слишком сложен для анализа. Так как предполагается применение аннотированных функций в процессе анализа программ на языке С, то логичным решением является использование для аннотаций С-подобного императивного языка.

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

Для ресурсов необходимо предусмотреть задание:

имени ресурса;

перечня возможных состояний; типа ресурса; атрибутов ресурса.

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

арифметические операции, операция присваивания;

операции с участками памяти (заполнение, копирование);

конструкция сигнализации о наличии дефекта; операции с работы ресурсами (создание, освобождение, смена состояния, изменение атрибутов).

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

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

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

за счет запрета использования рекурсивных вызовов функций в теле поведенческих описаний;

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

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

ветствии с перечисленными выше требованиями и ограничениями был разработан язык аннотаций PanLang (program annotation language). В качестве основы синтаксиса используется синтаксис языка С. Для реализации специфичных для описания семантики возможностей язык был расширен дополнительными конструкциями.

Типы данных. Язык PanLang использует те же базовые типы данных, что и целевой язык C. Поддерживаются константы базовых типов. Основным отличием от стандартных языков программирования является возможность использования интервальных значений для переменных. Интервальные значения служат основой для применения алгоритмов интервального анализа при проведении статического анализа программы. При этом объектам программы могут присваиваться не только конкретные значения, но также и интервалы, которые описывают диапазон значений, который могут принимать объекты после вызова аннотируемой функции. Такими объектами могут быть возвращаемые значения функции, параметры функции, переданные через указатели, или глобальные переменные.

Описание разработанного языка. В соот-

ге^гп [0:99]; // Возвращаемое значение функции может // принимать любое значение от 0 до 99 еггпо = [-1:+^^; // Переменная еггпо может принимать

// любое значение от -1 до максимального значения

Листинг 2: Пример использования интервальных значений

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

Описание ресурсов. Секция описания ресурсов предназначена для декларирования типов ресурсов, использующихся в библиотеке. Для каждого типа ресурса задаются имя и перечень состояний, в которых ресурс может находиться во время выполнения программы. Переход ресурса из одного состояния в другое осуществляется либо путем вызова функций библиотеки, либо по событию операционной системы. Для любого типа ресурса можно задавать атрибуты (например, ресурс «динамическая память» характеризуется атрибутом «размер») и дополнительные свойства. К таким свойствам в первую очередь относится свойство memory, определяющее, что ресурс представляет собой какой-либо вид памяти (т. е. допускает работу с ним как с массивом).

Пример описания ресурсов приведен в листинге 3. Следует отметить, что любой ресурс включает неявное состояние Closed; при переходе в данное состояние ресурс освобождается. resource File

{

Opened

resource Socket {

Created, Bound,

Established, Listen

resource Heap(int size, Memory) {

Allocated

}

Листинг 3: Пример описания ресурсов

Следует отметить, что в некоторых операционных системах (например, в ОС семейства Linux) используется универсальный дескриптор ресурсов, который в зависимости от контекста может оперировать как файлами, так и сокетами. В таком случае ресурсы «файл» и «сокет» в секции описания могут быть объединены.

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

Секция поведения содержит последовательность операторов, описывающих поведение аннотируемой функции. Поддерживаются несколько групп операторов.

Управление вычислительным процессом. Для описания хода вычислительного процесса в языке предусмотрены следующие конструкции:

конструкция ветвления if, поддерживаются также недетерминированные условия;

оператор завершения программы exit (status) .

Модификация значений объектов. Используемый набор операторов аналогичен имеющемуся в языке C:

оператор определения переменной type var = value - создает новую переменную заданного типа, может задать также ее начальное значение;

оператор присваивания значения

var = value, *ptr = value.

Работа с участками памяти. Так как, согласно требованиям, в языке PanLang отсутствуют циклы и рекурсии, в нем предусмотрены специальные групповые операции над участками памяти:

оператор копирования copy(dest, source, length) - осуществляет копирование одного участка памяти в другой;

оператор заполнения set(dest, value, length) - осуществляет заполнение участка памяти постоянными значениями.

Работа с ресурсами. Ресурсы являются специализированными объектами, действия с которыми выполняются через дескриптор. Дескрипторы ресурсов обладают всеми свойствами обычных переменных. Кроме этого, поддерживаются специальные операторы, работающие с дескрипторами ресурсов:

оператор выделения ресурса descr = new

Resource(type, state, attr, value, canleak), где descr - дескриптор ресурса; type - тип ресурса; state - начальное состояние ресурса; attr - начальное значение атрибута (если атрибут отсутствует, этот аргумент опускается); value - ассоциированное с дескриптором целое значение; canleak - признак возможности утечки ресурса;

оператор освобождения ресурса

delete(type) descr - уничтожает ресурс заданного типа, на который указывает переменная-дескриптор;

оператор установки атрибута ресурса descr->attr = attrvalue - задает новое значение атрибута ресурса, на который указывает переменная-дескриптор;

оператор изменения состояния ресурса state(descr) = newstate - задает новое состояние ресурса, на который указывает переменная-дескриптор.

Специальные операторы. Операторы данной группы ориентированы на конкретную задачу, решаемую системой статического анализа. При использовании языка PanLang для обнаружения дефектов в эту группу входит оператор сигнализации о дефекте defect(type, description). Этот оператор указывает, что выполнение данного участка программы является программной ошибкой (дефектом), задает тип дефекта и его описание.

Пример описания поведения функции. В качестве примера использования языка приведена аннотация для функции realloc (листинг 4).

Описание глобальных объектов. Секция глобальных объектов предназначена для специфицирования глобальных переменных, доступных из функций библиотеки, а также для описания создаваемых ресурсов при инициализации библиотеки. Такими ресурсами могут быть, например, стандартные потоки ввода-вывода (листинг 5).

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

утечку динамической памяти и ресурсов; двукратное освобождение динамической памяти и ресурсов;

нарушение протокола работы с ресурсами; некорректные значения параметров функций. В качестве простого примера можно рассмотреть фрагмент программы (листинг 6).

void* realloc(void* ptr, unsigned size) {

if (size <= 0) {

delete(Heap) ptr; return 0;

}

else {

void* res = new resource(Heap, Allocated, size, none, true); if (ptr->size <= size) { size = ptr->size;

}

copy(res, ptr, size); delete(Heap) ptr; return res;

}

}

Листинг 4: Пример описания поведения функции

globals

{

int stdin = new resource(File, Opened, 0, false); int stdout = new resource(File, Opened, 1, false); int stderr = new resource(File, Opened, 2, false); int errno = [0:+inf];

}

Листинг 5: Пример описания глобальных объектов

int s = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr un addr;

memset(&addr, 0, sizeof(struct sockaddr un)); addr.sun_family = AF_INET;

strncpy(addr.sun path, «example.com», sizeof(addr.sun path)); bind(s, (struct sockaddr*)&addr, sizeof(struct sockaddr un)); listen(s, 256); char buf[512];

// Дефект: получаем данные из несоединенного сокета

int res = recv(s, buf, 512, 0);

close(s);

Листинг 6: Пример аннотирующего описания

Для анализа этого фрагмента необходимо иметь аннотирующие описания функций socket, memset, strncpy, bind, listen, recv и close. Все они входят в стандартную библиотеку языка С. В листинге 7 приведен пример аннотации функции recv, внутри которой происходит обнаружение дефекта.

Вызов функции socket создает ресурс типа «сокет», который последовательно проходит состояния Created, Bound, Listen. В аннотации функции recv указано, что если переданный

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

В статье отражены основные результаты исследования в области построения частичных

long recv ( int s ,void* buf ,unsigned len ,int flags ) {

if

defect

defect

defect ( RES-06, «Получение данных из несоединенного сокета»);

set ( buf, [-inf:+inf], len ); return [0:len];

}

Листинг 7: Пример аннотации функции recv

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

С помощью разработанного языка создан набор аннотирующих описаний для стандартной библиотеки C (всего поддерживается более 500 функций). Разработанные описания успешно используются

в системе обнаружения дефектов в исходном коде на языке С [10] и позволяют анализировать сложные многокомпонентные проекты, использующие функции стандартной библиотеки.

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

Работа выполнена в рамках госконтракта № П2226 ФЦП «Научные и научно-педагогические кадры инновационной России на 2009-2013 гг.».

СПИСОК ЛИТЕРАТУРЫ

1. Ицыксон, В.М. Исследование средств построения моделей исходного кода программ на языках C и C++ [Текст]/В.М. Ицыксон, М.И. Глухих, А.В. Зозуля [и др.]//Научно-технические ведомости СПбГПУ Сер. Информатика. Телекоммуникации. Управление. -2009. -№ 1 (72).-С. 122-130.

2. Baudin, P. ACSL: ANSI/ISO C Specification Language. [Текст]/ P. Baudin, P. Cuoq [et al.]. -CEA LIST and INRIA, 2009.

3. [Электронный ресурс]/http://www.splint.org/ manual/

4. [Электронный ресурс]/http://clang-analyzer.llvm. org/annotations.html

5. [Электронный ресурс]/http://msdn.micшsoft.com/ enus/library/ms235402. aspx

6. [Электронный ресурс]/http://www.bezem.de/pdf/ htwpl.pdf

7. Hallem, S. A System and Language for Building System-specific, Static Analyses [Текс^/S. Hallem, B. Chelf [et al.]. -Stanford University, 2002.

8. Bidoit, M. CASL User Manual. Introduction to Using the Common Algebraic Specification Language [Текст]/М. Bidoit, P.D. Mosses-University of Aarhus, BRICS and Department of Computer Science, 2004.

9. Fortify Audit Workbench User Guide. Version 5.2 [Текст]. -Aug. 2008.

10. [Электронный ресурс]/http://s2a.ftk.spbstu.ru