Научная статья на тему 'Моделирование окружения драйверов устройств операционной системы Linux'

Моделирование окружения драйверов устройств операционной системы Linux Текст научной статьи по специальности «Компьютерные и информационные науки»

CC BY
429
82
i Надоели баннеры? Вы всегда можете отключить рекламу.
Ключевые слова
ОПЕРАЦИОННАЯ СИСТЕМА / ДРАЙВЕР / ОКРУЖЕНИЕ / ВЕРИФИКАЦИЯ / OPERATING SYSTEM / DRIVER / ENVIRONMENT / VERIFICATION

Аннотация научной статьи по компьютерным и информационным наукам, автор научной работы — Захаров И. С., Мутилин В. С., Новиков Е. М., Хорошилов А. В.

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

i Надоели баннеры? Вы всегда можете отключить рекламу.

Похожие темы научных работ по компьютерным и информационным наукам , автор научной работы — Захаров И. С., Мутилин В. С., Новиков Е. М., Хорошилов А. В.

iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.
i Надоели баннеры? Вы всегда можете отключить рекламу.

Environment Modeling of Linux Operating System Device Drivers

In static device driver verification of Linux operating system it is necessary to take into account the specifics of the communication between drivers and kernel core as far as it plays the main role in the drivers’ behavior. At the same time the verification of a driver together with kernel core source code is not feasible due to complexity and size of the resulting source code. As a solution the paper presents the modeling method of driver environment based on R.Milner  calculus and the method of  model translation into C program. Being linked with the driver the resulting program has the same scenarios of driver behavior as the real driver environment in operating system from the static verification tools point of view.

Текст научной работы на тему «Моделирование окружения драйверов устройств операционной системы Linux»

Моделирование окружения драйверов устройств операционной системы Linux1

Захаров И. С., Мутилин В. С., Новиков Е.М., Хорошилов А.В. {П]а.2аккагоу, тиШт, поуИсоу, khoroshilovj@ispras.ru

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

Ключевые слова: операционная система; драйвер; окружение; верификация.

1 Введение

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

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

1 Работа поддержана ФЦП "Исследования и разработки по приоритетным направлениям развития научно-технологического комплекса России на 2007-2013 годы" (контракт N 11.519.11.4006)

• Драйверы ОС 1лпих, входящие в ядро, это большой, стремительно растущий класс программных систем. На данный момент суммарный объем исходного кода драйверов составляет более 9 миллионов строк. За последний год он увеличился на полмиллиона строк.

1.1. Драйверы операционной системы Ыпих

В ядре 1лпих можно выделить «сердцевину» ядра и драйверы (см. Рис. 1). Сердцевина ядра отвечает за управление процессами и памятью, содержит сетевую подсистему и др. Драйверы позволяют ОС и пользовательским приложениям использовать возможности соответствующей аппаратуры. Они составляют большую часть исходного кода ядра - около 70%. Большинство драйверов могут быть скомпилированы как динамически загружаемые модули ядра Ьших.

Системные вызовы

Рис. 1. Драйверы в ядре ОС Linux

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

(cdc-phonet.c) из сетевой USB подсистемы (drivers/net/usb) ядра версии 3.0.1 (см. Рис. 2):

• Функция инициализации (далее функция in it). Драйвер начинает свою работу после загрузки соответствующего модуля в память. Загрузка модуля происходит в процессе старта ОС Linux или после того, как в процессе работы возникла необходимость использовать соответствующее устройство. В процессе загрузки модуля драйвера сердцевина ядра вызывает функцию инициализации драйвера. В примере на Рис. 2 такой функцией является usbpnjnit. Если инициализация проходит успешно, то возвращается значение «0», в противном случае возвращается соответствующий код ошибки.

• Функция выхода (далее функция exit). Устройство можно использовать до тех пор, пока соответствующий модуль не будет выгружен из памяти. Перед выгрузкой сердцевина ядра выполняет специальную функцию драйвера exit, в которой, например, освобождаются захваченные драйвером ресурсы. На Рис. 2 такой функцией является usbpn exit.

Рис. 2. Выдержки из исходного кода драйвера drivers/net/usb/cdc-phonet.c

• Функции-обработчики. Основная функциональность драйвера сосредоточена в функциях-обработчиках событий (далее обработчики). Обработчики вызываются сердцевиной ядра, когда требуется обработать события, связанные с драйвером, например, прерывания от устройств или системные вызовы от пользовательских приложений. В примере на Рис. 2 обработчиками являются функции изЬрп_ргоЬе и ш'Ьрп сНь'соппес

• Группы обработчиков. Большинство обработчиков объединены в группы. Каждая группа имеет определенный тип в зависимости от назначения ее обработчиков. Порядок вызова обработчиков и параметры их вызова сердцевиной ядра определяются ролями обработчиков в группе. В большинстве случаев, тип группы обработчиков можно определить по типу структуры, в полях которой хранятся функциональные указатели на обработчики (например, на Рис. 2 usbdriver). Поля структуры определяют роли, которые выполняют хранящиеся в этих полях указатели на функции-обработчики в конкретном драйвере. Для работы с USB устройством в примере на Рис. 2 представлена группа из двух обработчиков usbpn_probe и usbpndisconnect с типом usb driver. Данная группа задана при помощи структуры struct usb driver с полями probe и disconnect.

• Использование библиотечных функций сердцевины ядра. Так как

сердцевина ядра может вызывать только зарегистрированные обработчики, то наиболее важными библиотечными функциями с точки зрения моделирования окружения являются библиотечные функции регистрации и дерегистрации групп обработчиков драйвера. В начале некоторые обработчики регистрируются в процессе выполнения функции инициализации модуля, остальные регистрируются при вызове обработчиков, зарегистрированных ранее. Дерегистрация групп обработчиков часто осуществляется во время выполнения функции выхода драйвера. В примере на Рис. 2 группа обработчиков usb driver регистрируется при помощи функции usb register и дерегистрируется с помощью функции usb deregister, описанных в заголовочном файле ядра include/linux/usb.h. Данные функции вызываются в теле функций инициализации и выхода драйвера. Не все обработчики можно объединить в группы, например обработчики прерываний или функции выполнения отложенных задач в очередях, таймерах, тасклетах и т.д. Подобные обработчики также требуют соответствующей регистрации и дерегистрации и вызываются сердцевиной ядра.

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

1.2. Статическая верификация драйверов

Осуществлять поддержку корректности драйверов ОС Linux вручную слишком трудоемкая задача. Одним из направлений ее решения является применение инструментов статической верификации. Работа драйвера

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

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

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

1. Требования к порядку вызова функций in it и exit.

2. Требования к вызову обработчиков, принадлежащих одной группе, включающие:

а) Ограничения на параметры вызова обработчиков;

б) Ограничения на порядок вызова обработчиков.

3. Требования к порядку и к параметрам вызова обработчиков из разных групп обработчиков.

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

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

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

probe. Также при вызове disconnect должен передаваться тот же указатель на структуру usbinterface, которая была успешно инициализирована функцией probe. Если в группу типа usbdriver входят функции с ролями suspend, resume, то их вызов возможен только после успешного вызова probe и до вызова disconnect. Другой пример: перед вызовом некоторых функций требуется, чтобы был захвачен мьютекс.

В качестве примера требований на порядок вызова обработчиков из разных групп, возьмем группы обработчиков типа usb driver и file operations. Вызов функций из группы типа file operations возможен, только после успешной регистрации данной группы в обработчике с ролью probe из группы типа

usbdriver.

Другой пример требований: после успешного вызова probe окружение не должно вызывать функцию выхода exit до вызова disconnect.

1.3. Моделирование окружения драйверов

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

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

Переход к следующему процессу моделирует изменение состояния, в частности за счет этого моделируется порядок вызова обработчиков. Например, в реальном окружении до вызова обработчика с ролью probe, оно находится в состоянии, в котором нельзя вызывать обработчик с ролью disconnect. Эго моделируется процессом, принимающим только сигнал probe. Как только окружение получит от драйвера сигнал с успешным кодом возврата из probe, обработчик с ролью disconnect можно будет вызывать. Это моделируется переходом по приему сигнала успешного возврата к следующему процессу, в котором доступен сигнал disconnect. Тем самым обеспечивается порядок вызова обработчиков с ролями probe и disconnect.

Предлагаемый в данной статье метод построения синтезируемого окружения состоит из двух шагов (см. Рис. 3). На первом шаге строится л-модель реального окружения. Эта модель учитывает требования полноты и корректности, накладываемые на окружение. Для формального описания модели используется л-исчисление Р.Милнера [7]. С помощью данного исчисления удается довольно компактно и наглядно описывать последовательности вызовов обработчиков драйвера (сценарии воздействия на драйвер). Кроме того, 7г-исчисление позволяет учитывать многопоточность окружения и асинхронность взаимодействий драйвера с окружением.

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

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

> «одели (маанй=г эй яй з але нтноста

Рис. 3. Моделирование окружения драйверов

2. Основные определения

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

Сигналы могут иметь параметры. В определении «The Polyadic л-Calculus: а Tutorial, Robin Milner, October 1991» [7] допускается передача любых меток в качестве параметра сигнала.

Множество меток приема сигналов обозначим как А. Множество меток посылки сигнала обозначим как А, оно состоит из меток а, где а 6 А. Обозначим Л = A U А.

Множество действий включает в себя действия посылки/получения для меток, а также специальное пустое действие т, Act = Л U т.

Непустые действия могут иметь параметры, которые записываются в круглых скобках. Жирным шрифтом будем обозначать вектор параметров, например х. Для действий посылки сигнала а(е(х)) вычисляется значение выражения с(х). на которое заменяется переменная у в действии приема сигнала а (у). Кроме того, в определении для удобства записи добавлен оператор if-then-else.

Определение 1.

Процессы верхнего уровня:

р := р1\р2\\р\ (уа)Р | N

где

• Pi I Р2 — параллельная композиция, одновременно выполняются процессы Р1,Р2;

• (va)Р - создание новой метки а 6 А;

• ! Р - создание копии процесса;

• N - процесс нижнего уровня.

Процессы нижнего уровня:

N(x) ■■= 0 |Кг + —I- Кп| if b(х) then /V, (х) else N2(x)

• 0 - пустой процесс;

• Кг + —I- Кп - выбор, где процесс может продолжаться одним из вариантов:

о Nt = ai(yi).Ki(eL(х,yj). aL 6 Л - прием сигнала;

о Ni=ai(e(x)).Ki(ei(x)) - посылка сигнала;

о Nt = KL(eL(х)) - т действие;

• if b(x) then N^x) else N2(x) - если условие b(x) выполнено, то процесс продолжается как /V, (х). иначе как N2 (х).

Для сокращенного задания идентификаторов процессов нижнего уровня будем использовать запись:

Nj(x) := •••. (iVj) выражение1... которая означает:

Nj(x) := ••• -Ni(x) ...

Nt(x) := выражение1

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

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

Конструкция выбора Кг + —I- Кп позволяет задавать множество доступных для посылки сигналов и множество сигналов, которые процесс готов принять. При выборе одного из действий процесс переходит в следующее состояние, тем самым меняются множества доступных сигналов. Это позволяет моделировать различные ограничения на порядок вызова обработчиков. Моделируется это с помощью процессов нижнего уровня следующим образом. При получении сигнала вызова, для которого нужно наложить ограничение на порядок вызова, мы переходим к следующему процессу, в котором доступны уже другие сигналы в соответствии с допустимыми порядками вызова. Например, как только мы получили сигнал probe в некотором процессе /V,. он переходит к процессу N2. в котором множество доступных действий включает посылку сигнала disconnect.

3. Формальная модель драйвера и его окружения в

7Е-ИСЧИСЛеНИИ

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

Dwп — Pinit I Pexit I ■ Pfcall

Процессы Pinit и Рехц представляют функции инициализации init и exit соответственно. Эти процессы принимают на вход сигналы init(ret), exit(ret) и отвечают на них ret(x), retQ соответственно. Процесс Pfcau представляет обработчики драйвера, которые окружение может вызывать после регистрации. Эти процессы принимают сигнал вызова функции обработчика f(reti,fi,ctxi,paramsi'), в параметрах сигнала которого передаются: retL -метка сигнала для ответа, fa - вызываемая функция, ctxl - контекст вызова, paramsi - параметры вызова. Причем, так как обработчики могут выполняться параллельно, для каждого вызова порождается отдельная копия процесса. В ответ Pfcaii посылает сигнал с возвращаемым значением retiiresulti).

Во время выполнения инициализации, выхода, а также при выполнении других обработчиков драйвер может:

• обращаться к библиотечным функциям сердцевины ядра

g^re^.params-^,,gi(reti,paramsi) и получать от них ответы ret^resultj,...,reti(resulti),

• обращаться к глобальным переменным, с помощью сигналов setv.(x),getv.(x) к процессам Pv..

Таким образом, л-модель окружения предоставляет обработку:

• вызовов библиотечных функций

g-Liret-L.params-L),...,gi(reti,paramsi);

• обращений к глобальным переменным, с помощью сигналов setv. (x). getv.(x).

А также осуществляет вызовы:

• функций инициализации и выхода драйвера init(ret), exit(ret);

• функций-обработчиков для зарегистрированных групп

fireti, ft, ctxt, paramsi).

В самый начальный момент времени активируется основной процесс окружения Pmoduiey отвечающий за инициализацию и выход. Рассмотрим пример главного процесса окружения Pm„auie- который вызывает функции инициализации и выгрузки драйвера:

Рmodule — ^0

L0 := (vret)Ll

Процесс L1 посылает драйверу сигнал инициализации init. Драйвер осуществляет инициализацию, посылая сигналы сердцевине ядра. Например, для выделения необходимых ресурсов и регистрации группы функций обработчиков драйвера. Если она прошла успешно, то посылается сигнал ret с кодом ответа 0, в противном случае, посылается соответствующий код ошибки:

LI := init(ret). (L3)ret(r). (L4)i/ г == 0 then L2 else 0,

где у процесса L4 имеется параметр г, через который передается возвращаемое значение функции init.

iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.

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

L2 := mstop. (L5)exit(ret).(L6)ret.O

Процесс Pmodule обращается К процессу Ptrymoduleget, КОТОрыЙ МОДеЛИрует захват модуля драйвера, так чтобы его нельзя было выгрузить, т.е. окружение не вызвало функцию выхода exit. Процесс, захватывающий модуль драйвера, посылает сигнал tmg, и получает ответ tmgret (true) в случае успешного захвата модуля. Сигнал mstop нужен для того, чтобы процесс не допускал новые захваты.

Ptrymoduleget — М( 0)

М(0) := tmg. (Ml)tmgret(true). M(l) + mstop. MD Для i > 1:

M(i) ■= tmg. (М2 )tmgret(true).M(i + 1) + mput. (M3)mputret. M(i — 1) MD ■= mstop. MD + tmg.(MDl)tmgret(false).MD

На Рис. 4 показаны изображения процессов Pmoduie и Ptrymoduieget в виде графов. В вершинах изображены состояния процессов нижнего уровня, на дугах показаны события ПОСЫЛКИ И приема сигналов. Процесс Ptrymoduleget имеет потенциально неограниченное число состояний соответствующих параметру /.

PlIC. 4. Пример процесса Pmodule (слева) U npoifecca Ptrymoduleget (справа)

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

В примере драйвера на Рис. 2 вызывается функция-регистрации usbregister, которой передается указатель на переменную usbpndriver типа usb driver, содержащую обработчики probe и disconnect.

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

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

Рассмотрим пример моделирования регистрации для группы обработчиков типа usb driver. В этом примере для группы будем создавать единственный экземпляр PUSb_driver процесса для каждой регистрируемой группы, поэтому специального процесса регистрации не требуется.

Pusb_driver —

50 := \usb_register (usb_deregister,probe, disconnect). S1 Для краткости далее параметры probe и disconnect опускаются.

51:= usb_deregister. 0 + tmg.(S4)tmgret(r).(S5)if г thenS2 else 51

52 := /(ret,probe). (S6)ret(r).

(57) if r == 0 then 53 else 52 + usb_deregister. (S8)mput. 0

53 := f (ret, disconnect). (S9)ret.S2

Для моделирования зависимостей, как между группами, так и зависимостей с функциями инициализации и выхода используются сигналы между процессами. В примере на Рис.2 модуль драйвера нельзя выгружать после того как процесс перешел в состояние 52, в котором он готов вызывать

обработчики probe и disconnect. С точки зрения окружения, это означает, что нельзя вызывать функцию выхода exit в основном процессе окружения Pmoduie- Для хранения состояния счетчика захватов модуля используется процесс Ptrymoduleget-

Поэтому перед тем, как вызывать какие либо обработчики, процесс группы типа usb driver посылает сигнал tmg. В случае успеха, он переходит в состояние 52. В случае, если начата выгрузка модуля, то процесс остается в состоянии 51 и ожидает дерегистрацию, тем самым не осуществляя вызовы обработчиков группы usb_driver.

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

Ptrymoduleget-

Ограничения на порядок вызова моделируются с помощью процессов S1, ..., S9. Например, для установления порядка между probe и disconnect, как только мы получили сигнал probe в процессе 52, он переходит к процессу 53, в котором множество доступных действий включает посылку сигнала

disconnect.

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

4. Трансляция процессов в Си-программу

Трансляция процессов описана в приложении А диссертации [8]. В разделе А. 1 описан общий метод трансляции в многопоточную программу, а в разделе А.2 описывается его модификация для трансляции в последовательную Си программу.

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

Вместо того чтобы передавать непосредственно сами метки в качестве параметра, вводятся и передаются специальные параметры меток со значениями из потенциально бесконечного множества П. Метки, которые имеют специальные параметры, называются динамическими, обозначим Ф = F U F, где F - метки приема и F — метки посылки. Специальные параметры метки пишутся в квадратных скобках после метки.

Динамически связываемые метки приема сигнала /[pi] и посылки сигнала /[р2] осуществляют взаимодействие, только если значения параметров меток совпадают. Т.е. процесс получатель принимает сигнал /[pi], только если

значение передаваемого параметра метки р1 совпадает со значением получателя р2. Процесс отправитель считает сигнал /[р2] отправленным, только если нашелся получатель с совпадающим значением параметра метки Рг-

Отметим, что введение динамических меток необходимо нам лишь для удобства записи трансляции в Си программу. Любую модель, записанную в классических я-процессах, можно свести к я-процессам с динамическими метками. Метки, не передаваемые как параметры, записываются как статические. Каждой метке, передаваемой через параметры, ставим в соответствие натуральное число р 6 П. Вводим динамическую метку / Заменяем все места, в которых использовались метки для приема и посылки на динамическую метку (]р/ с соответствующим параметром р. Там, где метка передавалась в качестве параметра, передаем значение р.

Например, если имеются процессы:

Р := ух а(х). х()

(} := а(х).х()

то их можно переписать как:

Р := ура(р)./[р]()

<2 := а(р)./[р]()

4.1. Отношение редукции

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

Вспомогательное отношение структурной эквивалентности = определяется аналогично [7].

Определим отношение редукции -» над процессами с динамическими метками аналогично [7], при этом пометим пары в отношении метками а из Л и Р и {т}, т.е. метками приема сигналов и пустой меткой т.

а

Отношение редукции -» над процессами - это наименьшее отношение, удовлетворяющее следующим правилам:

• Если есть определение процессов К(х) и N (х). в каждом из которых

есть слагаемое, которое может взаимодействовать с другим, то это

взаимодействие осуществляется с меткой приема и значениями

переданных параметров.

K(x):=^-- + a(y).Ki(ei(x,y))'j

М(.х):=1-- + а(е(х))М](е](х)')

СОММЛа 6 А)\---------------------^------------------------------

а(еО)) / ,

А-(и)|И(г7)----->А-г[ег(и, е(»)]| И7-(е7-(г7))

Для динамических меток сравниваются также значения параметров (хь - ьый элемент векторах).

К(х):=(--- + а[х1](у).К1(е1(х,у))')

N(x):=\^- + a[xl](e(x)).Nj(ej(x))

СОММ2(а 6 F):--------------------Г Ul=Vj---------------------------

“[ui](eW) / . ,ч

K(u)\N(v)---------->А-г[ег(и, e(l7))J| Nj(ej(v))

Переход по т действию может осуществляться всегда. 7УШ- KM:=('"+T-K^et(x^)

if(u)iifj(ej(u))

if-then-else оператор.

K(x):=if b(я) tften К-!(я) else К2(л:)

IFTHEN\_________________b(u)=TRUE-------------

K(u)-*Ki (м)

K(x)\=if b(x) then K±(x) else K20*0

IFELSE:---------------b(u)—false

К(и)-*К2(.и)

В параллельной композиции редукция компонентов может осуществляться независимо.

рдр. У(Ц1)“у'(М2)

К(и\) | Л/’(г7)->К'/(«2) I Л/’Сг7)

Создание нового значения.

ДЕ5:

[ УХ}К!'1( | )^\ УХ) к! (и-1)

Редукция для структурно эквивалентных процессов.

К = N (г^) ЛГ (и2); ЛГ = К’

STRUCT-.

КСи^^К^Щ)

Понятие редукции позволяет нам определить множество возможных трасс выполнения 7Г процесса Р. обозначаемое как tracesn(P). Для редукции

Р -> Рг -> Р2---> Рп, определим трассу как подпоследовательность aL],, aik

последовательности аг,..., ап. включающую все непустые действия aL. Ф т. Тогда tracesn(P) - это множество трасс для всех возможных редукций процесса Р. Определим tracesn(P,D), где D с р и А, как множество трасс, в которых уделены метки не из множества!) (т.е. включаем только at. 6 D).

4.2. Общий метод трансляции в многопоточную программу

На входе общего метода трансляции А. 1 имеется модель окружения в виде 71-процессов и исходный код драйвера. В модели окружения есть два набора процессов верхнего уровня: первые - статические, существующие в единственном экземпляре и не использующие сигналы создания копии процесса, вторые - динамически порождаемые, создающиеся при получении сигнала копирования (например ! create^x^). С, (х,)).

У процесса верхнего уровня есть состояние, которое при трансляции моделируется структурой, например для процесса Pmoduie будет сгенерирована структура /. state:

struct L state {

struct list list;

enum L states state;

intp;

int L4_param;

}

Состояния хранятся в списке L state list для того, чтобы иметь возможность порождать неограниченное количество копий процесса. Поле state - отражает текущий процесс нижнего уровня или дополнительное состояние STOP, означающее процесс 0. Например, для процесса Pmoduie эт0 enum L states {STOP, LI, L2, L3, L4, L5}. Поле p - параметр динамических меток, генерируемый при создании процесса с помощью vp. Также структура включает в себя параметры процессов нижнего уровня. Например, L4par am -параметр процесса L4, которому передается возвращаемое значение функции init.

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

Функция, отвечающая за создание процесса, выделяет память под его состояние, инициализирует его функцией Linit и добавляет в список состояний /, state list, например для процесса Pmoduie '■

void create_L(int р) {

struct Lstate *s = malloc(sizeof(struct Lstate));

L_init(p, s);

li st_add(&L_state_li st, & s->li st) pthread_create(L_thread, s);

}

iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.

Функция L init инициализации устанавливает начальное состояние LI. L_init(intp, struct L state *s) { s->state = LI; s->p = p;

}

Далее в функции create L создается поток процесса с помощью функции pthread create библиотеки pthreads, выполняющий функцию Lthread. Функция L thread в бесконечном цикле выполняет шаг процесса L step, пока процесс не придет в состояние STOP. Так как потоки процессов выполняются параллельно, то необходимо захватить блокировку, чтобы шаг процесса выполнился атомарно.

void L_thread(struct L state *s) {

while(true) {

lock();

int should stop = L step(s); if(should_stop) { li st_del(L_state_li st, s); free(s); unlockQ;

break;

};

unlock();

}

}

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

Например, рассмотрим выдержки из функции L_step - шаг процесса Pmoduie ■ int L_step(struct S_state *s) { switch(nondet_int()) { case 0:

if(s->state == STOP) return 1; break;

case 3: if(s->state == L2) { int res = mstop(); if(res == 0) {

//Переход в состояние L5 s->state = L5;

}

}

break;

}

return 0;

}

В случае перехода в состояние STOP, при выполнении условия s->state==STOP, функция возвращает 1, что означает завершение процесса.

Для действий посылки сигналов сначала проверяется текущее состояние, в котором осуществляется посылка. Например, для посылки сигнала mstop:

L2 := mstop. (L5>...

проверяется нахождение в состоянии L2 (s->state == L2). Если условие выполнено, то осуществляется вызов функции обработки приема сигнала mstop. Сигнал считается принятым, только если функция обработки вернула 0, иначе сигнал не принят ни одним из процессов и необходимо перевыполнить посылку. Таким образом, процесс отправитель ожидает пока процесс получатель перейдет в состояние, в котором он готов принять соответствующий сигнал.

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

int mstop() { switch(VERIFIER_nondet_int()) {

case M: int k = list_size(M_state_list); if(k == 0) {return -1;} int i = nondetint(k-l); struct M_state *s = list_get(M_state_list, i); int r = M_mstop(s); if(r == 0) return 0; else return -1;

}

В функции mstop недетерминированно выбирается процесс получатель, для каждого из которых недетерминировано выбирается состояние одного из экземпляров в списке Mstatelist. Далее вызывается функция обработки сигнала Mjnstop ДЛЯ процесса Ptrymoduleget-

Функции обработки сигнала передается состояние экземпляра процесса и параметры сигнала. Например, ДЛЯ процесса Ptrymoduleget генерируется следующая функция М slop'.

int M_mstop(struct M_state *s) { int res = -1;

if(s->state == M && s->param == 0) { s->state = MD; res = 0;

}

if(s->state == MD) { s->state = MD; res = 0;

}

//установить res в 0 если сигнал обработан return res;

}

При получении сигнала mstop в состоянии М(0), соответствующая проверка л->state == М && s->param == 0, осуществляется переход в состояние MD, в котором последующие сигналы захватов tmg возвращают tmgret(false). В состоянии MD сигнал обрабатывается, но состояние не меняется.

Для связывания исходного кода драйвера с л-процессами, генерируется код для процессов-оберток. Рассмотрим процессы Pfcaii, для процессов Pinit и Pexit генерируется аналогичный код.

Pfcaii '•= ! f(pi,fptri,ctxi,paramsi).CALLED(pi,fptri,ctxi,paramsi)

CALLEDipi, fptri,ctXi,paramSi)-. = т. EXECUTING(pi, fptrt, ctxL, paramSi, result)

Процесс EXECUTING имеет специальное значение, его выполнение определяется кодом драйвера, вызывается функция драйвера fptri.

intFCALL_step(structM_state *s) {

case 1:

//lock should be held here if(s->state == CALLED) { s->state = EXECUTING;

fptr = s->fptr; //fptr - вызываемая фукнция драйвера, ctx = s->ctx; //ctx - контекст вызова params = s->params; //params - параметры функции } else { break; } unlock();

type у = *fptr(ctx, params); //EXECUTING: вызов функции fptr lockO;

s->state = RET; s->RET_result = y; break; case 2: if(s->state == RET) {

//Вызов функции действия с заданным параметром int res = /ret(s->p,s->RET_result); if(res == 0) {

//Переход в состояние STOP s->state = STOP;

}}

break;

}

return 0;

}

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

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

осуществляющая посылку сигналов соответствующим процессам. Например, для функции usb register, генерируется следующий код посылки сигнала start(usbpn_driver, usbpn_driver->probe, usbpn_driver->disconnect) процесса

Pusb_driver-

4.3. Модификация метода для трансляции в многопоточную программу

В последовательном случае при создании процесса, создается лишь экземпляр состояния и увеличивается счетчик активных процессов, создания потока с помощью pthread create не происходит.

void create_L(int р, f_ptr init, f_ptr exit) {

//pthread_create(L_thread, s); thread_cnt++;

}

В функции main осуществляется недетерминированный выбор одного из типов процессов, добавляется бесконечный цикл while(true).

void main() {

int p = globalId++;

сгеа1е_Ь(р);//Создать основной поток сгеа1е_М(0);//Создать поток trymoduleget while(true) { switch(nondet_int()) { case Llabel: lockO;

int stop loop = L_step_any(); unlock();

if(stop_loop) { goto breakloop;} break;

}

case ...

}

breakloop:

}

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

4.4. Упрощения генерируемого кода для верификации

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

неограниченными структурами данных в куче, такими как списки, деревья, и т.д. Поэтому необходимо минимизировать использование списков при генерации. На это нацелено первое упрощение, заключающееся в замене списков для хранения экземпляров состояний статических процессов на глобальные переменные. Например, для процесса Pmoduie'-struct Lstate LstateO;

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

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

Второе упрощение связано с тем, что инструменты верификации имеют ограниченную поддержку указателей на функции, поэтому в сгенерированном коде вызовы функций по указателю заменяются на явные вызовы. Это осуществляется генерацией для каяедой функции драйвера своего процесса обертки с ее явным вызовом. Конечно, в местах вызова функций по указателю должно быть заранее известно, какая функция вызывается. В примере выше связывание указателей на функции с их значениями происходит при регистрации группы usbdriver и посылке сигнала start(usbpn_driver, usbpn_driver->probe, usbpn driver->disconnect). В этом месте создается экземпляр процесса PUsb_driver> который вместо посылки сигналов вызова функций по указателю probe, disconnect осуществляет посылку конкретным экземплярам usbpn_probe, usbpn disconnect процесса Pfcau-

4.5. Теорема об эквивалентности

Далее мы покажем, что получающаяся в результате трансляции последовательная Си программа порождает те, и только те трассы взаимодействий, которые были в л-процессах. Так как исходный код драйвера в последовательном случае выполняется в одном потоке, то мы будем требовать, чтобы окружение в виде я-процессов одновременно осуществляло вызов только одной функции драйвера, т.е. после посылки сигнала / и до приема сигнала возврата fret, недопустимо посылать еще один сигнал/ Определим понятие трассы выполнения для последовательной Си программы СР. Трасса Си программы определяется событиями вызова функции приема сигнала в сгенерированном коде. Данные функции генерируются в пункте 2.3 приложения А метода трансляции. Функции приема сигналов для каяедой метки а[р](у) 6 A U F имеют вид:

inta(intp, type у)

Функции возвращают значение 0, если сигнал успешно принят, и -1 иначе. Событие а[р](у) добавляется в трассу, если функция а(р,у) завершается с кодом 0. Множество всех трасс обозначим как 1гасеБс(СР). Определим Сгасезс(СР, О), где О с Г и Л. как множество трасс, в которых удалены метки не из множества Б.

Определим понятие трассовой эквивалентности для 71-процесса Р и сгенерированной Си программы СР.

Определение, л-процссс Р трассово-эквивалентен сгенерированной Си программе СР на множестве событий Б, обозначим как Р ~0 СР, если Ьгасезп{Р,0') = Ьгасезс{СР,0').

Теорема.

Пусть Б - множество событий приема сигналов между драйвером и окружением

Я = {ШЫтГ*} и {ехИ, ехиге1} и и {д^д?*..., ди д[еЧ и

{^ец} и {деЬщ}.

Пусть 5у57г - модель В 7Г исчислении, состоящая из процессов окружения и процессов драйвера, т.е. 5уБп = Епуп \ Огуп.

Пусть Буэс - Си код, состоящий из кода, сгенерированного для окружения Епус (по методу А.2 [8]), и кода драйвера Огис,

Пусть Ог ип ~0 Ог ис. т.е. код драйвера трассово-эквивалентен п модели. Тогда 5у5п 5узс.

Таким образом, для любой трассы п процессов существует эквивалентная трасса в Си программе, и наоборот для любой трассы Си программы существует эквивалентная трасса 71-процессов.

Доказательство теоремы приведено в разделе А.З приложения диссертации [8].

iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.

Заключение

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

Метод трансляции 71-процессов обеспечивает необходимые требования инструментов высокоточного статического анализа, так как в результате

трансляции получается последовательная Си программа, что позволяет

осуществлять ее верификацию данными инструментами.

Описанные в работе подходы к моделированию окружения драйверов

используются в проекте верификации драйверов ОС Linux [9].

Литература

[1]. N. Palix, G. Thomas, S. Saha, С. Calves, J. Lawall, and Gilles Muller. Faults in Linux: Ten years later. Proceedings of the sixteenth international conference on Architectural support for programming languages and operating systems (ASPLOS '11), USA, 2011.

[2]. D. Beyer, T. Henzinger, R. Jhala, R. Majumdar. The Software Model Checker Blast: Applications to Software Engineering. International Journal on Software Tools for Technology Transfer (STTT), vol. 5, p. 505-525, 2007.

[3]. Shved P., Mandrykin М., Mutilin V. Predicate Analysis with Blast 2.7 //Proceedings of TACAS. 2012. Vol. 7214. P. 525-527.

[4]. D. Beyer, M.E. Keremoglu. CPAchecker: A Tool for Configurable Software Verification. In Gopalakrishnan, G., Qadeer, S. (eds.) CAV 2011. LNCS, vol. 6806, pp. 184-190. Springer, Heidelberg, 2011.

[5]. T. Ball, E. Bounimova, R. Kumar, V. Levin. SLAM2: Static Driver Verification with Under 4% False Alarms. FMCAD, 2010.

[6]. J. Corbet, G. Kroah-Hartman, A. McPherson. Linux kernel development. How Fast it is Going, Who is Doing It, What They are Doing, and Who is Sponsoring It. http: //go. linuxfoundation. org/who-writes-linux-2012,2012.

[7]. Milner R. The Polyadic 7i-Calculus: a Tutorial. LFCS, Department of Computer Science, University ofEdinburgh, 1991. P. 49.

[8]. Мутилин B.C. Верификация драйверов операционной системы Linux при помощи предикатных абстракций. Диссертация на соискание ученой степени к.ф.-м.н., 2012.

[9]. Мандрыкин М. У., Мутилин В. С., Новиков Е. М. и др. Использование драйверов устройств операционной системы Linux для сравнения инструментов статической верификации //Программирование. 2012. Т. 5. С. 54-71.

Environment Modeling of Linux Operating System Device Drivers

Zakharov I.S., Mutilin V.S., Novikov E.M., Khoroshilov A.V.

{ilja.zakharov, mutilin, novikov, khoroshilov}@ispras.ru

Abstract. In static device driver verification of Linux operating system it is necessary to take into account the specifics of the communication between drivers and kernel core as far as it plays the main role in the drivers’ behavior. At the same time the verification of a driver together with kernel core source code is not feasible due to complexity and size of the resulting source code. As a solution the paper presents the modeling method of driver environment based on R.Milner Jt-calculus and the method of Jt-model translation into C program. Being linked with the driver the resulting program has the same scenarios of driver behavior as the real driver environment in operating system from the static verification tools point of view.

Keywords: operating system; driver; environment; verification

i Надоели баннеры? Вы всегда можете отключить рекламу.