Научная статья на тему 'Анализ корректности синхронизации компонентов ядра операционных систем'

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

CC BY
92
13
i Надоели баннеры? Вы всегда можете отключить рекламу.
Ключевые слова
СОСТОЯНИЕ ГОНКИ / РАЗДЕЛЬНЫЙ АНАЛИЗ ПОТОКОВ / СТАТИЧЕСКАЯ ВЕРИФИКАЦИЯ / ОПЕРАЦИОННАЯ СИСТЕМА LINUX / DATA RACE / THREAD-MODULAR APPROACH / SOFTWARE VERIFICATION / LINUX KERNEL

Аннотация научной статьи по компьютерным и информационным наукам, автор научной работы — Андрианов П.С.

Большинство современных инструментов статической верификации плохо масштабируются на сложное программное обеспечение. Целью работы была разработка инструмента, который станет золотой серединой между точными и медленными инструментами статической верификации и быстрыми, но менее точными инструментами статического анализа. Основной идеей подхода является абстракция от точного взаимодействия потоков и анализ каждого потока отдельно от всех остальных, но в некотором окружении, которое моделирует влияние потоков друг на друга. Окружение содержит описание возможных действий над разделяемыми данными и примитивами синхронизации, а также условий их применения. Варьируя точность построения окружения, можно добиваться необходимого баланса между скоростью и точностью анализа в целом. Формальное описание предлагаемого подхода было сделано с использованием теории адаптивного статического анализа. Это позволило сформулировать условия и доказать корректность предлагаемого подхода в этих условиях. Для эффективного поиска состояний гонки используется специальная модель памяти, которая позволяет разделять области памяти на непересекающиеся регионы, соответствующие типам данных. Реализация предложенного подхода во фреймворке CPAchecker позволяет переиспользовать существующие техники анализа с минимальными изменениями. А реализация дополнительных техник анализа в рамках предложенной теории позволяет повысить точность анализа. Результаты проведенных экспериментов на двух наборах тестовых задач позволяют заключить о масштабируемости и практической применимости метода.

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

Analysis of correct synchronization of operating system components

Most of the software model checker tools do not scale well on complicated software. Our goal was to develop a tool, which provides an adjustable balance between precise and slow software model checkers and fast and imprecise static analyzers. The key idea of the approach is an abstraction over the precise thread interaction and analysis for each thread in a separate way, but together with a specific environment, which models effects of other threads. The environment contains a description of potential actions over the shared data and synchronization primitives, and conditions for its application. Adjusting the precision of the environment, one can achieve a required balance between speed and precision of the complete analysis. A formal description of the suggested approach was performed within a Configurable Program Analysis theory. It allows formulating assumptions and proving the soundness of the approach under the assumptions. For efficient data race detection we use a specific memory model, which allows to distinguish memory domains into the disjoint set of regions, which correspond to a data types. An implementation of the suggested approach into the CPAchecker framework allows reusing an existed approaches with minimal changes. Implementation of additional techniques according to the extended theory allows to increase the precision of the analysis. Results of the evaluation allow confirming scalability and practical usability of the approach.

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

DOI: 10.15514/ISPRAS-2019-31(5)-16

Анализ корректности синхронизации компонентов ядра операционных систем

П.С. Андрианов, ORCID: 0000-0002-6855-7919 <andrianov@ispras.ru>

Институт системного программирования им. В.П. Иванникова РАН, 109004, Россия, г. Москва, ул. А. Солженицына, д. 25

Аннотация. Большинство современных инструментов статической верификации плохо масштабируются на сложное программное обеспечение. Целью работы была разработка инструмента, который станет золотой серединой между точными и медленными инструментами статической верификации и быстрыми, но менее точными инструментами статического анализа. Основной идеей подхода является абстракция от точного взаимодействия потоков и анализ каждого потока отдельно от всех остальных, но в некотором окружении, которое моделирует влияние потоков друг на друга. Окружение содержит описание возможных действий над разделяемыми данными и примитивами синхронизации, а также условий их применения. Варьируя точность построения окружения, можно добиваться необходимого баланса между скоростью и точностью анализа в целом. Формальное описание предлагаемого подхода было сделано с использованием теории адаптивного статического анализа. Это позволило сформулировать условия и доказать корректность предлагаемого подхода в этих условиях. Для эффективного поиска состояний гонки используется специальная модель памяти, которая позволяет разделять области памяти на непересекающиеся регионы, соответствующие типам данных. Реализация предложенного подхода во фреймворке CPAchecker позволяет переиспользовать существующие техники анализа с минимальными изменениями. А реализация дополнительных техник анализа в рамках предложенной теории позволяет повысить точность анализа. Результаты проведенных экспериментов на двух наборах тестовых задач позволяют заключить о масштабируемости и практической применимости метода.

Ключевые слова: состояние гонки; раздельный анализ потоков; статическая верификация; операционная система Linux

Для цитирования: Андрианов П.С. Анализ корректности синхронизации компонентов ядра операционных систем. Труды ИСП РАН, том 31, вып. 5, 2019 г., стр. 203-232. DOI: 10.15514/ISPRAS-2019-31(5)-16

Analysis of correct synchronization of operating system

components

P.S. Andrianov, ORCID: 0000-0002-6855-7919 <andrianov@ispras.ru>

Ivannikov Institute for System Programming of the Russian Academy of Sciences, 25, Alexander Solzhenitsyn st., Moscow, 109004, Russia

Abstract. Most of the software model checker tools do not scale well on complicated software. Our goal was to develop a tool, which provides an adjustable balance between precise and slow software model checkers and fast and imprecise static analyzers. The key idea of the approach is an abstraction over the precise thread interaction and analysis for each thread in a separate way, but together with a specific environment, which models effects of other threads. The environment contains a description of potential actions over the shared data and synchronization primitives, and conditions for its application. Adjusting

the precision of the environment, one can achieve a required balance between speed and precision of the complete analysis. A formal description of the suggested approach was performed within a Configurable Program Analysis theory. It allows formulating assumptions and proving the soundness of the approach under the assumptions. For efficient data race detection we use a specific memory model, which allows to distinguish memory domains into the disjoint set of regions, which correspond to a data types. An implementation of the suggested approach into the CPAchecker framework allows reusing an existed approaches with minimal changes. Implementation of additional techniques according to the extended theory allows to increase the precision of the analysis. Results of the evaluation allow confirming scalability and practical usability of the approach.

Keywords: Data race; Thread-Modular approach; Software verification; Linux kernel

For citation: Andrianov P.S. Analysis of correct synchronization of operating system components. Trudy ISP RAN/Proc. ISP RAS, vol. 31, issue 5, 2019, pp. 203-232 (in Russian). DOI: 10.15514/ISPRAS-2019-31(5)-16

1. Введение

Верификация многопоточных программ всегда являлась более сложной задачей, чем верификация последовательных программ. Точное вычисление всех возможных чередований (interleavings), приводит к комбинаторному взрыву числа состояний. Поэтому, большинство инструментов статической верификации применяют различные техники оптимизации: редукция частичных порядков (partial order reduction) [1,2], абстракция счетчика (counter abstraction) [3] и другие. Тем не менее, большинство современных инструментов статической верификации плохо масштабируются на промышленное программное обеспечение. Этот факт подтверждается результатами сравнения инструментов статической верификации на наборе задач SV-COMP [4]. Задачи из категории «многопоточность», основанные на драйверах операционной системы Linux, вызывают значительные сложности для всех инструментов статической верификации. Одной из альтернатив методам проверки моделей являются методы статического анализа, которые нацелены на быстрый поиск ошибок без абсолютной уверенности в финальном вердикте. Такие инструменты применяют различные фильтры и эвристики для ускорения анализа и поэтому не могут гарантировать корректность, то есть отсутствие ошибок. В данной работе представлен подход к статической верификации многопоточного программного обеспечения, который позволяет выбирать баланс между скоростью и точностью проводимого анализа.

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

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

Точность построения окружения определяет во многом точность и скорость работы всего инструмента. Точность анализа можно повысить, комбинируя различные техники анализа. Для реализации этой идеи была использована платформа CPAchecker [5,6], 204

которая предоставляет богатый набор техник верификации. Анализ с раздельным рассмотрением потоков (thread-modular approach) [7-10] также можно реализовать как одну из техник, которая встраивается в CPAchecker и пополняет традиционный набор техник верификации таких как, например, CEGAR [11] и предикатная абстракция [12]. Эффективное расширение платформы CPAchecker требует не просто добавления еще одного вида анализа. В идеале нужно, чтобы максимальное число видов анализа могли работать одновременно и обмениваться данными между собой. Необходимым условием такой тесной интеграции является либо следование уже определенной теории CPA, либо модификация имеющейся теории таким образом, что старая теория оказывалась частным случаем новой. Именно такая задача ставилась в данном исследовании. Поиск состояний гонки обычно состоит из двух основных этапов:

1. построение множества достижимых состояний;

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

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

Инструменты статического анализа, которые ищут потенциальные состояния гонки, обычно используют Lockset алгоритм для поиска таких ошибок. В предложенном подходе используется более точный алгоритм, в котором потенциальное состояние гонки является парой совместных переходов, которые модифицируют одну и ту же память. Совместность здесь означает, что два частичных состояния двух потоков могут быть частью одного глобального состояния. Таким образом, если рассматривать только абстракцию над примитивами синхронизации, это сводится к алгоритму Lockset, но при использовании других вариантов анализа, является более точным. Предикатная абстракция вместе с моделью памяти BnB [14-16] позволяет значительно улучшить работу с доступами по указателю и позволяет сохранить корректность при разумных предположениях. Оценка подхода производилась на множестве задач, основанных на драйверах операционной системы Linux. Они были подготовлены с помощью системы Klever, которая позволяет проводить верификацию больших программных систем [17, 18]. Klever разделяет большой объем кода на отдельные фрагменты - верификационные задачи - и подготавливает для них модель окружения. Основным вкладом данной работы является:

1. развитие теории CPA, которая позволяет комбинировать технику thread-modular с другими подходами, такими как предикатная абстракция;

2. реализация предложенной теории в инструменте CPAchecker, который был успешно апробирован на множестве задач, основанных на драйверах операционной системы Linux.

Статья организована следующим образом. В разд. 2 представлены основные сложности современных инструментов статической верификации и основы предлагаемого подхода. В разд. 3 представлена основная идея подхода. Разд. 4 вводит основные определения и модель программы. Следующие 7 разделов посвящены описанию расширения теории CPA: разд. 6 описывает thread-modular подход в терминах CPA, разд. 7 - 12 содержат расширенное описание основных анализов (CPA). В разд. 13 описаны основные

особенности поиска состояний гонки в предлагаемом подходе. В разд. 14 представлены результаты работы инструмента на наборе SV-COMP и драйверах операционной системы Linux. В разд. 15 представлен краткий обзор родственных работ.

2. Пример запуска существующих инструментов верификации

Рассмотрим пример верификационной задачи1 из SV-COMP'19 [4]. Эта верификационная задача основана на реальном состоянии гонки2. Файл с исходным кодом содержит более 7 000 строк кода и 4 создаваемых потока: один поток для базовых функций platform устройства, один - для обработки прерываний, один - для функций power management и один начальный поток, который выполняет операции инициализации-деинициализации модуля. Все примитивы синхронизации ядра (мьютексы и спинлоки) были заменены на pthread мьютексы. Известная ошибка была записана, как задача достижимости, следующим образом: tmp = tspi->rst; assert(tmp == tspi->rst);

Подробные результаты работы инструментов могут быть найдены на официальном сайте SV-COMP3. В основном, все современные инструменты верификации столкнулись с проблемами:

• CBMC: «pointer handling for concurrency is unsound - UNKNOWN»;

• CPAchecker: «Unsupported feature: BDD-analysis does not support arrays»;

• SMACK: «Exception thrown in lockpwn»;

• yogar-cbmc: «out of memory»;

• Ultimate: «Ultímate could not prove your program».

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

1. Анализ многопоточных программ должен быть достаточно точным, чтобы выдавать как можно меньшее количество ложных предупреждений, но быть достаточно эффективным, чтобы решать реальные задачи. Многие эффективные виды анализа не поддерживают сложные структуры данных (например, BDD анализ [19], анализ явных значений [20]). И наоборот, точные подходы вызывают проблемы при анализе длинных путей с переключениями между потоками (например, анализ предикатов [21], подход с ограничиваемой проверкой модели [22]).

2. Эффективное представление примитивов синхронизации. Многие подходы кодируют блокировки как переменные, которые атомарно проверяются и присваиваются (например, [8, 10]). Кодированные таким образом блокировки смешиваются с другими переменными и усложняют общий анализ.

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

1 https://github.com/sosy-lab/sv-benchmarks.git, sv-benchmarks/c/ldv-linux-3.14-races/linux-3.14-drivers-spi-spi-tegra20-sHnk.koxü.i

2 https://patchwork.kernel.org/patch/9915305

3 https://sv-comp.sosy-lab.org/2019/results/results-verified/META ConcurrencySafety.table.html 206

возможные доступы к памяти. Это еще более усложняет задачу. В данной работе представлен разработанный метод, который позволяет решать такие задачи.

3. Схема предлагаемого метода

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

volatile int g = 0;

volatile int d = 0;

Threadl {

1 g = 1;

2 d = 1;

3

}

Thread2 {

4 if (d == 1) {

5 g = 2;

6 }

}

Рис. 1. Пример небольшой программы Fig. 1. An example of a small program Это некоторый модельный пример, в котором используется конструкция неявной синхронизации между потоками: первый поток инициализирует некоторые данные (в данном случае, глобальную переменную g), а затем выставляет флаг, что данные готовы. Второй поток может использовать эти данные только после выставления флага, поэтому в этом примере нет состояния гонки для переменной g. Классические методы проверки моделей перебирают все возможные варианты чередования двух потоков (пример одного из возможных вариантов приведен на рис. 2).

Thread 1 Thread 2

g = 1;

d = 1;

[d == 1]

g = 2;

Рис. 2. Пример одного варианта выполнения потоков Fig. 2. An example of an execution С точки зрения инструмента статической верификации, необходимо рассмотреть полное множество состояний программы, которые возникают при всех возможных чередованиях (рис. 3).

Рис. 3. Построение множества чередований Fig. 3. Construction of interleaving set Даже в простом примере и при различных оптимизациях общее число состояний растет с катастрофической скоростью. Происходит так называемый «комбинаторный взрыв» числа состояний, что приводит к исчерпанию ресурсов. Таким образом, классические методы проверки моделей не могут обеспечить доказательства корректности программы. Простые методы статического анализа пытаются вычислить аппроксимацию сверху возможных действий одного потока на другой, так называемый эффект потока. Однако, они не способны прослеживать сложные зависимости между переменными. Например, зависимости между глобальными переменными, которые, в свою очередь, могут быть модифицированы из других потоков. В общем случае, это требует вычисления некоторой неподвижной точки, что является нежелательным при статическом анализе, так как значительно возрастают требования к ресурсам. В итоге, в таких сложных случаях считается, что глобальные переменные могут принимать любые значения. А это, в свою очередь, снижает точность анализа.

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

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

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

Поток 1

Э = 1

3

Й—'1 S)

I/

3

Поток 2

'0 .0-

9-0 . (J-в I->

i/

Рис. 4. Построение абстрактных переходов двух потоков Fig. 4. Construction of abstract states of the first thread Рис. 4 показывает часть абстрактного графа достижимости (Abstract Reachability Graph, ARG) для первого и второго потока без влияния друг на друга. Представленный анализ основан на простом анализе явных значений [20], который отслеживает только явные значения переменных. Переход содержит в себе абстрактное состояние и абстрактную операцию. Первое абстрактное состояние содержит информацию только о значении глобальной переменной x. Новая информация о значении переменной y появляется в дочерних элементах, после того как выполнен переход, соответствующей инициализации переменной.

Теперь необходимо учесть влияние потоков друг на друга, то есть сформировать окружение. Будем называть проекцией операции потока описание ее эффекта, видимого для других потоков. Например, любые модификации локальных переменных потока не влияют на другие потоки, то есть их проекция является пустой операцией. Модификация глобальной переменной является значимой для всех потоков, поэтому ее проекция должна совпадать с самой операцией, либо аппроксимировать ее сверху, например, теряя информацию о точном присваиваемом значении. При этом в проекции может быть не только информация о самом действии, но и об условии на его применение к другому потоку. Например, на рисунке 5 первый поток, присваивая "g = 1" меняет значение переменной g с нуля на единицу. Можно представить проекцию этой операции таким образом: если значение переменной x равно нулю, то оно может быть изменено на единицу. Иными словами, проекция состоит из двух частей: условия ее применения ([g == 0]) и непосредственно действия (g ^ 1).

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

После построения переходов в потоках независимо друг от друга (рис. 4), для всех переходов вычисляются проекции. Для второго потока, например, это действие, которое меняет значение переменной x значение с нуля на тройку. Остальные действия второго потока не модифицируют глобальные переменные и не порождают значимых проекций. Затем эта проекция применяется к каждому состоянию первого потока. На рисунке 5 приведен результат применения к первому переходу. Также эту проекцию можно применить и ко второму, однако, никаких новых путей это не породит. К третьему переходу первого потока применить данную проекцию нельзя, так как значение переменной x не удовлетворяет условию проекции. Применение проекции к первому переходу порождает новый путь выполнения, который в свою очередь может породить новые проекции.

Рис. 5. Построение абстрактных переходов для двух потоков Fig. 5. Construction of abstract transitions for both of the threads На рис. 5 представлен вариант с точными проекциями, которые рассматривают переходы другого потока так, как они есть. На рисунке представлены не все возможные проекции и порожденные ими переходы. Например, отсутствует проекция перехода «g = 2» второго потока.

Для проверки возможности состояния гонки нам необходимо найти два перехода, которые модифицируют одну переменную: «g = 1» в первом потоке и «g = 2» во втором. Далее, необходимо проверить, являются ли два абстрактных состояния совместными, то есть, могут ли они быть частью одного глобального состояния. В данном случае, в частичных состояниях потока значения глобальных переменных имеют разные значения, а значит, они не могут быть частью одного глобального состояния, то есть, указанные два перехода не могут быть выполнены одновременно. Отсюда следует, что состояние гонки отсутствует.

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

Рис. 6. Построение абстрактных переходов для двух потоков Fig. 6. Construction of abstract transitions for both of the threads

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

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

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

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

Параллельная программа представляется автоматом потока управления (Control Flow Automaton, CFA [5]), который состоит из множества L точек программы (моделируются программным счетчиком, pc) и множеством G £ L*OpsxL дуг (ребер) потока управления (моделируют операции, которые выполняются, когда управление переходит от одной точки в программе к другой). Операция создания потока создает новый поток с идентификатором из множества T и этот поток начинает свое выполнение из некоторой точки программы из L. Множество переменных программы, которые встречаются в операторах присваивания и условиях из Ops обозначаются X, а их значения ограничим множеством целых чисел Ъ. Подмножества X, содержащие только локальные и глобальные переменные, обозначаются Xlocal и Xglobal соответственно. Операции захвата/освобождения примитивов синхронизации определяются на множестве переменных-блокировок S, которые имеют значения из T U {1T}, где t £ T означает, что соответствующая блокировка была захвачена потоком t, а 1T означает, что соответствующая блокировка не была захвачена.

Конкретным состояниям программы называется четверка (cpc , cl , cg , cs ), где

1. отображение cpc: T ^ L является частичной функцией из идентификаторов потока в точку программы, в которой находится этот поток;

2. отображение cl: T ^ Clocal является частичной функцией из идентификаторов потока в присваивание локальным переменным их значений, то есть Clocal : Xlocal ^ Ъ;

3. отображение cg: Xglobal ^ Ъ является присваиванием значений глобальным переменным;

4. отображение cs: S ^ T U {1T} является присваиванием значений переменным блокировки.

Множество конкретных состояний программы обозначается как C. Отображения cpc и cl представляют собой локальную часть состояния потока, а cg и cs - глобальную. Обозначим dom - домен частичной функции, например, dom(c;) = (t|3(t,-) £ cj.

Для каждого состояния c=(cpc,cl,cg,cs) ЕС должно выполняться условие dom(cpc)=dom(cl), означающее, что домены локальных частей состояния должны быть консистентны и содержать одинаковое число потоков. Это множество обозначим dom(c)=dom(cpc)=dom(cl).

g,t

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

Определим отношение переходов —>£ C х G х T х C, где дуга g Е G, а поток t Е T. Определим множество конкретных переходов T = C х G х T. Элемент т Е T - это тройка т

ч ^ ^ в!*1 92*2 ^

= (c, g, t). Будем писать Ti ^ Т2 если 3c3 Е C: ci —> c2 —> c3. Семантику операций определим позже. В любом случае корректный переход g = (1,,1') Е G должен удовлетворять следующим условиям:

1. переход начинается в состоянии c: t Е dom(c) Л cpc(t) = l.

2. программный счетчик потока t переходит к точке 1': t Е dom(c') Л c'pc(t) = l'.

3. каждый переход в потоке t может изменить локальные части только этого же потока, а локальные состояния других потоков должны остаться без изменений; заметим, что для программ с указателями это не верно, но пока мы не рассматриваем такие программы в теории.

Будем обозначать Reach^(f) = {т| 3ti, ..., Тп Е T, .т ^ Ti Тп = то}. Будем обозначать

eval(c,t,expr) значение выражения expr над переменными из Xlocal U Xglobal со значениями из состояния c ЕС потока t Е T.

4.1 Операторы условия

Для дуги проверки условия д = (I, assume (expr), V) Е G,t ЕТ,1,1' Е L переход с

g,t

—> с ,с = (срс,cl,сд, cs),с = (с рс,с i,cд,с s) Е С существует, если

dom(c) = dom(c'), то есть переход не меняет множества потоков;

• Ci = с i,cg = с g,cs = с s, то есть переход не меняет значений переменных;

• срс (t) = I, с'рс (t) = V, то есть переход соответствует общим условиям на начало и конец;

• eval(c,t,expr) Ф 0, то есть значения переменных потока удовлетворяют проверяемому условию.

4.2 Операторы присваивания

g,t

Для дуги присваивания д = (I, assign(y, expr), I) Е G,t Е T,l,l Е L переход с —> с ,с = (Cpc,Ci,cg,cs),^ = (с'рс,с'ьс'д,с's) Е С существует, если:

• dom(c) = dom(c'), то есть переход не меняет множества потоков;

• ЧхЕХ^ХЕТ.с'^х)^ с:(с')(х),есяихфуУСфС'

leval(c, t, expr), если х = yht = t . , „ ( сп(х),если хФу

у Keval(c, t, expr), если х = у

• cs = с's, множество блокировок не меняется при обычном присваивании.

• срс (t) = I, с'рс (t) = V, то есть переход соответствует общим условиям на начало и конец.

4.3 Операции над примитивами синхронизации

Определим операции над примитивами синхронизации acquire/release. Предполагаем, что операция acquire(s) в потоке t Е Т, где s Е S - это специальная переменная блокировки, имеет стандартную семантику: атомарная проверка значения переменной s и, в случае

если s=±t, присвоение ей идентификатора текущего потока. Например, такая же семантика рассматривается в [8].

g,t

Для дуги захвата блокировки g = (I, acquire (s), I) E G,t E T,l,l EL переход с —> с ,c = (cpcci,cg,cs),c' = (c'pcc'i,c'g,c's) e С существует, если:

• dom(c) = dom(c'), то есть переход не меняет множества потоков;

• cl = с\, сд = с'g, то есть переход не меняет значений переменных;

• срс (t) = I, с'рс (t) = V, то есть переход соответствует общим условиям на начало и конец;

• cs(s) = 1т A c's(s) = t, Vs' E S: s' Ф s ^ c's(s') = cs(s').

Операция освобождения блокировки release(s) является обратной к операции захвата блокировки и присваивает значение s = 1T, если эта блокировка была захвачена текущим потоком, то есть s = t.

Для дуги освобождения блокировки g = (l,release(s),V) EG,teT,l,l'eL переход с

g,t

—> с ,с = (срс,cl,Cg, cs),с = (с рс,с t,cg,c s) E С существует, если:

• dom(c) = dom(c'), то есть переход не меняет множества потоков;

• с1 = с'I, Cg = с'g, то есть переход не меняет значений переменных;

• срс (t) = I, с' рс (t) = V, то есть переход соответствует общим условиям на начало и конец;

• cs(s) = t A c's(s) = ±г, Vs' E S: s' Ф s ^ c's(s') = cs(s'). 4.4 Операция создания потока

Определим семантику операции thread_create(lv) таким образом, что текущий поток переходит в следующую точку программы, а новый поток создается с идентификатором v E T и начинает свое выполнение из lv E L.

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

Для дуги создания потока g = (l,thread_create(lv),l') E G,t E T,l,l' E L переход с

g,t

—> с ,c = (cpc,cl,cg, cs),с = (с pc,c l,cg,c s) E С существует, если:

• v & dom(c) A dom(c') = dom(c)U{v}, поток v добавляется во множество потоков;

• cl = c\,cg = с'g,cs = с's, то есть переход не меняет значений переменных текущего потока;

• срс (t) = I, с'рс (t) = V, то есть переход соответствует общим условиям на начало и конец;

• c'pc(v) = lv, то есть новый поток начинает свое выполнение из своего начального состояния

Пока мы не рассматриваем операции ожидания потока(thread_join (v)), так как они будут усложнять объяснение и доказательство основной теоремы корректности предложенного подхода. Тем не менее, их поддержка также может быть добавлена.

5. Адаптивный статический анализ с абстрактными переходами

В классической теории адаптивного статического анализа (Configurable Program Analysis, CPA) [5, 6], абстрактное состояние представляет множество конкретных состояний программы. В расширенной теории, абстрактное состояние является частичным и может не представлять никакое состояние программы. Вот почему функция конкретизации, которая сопоставляет абстрактные состояния с конкретными, в расширенной теории

отличается от классической. В частности, она определяется на множестве абстрактных состояний.

Как следствие, абстрактный переход также является частичным. Поэтому анализ не может гарантировать, что последующие конкретные переходы будут достижимы за один шаг оператора transfer. В общем случае для этого может понадобиться k шагов. Для подхода с раздельным анализом потоков k = 2: анализ выполняет обычный переход в потоке, а затем распространяет его на все остальные потоки в качестве перехода в окружении. Это требует двух итераций алгоритма.

Определим формально адаптивный статический анализ с абстрактными переходами ГО = (D, П, merge, stop, prec, Он состоит из абстрактного домена D, множество точности П, оператор слияния merge, оператор останова stop, оператор настройки точности prec, и отношение переходов

1. Абстрактный домен D = (T , £, [[•]]) определяется множеством T конкретных переходов, T £ C х G х T, полурешеткой £ над абстрактными переходами и функцией конкретизации [[.]]. Полурешетка £ = (E, i, Т, с, U) состоит из (возможно бесконечного) множества E абстрактных элементов, верхнего элемента решетки ТеЕ, нижнего элемента решетки iEE, частичный порядок с с E х E и функцию объединения U: E х E ^ E. Функция объединения определяет минимальный элемент решетки, который больше заданных элементов.

Функция конкретизации [[•]] : 2E ^ 2T для каждого множества абстрактных переходов R с E определяет множество соответствующих конкретных переходов программы. Основным отличием от классической функции конкретизации - это определение на множестве абстрактных элементов. Таким образом, VR с Е ■ [[Д]] 3 Ue6fi[[{e}]]. Это означает, что суммарное знание от множества абстрактных переходов может быть больше, чем объединение знания от каждого частичного перехода.

2. Множество точности П определяет возможную точность абстрактного домена. Анализ использует элементы точности, чтобы отслеживать различные абстрактные состояния с различной точностью. Пара (e, п) называется абстрактным элементом e с точностью п. Операторы на абстрактном домене параметризованы точностью.

Для RCExn будем обозначать [[Д]] = [[U(e^)6fi{e}]].

3. Отношение переходов E х П х 2e х E определяет для каждого частичного перехода е с точностью п возможный следующий переход e'. При этом результат может зависеть от множества достижимых элементов R С E. Будем обозначать (е, п) e if (е, п, R, e) Е^. Определим множество Reachk по индукции: VR С Е ■ Reach 0(R) = R

Vk > 1 ■ Reach k+1(R) = ^ {e' | e ^ e' } U Reachk(R)

eEReachk (R)

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

3k > 1 ■ VR С E ■ Reach k(R) = ^ {т'1 t ^ т'}

T Е[[й]]

Это требование ослабляет требование на transfer классической теории. Оно означает, что оператор должен выдавать соответствующие конкретные переходы на после одного шага, как в классической версии, а после k шагов. Для подхода с раздельным анализом потоков k = 2, как мы увидим в дальнейшем.

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

Ve,e' Е E,n Е П:е' с тегде(е,е',л)

5. Оператор останова stop: Ex2Exnxn^{true, false} проверяет, покрывается ли абстрактный элемент, данный в качестве первого параметра, множеством элементов, данных как второй параметр. Оператор останова может, например, искать среди множества элементов такой, который покрывает (с) данный элемент. Оператор останова должен удовлетворять следующим условиям:

Ve,e'Е Е,л Е n:stop(e,R,n) ^VR с E:[[{e}U R]] с [[ДиД]]

6. Функция настройки точности ргес:ЕхПх2ЕхП^Е>П вычисляет новый абстрактный элемент и новую точность для заданного элемента с точностью и множества абстрактных элементов. Функция настройки точности может выполнять ослабление (расширение) абстрактного элемента вместе с изменением точности. Функция настройки точности должна удовлетворять следующему требованию:

Ve,e' Е E,n,n' Е n,R СЕхП: (е',п') = prec(e,n,R) ^ е с е' В целом, множество точности П, оператор останова stop, оператор объединения merge, оператор настройки точности prec остаются такими же, как и в классической теории CPA. На рис. 7 представлен основной алгоритм, который вычисляет множество достижимых абстрактных переходов, также не изменяется за исключением расширения оператора transfer.

Рис. 7. Основной алгоритм CPA(D, eo, по) Fig. 7. The main algorithm CPA(D, eo, по) Для этого алгоритма основная теорема может быть доказана даже с ослабленными требованиями. Доказательство повторяет доказательство классической теоремы. Теорема (корректность). Для заданного адаптивного статического анализа с абстрактными переходами ГО, начального состояния e0 с точностью п0 алгоритм CPA(D, e0 , п0) вычисляет множество абстрактных переходов, которое аппроксимирует сверху множество достижимых конкретных переходов:

[[СРА(ГО,е0,л0)]] 3 Reach^([[[e0}]]) Следует отметить, что на практике используется одновременно несколько различных CPA для анализа исходного кода. При этом структура множества CPA напоминает древовидную, где корень этого дерева предоставляет операторы для основного алгоритма, представленного на рис. 7. Различные CPA взаимодействовать друг с другом для повышения точности анализа. Так, в разд. 6 будет представлен верхнеуровневый CPA,

215

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

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

6. ThreadModularCPA

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

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

6.1 Расширение внутреннего CPA

Определение CPA для подхода с раздельным анализом потоков расширяется тремя новыми операторами: I = (Di, П1, I, mergei, stopi, preci, compatibles^, composei). Абстрактный домен DI = (TI, £I, ф1) состоит из множества конкретных переходов TI, полурешетки £I, и оператора композиции частичных состояний ф1. Таким образом, внутренний анализ должен определить не функцию конкретизации [[•]], а оператор композиции ®, так как подход с раздельным анализом потоков требует одинаковой схемы вычисления конкретных состояний.

Как было уже сказано, состояния и переходы являются частичными, поэтому они могут не соответствовать напрямую конкретным состояниям и переходам. Чтобы получить полный переход, нужно взять композицию множества частичных переходов, которые соответствуют всем доступным потокам. Совместные частичные переходы могут быть объединены в полный конкретный переход с помощью оператора композиции Е х Т х 2ЕхТ ^ 2T. Он возвращает множество конкретных переходов, которое соответствует данным частичным переходам.

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

Оператор проверки совместности compatibles E х E ^ {true, false} проверяет, могут ли два частичных перехода начинаться из общего полного родительского состояния. Оператор проекции |p: E ^ E проецирует переход в потоке на другой поток. Например, проекция может содержать модификации глобальных переменных, но опускать изменения локальных данных для потока.

composes E х E ^ E объединяет два абстрактных перехода в один. Он применяет абстрактную дугу из одного перехода к абстрактному состоянию другого перехода.

В дальнейшем мы будем использовать оператор applyi, как комбинацию трех операторов: |p, composei и compatiblei:

Ve,e' E E: apply(e.e') = {composel{e,e'\p),ecnn compatible,{e,e'\p)

I 1, иначе

Таким образом, оператор applyI означает, что переходы могут быть объединены, только если они совместны. Результатом применения оператора является новый переход, который будем называть переходом в окружении, так как он представляет собой эффект окружения.

6.2 CPA для раздельного анализа потоков

Определим специальный CPA, который реализует логику раздельного анализа потоков: ТМ = (Dtm, птм, ^тм, mergeTM, stopTM, ргестм), который основан на внутреннем CPA I = (Di, П1, I, mergei, stopi, preci, compatiblei,|p, composei).

Абстрактный домен DTM = (T , £, [[ ]]), множество конкретных переходов T = TI, а решетка £= £I. Функция конкретизации [[•]] выражается через оператор композиции ф1:

ж

VR ь Е: [[«]]= U и ......3)

к = 1 е0,еъ...,ек E R to,ti, . ,tkE Т

Отношение переходов определяет следующие переходы, после чего применяются все достигнутые переходы, как переходы в окружении, к новым переходам, а новые переходы, как переходы в окружении, - к уже достижимым (рис. 8).

result := (1 ;

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

For each e : eo 1 e do result result U {?} : for each e £ reached do

result :— result U {apply (e\ e)} result:— result U {apply (e, e')} end

end

return result_

Рис. S. transferTM(eo, по, reached) Fig. S. transferTM(eo, по, reached) Множество точности П, операторы merge, stop, prec соответствуют операторам внутреннего CPA.

T. LocationCPA

В этом разделе представлен простой анализ точек программы (Location Analysis) L = (Dl, П], ^l, mergeL, stopL, precL, compatible], |p, composeL), который отслеживает абстрактные точки программы. Анализ расширяет классический LocationCPA для возможности применения его вместе с ThreadModularCPA.

1. Абстрактный домен DL = (TL, EL, ®L). Абстрактный переход состоит из абстрактного состояния s E El, и абстрактной дуги qE Е^. Е[ - это множество абстрактных точек программы (англ. program location), которые отображаются на конкретные точки программы в CFA с помощью функции loc: E[ . Tf означает, что анализ не знает, в какой именно точке программы находится анализ. Более формально, loc(TSL) = L. В общем случае анализ может использовать абстрактными точками программы, которые соответствуют нескольким конкретным точкам программы, но в дальнейшем будет

описан простой вариант анализа, который рассматривает только одиночные точки программы:

Vs£E[:s = TSLVs = ±1 V loc(s) = {i)£L В этом случае используемая решетка £[ является плоской, то есть любые два невырожденных состояния (неравные Т£ или ±L) несравнимы.

Абстрактная дуга основана на обычной CFA дуге и содержит только начальную точку программы и конечную: с е[ х е£.

2. Множество точности является вырожденным и содержит только один элемент: П5 = {0}.

3. Переход e ^L e', e = (s, g), e' = (s', g'), q = (pred, suc), q' = (pred', suc') существует, если изменение точки программы соответствует абстрактной дуге. Более формально, е е' о loc(s) П loc(pred) Ф 0,Вд' £ G: д' = (l'1,op,l'2) А 1'1 £ loc(pred') П loc(s') А 1'2 £ loc(suc') As' = suc.

4. Оператор слияния merge не объединяет элементы: mergeL(e, е', п) = е'.

5. Оператор останова stop рассматривает уникальные состояния: stopL(e, R,n) = (e £ R).

6. Точность не регулируется: precL(e,n,R) = (е,л).

7. Переход в одном потоке никак не влияет на переход в другом: Ve £ EL, e = (s,g): e\P = (s,e).

8. Ve,e' £ EL,e = (s,q):composeL(e,e') = ë = (s,q), где q = (s,s), так как переход в одном потоке никак не может повлиять на положение другого потока.

9. Ve1,e2 £ El: compatibleL(e1, e2) = true, так как два потока могут быть совместны в любых точках программы.

8. ThreadCPA

Определим простой анализ потоков T = (Dt, Пт, ^т, mergeT, stopT, ргест, compatible^ |p, composeT), который отслеживает идентификаторы потока.

Анализ потоков наследует ограничения из [7] и ограничен программами с конечным числом созданий потоков. Пусть в программе есть конечное число потоков, которые идентифицируются точками программы, в которых они начинают свое выполнение, то есть Т с L и для каждого оператора создания потока thread_create(pcv) всегда создается поток с идентификатором pcv. Заметим, что остальные типы анализов не ограничены числом созданий потоков, более того, возможно применение более сложного анализа потоков, который будет поддерживать неограниченное число созданий. Таким образом, общая теория поддерживает неограниченное число созданий потоков.

1. Абстрактный домен DT = (TT, £T, ®T) основан на плоской решетке над множеством потоков T, где £т = £T х £-£. Множество абстрактных состояний = T U {±7, ТИ, в котором любые два невырожденных состояния (неравные Т£ или ±г ) несравнимы. Множество абстрактных дуг содержит множество обычных CFA дуг и пустой переход в окружении: ET = {±т,е, Tx}UG.

2. Множество точности является вырожденным и содержит только один элемент: Пт = {0}.

3. Переход e е',е = (s,g),e' = (s', g'),g = (-,op,-) существует, если

• op Ф thread_create(pcv), s'=s,g'£G , то есть при любой операции, кроме создания потока, состояние не меняется;

• op = thread_create(pcv), s' = pcvVs' = s,g' £ G. При создании потока создаются два дочерних состояния: одно соответствует созданному потоку, а другое -родительскому.

4. Оператор слияния merge не объединяет элементы: mergeT(e, е',л) = е'.

5. Оператор останова stop рассматривает уникальные состояния: stopT(e,R,n) = (е Е R).

6. Точность не регулируется: precT(e, л, R) = (е, л).

7. Переход в одном потоке никак не влияет на переход в другом: Ve Е ET,е = (s,g): е\Р = (s,e). Переходы в окружении (е-переходы) не могут изменить идентификатор другого потока. Тем не менее, проекция влияет на совместность состояний, чтобы переходы в потоке не применялись, как эффекты окружения, к этому же самому потоку.

8. Ve,e' Е ET,e = (s,q),e' = (s', q'): composeT(e, e') = e = (s,q').

9. Ve1,e2 Е ET,ei = (si,qi),e2 = (s2,q2): compatibleT(e1,e2) = s1 Ф s2, так как два состояния могут быть совместны, если они относятся к разным потокам.

9. ValueCPA

Определим анализ явных значений V = (Dv, Пу, ^у, mergev, stopv, precv, compatiblev, |p, composev), который отслеживает явные значения переменных. Он состоит из следующих элементов.

1. Абстрактный домен dp = (Ту, £у, фу). £у = (Еу, ^у, Ту, Еу, Uv). Абстрактный переход состоит из абстрактного состояния s Е Еу, а абстрактная дуга q Е Еу, таким образом, Еу = Е$х EVT, а £у = £$х

Абстрактное состояние этого анализа является отображением из имен переменных в их значение: Vs Е Ey,s:X ^ Z, где Z = J,U{ Т2}. Таким образом, множество абстрактных состояний является плоской решеткой над целыми числами. Верхний элемент решетки Ту = [v\ VxЕX:v(x) = Tz}. является отображением, в котором любая переменная имеет любое значение. А нижний элемент решетки = [v\ Зх Е X: v(x) = ^z} является отображением, в котором никакая переменная не может иметь никакого явного значение. Такое состояние является недостижимым при реальном выполнении программы. Порядок является тривиальным: любые два невырожденных состояния (неравные Ту или ^V) несравнимы.

Множество абстрактных дуг содержит множество обычных CFA дуг и переходы в окружении, которые определяются изменением глобальных переменных: EV = 2X"ZUG.

2. Точность анализа явных значений определяется отслеживаемыми переменными, таким образом множество точности содержит подмножества из всех переменных программы: nv = 2х.

3. Отношение перехода е ™v е',е = (s,g),e' = (s',g').

I. g Е G,g = (-,op,-).

a. g = (■, assume (expr),-):

!^z, если $c.(x ^ с): (expr Ф 0)/s с, если З!с.(х ^ с): (expr Ф 0)/s или s(x) = с Tz,иначе

Здесь expr/v означает интерпретацию выражения expr над переменными из X для абстрактного присваивания v. А выражение (х ^ с): (expr Ф 0~)/s означает, что значение с у переменной х удовлетворяет интерпретации.

b. g = (■,assign(w,expr),■):

(exprесли x = w Vx Е X: s'(x) = \

(. s(x), иначе

c. В остальных случаях состояние не меняется s = s'.

II. g^.X^Z, это означает, что мы имеем переход, который меняет определенные переменные. В этом случае, следующее состояние

4. Оператор слияния merge не объединяет элементы: mergeV(e, е',п) = е'.

5. Оператор останова stop рассматривает уникальные состояния: stopV(e,R,n) = (е е R).

6. Функция настройки точности вычисляет новое абстрактное состояние и точность, ограничивая присваивания только теми переменными, которые содержатся в точности: precV(e,n,R) = (в\п,п).

7. Переход в окружении может затрагивать только глобальные переменные: Ve е EV,e = (s, g): е\Р = (salobal,galobal). Здесь отображение salobal означает только ту часть, которая относится к глобальным переменным.

8. Ve,e' е EV,e = (s,q),e' = (s',q'):composeV(e,e') = ё = (s,q').

9. Ve1,e2 е EV,e1 = (s1,q1),e2 = (s2,q2): compatibleV(ei,e2) = Vx е xalobal: (x е dom(s1) Ах е dom(s2)) ^ foCO Ç s2(x) Vs2(x) Ç s^x)), что означает консистентность значений глобальных переменных.

10. PredicateCPA

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

Пусть P - это множество формул над переменными программы в теории без кванторов. Пусть P £ P - это множество предикатов, а v: X ^ Ъ - это отображение из переменных в их значения. Определим v И <р, где v называется моделью формулы (р. Определим переименование переменных 9:X ^ X' из пространства имен X в X', которое применимо к формулам 0(<р). Обозначим

Обозначим (ф)п декартову предикатную абстракцию формулы ф. Обозначим SPop^) - сильнейшее постусловие формулы ф и операции op. Определим анализ предикатов P = (Dp, Пр, ^р, mergep, stopp, precp, compatiblep, |p, composeP), который отслеживает выполнимость предикатов над переменными программы. Он состоит из следующих компонентов:

1. Абстрактный домен Dp = (TP, £р, ®р). £p = (Ep, ^p, Tp, Çp, Up). Абстрактный переход состоит из абстрактного состояния s е Ер, а абстрактная дуга q е Ej, таким образом, EP = x E'j, а £р = £$x £j.

Ер = P, таким образом, состояние является формулой первого порядка. Верхний элемент решетки является тождественно истинной формулой TP = true, а нижний элемент -тождественно ложной: = false. Частичный порядок Çp£ Ер x Ер определяется как eÇp е' о e^ e'. Оператор слияния Up: Ер x Ер ^ Ер определяет ближайший элемент в соответствии с частичным порядком.

Абстрактная дуга qе Ej - это действие, которое может быть выражено или формулой, или обычной CFA дугой: Ер = Ер U G.

Ъ = {

x — x i, если хех .x — x, иначе ;

, и

x i ——> x, если x е X .x — x, иначе

Не будем приводить формальное определение ®р, так как оно требует достаточно много места. Основная идея состоит в том, что он возвращает множество конкретных переходов, которые соответствуют формуле (сильнейшему постусловию). Наиболее сложная часть определения это проверка, может ли множество частичных переходов соответствовать единственному глобальному переходу. Для перехода в потоке e0 с обычной CFA дугой q0 £ G и переходов в окружении ei, ..., en проверка состоит из двух частей:

a) для абстрактного состояния s0 из e0 и всех состояний из переходов в окружении si, ., Sn существует общая модель v;

b) дуга qp из проекции eo|p покрывается абстрактными дугами qj переходов в окружении qp Е qj.

Формально,

s т

Ve0, e1,...,en£ ЕР, ei = (Si, qi), st£ EP ,qt£ EP

Ccheckißo, {el ,..., eJ) = BV: V И S0 A exlocal Д O-J Л ■ ■ ■ Л exiocai n (sn) Л (q0 £ G AVO < j < n: q, &GAe0\p = (sp ,qp): qp Е q,

2. Множество точности Пр = 2P определяет точность абстрактного состояния как множество предикатов.

3. Оператор объединения merge может иметь несколько модификаций, например, mergejoin объединяет обе части перехода:

Ve,e' £ ЕР,л £ Пр,е = (s, g) :

!(s V s', g), если g = g',g £ G e', если g£GAg'£TVg£TAg'£G (sV s'.gV g'), если g,g' £T mergeEq объединяет только абстрактные дуги при равных (или покрытых) состояниях. mergesep не объединяет элементы.

4. Оператор останова stop проверяет, покрывается ли переход e некоторым другим переходом в множестве достижимости: stopP(e,R,n) = Be' £ R : (е Е е').

5. Функция настройки точности prec вычисляет предикатную абстракцию над предикатами в точности n: precP (e, n, R) = ел = (sn, q).

6. Отношение переходов e е',е = (s,g),e' = (s ,■). Так как анализ предикатов не отслеживает релевантные дуги, он возвращает все возможные.

Для g £ G существует переход е е' с g = (•, op, •), если s' = (SPop(e))n. Для g = <р £ Р следующее абстрактное состояние имеет вид s' = ф A е.

7. Определение проекции:

!е, если g & G

(вхюса1 ,env(s),exiocai env ^(з^.иначе

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

8. Ve,e' £ ЕР,е = (s,q),e' = (s',q'):composeP(e,e') = ё = (s,q')

9. Ve1,e2 £EP,et = (si,qi),si £ EP: compatibleP(ei,e2) = Bv:v И dxiocai 1(s1)A

Bxlocal 2(s2)

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

11. LockCPA

Определим анализ примитивов синхронизации § = (Ds, ns, ^s, merges, stops, precs, compatibles, |s, composes), который отслеживает множество захваченных блокировок (переменных синхронизации) для каждого потока. Он состоит из следующих компонентов:

1. Абстрактный домен Ds = (Ts, £s, ©s). £s = £s x £s. £s = 2s U { iS, TS} - это множество всех подмножеств из переменных синхронизации, Is TS и Is 3

^ т T T

Is' ^ Is is для всех элементов Is, Is Q S. ET = {^S, £, TS]UG.

2. Для этого анализа множество точности содержит только один элемент: П5 = {0}.

3. Оператор перехода добавляет захватываемую блокировку во множество Is по время оператора acquire и удаляет ее из него во время оператора release. Формально, переход е е',е = (ls,g),e' = (ls',g'),g = (-,ор,-) существует, если

• op = acquire(s) и s Is A Is' = Is U {s}, g' E G.

• op = release(s) и Is' = ls\{s},g' E G.

• op = thread create(lv) и Is' = 0,g' E G

• иначе, Is' = ls,g' E G.

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

4. Оператор слияния merge не объединяет элементы: merges(e, e',n) = e'.

5. Оператор останова stop проверяет, существует ли состояния, которое содержит меньше захваченных блокировок: stops(e,R,n) = Зе' E R . (е ^ е').

6. Точность не регулируется: precs(e,n,R) = (е,п).

7. Определение проекции: Ve E Es,e = (s,g): е\Р = (s,e).

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

8. Ve,e' E Es,e = (ls,q),e' = (ls',q'):composes(e,e') = ё = (ls,q').

9. Ve:,e2 E Es,et = (lsi,qi),lsi E E$: compatibleP(e:,e2) = (Is П Is' = 0). Проверка совместности сильно похожа на классический алгоритм Lockset. Если существует одна и та же блокировка в обоих состояниях, операции не могут быть объединены, так как два потока не могут дважды захватить одну и ту же блокировку.

12. CompositeCPA

Анализ используется для комбинации различных техник анализа вместе. Примеры таких анализов были описаны выше. С = (Dc, Пс, ^с, mergec, stopc, precc, compatiblec, |c, composec) включает в себя n вложенных анализов Д; = (Di, Пъ mergeb stopb precb compatiblei, |i, composeO.

1. Абстрактный домен DC = D1 x-- xDn реализует декартово произведение всех вложенных абстрактных доменов.

2. Множество точности также реализует декартово произведение вложенных множеств точности: Пс=П1 х — хПп.

3. Оператор перехода применяет вложенные операторы перехода к соответствующим частям состояния. Таким образом, переход возможен, если для всех вложенных анализов возможен переход между соответствующими вложенными состояниями:

Velt е2 Е EC: e1 ^C е2 о V1 < i < п: е\ ^ е12

4. Оператор слияния merge вызывает вложенные операторы: mergeC(e1,e2,n) = (merge1(e}, л1),..., merge^e™, е2, лп)).

5. Оператор останова stop вызывает вложенные операторы: Ve Е E,R Q Е,п Е n:stopC(e,R,n) = Эе Е R,V1 <i<n: stopi(ei,{el},ni).

6. Аналогично оператор настройки точности вызывает вложенные операторы: precC(e,n,R) = (prec1(e1,n1,R),...,precJl(en,nu,R)).

7. Аналогично оператор вычисления проекции вызывает вложенные операторы: Ve Е EC,: е\Р = (е1\Р.....еп\Р).

8. Ve1,e2 Е EC : composeC(e1,e2) = (compose1(e}, е^),..., composen(e",е2)).

9. Ve1,e2 Е EC : compatibleC(e1,e2) = (compatible^!, ег),..., compatiblen(e",e2)).

13. Поиск состояний гонки

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

Рис. 9. Пример конфигурации CPA Fig. 9. An example of CPA configuration На рис. 9 представлен пример конфигурации инструмента. Так, набор CPA содержит ThreadModularCPA (раздел 6), в качестве верхнеуровневого. В него вложен ARGCPA, который обеспечивает связь между различными абстрактными состояниями, в том числе связи «родительский переход - дочерний переход», «проецируемый переход - проекция» и др. CompositeCPA (разд. 12) обеспечивает параллельную работу вложенных в него CPA: LockationCPA (разд. 7) моделирует счетчик команд потока, CallstackCPA обеспечивает межпроцедурность анализа, LockCPA (разд. 11) отслеживает захваты блокировок, ThreadCPA(разд. 8) отслеживает точки создания потоков, PredicateCPA(разд. 10) реализует анализ предикатов. Важно отметить, что это не единственная возможная конфигурация инструмента. Более того, для решения различных задач могут потребоваться различные конфигурации, то есть наборы, используемых техник анализа. Итак, с помощью некоторой конфигурации инструмента производится построение абстрактного графа достижимости (англ. Abstract Reachability Graph, ARG) из абстрактных переходов. То есть, по завершению этого этапа имеется граф из переходов программы, которые являются достижимыми при заданном уровне абстракции, то есть, совсем не обязательно они будут достижимыми при реальном выполнении программы.

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

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

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

В случае, если обнаружилась пара доступов к одной области памяти, в данном случае, к одному региону, необходимо проверить возможность одновременного доступа к ней. Для этого используется понятие совместности переходов. Совместность означает, что два частичных абстрактных состояния могут быть частью одного глобального состояния. Или, другими словами, один переход может быть применен, как переход в окружении, к другому переходу и наоборот, откуда следует, что эти два перехода могут быть выполнены параллельно. Таким образом, предложенный подход является обобщением подхода Lockset[13], который определяет состояние гонки, как два доступа к некоторой памяти с непересекающимся множеством блокировок. Одним из ограничений подхода Lockset является отсутствие поддержки других типов синхронизации. В расширении подхода для этого используется оператор compatible. Так как проверка совместности использует различные типы анализа, включая анализ примитивов синхронизации, анализ предикатов и другие, такой способ является более точным, чем алгоритм Lockset. Итак, алгоритм поиска состояний гонки состоит из следующих шагов:

1. построить множество достижимых абстрактных переходов (алгоритм 1);

2. для каждого перехода определить регионы памяти, к которым может производиться доступ;

3. для каждого региона памяти попытаться подобрать пару совместных переходов, которые образуют состояние гонки;

4. проверить каждую пару путей, приводящих к состоянию гонки, на достижимость и уточнить предикатную абстракцию в случае необходимости [21].

Следует добавить, что алгоритм уточнения абстракции по контрпримерам (англ. Counterexample Guided Abstraction Refinement, CEGAR [11]) был использован без существенных модификаций. Однако, в таком виде он позволяет проводить уточнение только локальных путей в потоке, то есть не позволяет обнаружить противоречие между различными потоками. Однако, это не является принципиальным ограничением подхода, и при соответствующем расширении метода CEGAR на случай подхода с раздельным рассмотрением потоков, возможно получение более точных результатов.

14. Результаты

Подход с раздельным помодульным анализом потоков и абстракными переходами был реализован в инструменте CPAchecker, как композиция следующих типов анализа:

1. Анализ точек программы (англ. Location analysis) L;

2. Анализ стека вызовов функци (англ. Callstack analysis) CS;

3. Анализ потоков (англ. Thread Analysis) T;

4. Анализ примитивов синхронизации (англ. Lock analysis) S (раздел 8);

5. Анализ предикатов с опциями (англ. Predicate analysis) P (раздел 7):

I. Join - объединение абстрактных состояний и абстрактных дуг. В данном варианте все эффекты окружения предикатного анализа объединяются в один, что, естественно, снижает точность, но повышает скорость. II. Eq - объединение абстрактных дуг, если равны абстрактные состояния; III. Sep - переходы никогда не объединяются.

6. Анализ явных значений (Value analysis) V [20].

14.1 Результаты на наборе задач SV-COMP

Набор задач SV-COMP состоит из 1082 задач, большая часть из которых являются небольшими примерами около 100 строк кода. Однако, они содержат в себе нетривиальные механизмы синхронизации, в том числе алгоритмы Деккера, Петерсона и др. 7 задач были подготовлены на основе драйверов ОС Linux. Все задачи доступны в официальном репозитории SV-COMP4. Эксперименты проводились с использованием кластера из 191 машины VerifierCloud5. Были использованы ограничения по памяти в 8 Гб и по времени 15 минут.

Оценка результатов реализованного подхода в инструменте CPAchecker была проведена для следующих вариантов анализа:

1. Подходы с раздельным анализом потоков с абстракцией переходов:

a. Варианты объединения для предикатного анализа

i. MergeJoin. (L,CS,T,S,P) Join.

ii. MergeEq. (L,CS,T,S,P) Eq.

iii. MergeSep. (L,CS,T,S,P) Sep.

b. Value. Анализ явных значений (L, CS, T, S, V).

2. Threading. Анализ чередований, описанный в [17] использует классическую теорию

CPA и рассматривает все возможные чередования потоков.

Табл. 1. Результаты экспериментов Table 1. Experiment Results

Подход MergeJoin MergeEq MergeSep Value Threading

Найденные 1026 1027 763 963 720

ошибки

Истинные 805 806 548 752 720

Ложные 221 221 215 211 0

Доказательства 27 28 28 30 165

корректности

Истинные 27 28 28 30 165

Ложные 0 0 0 0 0

4 https://github.com/sosy-lab/sv-benchmarks

5 https://vcloud.sosy-lab.org/cpachecker/webclient/master/info

Незавершенный анализ 29 27 297 89 197

Процессорное время, с Астрономическое время, с 16 800 10 200 15 900 9 630 278 000 258 000 75 300 64 900 93 700 67 500

Результаты (табл. 1) подтверждают, что подход с раздельным анализом потоков не пропускает ошибок при некоторых заранее известных ограничениях. MergeJoin показывает результаты лучше, чем конфигурация MergeSep. Это происходит, в основном, из-за большого количества переходов в окружении, которые MergeSep рассматривает по одному. MergeJoin объединяет их в один и применяет за один раз. Это позволяет сохранить огромное количество времени. В то же время MergeSep позволяет избежать некоторых неточностей из-за анализа переходов по-отдельности и выдает меньшее количество ложных сообщений об ошибках.

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

Классический анализ Threading является корректным и точным и не выдает ложных вердиктов. Однако, он требует значительного числа ресурсов, это является главным недостатком подхода. Эта конфигурация решила только один из семи сложных задач, основанных на драйверах операционной системы Linux. Подходы с анализом потоков по-отдельности (MergeJoin, MergeEq, MergeSep) решают пять из семи таких задач. Большая часть новых доказательств корректности, полученных новыми подходами, (26 из 27 для MergeJoin) не находились классическими методами (Threading). Это также является одним из важных вкладов данного метода.

14.2 Поиск состояний гонки в драйверах устройств

Верификационные задачи, основанные на драйверах операционной системы Linux, были подготовлены системой Klever, которая предназначена для верификации различного программного обеспечения [17, 18]. Она разделяет большой объем целевого исходного кода на отдельные небольшие верификационные задачи. Для ядра операционной системы Linux верификационная задача соответствует одному модулю. Система Klever автоматически готовит модель окружения модуля, которая включает в себя модель потоков, модель сердцевины ядра и операций над модулем. После подготовки верификационной задачи Klever запускает верификацию через общий интерфейс -BenchExec [23].

Сравнение проводилось на подсистеме drivers/net/ ядра операционной системы Linux 4.2.6, для которой Klever подготовил 425 верификационных задач. Эксперименты также проводились с использованием кластера из 191 машины VerifierCloud. Были использованы ограничения по памяти в 8 Гб и по времени 15 минут. Алгоритм поиска состояний гонки с помощью инструмента был описан в разд. 12. При этом подготовленные задачи не содержали кодирование состояния гонки в виде проверки задачи достижимости, поэтому было невозможно снова включить в сравнение участников соревнования SV-COMP, так как они не поддерживают поиск состояний гонки. В сравнении (табл. 2) участвовали следующие конфигурации:

1. Base. Конфигурация MergeJoin из предыдущего пункта.

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

Табл. 2. Сравнение конфигураций Base и Havoc Table 2. Comparing Base and Havoc Configurations

Подход Base Havoc

Модули с ошибкой б 2б

Корректные модули 2б2 254

Незавершенный анализ l57 l45

Процессорное время, с l37 000 l25 000

Модули с ошибкой означают, что в них было найдено хотя бы одно потенциальное состояние гонки. Конфигурация Havoc чуть более быстрая, но менее точная, поэтому она не может доказать корректность 8 корректных модулей, однако способна обнаружить больше ошибок. При этому 6 из этих ошибок были найдены в пропущенных корректных модулях, что означает, что найденные ошибки - ложные из-за неточности анализа. Но 13 ошибок были соответствуют незавершенному анализу у Base конфигурации, что означает, что Havoc может находить новые ошибки.

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

Часть полученных ошибок была проанализирована. Обычно для каждой верификационной задачи могут быть получены несколько потенциальных состояний гонки. Будем называть их предупреждениями. Соотношение истинных предупреждений к общему количеству составляет около 42% (34 корректных предупреждения и 47 некорректных). Основной проблемой ложных предупреждений об ошибках являются проблемы с моделью памяти (более 90%). Остальные предупреждения связаны со спецификой ядра (например, обработкой прерываний), анализом функциональных указателей, другими примитивами синхронизации и др. Интересно, что таких проблем, связанных с неточностью подхода, как были в задачах SV-COMP, в модулях операционной системы Linux не наблюдается. Это подтверждает предположение, что задачи SV-COMP являются слишком искусственными, по крайней мере в категории многопоточных задач (Concurrency).

Из 24 сообщений о возможном состоянии гонки 14 были подтверждены разработчиками, для 8 не было получено обратного ответа, и только в двух случаях найденная гонка была признана недостижимой из-за аппаратной синхронизации устройства. Однако большинство истинных ошибок были найдены в «древних» драйверах, а их исправление не является актуальной задачей. Только 4 исправления из 14 были включены в основную ветку разработки ядра.

Результаты экспериментов позволяют утверждать, что удалось успешно интегрироваться с другими техниками анализа, которые доступны во фреймворке CPAchecker. Такие подходы, как CEGAR, могут быть применены с минимальным количеством изменений технического характера. Некоторые реализации конкретных видов анализа могут быть использованы без модификаций (AutomatonCPA) или с минимальными изменениями в коде (CallstackCPA). Для некоторых существующих техник анализа (PredicateCPA, ValueCPA, CompositeCPA) была реализована поддержка операций над эффектами окружения, при этом основная функциональность анализа осталась без изменений. Изменения параметров работы инструмента (вариантов анализа) достигается лишь опцией запуска, то есть пересобирать инструмент не требуется.

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

15. Обзор похожих работ

Существующие подходы к анализу многопоточного программного обеспечения имеют различные свойства и производительность. С одной стороны, существуют точные подходы, которые могут доказать корректность программ при некоторых определенных предположениях. Начиная с ограничиваемой проверки моделей, большинство подходов прилагают значительные усилия для оптимизации обхода пространства состояний программы. Примерами таких оптимизаций могут стать редукция частичных порядков [1], ограничение контекста [24, 25], и др.

Попыткой абстрагироваться от нерелевантного окружения является подход с раздельным анализом потоков (thread-modular approach), который впервые был предложен в [7]. Эта версия еще не использовала никакую абстракцию. Подход с раздельным анализом потоков для формальной верификации был представлен в [26]. Основная идея была в поиске инвариантов для каждого процесса, из которых потом должно следовать проверяемое свойство. Оценка подхода проводилась для двух протоколов взаимного исключения. Предикатная абстракция была добавлена к этому подходу в [27]. Основным отличием было то, что рассматривался только один поток из нескольких копий. Поэтому окружение формировалось этим же потоком. При этом никакие примитивы синхронизации не рассматривались.

Расширение подхода с раздельным анализом потоков, которое использует абстракцию, был представлен в [28] и затем реализован в инструменте TAR [8]. Описанный в данной статье подход имеет ряд отличий:

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

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

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

Похожий подход также был реализован в инструменте Threader [29]. Этот инструмент использует аппроксимацию сверху для построения окружения, основанную на дизъюнктах Хорна. Также как и в инструменте CPALockator, Threader может искать модульные доказательства, но при этом он может искать и немодульные, в которых учитыватся полной взаимодействие между потоками.

Существуют различные техники трансформации параллельной программы в последовательную (англ. sequentialization) для последующей проверки ее обычными инструментами статической верификации [30-32]. Один из примеров является WHOOP [33], который использует эту технику трансформации и не учитывает взаимодействие потоков. Более того, он сильно использует алгоритм Lockset и не может быть расширен 228

какими-нибудь другими анализами. Он применяется в качестве преданализа для более точного инструмента CORALL [34].

С другой стороны, существуют множество легковесных подходов, которые могут применяться к огромному объему кода. Такие техники определяются слабыми требованиями к ресурсам и низкой точностью. Примерами являются RELAY [35] и Locksmith [36], которые применялись для анализа кода операционной системы Linux. RELAY нашел несколько тысяч предупреждений, что потребовало применения неточных фильтров для сокращения этого количества. Этот инструмент вообще не учитывает взаимодействие потоков.

Инструмент Locksmith, напротив, учитывает точки создания потоков, но не может точно определить разделяемые данные и способы взаимодействия потоков. Он вычисляет общий эффект от потока. В терминах представленной теории используется оператор merge, объединяющий все переходы в окружении в один. При этом экспериментальная оценка Locksmith показала, что он имеет проблемы с масштабируемостью. В [37] авторы представили расширение анализа алиасов Андерсена на случай многопоточных программ. Идея был похожа на подход с раздельным анализом потоков, так как вычислялось множество операторов, которое могло выполняться параллельно, а затем эти операторы применялись к другим потокам.

Также существуют различные подходы для поиска состояний гонки в специфичном программном обеспечении. Например, в низкоуровневом программном обеспечении со вложенными прерываниями [38], поиск гонок во FreeRTOS [39, 40]. А также состояний гонки специального вида - использования памяти после ее освобождения (англ. use-after-free) в драйверах устройств операционной системы Linux. Такие подходы показывают достаточно хорошие результаты, но значительно используют специфику исходного кода или проверяемого свойства. Представленный в данной статье теоретический подход претендует на то, чтобы быть более общим, однако это не отменяет того факта, что он может быть также улучшен при использовании дополнительной информации об исходном коде или проверяемом свойстве.

16. Заключение

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

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

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

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

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

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

Список литературы / References

[1]. Parosh Abdulla, Stavros Aronis, Bengt Jonsson, and Konstantinos Sagonas. Optimal dynamic partial order reduction. SIGPLAN Notices, vol. 49, issue 1, 2014, pp. 373-384.

[2]. Patrice Godefroid. Partial-Order Methods for the Verification of Concurrent Systems: An Approach to the State-Explosion Problem. Springer-Verlag, Berlin, Heidelberg, 1996, 143 p.

[3]. Gérard Basler, Michele Mazzucchi, Thomas Wahl, and Daniel Kroening. Symbolic counter abstraction for concurrent software. Lecture Notes in Computer Science, vol. 5643, 2009, pp. 64-78.

[4]. Dirk Beyer. Automatic verification of C and Java Programs: SV-COMP. Lecture Notes in Computer Science, vol. 11429, 2019, pp. 133-155.

[5]. Dirk Beyer, Thomas A. Henzinger, and Grégory Théoduloz. Configurable software verification: concretizing the convergence of model checking and program analysis. Lecture Notes in Computer Science, vol. 4590, 2007, pp. 504-518

[6]. D. Beyer, T.A. Henzinger, and G. Theoduloz. Program analysis with dynamic precision adjustment. In Proc. of the 23rd IEEE/ACM International Conference on Automated Software Engineering, 2008, pp. 29-38.

[7]. Cormac Flanagan and Shaz Qadeer. Thread-modular model checking. Lecture Notes in Computer Science, vol. 2648, 2003, pp. 213-224.

[8]. Thomas A. Henzinger, Ranjit Jhala, Rupak Majumdar, and Shaz Qadeer. Thread-Modular Abstraction Lecture Notes in Computer Science, vol. 2725, 2003, pp, 262-274.

[9]. Byron Cook, Daniel Kroening, and Natasha Sharygina. Verification of Boolean programs with unbounded thread creation. Theoretical Computer Science, vol. 388, issue 1-3, 2007, pp. 227-242.

[10]. Ashutosh Gupta, Corneliu Popeea, and Andrey Rybalchenko. Threader: A constraint-based verifier for multi-threaded programs. Lecture Notes in Computer Science, vol. 6806, 2011, pp. 412-417.

[11]. E.M. Clarke, O. Grumberg, S. Jha, Y. Lu, and H. Veith. Counterexample-guided abstraction refinement. Lecture Notes in Computer Science, vol. 1855, 2000, pp. 154-169.

[12]. Susanne Graf and Hassen Saidi. Construction of abstract state graphs with PVS. Lecture Notes in Computer Science, vol. 1254, 1997, pp. 72-83.

[13]. Stefan Savage, Michael Burrows, Greg Nelson, Patrick Sobalvarro, and Thomas Anderson. Eraser: A dynamic data race detector for multi-threaded programs. ACM SIGOPS Operating Systems Review, vol. 31, issue 5, 1997, pp. 27-37.

[14]. Richard Bornat. Proving pointer programs in Hoare logic. Lecture Notes in Computer Science, vol. 1837, 2000, pp. 102-126.

[15]. R M Burstall. Some techniques for proving correctness of programs which alter data structures. Machine Intelligence, vol. 7, 1972, pp. 23-50.

[16]. Pavel Andrianov, Karlheinz Friedberger, Mikhail Mandrykin, Vadim Mutilin, and Anton Volkov. CPA-BAM-BnB: Block-abstraction memoization and region-based memory models for predicate abstractions. Lecture Notes in Computer Science, vol. 10206, 2017, pp. 355-359.

[17]. Evgeny Novikov and Ilja Zakharov. Towards automated static verification of GNU C programs. Lecture Notes in Computer Science, vol. 10742, 2018, pp. 402-416.

[18]. Evgeny Novikov and Ilja Zakharov. Verification of operating system monolithic kernels without extensions. Lecture Notes in Computer Science, vol. 11247, 2018, pp. 230-248.

[19]. Dirk Beyer and Karlheinz Friedberger. In Proc. of the 11th Doctoral Workshop on Mathematical and Engineering Methods in Computer Science, 2016, pp. 61-71.

[20]. Dirk Beyer and Stefan Löwe. Explicit-state software model checking based on CEGAR and interpolation. Lecture Notes in Computer Science, vol. 7793, 2013, pp. 146-162.

[21]. D. Beyer, M.E. Keremoglu, and P. Wendler. Predicate abstraction with adjustable-block encoding. In Proc. of 10th International Conference on Formal Methods in Computer-Aided Design, 2010, pp. 189197.

[22]. Armin Biere, Alessandro Cimatti, Edmund M. Clarke, and Yunshan Zhu. Symbolic model checking without bdds. Lecture Notes in Computer Science, vol. 1579, 1999, pp. 193-207

[23]. Dirk Beyer, Stefan Löwe, and Philipp Wendler. Reliable benchmarking: requirements and solutions. International Journal on Software Tools for Technology Transfer, vol. 21, issue 1, 2019, pp. 1-29.

[24]. Shaz Qadeer and Jakob Rehof. Context-bounded model checking of concurrent software. Lecture Notes in Computer Science, vol. 3440, 2005, pp. 93-107.

[25]. Lucas Cordeiro, Jeremy Morse, Denis Nicole, and Bernd Fischer. Context-bounded model checking with esbmc 1.17. Lecture Notes in Computer Science, vol. 7214, 2012, pp. 534-537.

[26]. Ariel Cohen and Kedar S. Namjoshi. Local proofs for global safety properties. Formal Methods in System Design, vol. 34, issue 2, 2009, pp. 104-125.

[27]. Thomas A. Henzinger, Ranjit Jhala, and Rupak Majumdar. Race checking by context inference. In Proc. of the ACM SIGPLAN 2004 Conference on Programming Language Design and Implementation, 2004, pp. 1 -13.

[28]. Alexander Malkis, Andreas Podelski, and Andrey Rybalchenko. Thread-modular verification is cartesian abstract interpretation. Lecture Notes in Computer Science, vol. 4281, 2006, pp. 183-197.

[29]. Ashutosh Gupta, Corneliu Popeea, and Andrey Rybalchenko. Predicate abstraction and refinement for verifying multi-threaded programs. ACM SIGPLAN Notices, vol. 46, issue 1m 2011, pp. 331-344.

[30]. Akash Lal and Thomas Reps. Reducing concurrent analysis under a context bound tosequential analysis. Formal Methods in System Design, vol. 35, issue 1, 2009, pp. 73-97.

[31]. Salvatore La Torre, P. Madhusudan, and Gennaro Parlato. Reducing context-bounded concurrent reachability to sequential reachability. Lecture Notes in Computer Science, vol. 5643, 2009, pp. 477492.

[32]. Ermenegildo Tomasco, Omar Inverso, Bernd Fischer, Salvatore La Torre, and Gennaro Parlato. MU-CSeq: Sequentialization of C programs by shared memory unwindings. Lecture Notes in Computer Science, vol. 8413, 2014, pp. 402-404.

[33]. Pantazis Deligiannis, Alastair F. Donaldson, and Zvonimir Rakamaric. Fast and precise symbolic analysis of concurrency bugs in device drivers (t). In Proc. of the 2015 30th IEEE/ACM International Conference on Automated Software Engineering (ASE), 2015, pp. 166-177.

[34]. Akash Lal, Shaz Qadeer, and Shuvendu K. Lahiri. A solver for reachability modulo theories. Lecture Notes in Computer Science, vol. 7358, 2012, pp. 427-443.

[35]. Jan Wen Voung, Ranjit Jhala, and Sorin Lerner. RELAY: Static race detection on millions of lines of code. In Proceedings of the 6th Joint Meeting of the European Software Engineering Conference and the ACM SIGSOFT Symposium on The Foundations of Software Engineering, 2007, pp. 205-214.

[36]. Polyvios Pratikakis, Jeffrey S. Foster, and Michael Hicks. LOCKSMITH: Context-sensitive correlation analysis for race detection. In Proc. of the 27th ACM SIGPLAN Conference on Programming Language Design and Implementation, 2006, pp. 320-331.

[37]. Peng Di and Yulei Sui. Accelerating dynamic data race detection using static thread interference analysis. In Proc. of the 7th International Workshop on Programming Models and Applications for Multicores and Manycores, 2016, pp. 30-39.

[38]. Daniel Kroening, Lihao Liang, Tom Melham, Peter Schrammel, and Michael Tautschnig. Effective verification of low-level software with nested interrupts. In the Europe Conference & Exhibition on Design, Automation & Test, 2015, pp. 229-234.

[39]. Suvam Mukherjee, Arun Kumar, and Deepak D'Souza. Detecting all high-level dataraces in an RTOS kernel. Lecture Notes in Computer Science, vol. 10145, 2017, pp. 405-423.

[40]. Nikita Chopra, Rekha Pai, and Deepak D'Souza. Data races and static analysis for interrupt-driven kernels. Lecture Notes in Computer Science, vol. 11423, 2019, pp. 697-723.

[41]. Jia-Ju Bai, Julia Lawall, Qiu-Liang Chen, and Shi-Min Hu. Effective static analysis of concurrency use-after-free bugs in Linux device drivers. In the USENIX Annual Technical Conference, 2019, pp. 255-268.

[42]. Dirk Beyer, Marie-Christine Jakobs, Thomas Lemberger, and Heike Wehrheim. Reducer-based construction of conditional verifiers. In Proceedings of the 40th International Conference on Software Engineering, 2018, pp. 1182-1193.

Информация об авторах / Information about authors

Павел Сергеевич АНДРИАНОВ - младший научный сотрудник. Сфера научных интересов: статическая верификация, анализ многопоточных программ.

Pavel Sergeevich ANDRIANOV - junior researcher. Research interests: software model checking, analysis of multithreaded software.

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