Научная статья на тему 'Автоматическое доказательство корректности программ с динамической памятью'

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

CC BY
130
25
i Надоели баннеры? Вы всегда можете отключить рекламу.
Ключевые слова
ФОРМАЛЬНАЯ ВЕРИФИКАЦИЯ / АВТОМАТИЧЕСКАЯ ВЕРИФИКАЦИЯ / СИМВОЛЬНОЕ ИСПОЛНЕНИЕ / АНАЛИЗ ПРОГРАММ / ДИНАМИЧЕСКАЯ ПАМЯТЬ / КОМПОЗИЦИОНАЛЬНОСТЬ / ЧИСТЫЕ ФУНКЦИИ / FORMAL VERIFICATION / AUTOMATIC VERIFICATION / SYMBOLIC EXECUTION / STATIC ANALYSIS / DYNAMIC MEMORY / HEAP ANALYSIS / COMPOSITIONALITY / PURE FUNCTIONS

Аннотация научной статьи по компьютерным и информационным наукам, автор научной работы — Костюков Ю.О., Батоев К.А., Мордвинов Д.А., Костицын М.П., Мисонижник А.В.

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

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

Automatic verification of heap-manipulating programs

Theoretical foundations of compositional reasoning about heaps in imperative programming languages are investigated. We introduce a novel concept of compositional symbolic memory and its relevant properties. We utilize these formal foundations to build up a compositional algorithm that generates generalized heaps, terms of symbolic heap calculus, which characterize arbitrary cyclic code segments. All states inferred by this calculus precisely correspond to reachable states of the original program. We establish the correspondence between inference in this calculus and execution of pure second-order functional programs. The contribution of this work is as follows: (1) a formal model of compositional symbolic memory is proposed; (2) the properties of its correctness are formulated; (3) the calculus of symbolic heaps has been introduced: the conclusions in this calculus give all attainable states of the program; (4) the concept of generalized heaps is introduced, an algorithm for automatic modular construction of generalized heaps according to an imperative program is proposed; (5) an approach is proposed to reduce the problem of finding an output in calculus of symbolic heaps to the problem of proving the safety of functional programs.

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

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

Автоматическое доказательство корректности программ с динамической памятью

1Ю. О.Костюков, ORCID: 0000-0003-4607-039X <kostyukov.yurii@gmail.com> 1 К.А.Батоев, ORCID: 0000-0003-1124-7909 <konstantin.batoev@gmail.com> 1'2Д.А.Мордвинов, ORCID: 0000-0002-6437-3020 <dmitry.mordvinov@jetbrains.com> 1 М.П.Костицын, ORCID: 0000-0001-9982-6571 <mishakosticyn@yandex.ru> 1 А.В.Мисонижник, ORCID: 0000-0002-5907-0324 <misonijnik@gmail.com>

1 Санкт-Петербургский государственный университет, 199034, Россия, Санкт-Петербург, Университетская набережная, д. 7-9

2 JetBrains Research 197342, Россия, Санкт-Петербург, Кантемировская ул., 2

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

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

Для цитирования: Костюков Ю.О., Батоев К.А., Мордвинов Д.А., Костицын М.П., Мисонижник А.В. Автоматическое доказательство корректности программ с динамической памятью. Труды ИСП РАН, том 31, вып. 5, 2019 г., стр. 37-62. DOI: 10.15514/ISPRAS-2019-31(5)-3

Automatic verification of heap-manipulating programs

1 Yu.O. Kostyukov, ORCID: 0000-0003-4607-039X <kostyukov.yurii@gmail.com> 1 K.A. Batoev, ORCID: 0000-0003-1124-7909 <konstantin.batoev@gmail.com> uD.A. Mordvinov, ORCID: 0000-0002-6437-3020 <dmitry.mordvinov@jetbrains.com> 1 M.P. Kostitsyn, ORCID: 0000-0001-9982-6571 <mishakosticyn@yandex.ru> 1 A.V. Misonizhnik, ORCID: 0000-0002-5907-0324 <misonijnik@gmail.com>

1 Saint Petersburg State University, 3A bld. 1, Kantemirovskaya st., St Petersburg, Russia, 194100 2 JetBrains Research 2, Kantemirovskaya st., St Petersburg, Russia, 197342

Abstract. Theoretical foundations of compositional reasoning about heaps in imperative programming languages are investigated. We introduce a novel concept of compositional symbolic memory and its relevant properties. We utilize these formal foundations to build up a compositional algorithm that generates

37

generalized heaps, terms of symbolic heap calculus, which characterize arbitrary cyclic code segments. All states inferred by this calculus precisely correspond to reachable states of the original program. We establish the correspondence between inference in this calculus and execution of pure second-order functional programs. The contribution of this work is as follows: (1) a formal model of compositional symbolic memory is proposed; (2) the properties of its correctness are formulated; (3) the calculus of symbolic heaps has been introduced: the conclusions in this calculus give all attainable states of the program; (4) the concept of generalized heaps is introduced, an algorithm for automatic modular construction of generalized heaps according to an imperative program is proposed; (5) an approach is proposed to reduce the problem of finding an output in calculus of symbolic heaps to the problem of proving the safety of functional programs.

Keywords: formal verification; automatic verification; symbolic execution; static analysis; dynamic memory; heap analysis; compositionality; pure functions

For citation: Kostyukov Yu.O., Batoev K.A., Mordvinov D.A., Kostitsyn M.P., Misonizhnik A.V. Automatic verification of heap-manipulating programs. Trudy ISP RAN/Proc. ISP RAS, vol.31, issue 5, 2019, pp. 37-62 (in Russian). DOI: 10.15514/ISPRAS-2019-31(5)-3

1. Введение

Большая часть современного программного обеспечения написана на языках с динамически выделяемой памятью, таких как C++, Java, C#. Автоматическая верификация и анализ таких программ является очень трудоёмкой задачей [1]. При этом даже корректные с теоретической точки зрения методы могут оказаться неэффективными из-за больших размеров анализируемых программ [2]. Для решения этой проблемы были предложены композициональные техники, которые хорошо зарекомендовали себя на практике [3, 4, 5, 6]. Такие техники выполняют анализ функций в изоляции, т. е. вне контекста конкретного вызова, и в дальнейшем переиспользуют промежуточные результаты анализа. Таким образом, можно свести верификацию больших систем к задаче верификации набора небольших фрагментов кода.

Большинство существующих композициональных техник являются неточными в том смысле, что они аппроксимируют пространство состояний программы снизу или сверху. Аппроксимирующие снизу подходы рассматривают не все сценарии поведения программы, например, при помощи раскрутки циклов на конечное число шагов [7]. Это позволяет находить ошибки, но не доказывать корректность произвольных программ. Аппроксимирующие сверху подходы анализируют упрощённую версию программы, что на практике приводит к большому числу ложноположительных срабатываний [8]. Таким образом, остаётся актуальной задача точного анализа программ с помощью композициональных техник.

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

В данной статье предложен подход к модульной верификации программ с динамической памятью. Подход основывается на новой модели композициональной символьной памяти. Эта модель основывается на идее ленивого инстанцирования [9] и описывает состояние динамической памяти программы символьными выражениями над значениями входных ячеек памяти. Адреса этих ячеек могут быть символьно описаны с использованием содержимого других ячеек; это позволяет выражать эффект любой функции на произвольной рекурсивной структуре данных.

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

программы. Термы этого исчисления (т.н. обобщённые кучи), построенные по программе, в точности описывают эффект этой программы на произвольном состоянии. В статье также предложена автоматическая процедура композиционального построения обобщённых куч, описывающих произвольные фрагменты императивной программы. Поскольку для вывода в исчислении символьных куч не требуется хранения контекста, по любой обобщённой символьной куче можно построить эквивалентную ей функциональную программу, сведя задачу проверки корректности императивных программ с динамической памятью к задаче проверки корректности чистых функций. В статье предлагается сведение задачи анализа императивных программ к задаче вывода уточнённых типов (refinement types) [10] для функциональных программ, получаемых из обобщённых куч.

Доказательства сформулированных в работе свойств и теорем в основном опущены в виду ограничений на размер статьи. Читатель может ознакомиться с ними в [27]. Вклад данной работы заключается в следующем: (1) предложена формальная модель композициональной символьной памяти; (2) сформулированы свойства её корректности; (3) введено исчисление символьных куч: выводы в этом исчислении дают все достижимые состояния программы; (4) введено понятие обобщённых куч, предложен алгоритм автоматического модульного построения обобщённых куч по императивной программе; (5) предложен подход к сведению задачи поиска вывода в исчислении символьных куч к задаче доказательства безопасности функциональных программ.

2. Обзор

Существует большое количество работ, посвящённых автоматическому анализу и доказательству корректности программ с динамической памятью [1, 2, 5, 6]. Подходы делятся на аппроксимирующие снизу, аппроксимирующие сверху и точные. Аппроксимирующие снизу подходы исследуют только часть пространства состояний; они пригодны для поиска ошибок в программах, но не годятся для доказательства корректности программ. Подходы, аппроксимирующие пространство состояний сверху, пригодны для доказательства корректности программ, но на практике порождают большое количество ложноположительных срабатываний. Точные подходы исследуют все пространство состояний, но, насколько известно авторам, в данной статье представлен первый полностью автоматический подход к точному анализу императивных программ с динамической памятью.

К подходам, аппроксимирующим пространство состояний снизу, можно отнести динамическое символьное исполнение [11] и ограничиваемую проверку моделей [12]. Одна из основных идей в символьном исполнении программ с динамической памятью -ленивое инстанцирование [9]. Оно позволяет производить анализ программ с рекурсивными структурами данных без ручной спецификации размеров этих структур. Существует множество работ, развивающих эту идею [13, 14], но все они следуют идее классического символьного исполнения с раскруткой отношения перехода. Алгоритм, представленный в данной работе, не раскручивает отношение перехода программы, а строит систему ограничений в формализме композициональной символьной памяти, точно описывающих поведения программы.

Как правило, подходы, аппроксимирующие пространство состояний сверху, основаны на абстрактной интерпретации программ [2, 6, 8]. Существует множество подходов к абстрактной интерпретации программ с динамической памятью. Подавляющее большинство из них основаны на логике с разделением (separation logic) [15]; абстрактный домен таких анализаторов хорошо подходит для автоматического анализа формы куч [16], т.е. анализом того, как объекты в куче связаны друг с другом, а не их содержимого.

Анализаторы, которые пользуются логикой с разделением, являются, как правило, композициональными [2, 5, 6]. Логика с разделением адаптирована для символьного исполнения программ [17]. Основной проблемой логики с разделением является её невыразительность — она хорошо подходит для рассуждения о форме куч, но не подходит для анализа данных в динамической памяти, потому что введённая в этой логике разделяющая конъюнкция не позволяет выражать сложных свойств. Существуют подходы на основе логики с разделением, которые позволяют производить более точный анализ динамической памяти вплоть до побайтовых манипуляций с памятью [18]. Однако практическая применимость таких подходов сопряжена с большим количеством ложноположительных срабатываний.

Существуют также работы, основанные на идее сведения задачи верификации программ с динамической памятью к решению системы рекурсивно-логических ограничений [3, 20]. Такие подходы, как и наш, используют 8МТ-решатели [19] для решения ограничений и вывода индуктивных инвариантов системы. Логические решатели позволяют верифицировать программы, не порождая ложных срабатываний.

3. Демонстрационный язык

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

Program:: = Statement*

Statement:: = label: Statement

| Location ■ = Expression

| goto [Expression ^ label}+

1 fail 1 halt

Expression: = null 1 true 1 false | N

| Expression BinOp Expression

1 UnOp Expression

| Location

| new [ident = Expression}+

Location:: = ident | Location. ident

Рис. 1. Синтаксис демо-языка Fig. 1. Demo language syntax

Состояния программы на этом языке описываются состояниями динамической памяти. Язык не содержит функций, операций ветвления и циклов, однако включает оператор условного перехода по метке goto [Expression ^ label}+. Слева от стрелки находится условие перехода, справа — метка, на которую нужно перейти. Оператор последовательно вычисляет условия слева от стрелок и переходит по первой метке справа от стрелки, чьё условие истинно.

Идентификаторами ident в языке называются идентификаторы в стиле языка C; BinOp и UnOp это простые арифметические и булевы операторы.

Оператор new [ident = Expression}+ выделяет в памяти новый объект с именами полей ident, инициализируя каждый из них соответствующим Expression. Используя точку, как в определении Location, можно получить доступ к полям объектов. Доступ может быть вложенный, например list. RootNode. Next. Key.

В каждой переменной находятся либо примитивные значения (null, true, 42), либо ссылки на объекты в динамической памяти. Оператор := перезаписывает ссылку. Например, в листинге 1 в строке 3 в переменную р помещается ссылка на I, сам объект не копируется. Затем, в строке 7 переменная р начинает ссылаться на новый объект, а переменная I по-40

прежнему указывает на старый. В строке 10 меняется само содержимое памяти, независимо от переменных р и I.

I RemoveAll:

^ l := new {Key = x, Next = l}

p := p.Next N goto {true -> RemoveAllIterate} RemoveAllRemoveElement: p.Next := p.Next.Next I goto {true -> RemoveAllIterate}

RemoveAllFinalize: 13. 1 := 1. Next

15. p : = 1

Contains: I" goto {p = null -> Exit, Is p.Key = x -> Error}

p := p.Next

goto {true -> Contains}

21.

Error: fail 23. Exit: halt

Листинг 1. Программа, модифицирующая динамическую память Listing 1. Example of heap-manipulating program.

Далее мы будем рассматривать только корректно типизированные программы: арифметические операции применяются согласно их стандартной семантике; поле и то, что в него записывается, согласованы по типам; выражения в goto имеют булевский тип; поля всегда читаются успешно (кроме чтения из null); выражения и имена полей, используемые в операторе new, согласованы по типам. Также мы предполагаем, что программы не читают из неинициализированных локаций, всякая программа содержит инструкцию halt, все метки, на которые есть переходы, определены и имена идентификаторов, меток и ключевые слова языка не пересекаются. Предложенный язык не даёт возможности управлять памятью на низком уровне, в частности, освобождать выделенную память. Таким образом, программы на этом языке допускают всего два вида ошибок, которые необходимо находить: доступ к полю по ссылке, содержащей null, и достижимость инструкции fail.

4. Композициональная символьная память

В центре нашего подхода стоит формализм композициональных символьных куч. Концепция композициональной символьной памяти (КСП), определяемая в этом разделе, основана на идее ленивого инстанцирования [9, 13]. Ленивое инстанцирование — это техника, позволяющая строить конечные символьные выражения для рекурсивных структур данных, таких как связные списки и деревья. При этом предлагается инициализировать поля рекурсивных структур данных по требованию вместо инициализации всей структуры одномоментно, что потребовало бы заранее заданных ограничений на её размер. Это, например, позволяет анализировать списки и деревья, не зная заранее их размер.

4.

5.

6.

p := l

RemoveAllIterate:

goto {p.Next = null -> RemoveAllFinalize,

p.Next.Key = x -> RemoveAllRemoveElement}

1. goto {true > F} c

2. G:

3. x := x + 1 G (стр. 2-4): {x » Ll(x) + 1}

4. goto { true -> Exit} F (стр. 5-7): {x » 42} о {x » LI(x) + 1} =

5. F: = {x»42 + 1}

б. x : = 42

5. goto { true -> G}

S. Exit halt

Рис. 2. Пример композиции состояний Fig. 2. Heap composition example.

Основная идея КСП состоит в том, чтобы трактовать лениво инициализированные локации LI(x)1 как ячейки, в которые будут подставлены значения из контекстного состояния. Например, состояние, описывающее код после метки G на рис. 2 (стр. 2-4), содержит незаполненную ячейку x. Это означает, что оно описывает эффект этого фрагмента кода на произвольном контекстном состоянии, т.е. состоянии с произвольным значением x. Таким контекстным состоянием может быть, например, состояние после метки F до перехода на G (стр. 5-6). Чтобы получить полное состояние после метки F (стр. 5-7), необходимо заранее подсчитанный эффект G применить к текущему состоянию F. Для этого нужно заполнить ячейки состояния G значениями из текущего контекста F. Кучу, полученную в результате этого процесса, мы называем композицией куч. Заметим, что адаптация этой идеи к программам произвольной сложности требует некоторых дополнительных усилий.

Далее будет описан формализм композициональной символьной памяти. Доказательства теорем приведены в [27].

4.1 Символьные выражения

Чтобы представлять произвольные состояния программ на нашем демо-языке, необходимо ввести понятие символьных выражений. Определение символьного выражения представлено на рис. 3.

Символьный терм (term) — это либо арифметическое выражение (arith), либо символьный адрес локации в памяти (loe).

term: : = arith | loc

arith: : = N | arith ± arith | - arith | LIarith(loc)

1 unionarith((guard, arith)*)

loe: : = null | 0x[0 - 9]+ | ident

| loc.FieldName| LIloc(loc)

| unionloc((guard, loc)*)

guard ■ ■= T |±|—guard | guard A guard |guard V guard

| arith = arith | arith < arith | loc = loc

Рис. 3. Грамматика символьных выражений Fig. 3. Symbolic terms.

1 LI — lazy instantiation 42

Символьные значения записываются как LI*(x), что означает «ленивое инстанцирование х». Далее всюду тип символьного значения либо не важен, либо очевиден из контекста, потому мы будем его опускать и писать просто LI(x). Заметим, что локация-источник символьного значения LI может быть также символьной, так что, например, термы вида LI(LI(list). Key) + 1 также допустимы.

а

0x1. Key 0x1. Next LI (b). Next

vC

» 0x1 » 15 » 0x1 ^ 0x1

» LI (LI(d). Next)

Рис. 4. Пример различных типов локаций Fig. 4. Different location types example.

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

Локации могут быть конкретными, т.е. известными в текущем контексте (0х[0 — 9]+, порождаются оператором new); именованными (ident, глобальные переменные, а, Ъ, с); ссылками на конкретное поле; ленивыми инстанцированиями других локаций. Например, на рис. 4, где представлен фрагмент кода и соответствующее ему символьное состояние памяти, локация Ъ неизвестна, поэтому запись в её поле Next порождает запись по символьной локации LI(b). Next. Аналогично неизвестна локация d, но также неизвестен и элемент, на который ссылается её поле Next, из-за чего символьная локация с указывает на символьный терм LI(LI(d). Next).

Вернёмся к определению символьных выражений (рис. 3). На этом рисунке guard обозначает ограничение, представленное в виде логической формулы (условие пути или условие перехода по метке). Такие формулы «защищают» элементы символьных объединений. Символьное объединение2 (union) — это обобщение символа ite(cond,x,y). Как и в случае символьных значений, мы опускаем тип символьных объединений и пишем просто union(*). За символьным объединением мы закрепляем следующую семантику: х = union((g1,v1),... ,(gn,vn)) тогда и только тогда, когда (g1 А х = v1)V ... V (gn Ах = vn). Символьные объединения позволяют при помощи одного символьного состояния описать несколько веток исполнения программы. Замечание. Потребуем, чтобы ограничения в объединениях не пересекались, т. е. только одно ограничение должно выполняться при подстановке конкретных значений вместо символьных. Однако несколько ограничений могут выполняться одновременно в том случае, если они «защищают» одно и то же значение. Например, допустимы объединения вида

union((LI(x) = LI (у), LI (LI (у). Key) + 7), (LI(x) = LI (z), LI (LI(z). Key) + 7)). Мы рассматриваем содержимое объединений как множество пар, потому пишем, например, union([(g, v)} U X). Чтобы избежать перегрузки синтаксиса лишними скобками, мы опускаем их далее при записи одноэлементных множеств. Так, пример выше можно записать следующим образом: union((g,v) U X). Также мы опускаем круглые скобки там, где не возникает двусмысленности, например, пишем union(x > 5,42) или union[(gi,Vi)l1 < i <п}

Выражение на рис. 3 — это либо терм, либо ограничение. Примитивные выражения — это натуральные числа, именованные локации, конкретные адреса в памяти, null, Т и 1. Операциями являются сложение, вычитание, унарный минус, сравнения, логические

2 Понятие символьных объединений заимствовано из [21]

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

Замечание. Равенство символьных термов является семантическим, например, 2 * (х + 1) = х + х + 4- 2 и union({x + 5 = у + 4,7), <1,42)) = union{x + 1 = у, 7). Далее мы перечисляем некоторые очевидные свойства символьного объединения. Утверждение 1.

(a) union(J,v) = v

(b) union({1,v) U X) = union(X)

union((g, union((gi, Vi),..., (gn, vn))) U X) =

= union([{g Agi,Vi),...,{g A gn, vn)} U X)

В частности,

• union((g,union(<s)) UX) = union(X)

• union(giV ...V gn,union((gi,Vi),...,(gn,vn))) = = union«gi,Vi),...,{gn,vn))

(c) union({{gi,v),...,{gn,v)]uX) = union({giV ...V gn,v) U X) В частности,

union([{g, v), {g A gi, v),...,{g A gn, v)} U X) = union({g, v) U X)

(d) op(union({glei).....{g^e^)).....union({g?,e?).....{g™m,e™m))) =

= union([{gii A ...Ag^opie!.....e™J)l1 < ij < n,})

В частности, для непересекающихся ограничений д1, ...,дп

op(union((g1, el),..., (дп, е^)),..., ип1оп({дг, е?),..., (дп, е™))) = = union((gi,op(el..., е?)),..., (дп, ор(е^,..., е™)))

4.2 Символьные кучи

Определение 1. Символьная куча — это частичная функция a: loe ^ term, удовлетворяющая следующему требованию (инвариант кучи): Чх,у Е dom(a),union(x = у,а(х)) = union(x = у,а(у)). (1)

Заметим, что это более сильное ограничение, чем накладывает само понятие функции (х = у ^ &(х) = а(у)). Это связано с тем, что равенство термов и локаций в нашем подходе является не синтаксическим, а семантическим. Так, например, функция [LI(x). Key ^ 10; LI (у). Key ^ 15} не является символьной кучей, т.к. при подстановке вместо х и у, например, 0x1, получится, что этот адрес указывает на два разных значения. Напротив, следующая функция является символьной кучей: [LI(x).Key^ union((LI(x) = LI(y),15),(LI(x) Ф LI(y),10));LI(y).Key ^ 15}.

Определение 2. Пустая куча е — это частичная функция с областью определения dom(e) = 0 (она, очевидно, удовлетворяет (1)).

Определение 3. Пусть х Е dom(a) или х — символьная локация. Тогда определим чтение локации х в символьной куче а следующим образом:

read(a,x) = union([(x = 1,а(1))Ц Е dom(a)}U ( А хФ1,И(х))). (2)

lEdom(G)

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

Очевидно, что при х 6 dom(a) выполнено read (а, х) = а(х). Одно из ограничений х = I будет выполняться, тогда как один из элементов конъюнкции Al6dom(„) х Ф I будет, наоборот, невыполним, следовательно read(a,x) не сможет вернуть значение LI(x). Сделаем следующее наблюдение: фактически, из опр. 3 следует, что множество ограничений в формуле (2) может содержать пересечения, т.е. в куче могут содержаться две (или более) символьные локации, которые совпадают при некоторых конкретных подстановках. Инвариант символьной кучи (1) позволяет обойти возможную проблему с совпадающими адресами: благодаря ему при совпадении ограничений не будет конфликтов между «защищаемыми» значениями. Пример 1. Пусть а = [0x1. А » 42; LI(x). В » union((LI(x).B = 0x1.A,42),(LI(x).B Ф 0x1. А,7))}. Тогда read(a,0x1.A) = union((0x1.A = 0x1. А, 42), (0x1. А = LI(x).B,union(...)),

(0x1. А Ф 0x1.AALI(x).B Ф 0x1. A, LI(0x1. А))) = = union((T,42),(0x1.A = LI(x).B,union(...)),(1,LI(0x1.A))) = 42 read(a,LI(y).B) =

= union((LI(y).B = 0x1.A,42),(LI(y).B = LI(x).B,

union((LI(x).B = 0x1.A,42),(LI(x).B Ф 0x1.A,7))),

утв. 1

(LI (у). В Ф 0x1.AALI(y).B Ф LI(x).B,LI(LI(y).B))) = = union((LI(y). В = 0x1. A, 42), (LI(у). В = LI(x). В A LI(x). В Ф 0x1. A, 7), (LI(y).B Ф 0x1.AALI(y).B Ф LI(x).B,LI(LI(y).B)))

4.3 Композиция символьных куч

Определение 4. Уточнение выражения е в контексте символьной кучи а обозначим а • е и определим следующим образом.

1. Если е — это примитивное значение, то а • е = е.

2. а • ор(еи ..., еп) ор(а • еи еп).

3. а• union[(g1, t^,..., (gn, tn)} union[(a • g^a • t^,... ,(a • gn,a • tn)}.

4. a • LI (I) = read (a, a • I).

Интуитивно, a • e — это выражение, получаемое подстановками значений из а в символьные ячейки е: первые три пункта определения сохраняют структуру е, а п.4 заполняет ячейку значением из а.

Определение 5. Композиция символьных куч а и а' — это частичная функция а ° a': loc » term, определяемая так: (а ° о')(х) =

= union([(x = а • 1,а • (а'(1)))11 6 dom(a')}U ( A х Ф а • 1,а(х))).

l6dom(G')

Композиция а ° а' определена на всех локациях, удовлетворяющих ограничению х = а • I, где I 6 dom(a'), и на всех локациях dom(a) (здесь и далее запись [а • ala 6 А} сокращается как а • А). Из этого следует, что:

dom(a ° а') = dom(a) Ua • dom(a'). (3) Композиция символьных куч отражает последовательную композицию в программировании: если а1 — это эффект фрагмента кода A и а2 — эффект фрагмента кода B, тогда а1° а2 — это эффект A;B. Интуитивно, а1° а2 — это символьная куча, полученная заполнением символьных ячеек из а2 значениями из контекста а1 с последующей их записью в контекст а1.

Пример 2. Пусть а = [х » 42; у » 7} и а' = [у » LI(x) — LI (у)}. Тогда а ° а' = [х » 42; у »42 — 7}.

Теорема 1. Если а и a' — символьные кучи, то а ° а' также символьная куча.

Теорема 2. Для произвольной символьной кучи а, локации х и выражения е справедливо

следующее:

(a) read(e,x) = LI(x)

(b) £ • e = e

(c) £ ° a = a

(d) a ° £ = a

Стоит отметить, что имеется некоторое сходство между чтением (опр. 3) и композицией (опр. 5): объединения осуществляют поиск х среди локаций кучи. Если поиск был успешен, возвращается соответствующее (возможно изменённое) значение, в ином случае — значение по умолчанию. Воспользуемся этим сходством для определения оператора find, который далее используется для трансляции обобщённых куч в чистые функции.

Определение 6. find (а, х, т, d) =

= union([(x = т • 1,т • (a(l)))ll 6 dom(a)}U ( A хФт^1^))

l6dom(G)

Теор. 2 позволяет компактно выразить read и композицию куч через find: read (а, х) = find(a,x, £, LI (х)) (4) (а ° о')(х) = find(a',x, а, а(х)) (5) Теорема 3. Для всех символьных куч а, а' и символьных локаций х справедливо следующее:

о • read (а', х) = read (а °а',а • х).

1. goto {true -> F}

2. G: ос = {x» (Ы (Ll(a).Key) + 5)}

з. x : = a.Key + 5 read(aG,x) = aG(x) = Ll(Ll(a). Key) + 5

4. goto { true -> Exit} aF • read (aG, x) = 10 + 5

5. F:

6. a : = new {Key = 10} aF °aG = {a » 0x1; 0x1. Key » 10; x » (10 + 5)}

7. goto { true -> G} read(aF ° aG,aF • x) = read(aF ° aG,x) = 10 + 5

8. Exit

9. r : = x

10. halt

Рис. 5. Чтение из композиции состояний Fig. 5. Read from composition example

Теорема 3 говорит о корректности чтения из композиции состояний. Например, имея состояния ор и ас исполняемых друг за другом фрагментов кода (как на рис. 5) и читая после фрагмента б переменную х, можно прочитать переменную из позднего состояния б, а затем заполнить результат из контекста Р. Однако возможно также прежде воспроизвести эффект б поверх эффекта Р (ар ° ас), и прочитать х из уточнённого состояния. Теорема 3 утверждает, что результаты двух таких операций совпадут. Теорема 4. Для всех символьных куч а, а' и символьного выражения е справедливо следующее: (а ° а') • е = а • (а' • е).

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

Теорема 5. Для всех символьных куч оъ а2 и а3 справедливо следующее:

(di о а2) °а3 = (а2 ° а3). Теорема 6. Пусть Е — множество всех символьных куч. Тогда (Е,о) — моноид. Доказательство. Из теоремы 1 следует замкнутость множества £ относительно операции о, по теореме 2 е — нейтральный элемент, и по теореме 5 операция о ассоциативна.

4.4 Объединение символьных куч

До сих пор мы рассматривали символьные состояния только для линейных фрагментов кода, в которых все переходы по меткам были безусловными (goto [true ^ •••}). Для более сложных состояний необходим новый оператор, позволяющий объединить несколько состояний в одно.

Определение 7. Объединением а = merge((g1, а1),..., (дп, ап)) символьных куч а1,..., ап по непересекающимся ограничениям д1,.,дп будем называть частичную функцию с dom(a) = ^1П=1 dom(ai), для которой выполняется следующее: (merge{gi,ai))(x) = union{gi,read(ai,x)).

Теорема 7. Для любой символьной кучи а и произвольных символьных локаций х, у справедливо следующее:

union({x = y,read(a,x))) = union({x = y,read(a,y))). Далее покажем, что оператор merge обладает интуитивными свойствами. Теорема 8. Для любых символьных куч а1, . , ап и произвольных непересекающихся ограничений g1,..., gn, справедливо утверждение о том, что merge{gi, ai) — символьная куча. Теорема 9. Для любых символьных куч а1,.,ап, произвольных непересекающихся ограничений g1,..., gri и для любых локаций х справедливо следующее: г е ad(mer ge{gi,ai),x) = union{gi,read(ai,x)).

Рис. 6. Композиция объединения куч Fig. 6. Composition of heap merge Как уже было показано, результат вычисления символьного состояния не зависит от порядка применения эффектов. Необходимо показать, что это свойство КСП выполняется и для нового оператора объединения состояний. Для этого нужно рассмотреть два случая расстановки объединения и композиции, представленные на рис. 6. Теорема 10. Для любых символьных куч o,oi, . ,ап и произвольных непересекающихся ограничений д1,..., дп выполняется следующее утверждение:

а о merge((gu Oi),..., (дп, ап)) = merge ((а • д^а о а^,..., (а • дп,а о а^). Интересно, что симметричный случай гораздо сложнее. Например, рассмотрим а1 = {х » LI(a)}, а2={х » LI(b)}, а = {LI(x).Key » 42}. Тогда: dom(merge((g, Oi), (—д, 02)) о а) =

= dom(merge((g, Oi), (—д, 02))) U merge((g, Oi), (—д, 02)) • dom(a) = = {х, union((g, LI(a). Key), (—g, LI(b). Key))}, что не то же самое, что

dom(merge((g, ai о о), (—g, а2 о а))) = dom(ai oa)U dom(a2 оа) =

= {LI(a).Key,LI(b).Key,x}.

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

LI(union((g1,x1),...,(gn,xn))) = union((g1,LI(x1)), ...,{gn,LI(xn))). (6)

Теорема 11. Для любой символьной кучи а и произвольных непересекающихся

ограничений д1,.,дп , а также любых локаций х1,.,хп справедливо следующее

утверждение:

read(a,union{gi,xi)) = union{gi,read(a,xi)). Теперь необходимо сформулировать вспомогательное утверждение, которое позволит доказать симметричную теорему о композиции с объединением: тег де{дьа^) • е = union(gi,ai • е). Однако оно не всегда верно: может случиться так, что g1 V ..V дп Ф Т, что, в свою очередь, может привести к попытке уточнить терм в несуществующей куче. Например, можно ожидать, что уточнение терма 42 в любой куче даст 42, однако в рамках наших определений мы получим union((g1,42),... ,{д1,42)) = union(g1 V ... V дп,42) Ф 42. Чтобы указанное выше утверждение выполнялось, необходимо ограничить результат условием существования вычисления g1V .V дп.

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

union{g1V .V gn,merge{gi,ai) • е) = union{gi,ai • е). Таким образом, нельзя приравнять merge({д1,а1),...,{дп,оп)) ° а и тегде({д1,а1о о),... ,{дп,оп° о)) как теоретико-множественные объекты. Однако следующая теорема показывает, что определение операции чтения позволяет избежать этой проблемы: в теоретико-множественном смысле кучи могут быть не равны как отображения различных множеств ключей, однако с точки зрения операции чтения они будут совпадать. Теорема 13. Для любых символьных куч а,^, ...,оп, произвольных непересекающихся ограничений д1,..., дп и произвольной локации х справедливо следующее:

union{g1V ...V gn,read(merge{gi,<Ji) °а,х)) = read{merge{gi,ai °а),х).

1. Abs:

2. goto {х >= 0 -> Exit} v x := -x

4. Exit: halt Листинг 2. Объединение состояний Listing 2. Merge states

При помощи операции объединения возможно описать состояние программы на лист. 2 следующим образом: а- = {х » —LI(x)}

а = merge({LI(x) > 0,e),{-(LI(x) > 0),а-))

Теор.11

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

read(a,x) = union({LI(x) > 0,read(e,x)),{—(Ll(x) > 0),read(a-,x))) = = union({LI(x) > 0,LI(x)), {—(LI(x) > 0), -Ll(x))).

4.5 Запись в символьную кучу

До сих пор были рассмотрены операции с кучами как с готовыми объектами. Для того, чтобы строить кучу из пустого состояния е, необходима операция записи в символьную память. Для её определения воспользуемся следующим сокращением: ite(c,a,b) = union({c, а), {—с, b)).

Определение 8. Запись символьного значения v в символьную локацию у символьной кучи а — это символьная куча write(a,y,v), такая что для всех x Е dom(write(a,y,■)) = dom(a) U [yy}, (write(a,y,v))(x) = ite(x = y,v,a(x)).

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

Теорема 14. Для любой символьной кучи а, произвольных символьных локаций x, y и любого символьного выражения v справедливо следующее:

re ad(wr i te(o,y,v),x) = ite(x = y^^ead^^)). Теорема 15. Для любых символьных куч а, а', произвольной символьной локации y и произвольного символьного выражения v справедливо следующее: а о write^', y, v) = write (а о а',а • y^ • v). Теорема 16. Для любых символьных куч а1,.,ап, любых непересекающихся ограничений g1,..., g^ и произвольной символьной локации y и символьного выражения v справедливо следующее:

write(merge{gi,аi),y,v) = merge{gi,write(аi,y,v)).

5. Исчисление символьных куч

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

5.1 Обобщённые символьные кучи

Для начала определим формальный язык Heap нашего исчисления. Термы языка Heap будем называть обобщёнными кучами. Напомним, что £ — это множество всех символьных куч, которые были определены в предыдущем разделе. Обобщённая куча может быть либо обычной символьной кучей (из £), которую мы будем называть определённой, либо объединением обобщённых куч по непересекающимся ограничениям, либо композицией обобщённых куч, либо записью в обобщённую кучу, либо неподвижной точкой цикла в графе потока управления, которую мы будем называть рекурсивным состоянием (рис. 7).

Heap:: = £

| Heap ° Heap I merge((guard.Heap)*) I write(Heap,loc,term) | Rec(id)

Рис. 7. Обобщённые кучи Fig. 7. Generalized heaps

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

образом описывает цикл в графе потока управления: он состоит из метки из исходного кода и некоторого.

Чтобы адаптировать операции КСП, необходимо расширить синтаксис символьных выражений, как показано на рис. 8.

term:: = arith | loc

arith:: = N | arith ± arith | - arith | LIarith(Heap, loc)

1 unionarith((guard, arith)*)

loc:: = null | 0x[0 - 9]+ | ident

| Ioc.FieldName | LIloc (Heap, loc)

| unionloc((guard, loc)*)

guard: := T | 1 | — guard | guard A guard | guard V guard

| arith = arith | arith < arith | loc = loc

Рис. 8. Грамматика обобщённых символьных выражений Fig. 8. Symbolic generalized terms

На чтение из определённой кучи геad^,x) = LI(x) можно смотреть как на следующее сообщение: «в а недостаточно информации, чтобы узнать x, — требуется дополнительный контекст». Сама операция чтения построена таким образом, что мы можем однозначно сказать, было ли успешно чтение x из а, — и если было, то предъявить результат. При чтении из обобщённой кучи, например, read(Rec(F), x), могут возникнуть сложности. Если циклический фрагмент кода по метке F меняет содержимое x, то мы не можем его корректно прочитать — для этого необходимо заранее знать, сколько раз исполнится F, что в общем случае невозможно. Очевидно также, что мы не можем отбросить Rec(F) и вернуть просто LI(x).

Таким образом, основная идея расширения термов состоит в «запоминании» источника каждого символьного значения (выделено жирным на рис. 8). Такое расширение является корректным, поскольку старые символьные значения LI(x) теперь станут Ll(e,x). Уточнение также может быть расширено: т • L 1(а, x) = read(т о а,т • x). Это не нарушит свойства КСП, так как по теор. 3, мы имеем т • re ad (а, x) = read(j оа,т • x).

5.2 Правила редукции

Правила редукции символьных куч представлены на рис. 9. Буквами H обозначены элементы языка Heap, буквами t — символьные термы. H[A/X] означает одновременную подстановку A вместо X в обобщённую кучу H; здесь A и X могут быть как обобщёнными кучами, так и символьными термами.

Корректность всех правил, кроме (8), обоснована в разд. 4. Правила (7) и (8) представляют собой аналог «-конверсии и ^-редукции в Я-исчислении. Body(x) представляет собой описание поведения участка кода, которому соответствует обобщённая куча Rec(x). Если посмотреть на исчисление символьных куч как на некоторый язык программирования, то Rec(x) соответствовало бы имени функции, а Body(x) — её телу. Обобщённую кучу назовём нередуцируемой, если к ней нельзя применить ни одного правила за исключением (7). H —п H' означает, что куча H редуцируется в H' за n шагов. H —* H' означает, что существует n > 0, что H —>п H'.

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

состояний. Однако можно построить обобщённую кучу Body(Inc), описывающую поведение метки I п с.

—-(7)

H » H[t2/tl] ( )

H » H[Body(id)/Rec(id)] (8)

eoH »H

H °e»H

Hio(H2oH3)»(HioH2)oH3

H o merge(gi, Hi) » merge(H • gi,HoHi)

merge(gi,Hi) oH » merge(gi,Hi o H)

H o write(H', x, v) » write (H o H', H • x, H • v)

write (merge (gi, H i),x,v) » merge(gi,write(Hi,x,v)) g невыполнимо

merge((g,H) U X) » merge(X)

g общезначимо

merge(g,H) » H

Рис. 9. Правила редукции Fig. 9. Reduction rules

1. Inc:

2 goto {p = null -> Exit} .1 p.Key := p.Key + 1 4 p := p.Next ^ goto {true -> Inc} C| Exit: halt Листинг 3. Фрагмент кода с циклом в графе потока управления Listing 3. Code snippet with a cycle in a control flow graph

Пусть a0 — некоторая символьная куча, представляющая начальное состояние исполнения, а обобщённая куча Rec(lnc) соответствует метке Inc. Тогда поведение всего кода в лист. 3 на состоянии а0 будет описываться обобщённой кучей а0 o Rec(Inc). Применение правил редукции к a0o Rec(Inc) будет соответствовать вычислению кода с метки Inc на состоянии а0. Покажем это на примере.

Пусть а0 = {р » 0x1,0x1. Key » 10,0x1. Next » 0x2,0x2. Key » 20, 0x2. Next » null}.

Опишем теперь поведение циклического региона, помеченного In c. В начале исполнения происходит ветвление по условию р = null. Если р Ф null, то стр. 3-5 увеличивают значение ключа в узле связного списка и переходят к следующему элементу. Поведение этого участка кода можно описать символьным объединением двух эффектов: пустого эффекта £ (т.к. переход на стр. 2 не меняет состояния) и эффекта а нерекурсивного кода на стр. 3-4, где а = [LI(p).Key » LI (LI (р). Key) + 1,р » LI (LI(р). Next)}. Таким образом, поведение региона n описывается обобщённой кучей

Body(Inc) = merge^LI^) = null, e), (LI(р) Ф null, a o Rec(Inc))). Теперь опишем процесс редукции кучи а0 o Rec(Inc). а0 o Rec(Inc) » <J0 ° Body(Inc) »

merge((u0 • (L1(р) = null),a0 ° e),(u0 • (L1(р) Ф null),a0 ° (a ° Rec(Inc)))) »4 merge((0x1 = null, o0), (0x1 Ф null, (u0 ° a) ° Rec(Inc))) »2 o1 ° Rec(Inc) »2

merge((a1 • (LI(p) = null),a1 ° e),(a1 • (LI(p) Ф null),a1 ° (о ° Rec(Inc)))) —4 merge((0x2 = null, о), (0x2 Ф null, (о1 ° о) ° Rec(Inc))) —2 о2 ° Rec(Inc) —>2 merge((a2 • (LI(P) = null),a2 ° e),(a2 • (LI(p) Ф null),a2 ° (о ° Rec(Inc)))) —4 merge((null = null, о2), (null Ф null,(a2 ° о) ° Rec(Inc))) —2 о2 Здесь

Oi^ о0°о = [p ^ 0x2,0x1. Key ^ 11,0x1. Next ^ 0x2,

0x2. Key ^ 20,0x2. Next ^ null}, о2 = о = [p ^ null, 0x1. Key ^ 11,0x1. Next ^ 0x2,

0x2. Key ^ 21,0x2. Next ^ null}. Обобщённая куча о2 не редуцируема (заметим, что нередуцируемым будет любой элемент £). о0 и о1 представляют собой состояния изначальной императивной программы в процессе её исполнения, а о2 — её конечное состояние (при запуске на о0). Исчисление символьных куч позволяет описывать произвольные поведения программ с динамической памятью без потери информации.

6. Композициональное символьное исполнение

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

6.1 Метод описания путей в графе потока управления

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

Прямолинейным способом построения обобщённых куч по императивной программе является введение куч Rec(l) для каждой инструкции I и взятие композиции со всеми Rec(l'), в которые есть переход из инструкции I. Однако такой подход порождает слишком большую систему взаимно-рекурсивных определений: фактически, количество символов-абстракций Rec(l) было бы равно количеству инструкций в программе. В данном разделе описывается метод описания всех возможных путей исполнения программы через введение меньшего числа символов-абстракций.

Определение 9. Вершинами VG графа потока управления G будут номера I инструкций, а рёбрами eg — пары номеров (1Х, 1у), указывающие на возможность передачи управления от инструкции 1Х к инструкции 1у. В графе потока управления существует начальная вершина, которая соответствует первой инструкции программы и в которую не ведёт ни одно ребро. Согласно грамматике демо-языка (см. рис. 1), большинство рёбер будут иметь вид ( l,suсс(I)). Однако благодаря оператору goto возможны переходы к произвольным инструкциям, кроме начальной.

Определение 10. Проведём обход графа G в глубину и для каждой вершины v вычислим время «выхода» time(v) из обхода. Вершина I называется рекурсивной, если существует ребро (l',l) такое, что time(I') > time(I). Множество рекурсивных вершин будем обозначать R V.

Для описания путей необходимо ввести операцию конкатенации двух путей в графе. Неформально, конкатенация путей p1 и p2 — это путь p1 °p2, который содержит рёбра пути p1, за которыми следуют ребра пути p2. Конкатенация двух множеств путей P-t и Р2

определяется через конкатенацию двух путей: Р1°Р2 = [Pi 0P2\Pi е Pi'P2 е Р2}- Символ £ означает пустой путь, а символ U — объединение множеств путей. Предлагаемый метод позволяет описывать в точности все пути в произвольном графе потоке управления при помощи рекурсивных символов и их рекурсивных описаний, на базе которых далее будут строиться обобщённые символьные кучи.

n(u,v,D)= ^ [(u,v)}U ^ (u,t) on(t,v,D)U

(u,v)EEc t$:RVU[v}

(и, t)EEc

(u,t)o Ree(t,Du[t})on(t,v,DU {t})

и

tefl^\(ou{v}) (u,t)EEc

Rec(u,D) = {e} u n(u,u,D) о Rec(u.D) Символом n(u,v,D) обозначим множество путей из вершины u в вершину v, параметризованное множеством пройденных рекурсивных вершин D. Множество D ограничивает переходы по рёбрам: ребро (1Х, 1у) является «допустимым», если оно ведёт в конечную вершину (т.е. 1у = v) или 1у Ф v и вершина 1у не была ещё посещена (т.е. 1у £ D). Рекурсивным символом Rec(u,D) обозначим множество путей-циклов из вершины u в вершину u с множеством D, имеющим тот же смысл, как и для n(u,v,D). Интуитивно, n(u,v,D) соответствует символьным кучам, полученным в рсиульчачс символьного исполнения программы от инструкции с номером u до инструкции с номером v, когда исполнение не посещало инструкции с номерами из множества D\{v}. В свою очередь, рекурсивный символ Rec(u,D) соответствует обобщённой символьной куче Rec(id), у которой уникальным идентификатором id является пара (u,D), а его описание — это обобщённая символьная куча Body(u,D). Кроме того, оператор о соответствует операции композиции состояний, символ u — операции объединения состояний (merge), а £ — пустой куче е.

Рис. 10. Пример графа потока управления с вложенными циклами Fig. 10. An example of a control flow graph with nested loops Покажем на примере, как можно описать все пути в графе потока управления при помощи итеративного построения П и Ree. На рис. 10 представлен пример графа потока управления программы с вложенными циклами. Для простоты изложения номер вершины v равен времени time(v). На рис. 10 вершины 1 и 2 являются рекурсивными. Ниже представлено описание всех путей в графе из 0 в 5, использующее рекурсивные символы. П(0,5,0) = (0,1) ° Rec(1, {1}) о (1,2) ° Rec(2, {1,2}) ° (2,3) ° (3,4) ° (4,5) Rec(1, {1}) = (1,2) ° Rec(2, {1,2}) ° (2,3) ° (3,4) ° (4,1) ° Rec(1, {1}) U {£} Rec(2, {1,2}) = (2,3) ° (3,2) ° Rec(2, {1,2}) U {£}

П(0,5,0) обозначает все пути из вершины 0 в вершину 5. При переходе по ребру (0,1) метод попадает в вершину 1. Так как она рекурсивная (поскольку лежит в RV), метод вводит для неё символ Rec(1,{1}) и начинает создавать рекурсивное описание этого символа.

Это описание начинается с перехода в вершину 2 и введения рекурсивного символа для неё. Поскольку были пройдены обе рекурсивные вершины, то при создании символа Rec(2,{1,2}) множество D = {1,2}. Рекурсивное определение для Rec(2,{1,2}) выглядит следующим образом: все пути из 2 в 2, не проходящие через D = {1,2} в середине пути,

— это повторения пути (2,3) ° (3,2). Кроме того, любое множество Rec(-) путей из себя в себя содержит пустой путь е.

Далее продолжается описание Rec(1,{1}): после перехода из 1 в 2 происходит конкатенация пути (1,2) и путей Rec(2,{1,2}) из 2 в себя, а к множеству D добавляется вершина 2. После этого путь, возвращающийся в вершину 1, очевиден: (2,3) ° (3,4) о

(4,1).

Затем процесс возвращается к построению П(0,5, е>). После перехода по ребру (0,1) и создания символа Rec(1,{1}) к множеству D добавляется вершина 1. Затем происходит переход по ребру (1,2) и добавление соответствующего символа Rec(2,{1,2}), а также к множеству D добавляется 2. Поскольку описание для символа Rec(2,{1,2}) уже было построено при построении описания Rec(1, {1}), то метод не будет строить его заново. Далее следует тривиальный путь до вершины 5: (2,3) о (3,4) о (4,5). Таким образом, метод позволяет описывать все пути в графе потока управления и только их. Формальное доказательство этого факта приведено в [27].

6.2 Алгоритм композиционального символьного исполнения

Благодаря соответствию между рекурсивными символами и их описаниями, с одной стороны, и обобщёнными кучами Rec(id) и Body(id), с другой стороны, можно получить

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

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

Определение 10. Состояние исполнения — это кортеж ( l,pc,a,D), где I— номер инструкции, pc - условие пути (path condition — символьная формула, описывающая ограничения на достижимость инструкции), а — символьная куча, D — множество посещённых рекурсивных вершин.

Важнейшей функцией алгоритма является Exec, которая может быть вызвана либо из начальной (стр. 2), либо из рекурсивной вершины (стр. 51). В первом случае её результатом является символьная куча, соответствующая путям исполнения, которые привели к инструкциям hait (стр. 10), а также выводится множество путей, приводящих к ошибкам (стр. 53). Во втором случае результатом является символьная куча Body(l0, D0), описывающая поведения циклического участка графа потока управления. В стр. 37-40 происходит добавление обобщённого состояния, представляющего собой композицию построенного состояния а' с рекурсивным состоянием Rec(l0,D0). В конце функции (стр. 52) добавляется завершающее состояние для случая, когда исполнение не вернулось в рекурсивную вершину I0, соответствующее пустому пути е из определения рекурсивных символов в методе описания путей в графе.

Алгоритм выбирает следующее состояние исполнения из рабочего множества (pickNext), затем исполняет соответствующую инструкцию, порождая новые состояния исполнения (стр. 9-35), и добавляет их в рабочее множество, объединяя те состояния исполнения, у которых равны номера инструкций и совпадают множества посещённых рекурсивных вершин D (стр. 47-50). Стоит отметить, что предлагаемый алгоритм не раскручивает циклы, а вводит рекурсивные состояния Rec(l0,D0) (стр. 12) и обобщённые кучи Body(l0, D0) (стр. 46) для их описания. Все обобщённые кучи Body(-,-) используются для определения выполнимости ограничений пути (стр. 21, 23, 31, 34).

1 V l e RV, VD Body(l, D) ^ €;

2 return ExEC(start, 0);

3 Function ExECflo : Vertex, Do : Vertex set)

4 pcr, or ^ €);

5 W ^ {(l0, T, €, D0)}; Errors ^ 0;

6 while W * 0 do

7 (l, pc, o, D), W ^ pickNext(W);

8 S ^ 0;

9 switch instr(l)

10 case halt:

11 if l0 e RV then

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

12 pcr ^ pcr V pc;

13 or ^ merge((pcr , or ), (pc,o));

14 case fail: Errors ^ Errors U {pc};

15 case ident expression

16 I o, value ^ Eval(o,expression);

17 I S ^ {(succ(l), pc, write(o, ident, value))};

18 case Location.field expression

19 o, value ^ Eval(o,expression);

20 o, loc ^ Eval(o,Location);

21 if SAT (Body, o, pc A loc * null) then

22 | S ^ {(succ(l), pc A loc * null, write(o, loc.field, value))};

23 if SAT (Body, o, pc A loc = null) then

24 | Errors ^ Errors U {pc A loc = null};

25 case label : statement

26 | S ^ {(succ(l), pc, o)};

27 case goto labels

28 guardsucc ^ T;

29 forall (expression ^ l') e labels do

30 o, guard ^ Eval(o, expression);

31 if SAT (Body, o, pc A guard A guardsucc) then

32 | S ^ S U {(l', pc A guard A guardsucc, o)};

33 guardsucc ^ guardsucc A-guard;

34 if SAT (Body, o, pc A guardsucc) then

35 | S ^ S U {(succ(l), pc A guardsucc,o)};

36 forall (l', pc', o') e S do

37 if l' = lo then

38 o' ^ o' ◦ Rec(l0, D0);

39 pcr, or ^ (pcr V pc', merge ((pcr, or ), (pc', o')));

40 continue;

41 elif l' e D then continue;

42 elif l' e RV then

43 D ^ D U {l'};

44 o' ^ o' ◦ Rec(l', D);

45 if Body(l', D) = (±, e) then

46 | Body(l', D) ^ Exec(1', D);

47 if 3(l", pc'', o", D") e W : l' = l" A D = D" then

48 W ^ W \ {(l", pc", o", D")};

49 W ^ W U {(l', pc' Vpc", merge((pc', o'), (pc", o'')), D)};

50 else W ^ W U {(l', pc', o', D)};

51 if l0 e RV then

52 | return merge (pcr,or),(-pcr,e) ;

53 print Errors;

54 return or

Листинг 4. Алгоритм композиционального символьного исполнения Listing 4. Compositional symbolic execution algorithm

Функция Eval(a,expression) вычисляет выражения, трактуя арифметические и булевы операции стандартным образом и читая переменные из состояния а. Стоит заметить, что в качестве побочного эффекта Eval(a, expression) может добавить к множеству Errors новое условие пути, защищающее обращение к нулевому адресу. Для вычисления Eval(a,new {Fields »Expr{}) будет создан новый уникальный адрес

0xNNN3, все Expr вычислятся в vt и состояние будет итеративно обновлено: а' = write(a, OxNNN. Fieldt, vt). Функция Eval вернёт новое состояние а' и адрес созданного объекта OxNNN. Например, для фрагмента на лист. 5 может быть получено следующее новое состояние:

0x40. К ^ 30 0x41. К ^10 0x42. К » 50 0x40. L » 0x41 0x41. L » null 0x42. L » null 0x40. R 0x42 0x41. R null 0x42. R null

1. x = new {K = 30;

2. L = new {K = 10; L = null; R = null};

3. R = new {K = 50; L = null; R = null}} Листинг 5. Программа, выделяющая память

Listing 5. Heap-allocating program

6.3 Корректность алгоритма композиционального символьного исполнения

Определение 12. Замкнутым назовём терм, не содержащий LI(-).

Конкретная куча — это (тотальное) отображение из замкнутых локаций в замкнутые термы. Множество конкретных куч обозначим за Ес.

Конкретная куча представляет собой состояние динамической памяти при конкретном исполнении программы. Заметим, что (Е с,°) — правый идеал в моноиде (Е,°): если а Е Е с,т Е X, то а ° т ЕЕ с. Этот факт позволяет легко доказать следующее утверждение. Утверждение 2. Если для а Е Ес и обобщённой кучи Н, а ° Н Н' для некоторой нередуцируемой Н' Е Heap, то Н' Е Ес.

Пусть Т: И х Ес ^ И х Ес — отношение перехода некоторой программы на демо-языке (т.е. отображение, которое номеру инструкции и состоянию программы сопоставляет следующую инструкцию и состояние, полученное исполнением входной инструкции на входном состоянии). Обозначим Tn(l, а) = Т(... Т (l, а)).

п раз

Следующая теорема (которую мы оставляем без доказательства) говорит о корректности алгоритма на лист. 4.

Теорема 17. Пусть Т — отношение перехода программы Р на демо-языке, l0 — номер начальной инструкции, F — множество номеров инструкций halt и fail в программе, а0 Е Ес, Н = Exec(l0,e>). Также допустим, что оракул SAT всегда отвечает правильно. Тогда Тп (l0, а0) = (f, т) для некоторых n Е И, f Е f,t Е Ес т. и т. т., к. а0 ° Н т.

7. Трансляция символьных куч в чистые функции

Для проверки достижимости некоторого пути исполнения программы, алгоритм из разд. 6 обращается к функции-оракулу SAT. В данном разделе мы определяем SAT(Body, а, д) и тем самым завершаем построение композициональной процедуры верификации. Мы сведём задачу выполнимости ограничения д к задаче доказательства безопасности функциональной программы без эффектов, состоящей из чистых функций второго порядка4.

3 В текущем изложении некорректно обрабатывается случай с выделением объекта в циклическом регионе; для корректной работы определение уточнения должно быть изменено: а • 0xNNN должно порождать новый адрес ОхМММ

4 Функцией первого порядка называется функция, которая не принимает в аргументы другие функции. Функцией второго порядка называется функция, которая принимает в аргументы функции только первого порядка — и не выше.

7.1 Оператор Find

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

Утверждение 3. Для всех определённых символьных куч а, а', т таких, что для каждого символьного выражения е, выполняется (г ° а) • е = т • (а • е), и всех символьных выражений х Е loe, d Е term, верно следующее:

т • find(a',x, а, d) = find(a',T • х,т ° а,т • d). Далее, можно заметить, что:

r е ad^,x) = [1^(а,х,е,Ы(х)) = find^.x.e.read^.x)), (а о а')(х) = /1^(а',х,а,а(х)) = find^^x^^ead^^)). Это даёт возможность определить оператор find для обобщённых куч следующим образом (обозначив прежний f ind как f indE):

find^x.r) * уыЧмтЛШЬъе» если а Е Z I И(тоа,х), иначе

7.2 Трансляция обобщённых куч в функции второго порядка

Будем говорить, что символьный терм t находится в нормальной форме, если он содержит объединения только на верхнем уровне, т.е. t = union((g1,t1),... ,(gn,tn)) и ни одно из ограничений g¿ и ни один из термов t¿ не содержат внутри union. Будем также говорить, что ограничение находится в нормальной форме, если оно не содержит объединений. Каждое символьное выражение может быть нормализовано: по утв. 1 и (6), вложенные объединения могут быть линеаризованы. Если t не содержит объединений, тогда его нормальной формой будем называть union(T, t). По определению, ограничение g = union((g1, e^),..., (gn, en)) может быть переписано в (g1 Ae1)V ..V (gn A en). Рассмотрим символьную ячейку LI (а, x). Заметим, что такие ячейки с а Ф е появляются только в последней ветке определения (10), т.е. можно рассматривать Ы(а,х) как find(а,х,е). Уточнение такого выражения в контексте т даст т•

утв.3

find(а,х,е) = find^^ • х,т). Это даёт возможность транслировать символьные выражения в функции второго порядка.

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

1. Нормализация е и преобразование верхнеуровневого объединения в конструкцию ветвления.

2. Замена всех ячеек LI (а, х) на find (а, \х]т, т).

3. Специализация оператора f ind согласно правилам (10). На этом шаге все термы вида find(а,х,т) транслируются в применения функций второго порядка findff. Телом функции find а будет результат применения этих трёх шагов к соответствующему правилу (10). При появлении композиции а о а' контекстное состояние становится частичным применением find а к текущему контекстному состоянию т.

Вместо формального описания целевого функционального языка программирования и алгоритма трансляции мы продемонстрируем процесс трансляции на примере. Допустим, необходимо ответить на запрос SAT(Body,Ree(f),LI(e,a) *3 < 17). Пусть Body(f) = merge^e^)^—^ о Ree(f))), где а — это некоторая обобщённая куча. Тогда

необходимо проверить выполнимость ограничения д = (Rec(f) • (LI(e,a) * 3) < 17) = (LI(Rec(f),a) * 3 < 17).

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

Первый шаг не порождает условных конструкций. После второго шага выражение д становится следующим: find(Rec(f),a,r0) * 3 < 17. Третий шаг порождает новую функцию второго порядка find Rec(f). Таким образом, закодированное значение д будет:

Ш То = (fíndRec(f) т0 a) *3 < 17.

Проверить выполнимость д — это то же самое, что проверить безопасность программы "assert(-i g)".

Теперь мы должны задать тела полученных функций find. Пусть findf принимает контекстную функцию первого порядка т и локацию х. Тело функции find Recf мы получим, применяя шаги 1-3 к Body(f):

(10)

[1^(тегде((с, е), (—с, а ° Rec(f))),x,r) = = ite(T • c,find(e,x, x),find(a ° Rec(f),x,T)) Это объединение будет нормализовано и транслируется в ветвление в теле find ReC(jy, ленивые ячейки в заменятся применениями f n d, которые будут также специализированы. Итеративное применение этих шагов даст код для д, представленный ниже.

Идея такой трансляции заключается в том, что операции композиции могут быть заменены частичными применениями функций. Это позволяет сохранять справедливость того факта, что контекстные функции не поднимаются выше первого порядка. Таким образом получается трансляция в чистые функции второго порядка. I assert (not ((findRec(f) т a) * 3 < 17))

: findRec(f) т x = 3. if !д]т then find6 т x 4 else find0oRec(f) т x 5. find6 т x = т x

<'. find0oRec(f) т x = findRec(f) (finda т) X " findCT т x = ...

Замечание. Есть несколько способов улучшить эту трансляцию. Во-первых, можно специализировать не только по куче, но и по типу локации, что даст более специализированные функции. Это также необходимо, чтобы получить из алгоритма трансляции типобезопасные функции. Во-вторых, полученная программа может быть частично исполнена, чтобы удалить тривиальные чтения, как, например, find е. В-третьих, именованные локации могут передаваться как обычные аргументы, так как их адреса никогда не меняются. Эти три улучшения позволяют получить автоматическую трансляцию, результаты которой будут схожи с приведённой в [27] (прил. A).

7.3 Корректность трансляции в чистые функции

Следующая теорема (которую мы оставляем без доказательства) говорит о корректности алгоритма из разд. 7.2.

Теорема 18. Пусть Н Е Heap, а0ЕЕс, д — символьное ограничение. Тогда т»д выполнимо для некоторого т Е Ес, такого что а0°Н т, т. и т. т., к. небезопасна

функциональная программа, полученная из о0° H и g применением алгоритма кодирования обобщённых куч из разд. 7.2.

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

Теорема 19. Пусть isSafe(p) — оракул, проверяющий безопасность функциональных программ, который всегда возвращает верный результат. Тогда программа на демонстрационном языке безопасна т. и т. т., к. алгоритм с лист. 4 выводит Err ors = 0.

Доказательство. Следует из теор. 17 и теор. 18.

7.4 Верификация функциональных программ без эффектов

Для доказательства безопасности функциональных программ без эффектов можно применить различные классические техники. Одной из самых успешных является вывод уточнённых типов (refinement type inference) [10, 23, 24, 25]. Фреймворки вывода уточнённых типов строят индуктивные инварианты функциональных программ высших порядков из инвариантов первого порядка над значениями с закрытыми типами (ground-types). Более точное описание этого процесса содержится в [25].

Тот факт, что получаемые в результате нашей трансляции функции не выше второго порядка, позволяет специализировать и оптимизировать процедуру вывода уточнённых типов. В контексте нашей работы, наиболее интересны композициональные фреймворки вывода уточнённых типов. Примером такого фреймворка является [24].

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

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

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

Вне контекста формальной верификации работа может рассматриваться как построение интересного соответствия между императивными программами с динамической памятью и чистыми функциями: описанное в статье сведение способно по императивной программе с динамической памятью породить эквивалентную программу на чистом функциональном языке, причём порождённые функциональные программы неочевидным образом кодируют операции над динамической памятью, а композициональное сведение позволяет порождать код функций, не зависящий от её отдельных вызовов. Предложенный в статье подход выглядит перспективным при обеспечении качества встроенных систем реального времени [28], а также при разработке операционных систем реального времени [29]. Также перспективным является интеграция данного подхода со средствами визуального моделирования ПО, в частности, при верификации исполняемых визуальных спецификаций [30].

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

[1]. Distefano D. Attacking large industrial code with bi-abductive inference. Lecture Notes in Computer Science, vol. 5825, 2009, pp. 1-8.

[2]. Calcagno C. et al. Compositional shape analysis by means of bi-abduction. Journal of the ACM, vol. 58, no. 6, 2011, p. 26:1-26:66.

[3]. Gurfinkel A. et al. The SeaHorn verification framework. Lecture Notes in Computer Science, vol. 9206, 2015, pp. 343-361.

[4]. Anand S., Godefroid P., and Tillmann N. Demand-driven compositional symbolic execution. Lecture Notes in Computer Science, vol. 4963, 2008, pp. 367-381.

[5]. Distefano D. and Parkinson J M. J. jStar: Towards practical verification for Java. ACM Sigplan Notices, vol. 43, issue 10, 2008, pp. 213- 226.

[6]. Calcagno C. and Distefano D. Infer: An automatic program verifier for memory safety of C programs. Lecture Notes in Computer Science, vol. 6617, 2011, pp. 459-465.

[7]. Tillmann N. and De Halleux J. Pex-white box test generation for. net. Lecture Notes in Computer Science, vol. 4966, 2008, pp. 134- 153.

[8]. Cousot P. and Cousot R. Abstract interpretation: a unified lattice model for static analysis of programs by construction or approximation of fixpoints. In Proc. of the 4th ACM SIGACT-SIGPLAN symposium on Principles of programming languages, 1977, pp. 238- 252.

[9]. Khurshid S., Pâsâreanu C. S., and Visser W. Generalized Symbolic Execution for Model Checking and Testing. Lecture Notes in Computer Science, vol. 2619, 2003, pp. 553-568.

[10]. Vazou N., Bakst A., and Jhala R. Bounded refinement types. ACM SIGPLAN Notices, vol. 50, issue 9, 2015, pp. 48-61.

[11]. Godefroid P. Compositional Dynamic Test Generation. ACM SIGPLAN Notices, vol. 42, issue 1, 2007, pp. 47-54.

[12]. Biere A. et al. Symbolic Model Checking without BDDs. Lecture Notes in Computer Science, vol. 1579, 1999, pp. 193-207.

[13]. Deng X., Lee J. et al. Efficient and Formal Generalized Symbolic Execution. Automated Software Engineering, vol. 19, issue 3, 2012, pp. 233-301.

[14]. Braione P., Denaro G., and Pezz'e M. JBSE: A symbolic executor for java programs with complex heap inputs. In Proc. of the 24th ACM SIGSOFT International Symposium on Foundations of Software Engineering, 2016, pp. 1018-1022.

[15]. Reynolds J. C. Separation logic: A logic for shared mutable data structures. In Proc. 17th Annual IEEE Symposium on Logic in Computer Science, 2002, pp. 55-74.

[16]. Sagiv M., Reps T., and Wilhelm R. Parametric shape analysis via 3-valued logic. ACM Transactions on Programming Languages and Systems, vol. 24, issue 3. 2002, pp. 217-298.

[17]. Berdine J., Calcagno C., and O'Hearn P.W. Symbolic execution with separation logic. Lecture Notes in Computer Science, vol. 3780, 2005, pp. 52-68.

[18]. Dudka K., Peringer P., and Vojnar T. Byte-precise verification of lowlevel list manipulation. Lecture Notes in Computer Science, vol. 7935, 2013, pp. 215-237.

[19]. Barrett C. and Tinelli C. Satisfiability modulo theories. In Handbook of Model Checking, Springer, 2018, pp. 305-343.

[20]. Kahsai T. et al. Quantified heap invariants for object-oriented programs. In Proc. of the 21st International Conference on Logic for Programming, Artificial Intelligence and Reasoning, 2017, pp. 368- 384.

[21]. Torlak E. and Bodik R. A lightweight symbolic virtual machine for solver-aided host languages. ACM SIGPLAN Notices, vol. 49, issue 6, 2014, pp. 530-541.

[22]. Baldoni R. et al. A survey of symbolic execution techniques. ACM Computing Surveys, vol. 51, no. 3, 2018, pp. 50:1-50:39.

[23]. Unno H., Terauchi T., and Kobayashi N. Automating relatively complete verification of higher-order functional programs. ACM SIGPLAN Notices, vol. 48, issue 1, 2013, pp. 75-86.

[24]. Zhu H. and Jagannathan S. Compositional and lightweight dependent type inference for ML. Lecture Notes in Computer Science, vol. 7737, 2013, pp. 295-314.

[25]. Cathcart Burn T., Ong C.-H.L., and Ramsay S. J. Higher-order constrained horn clauses for verification. Proceedings of the ACM on Programming Languages vol. 2, no. POPL, 2017, p. 11:111:27.

[26]. Мандрыкин М.У., Мутилин В.С.. Обзор подходов к моделированию памяти в инструментах статической верификации. Труды ИСПРАН, том 29, вып. 1, 2017 г., стр. 195-230 / Mandrykin M.U., Mutilin V.S. Survey of memory modeling methods in static verification tools. Trudy ISP RAN / Proc. ISP RAS, vol. 29, issue 1, 2017, pp. 195-230 (in Russian). DOI: 10.15514/ISPRAS-2017-29(1)-12.

[27]. Костюков Ю. О., Батоев К. А., Мордвинов Д. А., Костицын М. П., Мисонижник А. В. Автоматическое доказательство корректности программ с динамической памятью. arXiv:1906.10204, 2019 г. / Kostyukov Yu.O., Batoev K.A., Mordvinov D.A., Kostitsyn M.P., Misonizhnik A.V.. Automatic verification of heap-manipulating programs. arXiv:1906.10204, 2019 (in Russian).

[28]. Терехов А.Н. Технология программирование встроенных систем. автореферат дис. доктора физико-математических наук. Новосибирск, 1991 г. / Terekhov A.N. Technology for programming of embedded systems. Abstract of Thesis for obtaining the degree of Doctor of physical and mathematical sciences. Novosibirsk, 1991 (in Russian).

[29]. Новиков Е.М. Развитие ядра операционной системы Linux. Труды ИСП РАН, том 29, вып. 2, 2017 г., стр. 77-96 / Novikov E.M. Evolution of the Linux kernel. Trudy ISP RAN/Proc. ISP RAS, vol. 29, issue 2, 2017, pp. 77-96 (in Russian). DOI: 10.15514/ISPRAS-2017-29(2)-3.

[1]. Ольхович Л.Б., Кознов Д.В. Метод автоматической валидации UML-спецификаций на языке OCL. Программирование, том 29. № 6, 2003 г., стр. 44-50 / Ol'khovich L., Koznov D.V. OCL-based automated validation method for UML specifications. Programming and Computer Software, vol. 29, № 6, 2003, pp. 323-327.

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

Юрий КОСТЮКОВ получил степень бакалавра в области информационных технологий в Санкт-Петербургском государственном университете в 2019 г. Его исследовательские интересы включают автоматическую верификацию, теорию моделей и конструктивную математику.

Yurii KOSTYUKOV received the bachelor's degree in information technology from Saint Petersburg State University in 2019. His research interests include automatic verification, model theory and constructive mathematics.

Константин БАТОЕВ получил степень бакалавра в области информационных технологий в Санкт-Петербургском государственном университете в 2019 г. В число научных интересов входят анализ графов потока управления и символьное исполнение.

Konstantin BATOEV received the bachelor's degree in information technology from Saint Petersburg State University in 2019. His research interests include control flow graph analysis and symbolic execution.

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

Dmitry MORDVINOV is working as senior lecturer in Saint Petersburg State University. He is currently pursuing the Ph.D. degree in information technology. His area of interest is formal verification, program synthesis and Horn clauses solving.

Михаил КОСТИЦЫН получил степень бакалавра в области информационных технологий в Санкт-Петербургском государственном университете в 2019 г. Его исследовательские интересы включают компьютерную безопасность, анализ строковых выражений и анализ динамической памяти.

Michael KOSTITSYN received the bachelor's degree in information technology from Saint Petersburg State University in 2019. His research interests include computer security, string and heap analysis.

Александр МИСОНИЖНИК получил степень бакалавра в области информационных технологий в Санкт-Петербургском государственном университете в 2018 г. В число научных интересов входят системы типов, интуиционистская логика и теория категорий.

Aleksandr MISONIZHNIK received the bachelor's degree in information technology from Saint Petersburg State University in 2018. His research interests include type systems, intuitionistic logic and category theory.

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