DOI: 10.15514/ISPRAS-2026-38(3)-45
А.С. Проценко, ORCID: 0009-0001-4240-2986 <protsenko@ispras.ru>
Институт системного программирования им. В.П. Иванникова РАН,
Россия, 109004, г. Москва, ул. А. Солженицына, д. 25.
Аннотация. Языковые виртуальные машины (ВМ) обычно используются в инфраструктуре систем программирования для объектно-ориентированных языков высокого уровня. Такие языки пользуются популярностью среди разработчиков и исследователей благодаря следующим особенностям: кроссплатформенности, автоматическому управлению памяти (сборке мусора) и изолированной средой исполнения программы, которая, совместно с верификатором кода загружаемых классов, гарантирует определенный уровень безопасности исполняемой программы. В настоящее время существует большое количество как архитектур системы команд ВМ, так и их реализаций. В данном обзоре приводятся список архитектур ВМ и список популярных реализаций ВМ. В статье иллюстрируется схема работы типовой ВМ. Разработка ВМ – сложный процесс, в ходе которого могут совершаться ошибки. Для обеспечения качества реализации ВМ процесс разработки обязательно включает в себя этап тестирования. В обзоре рассматриваются подходы, нацеленные на тестирование ВМ, и производится их сравнение. Рассматривается возможность использования методов тестирования, приведенных в обзоре, для функционального тестирования реализаций существующих и разрабатываемых архитектур ВМ.
Ключевые слова: языковые виртуальные машины; процессные виртуальные машины; виртуальные машины; архитектура системы команд; ISA; виртуальная архитектура системы команд; тестирование; функциональное тестирование.
Для цитирования: Проценко А.С. Обзор языковых виртуальных машин и подходов к их тестированию. Труды ИСП РАН, том 38, вып. 3, часть 4, 2026 г., стр. 37–58. DOI: 10.15514/ISPRAS-2026-38(3)-45.
В основе виртуальных машин (ВМ) лежит виртуализация. В соответствии с ГОСТ [1] виртуализация – это группа технологий, основанных на преобразовании формата или параметров программных или сетевых запросов к компьютерным ресурсам с целью обеспечения независимости процессов обработки информации от программной или аппаратной платформы информационной системы. Первые исследования ВМ [2-5] были начаты в 60-х годах прошлого столетия. В это же время были сформулированы основные концепции системы команд (Instruction Set Architecture, ISA), виртуализации и ВМ.
В современных работах [6, 7] ВМ делят на две большие группы. Это системные ВМ (system virtual machine) и процессные ВМ (process virtual machine). Процессные ВМ предназначены для запуска отдельного приложения. Архитектура процессной ВМ представлена на рис. 1. Хост-платформой в процессных ВМ является аппаратное обеспечение совместно с ОС. Программное обеспечение для виртуализации в процессных ВМ часто называют средой исполнения. Гостевая платформа создается для отдельного процесса приложения. Запускаемое приложение представляет собой исполняемый файл в формате понятном для среды исполнения.
Наиболее популярными и известными среди процессных ВМ являются языковые ВМ (language virtual machine). В языковых ВМ используются специальные системы команд, обычно не имеющие аппаратной реализации. Такие системы команд еще называют виртуальными системами команд (Virtual ISA, V-ISA) или байт-кодом. Языковые ВМ обычно используются в инфраструктуре систем программирования для объектно-ориентированных языков высокого уровня. В методологии объектно-ориентированного программирования (ООП) предметная область описывается с помощью иерархии классов. Класс в такой методологии представляет собой шаблон, по которому можно создать объект определенного типа, с заданной структурой и поведением (алгоритмами работы). Информация о классах в ВМ представляется с помощью метаданных. Языковые ВМ, поддерживающие ООП методологию, обладают следующими свойствами: наличием загрузчика классов и верификатора, наличием интерпретатора байт-кода, работой с библиотеками классов и метаданными, обработкой исключений и использованием сборщика мусора. Далее в работе под ВМ мы будем понимать именно языковые ВМ и их систему команд, если не указано иное.

Рис. 1. Архитектура процессной ВМ.
Во втором разделе статьи приведена общая схема работы ВМ. Рассматриваются особенности ВМ. Приводится список существующих систем команд ВМ и приводятся примеры существующих реализаций. В третьем разделе проводится обзор существующих подходов к тестированию ВМ, проводится сравнения применимости подходов для функционального тестирования как существующих ВМ, так и разрабатываемых. Заканчивается статья заключением.
В разделе описываются особенности рассматриваемых в работе ВМ, приводится общая схема работы ВМ, рассматривается стековые и регистровые ВМ. В разделе описываются существующие системы команд ВМ и приводятся примеры их реализаций.
В классических реализациях ВМ для интерпретации байт-кода используется интерпретатор. В этом заключалась причина основного недостатка языковых ВМ, а именно в более медленном исполнении программ, по сравнению с непосредственным исполнением аналогичных программ на аппаратных средствах. Для ускорения исполнения программ на ВМ байт-код транслируют в нативный код целевой архитектуры и исполняют на аппаратной части. Для этого используют следующие подходы:
Программы для языковых ВМ являются кроссплатформенными. Они не зависят от операционных систем и микропроцессорных архитектур. Они зависят только от ВМ и если для операционной системы и микропроцессорной архитектуры существует работающая на ней ВМ, то и программу для этой ВМ можно исполнить на этих платформах. Достаточно знаменит слоган “Write once, run anywhere” [8] придуманный в компании Sun Microsystems для демонстрации преимуществ кроссплатформенности языка Java [9], использующего в своей инфраструктуре ВМ.
Непосредственный доступ к памяти и ручная работа с ней часто влечет за собой ошибки. В ВМ для работы с памятью реализован менеджер управления памятью, который снимает с разработчика эту часть задач. Важным механизмом является сборщик мусора, который периодически (или по необходимости) запускается и освобождает память от артефактов, которые больше не используются. Эта технология позволяет упростить разработку ПО.
Перед началом работы программы, ее исполняемые файлы необходимо загрузить в ВМ. За стратегию загрузки отвечает загрузчик классов, который производит поиск, загрузку и связывание классов. Во время работы загрузчика происходит проверка загружаемых классов на соответствие требованиям с помощью верификатора.
Для исполнения программы ВМ создает для нее изолированную среду, что предотвращает доступ к системным ресурсам, которые программе не требуются. При работе программы все взаимодействие с внешней средой осуществляется через ВМ. Такой подход позволяет обеспечивать определенный уровень безопасности при исполнении программ с помощью ВМ.
Благодаря своим особенностям ВМ получили широкое распространение, и в настоящее время встречаются повсюду: в сотовых телефонах, ноутбуках, серверах и других электронных вычислительных устройствах.
Общая схема применения ВМ для исполнения программы на языке высокого уровня (ЯВУ) приведена на рис. 2.

Рис. 2. Общая схема применения ВМ для исполнения программы.
Схема состоит из следующих этапов. Сначала программа разрабатывается на ЯВУ, после чего программа на ЯВУ с помощью транслятора с ЯВУ транслируется в программу в текстовом формате на ассемблере. Полученная текстовая программа ассемблируется в исполняемую программу, сохраняемую в исполняемый файл. Исполняемая программа содержит:
Далее исполняемый файл подается ВМ, где верификатор ВМ проверяет его корректность и соответствие требованиям к байт-коду. После этого загрузчик ВМ загружает необходимые метаданные и байт-код в память ВМ и исполняющий механизм начинает исполнять программу.
Основным исполняющим механизмом ВМ является интерпретатор, и именно он обязан быть в состоянии исполнить любой корректный байт-код программы, переданной на исполнение. Во время такого исполнения может создаваться трасса исполнения программы, которая обычно содержит информацию об исполненных инструкциях и изменениях состояния ВМ. Нативный код, полученный при помощи JIT- и AOT-компиляции, исполняется непосредственно на аппаратном обеспечении и, в отличии от режима интерпретации байт-кода, в трассу исполнения программы никакой информации не передает.
Объекты, создаваемые во время исполнения программы, хранятся в области памяти называемой кучей. Сборщик мусора позволяет автоматически уничтожать объекты из кучи, которые в программе более не могут быть использованы. При каждом вызове метода создается фрейм, содержащий данные (такие как стек операндов или регистры), необходимые для выполнения байт-кода метода. Создаваемые фреймы сохраняются в стек фреймов. После завершения метода, соответствующий фрейм удаляется из стека фреймов и уничтожается.
После завершения исполнения программы, ВМ возвращает результат и трассу исполнения. В данной статье в качестве результата исполнения программы рассматривается возвращаемое значение. Стоит отметить, что для получения трассы исполнения от ВМ при запуске исполнения программы необходимо передавать в ВМ соответствующие флаги.
Помимо языковых ВМ в некоторой литературе иногда упоминаются названия “ВМ для языков высокого уровня” (High Level Language Virtual Machine, HLL VM) [10] и “ВМ для виртуальной системы команд” (Virtual ISA virtual machine) [7], которые в данной работе рассматриваются как синонимы для языковых ВМ. Такое разнообразие названий для языковых ВМ можно объяснить тем, что в настоящий момент нет устоявшейся терминологии в этой области и наличием исследований по этой тематике от различных научных групп и команд.
ВМ можно разделить на две группы по типу используемых операндов в системе команд: регистровые и стековые. Регистровые ВМ для передачи операндов и хранении промежуточных значений используют регистры, в то время как стековые ВМ для этого используют стек операндов.
При использовании регистров для передачи операндов возрастает размер инструкции, так как в кодировку инструкции, помимо кода инструкции, добавляются индексы используемых регистров. Но в регистровых ВМ нет необходимости каждый раз перезагружать данные в регистре, если до этого он уже был инициализирован.
При использовании стека операндов размеры инструкций не содержат дополнительной информации помимо кода самой инструкции. Но перед использованием целевой операции в стековой ВМ приходится использовать несколько дополнительных операций, которые должны загрузить операнды в стек, преобразование над которыми будет совершать целевая операция, после работы которой использованные данные из стека операндов будут удалены.
В настоящее время ни один из подходов не считается более эффективным в целом и при создании новых ВМ выбор организации работы с операндами связан с особенностями планируемой реализации ВМ. Хотя существуют исследования [11, 12] в которых было показано, что регистровая ВМ при исполнении программ стандартного тестового набора только с помощью интерпретатора затрачивает до 20% времени меньше, чем аналогичная стековая ВМ.
В настоящее время существует большое количество систем команд ВМ и еще большее количество ВМ их реализующих. Пример популярных и упоминаемых в литературе систем команд ВМ представлены в табл. 1.
Табл. 1. Системы команд и реализации ВМ.
| № | Система команд | Тип операндов ВМ | Реализация ВМ | Год создания |
|---|---|---|---|---|
| 1 | P-code [13] | Стековая | The P-code Machine [13] | 1972 |
| 2 | PL/EXUS [14] | Стековая | PLEXUS | 1973 |
| 3 | Smalltalk bytecode [15] | Стековая | Smalltalk-80 System [16, 17] | 1983 |
| Squeak [18] | 1996 | |||
| Pharo [19] | 2008 | |||
| 4 | Self bytecode [20] | Стековая | Self VM [21, 22] | 1987 |
| 5 | Lua bytecode [23]. | Регистровая | Lua [24] | 1993 |
| 6 | K-code [25] | Регистровая | K-machine (Kaleidoscope’93) [25] | 1993 |
| 7 | Python bytecode [26] | Стековая | CPython [27] | 1994 |
| 8 | Java bytecode [28] | Стековая | Kaffe [29] | 1996 |
| Cacao [30] | 1997 | |||
| Jalapeno VM [31, 32] | 1998 | |||
| HotSpot [33] | 1999 | |||
| Jikes RVM (Research Virtual Machine) [34] | 1999 | |||
| SableVM [35] | 2000 | |||
| JRockit [36] | 2002 | |||
| OpenJ9 [37] | 2017 | |||
| GraalVM [38, 39] | 2019 | |||
| 9 | SpiderMonkey bytecode [40] | Стековая | SpiderMonkey [41] | 1996 |
| 10 | CIL Instruction Set [42] | Стековая | .NET Framework (Common Language Runtime) [43] | 2002 |
| The Mono Runtime [44] | 2004 | |||
| 11 | V8 bytecode [45] | Регистровая | V8 engine [46] | 2008 |
| 12 | Dalvik bytecode [47] | Регистровая | Dalvik [48] | 2008 |
| Android Runtime (ART) [49, 50] | 2013 | |||
| 13 | Parrot bytecode [51] | Регистровая | Parrot [52] | 2009 |
| 14 | Mu Instruction Set [53] | Регистровая | Mu [54] | 2015 |
| 15 | WebAssembly [55] | Стековая | WARDuino [56, 57] | 2019 |
| 16 | Panda bytecode [58] | Регистровая | ARK (static core) [59] | 2022 |
Как видно из табл. 1, для некоторых систем команд ВМ может быть создано несколько реализаций ВМ, чаще всего от различных компаний. При этом для систем команд ВМ могут быть созданы трансляторы с различных языков программирования. Программы на языке JavaScript [60] можно исполнить на таких ВМ как WARDuino, SpiderMonkey и V8. Программы на языке Java можно транслировать в Java байт-код и Dalvik байт-код. При этом для получения Java байт-кода можно использовать программы на таких ЯВУ как Java, Kotlin, Scala, Groovy и др., после чего его можно будет исполнить на одной из реализаций Java Virtual Machine (JVM).
Для языка Python, помимо варианта трансляции в Python байт-код и исполнении на CPython, существуют проекты: Jython [61], позволяющий получать Java байт-код и использовать JVM, и IronPython [62], использующий в своей инфраструктуре .NET и Mono ВМ для исполнения CIL байт-кода.
Такие ВМ как Self, Jalapeno, Cacao и ART не имеют своего интерпретатора и всегда транслируют байт-код в машинный код хост-системы. Реализации Self, Jalapeno и Cacao используют JIT-компиляцию для ускорения выполнения программ. ART ВМ использует AOT-компиляцию, что позволяет добиться ускорения исполнения программ за счет увеличения времени установки приложения в ОС Android [63], во время которой и выполняется трансляция байт-кода в машинный код.
Среди приведенных ВМ существуют реализации, написанные на языке, в инфраструктуре которого они могут быть использованы. К таким ВМ относится GraalVM, написанная на языке Java.
Из современных разработок можно рассмотреть подробнее ВМ ARK (static core), реализующую Panda байт-код. Данная ВМ принимает на вход программы на Panda ассемблере, в которых используется синтаксис байт-кода для описания тел методов класса и специальные ключевые слова для описания классов. После того, как программа на ассемблере транслируется в исполняемый файл, он проверяется с помощью верификатора. Для исполнения программы используется интерпретатор. В ВМ ARK на февраль 2024 года было реализовано 2 интерпретатора: интерпретатор cpp, написанный на языке высокого уровня cpp и интерпретатор irtoc [64], получивший свое название от словосочетания “Ir-To-Code”, который автоматически генерируется из рукописного внутреннего представления. В ВМ ARK реализованы JIT- и AOT-компиляция.
Как видно из табл. 1, существует большое количество систем команд ВМ и их реализаций. При этом сохраняется тенденция к созданию новых систем команд, а реализации ВМ разрабатываются как для новых, так и для существующих систем команд.
В разделе представлена общая схема функционального тестирования и приводятся требования к организации функционального тестирования ВМ. Проводится обзор подходов к тестированию ВМ. Производится оценка соответствия существующих подходов к тестированию ВМ требованиям к функциональному тестированию ВМ.
Разработка ВМ – сложный процесс, в ходе которого могут совершаться ошибки. Для обеспечения качества реализации ВМ процесс разработки включает в себя этап функционального тестирования. Функциональное тестирование программы – это исследование программы для выявления отличий между ее реально существующим поведением и требуемым поведением в ситуациях из выделенного конечного набора [65, 66]. Общая схема функционального тестирования представлена на рис. 3.
Сформулируем список основных элементов схемы, которые необходимы для организации функционального тестирования ВМ:
Элементы, представленные на схеме рис. 3, являются абстрактными понятиями. Так, например, спецификацией может быть, как документация на тестируемую ВМ (в таком случае разработка тестовых программ осуществляется вручную), так и описание на формальном языке, где для разработки тестовых программ используются генераторы на основе формального описания.

Рис. 3. Общая схема функционального тестирования.
При организации функционального тестирования важно ответить на следующие вопросы:
В подразделе представлен обзор работ из открытых источников. Рассматривались работы, в которых описаны подходы, применяемые или которые могут быть полностью или частично применены к тестированию ВМ. В рассматриваемых работах основное внимание уделялась следующим вопросам:
Рассмотрим работы, посвященные тестированию ВМ.
В статье [67] авторы из США предлагают подход для тестирования JVM, в основе которого лежат порождающие грамматики. В работе описывается предметно-ориентированный язык lava, применяемый для описания порождающих грамматик, с помощью которых генерируются тестовые программы. Для проверки результата исполнения тестовой программы в статье используются два подхода: дифференциальное тестирование [72] и оракул на основе сертификатов. Для дифференциального тестирования в статье используются следующие реализации JVM: Sun JDK 1.0.2 [68] и Microsoft JVM из Internet Explorer 4.0 [69]. Сертификат представляет собой краткое описание ожидаемого поведения системы для заданной тестовой программы, он создается на основе специального расширения порождающих грамматик, доступного в lava. Для создания исполняемых файлов из программ на байт-коде в работе используется инструмент Jasmin [70, 71].
При дифференциальном тестировании случайно сгенерированные тестовые программы подаются двум и более сопоставимым системам [72]. В более поздних работах по тестированию ВМ авторы под применением дифференциального тестирования чаще всего понимают фаззинг подход для создания тестовых программ и использование нескольких реализаций ВМ для оценки результатов тестовых воздействий. В оригинальной статье автор William M. McKeeman, хоть и предлагал использовать случайную генерацию для создания большого количества тестовых программ, обращал свое внимание на то, что для того, чтобы сделать дифференциальное тестирование эффективным, нужно улучшать качество тестов. В работах некоторых авторов для создания тестовых программ используется не случайная генерация, но для оценки результатов тестовых воздействий используются две и более сопоставимых систем, именно такую оценку они называют дифференциальным тестированием. Чтобы отличить оригинальный подход дифференциального тестирования от части подхода для оценки результатов тестирования в обзоре иногда будет использоваться понятие дифференциального сравнения, под которым понимается сравнение результатов исполнения тестовых программ на двух разных реализациях ВМ, версиях ВМ или механизмах исполнения программ. При использовании дифференциального сравнения считается, что в одной из систем содержится ошибка, если результаты исполнения программ на разных реализациях расходятся.
В работе [73] авторы предыдущей статьи [67] продолжают исследования, направленные на тестирование JVM, и предлагают, помимо использования порождающих грамматик, применять фаззинг тестирование. Для создания тестовых программ использовались вручную написанные тестовые базы, к которым применялись однобайтовые случайные мутации кода. Для дифференциального сравнения используются следующие реализации JVM: Sun JDK 1.0.2, Microsoft JVM из Internet Explorer 4.0 и Netscape 4.0 [74].
В работе [75] исследователи из Fujitsu предлагают подход случайной генерации тестовых программ для JIT компилятора JVM. Для создания тестовых программ вначале случайным образом генерируются классы, методы классов и поля классов со случайными именами и модификаторами доступа. Классы соответствуют ациклическому графу иерархии. Методы классов разделяются на разные уровни и при генерации тестовых программ методы классов могут вызывать только методы классов более низкого уровня. Далее строится граф потока управления и ограничений по данным, в соответствии с которым тела методов классов заполняются байт-код инструкциями, среди которых могут использоваться последовательности, которые взаимодействуют с полями классов или вызывают методы классов более низкой иерархии. Во избежание зацикливания, все циклы снабжаются счетчиком и инструкцией выхода из цикла, срабатывающей при достижении счетчиком определенного ранее значения. Для проверки результатов исполнения тестовой программы в них встраиваются операции вывода состояния некоторых переменных и полей классов. Значения этих переменных сравниваются после исполнения тестовых программ с использованием тестируемого JIT компилятора и после исполнения этих же программ в других режимах или на других версия JVM.
В статье [76] авторы представляют метод генерации тестовых программ на основе комбинаторного перебора пар байт-код инструкций JVM. Идея такого перебора основана на том, что операнды инструкций должны иметь определенный тип, и для каждого типа есть специальные инструкции загрузки данных в стек операндов, загрузки данных в стек локальных переменных и так далее. В своей работе авторы разделяют байт-код инструкции по группам, в соответствии с типами операндов. Далее на основе этих групп они создают некорректные комбинации инструкций. Цель создания таких комбинаций – это проверка реализации JVM на типобезопасность. Полученные комбинации инструкций проверяют методом проверки моделей на формальной модели JVM, описанной на NuSMV [77], и получают информацию о корректности таких комбинаций. С помощью BCEL [78] создают программы на байт-коде и затем проверяют их с помощью встроенного верификатора. Результаты, полученные от верификатора, сравнивают с результатами, полученными от модели JVM на NuSMV. Результаты от обоих источников должны совпадать.
В работе [79] авторы представляют инструмент DexFuzz, реализующий метод дифференциального тестирования на основе бинарного фаззинга байт-кода DEX (Dalvik Executable). В качестве исходных программ для мутаций взят тестовый набор для ART ВМ, состоящий из 200 тестов. Оценка результатов исполнения тестов производится с помощью дифференциального тестирования, где тестовые программы исполняются с помощью бэкендов (backends) ART ВМ: интерпретатора, быстрого AOT компилятора и оптимизирующего AOT компилятора.
В статье [80] предложен метод фаззинг тестирования ВМ, реализованный в инструменте ClassFuzz. В работе для создания тестовых программ используется фаззинг, ориентированный на увеличения покрытия кода JVM. Для этого в инструменте было реализовано 129 операций мутации. Для внедрения мутаций в Java байт-код применяются преобразования над Jimple [81]. Jimple – промежуточное представление фреймворка Soot [82], используемое для анализа Java классов и получение их представление в байт-код подобном виде. Для изменения исполнимых файлов JVM *.class на уровне байт-кода создано 6 мутаций, остальные 123 мутации нацелены на изменения на уровне высокоуровневого языка и могут, например, менять атрибуты классов, добавлять в класс интерфейсы или возможные исключения. В качестве зерен для мутаций были взяты 1216 классов из JRE7 библиотек. Результаты исполнения программ оцениваются с помощью дифференциального сравнение, где в качестве ВМ используются следующие реализации: HotSpot для версий Java 7/8/9, J9 для IBM SDK8 [83] и GIJ 5.1.0 [84].
В работе [85] описан подход дифференциального тестирования JVM. Подход реализован в инструменте Classming. Для генерации тестовых программ используется фаззинг. В качестве зерен для мутаций используется тестовый набор DaCapo [86] для языка Java. В подходе используются мутации байт-кода метода, направленные на изменения потока данных и потока управления. Для изменения исполнимых файлов используется фреймворк Soot и его промежуточное представление для байт-кода Jimple. Для внедрения мутаций в метод используются следующие 5 инструкций: goto, return, throw, lookupswitch, tableswitch доступные в Jimple. Результаты исполнения тестовых программ оцениваются с помощью дифференциального сравнения, где в качестве ВМ используются следующие реализации: HotSpot из инструментария OpenJDK [87] и J9 от IBM [88], в настоящее время известная как OpenJ9.
В статье [89] описан подход для тестирования ВМ с интерпретатором байт-кода, основанный на колколическом тестировании (concolic testing). Конколическое тестирование – это гибридная техника тестирования, объединяющая в себе конкретное (concrete) и символическое (symbolic) выполнение для автоматической генерации тестовых случаев [90]. Авторы применяют конколическое тестирование к интерпретатору ВМ для получения списка значений для покрытия всех возможных путей исполнения интерпретатора. На основе полученных данных формируется тестовый набор, который подается для исполнения на ВМ в двух режимах: интерпретатора и JIT-компилятора. Для оценки результатов используется дифференциальное сравнение. Схема тестовой системы, применяемой в работе, описана в работе [91] и базируется на использовании модульного тестирования (unit testing). Подход был применен к 4 различным версиям JIT-компиляторов Pharo ВМ, в работе которых были найдены расхождения.
В диссертации [92] описан подход для проверки семантической корректности сгенерированных ВМ с помощью среды генерации ВМ Slang на основе моделирования (simulation-based VM generator framework). Генератор ВМ Slang принимает в качестве входных данных описание ВМ на языке Pharo, а в качестве выходных данных возвращает реализацию ВМ на языке C. Для проверки семантической корректности используется дифференциальное сравнение результатов исполнения тестовых программ на сгенерированной ВМ и модели из среды генерации ВМ Slang. Сравниваемыми результатами работы программ являются корректное завершение программы или ошибка. Ошибки, возникающие в тестовых программах, могут быть следующих типов: результат работы программы не совпадает с эталонным, генерация исключения во время выполнения программы и ошибка во время компиляции программы. В работе описано использование двух видов тестовых наборов. Первый тестовый набор является рукописным, который использовался для отладки модели ВМ в среде генерации. Второй набор был получен путем мутаций рукописных тестов. С помощью описанного подхода были найдены семантические расхождения при работе ВМ, что повлекло внесение правок в генератор ВМ Slang.
В работе [93] авторы представляют инструмент JavaTailor, являющийся файзером для JVM. Метод, лежащий в основе инструмента, описывается с помощью трех этапов: извлечение “ингредиентов” из набора тестовых программ, с помощью которых были ранее найдены ошибки; генерация тестовых программ на основе ингредиентов и зерна (seed) в виде класса на языке Java; проверка результата исполнения программы с помощью дифференциального тестирования. Для дифференциального тестирования в работе использовались OpenJ9 c SDK (Software Development Kit) от IBM [94] и HotSpot разных версий. Авторы описывают свой подход как направленный на создание тестовых программ для поиска ошибок, в то время как подходы, реализованные в инструментах ClassFuzz и Classming, по их мнению, нацелены на создание тестовых программ с разнообразным потоком управления и данных путем накопления незначительных мутаций.
В статье [95] описан фреймворк JITfuzz, реализующий метод фаззинг тестирования, ориентированный на покрытие кода JIT компилятора JVM. Авторы подчеркивают, что из-за особенностей работы JIT компилятора, обычными мутациями программ сложно достичь высокого покрытия его кода. В работе предложены 4 мутации, нацеленные на активацию оптимизаций, и 2 мутации, нацеленные на обогащение графа потока управления. Для внедрения мутаций в Java байт-код применяются преобразования над Jimple. Для гарантированного включения JIT оптимизации Java класса используется специальная опция JVM. В качестве начальных зерен для мутаций используются программы на Java из открытых проектов и специальных тестовых наборов для тестирования JVM. Для проверки результатов исполнения программ в работе применяется дифференциальное тестирование. В качестве ВМ используются HotSpot и OpenJ9.
В работе [96] представлен инструмент SJFuzz, являющийся фаззером для JVM. Авторы характеризуют свой инструмент как более управляемый, чем инструменты ClassFuzz и Classming, и, как следствие, дающий лучшие результаты, в том числе и по сравнению с JavaTailor. Для внедрения мутаций в Java байт-код применяются преобразования над Jimple. Для управление графом потока управления используются инструкции: goto, lookupswitch и return. Авторы предлагают несколько алгоритмов, позволяющих разнообразить мутации и уменьшить количество мутаций, приводящих к одинаковым ошибкам. Например, для сравнения тестовых программ между собой используется метрика, основанная на сравнении инструкций метода, которые были исполнены. Для оценки результатов используется дифференциальное тестирование, а в качестве ВМ используются HotSpot, DragonWell от Alibaba [97], OpenJ9, Zulu от Azul [98] и интерпретатор GIJ GNU.
Тестирование компиляторов для языков высокого уровня. Помимо работ, посвященных непосредственно тестированию ВМ, существует ряд работ, в которых описаны подходы, которые полностью или частично можно применить к тестированию ВМ. Рассмотрим такие работы.
В статье [99] описывается инструмент Csmith, предназначенный для тестирования компиляторов для языка программирования C [100]. Генератор Csmith случайным образом генерирует программы на языке C на основе требований грамматики и ограничений стандарта. Полученные программы компилируются с помощью разных компиляторов и для разных микропроцессорных архитектур. Далее скомпилированные исполняемые файлы запускаются. Для оценки результата исполнения полученных программ используется дифференциальное сравнение.
В работе [101] представлен подход SPE (Skeletal Program Enumeration) для тестирования компиляторов. Идея подхода заключается в следующем. Из программы можно получить синтаксический каркас, где вместо имен используемых переменных образуются пропуски, которые нужно заполнить, и список переменных, которые можно использовать для заполнения каркаса. Заполняются пропуски именами переменных таким образом, чтобы перебрать все возможные варианты использования переменных в синтаксисе каркаса. Авторы пишут, что на основе анализа сообщений об ошибках в репозиториях компиляторов GCC [102] и Clang [103] было подсчитано, что минимальные тестовые программы, выявляющие ошибки, состоят из порядка 30 строк кода. А после анализа тестового набора c-torture [104] для GCC-4.8.5 исследователи подсчитали, что каждая функция в среднем содержит только 3 переменные с 7 местами, где она используется. То есть перебор всех возможных вариантов использования переменных в создаваемых синтаксических каркасах возможно произвести за относительно небольшое время. При этом из групп семантически эквивалентных программ необходимо оставить только по одному экземпляру. Исследователи использовали тестовый набор для GCC-4.8.5 в качестве основы для получения синтаксических каркасов и тестировали две стабильные версии компиляторов GCC-4.8.5 и Clang-3.6.1. Для анализа результатов исполнения тестовых программ использовалось дифференциальное сравнение. Применение данного подхода позволило обнаружить более 217 ошибок.
В статье [105] представлен инструмент JAttack, разработанный для тестирования компиляторов языка Java. Для создания тестовых программ на языке Java инструмент JAttack использует шаблоны на предметно-ориентированном языке (Domain-Specific Language, DSL) Java, в которых можно обозначить пропуски, которые во время генерации тестов заполняются случайными выражениями и значениями, доступными в пространстве поиска, определяемом пропуском. В статье описано использование шаблонов, разработанных вручную и полученных на основе проектов с открытым исходным кодом. Помимо тестирования компилятора с языка Java, инструмент был применен для тестирования JIT компилятора. Для проверки результатов в работе применяется дифференциальное сравнение и используются следующие комплекты разработчика приложений для языка Java версии 11.0.8: Oracle JDK [106], OpenJDK и OpenJ9. С помощью инструмента было найдено 6 ошибок, подтвержденных разработчиками из компании Oracle.
В диссертации [107] представлен подход для фаззинг тестирования компиляторов для языка Kotlin. Для генерации тестовых программ используется подход типо-ориентированной генерации по шаблону, являющийся развитием идеи из работы [101]. В качестве основы для создания тестовых программ используется набор программ на языке Kotlin. Из набора берется несколько программ: одна программа используется как основа для создания шаблона, вторая используется как источник для внесения мутаций в шаблон. После слияния программ они трансформируются в синтаксический шаблон, содержащий ячейки для заполнения в местах использования переменных. Для заполнения ячеек используется множество доступных в области видимости переменных и сгенерированные случайные выражения, имеющие совместимый тип. Для оценки результатов тестирования используется дифференциальное сравнении, в котором используются различные реализации, версии и режимы запуска компиляторов.
Рукописные тестовые наборы. Для тестирования и оценки характеристик ВМ применяются рукописные тестовые наборы. Такие тестовые наборы обычно нацелены на разные механизмы и специфицированное поведение проверяемых ВМ, что делает их интересным источником для создания новых тестовых программ. Примеры таких наборов представлены ниже.
Для оценки характеристик производительности ВМ Java разработан тестовый набор DaCapo [86, 108]. Он содержит используемые сообществом разработчиков программы с открытым исходным кодом, содержащими нетривиальные нагрузку на память.
Для обеспечения совместимого поведения между реализациями платформы Java SE (Standard Edition) используется набор тестов Java Compatibility Kit (JCK) [109]. Данный тестовый набор создан на основе требований JSR (Java Specification Request) к платформе Java.
Некоммерческая корпорация SPEC (Standard Performance Evaluation Corporation) предлагает для оценки производительности ВМ Java и используемых аппаратных систем многопоточный тестовый набор SPECjvm2008 [110]. Он содержит несколько реальных приложений и тестовых наборов, нацеленные на основные функциональные возможности Java платформы.
На основе проведенного обзора можно выделить следующие техники создания тестовых программ:
При создании тестовых программ важно представить их в формате, который пройдет проверки компилятора и верификатора. Для этого нужна дополнительная информация о бинарном или текстовом формате тестовых программ. На основе проведенного обзора литературы составим список источников, используемых для получения информации о формате тестовой программы:
При создании тестовых программ необходима информация о тестируемой системе, которая, например, может быть использована для формирования итогового вида тестовой программы или использоваться в техниках генерации. На основе проведенного обзора составлен список источников, содержащих информацию о тестируемой системе:
В некоторых подходах [89] техники генерации тестовых программ могут использоваться совместно с моделью, позволяющей отслеживать состояние системы, для построения сложных тестовых последовательностей, проверки корректности генерируемых программ или управления генерацией, в зависимости от покрытия.
На основе проведенного обзора был составлен список вариантов представления тестовых программ:
Варианты используемых техник создания тестовых программ, вариантов предоставления информации о тестируемой системе и формате тестовых программ довольно разнообразны. Хотя в последние годы фаззинг на основе мутаций стал наиболее популярным подходом, встречающимся в работах посвященным тестированию ВМ.
После создания тестовой программы и ее исполнения на тестируемой системе, необходимо оценить результат исполнения тестовой программы. Для этого создаются специальные артефакты, называемые оракулами. Термин тестовый оракул (test oracle) впервые был использован Уильямом Хауденом в 1978 [111]. Тестовый оракул – это механизм, позволяющий оценить результат тестового воздействия. Оракул сравнивает эталонный (корректный) результат исполнения тестовой программы с результатом от реализации, полученного при исполнении тестовой программы на тестируемой системе, что дает нам информацию о наличии расхождений. Если расхождения нет, то это говорит об отсутствии обнаруженных ошибок. Если есть расхождение, то говорится о нахождении ошибки.
На основе ранее проведенного обзора литературы составим список применяемых подходов для создания тестовых оракулов:
Из обзора видно, что наиболее популярным является дифференциальное сравнение. Это объясняется низкими затратами на создание оракула таким способом.
В рассмотренных работах для оценки тестового покрытия использовалась оценка покрытия тестируемой системы. Оценка тестового покрытия спецификаций, в рассмотренных работах, не предлагалась. Хотя в работе [89] авторы называют реализацию интерпретатора – исполнимой спецификацией, используют ее для генерации тестовых программ для JIT компилятора и покрытие кода интерпретатора достигается благодаря применяемому колколическому подходу.
На основе проведенного анализа предметной области, общей схемы функционального тестирования, рассмотренных типов существующих ВМ можно сформулировать ряд требований к методу функционального тестирования ВМ для объектно-ориентированных языков программирования.
Общие требования. Метод должен:
Для проверки функциональных требований с помощью функционального тестирования необходимы знания о спецификации ВМ. Для автоматизации использования документации на систему команд ВМ создают формальные спецификации ВМ. Составим список требований к таким спецификациям. Спецификации должны:
При функциональном тестировании ВМ в режиме “черного ящика” воздействие на систему необходимо осуществлять с помощью тестовых программ. Тестовые программ должны:
Оракул, осуществляющий проверку результатов тестирования, должен:
В табл. 2 представлено соответствие требованиям из подраздела 3.4 существующих подходов тестирования ВМ из подраздела 3.2.
Рассмотрим требование использования спецификаций. Этому требованию удовлетворяют два из рассматриваемых подходов: конколическое тестирование, предложенное Guillermo Polito, и ручная разработка. При этом для конколического тестирования в качестве спецификаций используется реализация интерпретатора, которую автор подхода называет “исполнимой спецификацией”, что нарушает требование к тестированию системы как черного ящика. При ручной разработке спецификации тестируемой системы изучаются инженерами-тестировщиками, что позволяет им использовать эти знания при написании тестовых программ, но такой подход невозможно автоматизировать. Как видно из табл. 2, ни один из приведенных подходов не позволяет специфицировать систему команд ВМ.
Табл. 2. Соответствие требованиям существующих подходов.
| Группа | Общее | Специ-фикация | Тестовые программы | Оракул | По-кры-тие | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| № | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| Ограничения и требования \ Инструменты и технологии | Система команд (ISA) | Трудоемкость | Уровень автоматизации | Применимость к новым ISA ВМ | Применимость на ранних этапах разработки ВМ | Использование спецификации | Возможность специфицировать ISA ВМ | Программа на байт-коде | Не зависит от корпусов программ | Работа с метаданными | Проверка возвращаемого значения | Проверка трассы | Оценка критерия тестового покрытия |
| Порождающие грамматики (Sirer) | Java | Ср. | Ср. | Нет | Нет | Нет | Нет | Да | Да | Нет | Да | Нет | Нет |
| Случайная генерация (Yoshikawa) | Java | Низ. | Выс. | Нет | Нет | Нет | Нет | Да | Да | Да | Да | Нет | Нет |
| Комбинаторный перебор (Calvagna) | Java | Ср. | Ср. | Нет | Да | Нет | Нет | Да | Да | Нет | Да | Нет | Нет |
| Бинарный фаззинг: DexFuzz | Dalvik | Низ. | Выс. | Да | Да | Нет | Нет | Да | Нет | Да | Да | Нет | Нет |
| Фаззинг: ClassFuzz | Java | Низ. | Выс. | Нет | Нет | Нет | Нет | Да | Нет | Да | Да | Нет | Нет |
| Фаззинг: Classming, JavaTailor, SJFuzz | Java | Низ. | Выс. | Нет | Нет | Нет | Нет | Да | Нет | Нет | Да | Нет | Нет |
| Конколическое тестирование (Polito) | Smalltalk | Низ. | Выс. | Нет | Нет | Част. | Нет | Да | Да | Да | Да | Нет | Част. |
| Рукописные тестовые наборы | Smalltalk, Java | Выс. | Низ. | Да | Да | Да | Нет | Нет | Да | Да | Да | Нет | Нет |
| Фаззинг: JITfuzz | Java | Низ. | Выс. | Нет | Нет | Нет | Нет | Да | Нет | Нет | Да | Нет | Нет |
Следующая группа требований связана с использованием тестовых программ для воздействия на тестируемую систему. За исключением рукописных тестовых наборов, все остальные приведенные в табл. 2 подходы используют тестовые программы на байт-коде. При этом для возможности применения подхода к новым системам команд ВМ необходимо отсутствие зависимости от корпусов программ, которые в некоторых подходах используются как основы для создания тестовых программ. Для создания разнообразных тестовых программ, охватывающих многие особенности ВМ, необходима возможность манипуляций с метаданными классов в тестовых программах, которая присутствует не во всех рассматриваемых подходах.
Все подходы, приведенные в табл. 2, позволяют оценивать результат тестового воздействия. В большинстве подходов для этого используется дифференциальное сравнение, где в качестве эталонной ВМ используется реализация от сторонних разработчиков. Что делает такие подходы не применимыми для проверок новых ISA ВМ, для которых не существует реализаций от сторонних разработчиков. Подходы, использующие для оценки результатов тестирования отличные от дифференциального сравнения механизмы, могут быть применены к новым системам команд ВМ. К таким подходам относится конколическое тестирование, ручная разработка и подход на основе комбинаторного перебора от Andrea Calvagna и Emiliano Tramontana. При этом ни в одном из рассмотренных подходов не используется проверка трасс, хотя именно она позволяет оценить корректность исполнения каждой инструкции тестовой программы.
Часть рассмотренных подходов использует критерий тестового покрытия в фаззинг тестировании для оценки полученных мутантов и управления мутациями. В подходе на основе конколичекого анализа генерируемые тестовые программы должны покрывать все пути исполнения инструкций по умолчанию. Но у большинства из рассмотренных подходов отсутствуют какие-либо встроенные механизмы для оценки критерия тестового покрытия спецификации. При этом для оценки критерия тестового покрытия достаточно часто используют покрытие кода тестируемой системы с помощью сторонних специальных инструментов сбора покрытия.
На основе проведенного анализа соответствия требованиям существующих подходов можно сделать вывод о том, что существующие подходы лишь частично соответствуют предъявляемым требованиям.
В статье была рассмотрена общая схема работы языковых ВМ, описаны различия между стековой и регистровой ВМ. Был проведен обзор существующих систем команд ВМ и приведены примеры наиболее популярных ВМ их реализующих. Из обзора видно, что тенденция к разработке как новых систем команд ВМ, так и их реализаций, по-прежнему сохраняется.
В работе представлен обзор существующих подходов тестирования ВМ и сформулированы требования к функциональному тестированию ВМ. Показано, что существующие подходы к тестированию ВМ лишь частично соответствуют предложенным требованиям к функциональному тестированию ВМ. А именно: отсутствует возможность использовать спецификации ВМ и возможность специфицировать новые системы команд ВМ; большинство предложенных подходов к созданию тестовых программ используют существующие корпусы программ, а для оценки результата тестового воздействия используют дифференциальное сравнение, что делает невозможным применение таких подходов к ВМ, реализующим новые системы команд; отсутствует оценка тестового покрытия спецификаций ВМ. Все это делает разработку подхода к функциональному тестированию ВМ, соответствующего предложенным в работе требованиям, актуальной задачей.
Александр Сергеевич ПРОЦЕНКО – научный сотрудник отдела технологий программирования ИСП РАН. Область научных интересов: языковые виртуальные машины, микропроцессоры, архитектура системы команд, верификация и тестирование.