Программирование на языке Ada

Язык программирования Ада

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

Программирование на языке Ада

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

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

Про сборку программ на аде было рассказано в моей предыдущей статье «Язык программирования Ada», но если пропустили — не страшно, ничего сложного в этом нет. Нужно поставить GNAT — он входит в состав GCC и всегда есть в репозиториях, — сохранить код в файл something.adb и выполнить gnatmake something.adb.

Желательно, чтобы на месте something в имени файла было имя основной процедуры, иначе компилятор выдаст предупреждение. Исполняемый файл gnatmake автоматически назовет по имени файла с кодом, а не a.out.

РЕКОМЕНДУЕМ:
Как создать сетевой протокол

По ряду причин я решил написать утилиты для определения гипервизора, в котором работает виртуальная машина. В VyOS мы включаем эту информацию в вывод команды show version. Для получения самой информации исторически использовалась самописная утилита на довольно грязном C, которая не поддерживала некоторые менее популярные гипервизоры, и у меня давно было желание ее на что-нибудь заменить.

Существующие решения, такие как virt-what, вызывают у меня смешанные чувства. Смесь C и скриптовых языков, на мой взгляд, выглядит неэстетично. Эстетика — вещь субъективная, но есть и объективные проблемы, например поддержка только GNU/Linux и отказ работать без прав суперпользователя.

Мне хотелось, чтобы замена старому коду принесла пользу не только мне и пользователям VyOS, поэтому я поставил следующие требования:

  • поддержка как минимум GNU/Linux и FreeBSD;
  • возможность работы с правами обычного пользователя;
  • по крайней мере техническая возможность работы на разных архитектурах;
  • простая и доступная незнакомому с адой пользователю процедура сборки.

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

В этой статье мы рассмотрим проект изнутри и познакомимся с возможностями языка ада и инструментами GNAT, которые потребовались для его разработки.

Способы определения гипервизора

Как, собственно, определить, работает ли система в виртуальной машине, и если да, то на каком гипервизоре? На платформе x86 все системы виртуализации с этой точки зрения можно поделить на две группы: одни поддерживают общий стандарт de facto — передачу информации через вызов инструкции cpuid, другие не поддерживают.

Определение через [crayon-662bad2185927845851435-i/]

К первой группе относятся Xen в режиме аппаратной виртуализации, KVM, bhyve, VMware и Hyper-V. Я не уверен, кто из них ввел этот механизм первым, но работает он у всех одинаково.

Инструкция cpuid была впервые реализована компанией Intel и с тех пор присутствует во всех процессорах x86. Стоит отметить, что для совместимости она использует 32-разрядные регистры даже в 64-разрядном режиме. Вид возвращаемой информации зависит от значения в регистре eax.

Гипервизоры из первой группы перехватывают вызовы cpuid и обладают дополнительными возможностями. Для передачи информации о самом факте работы ОС в виртуальной машине применяется разряд 31-го регистра ecx. На физических машинах он всегда установлен на ноль согласно документации Intel, а гипервизоры устанавливают его на единицу.

Получить название гипервизора можно, вызвав cpuid со значением 0x40000000 в регистре eax. Название передается в виде строки длиной до двенадцати символов в регистрах ebx, ecx и edx. К примеру, Xen использует строку XenVMMXenVMM, а VMware — VMwareVMware.

Название производителя процессора передается таким же способом. Вот поэтому используются строки вроде GenuineIntel и AuthenticAMD — не чтобы убедить пользователя, что процессор не поддельный, а чтобы строка укладывалась в три 32-разрядных регистра без дополнения нулями.

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

Прочие способы

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

В этих случаях приходится применять другие способы, такие как проверка названия производителя из SMBIOS или наличия специфичных устройств PCI, вроде видеокарты innotek Gmbh в VirtualBox.

Эти способы, в отличие от cpuid, не так универсальны, и на разных ОС их придется реализовать по-разному. Чтобы это сделать, нам придется использовать интерфейс с libc и работу с файлами.

Работа с машинным кодом и данными

Ассемблерные вставки

Ада проектировалась как язык системного программирования, а какое системное программирование совсем без машинного кода? Все возможности для этого присутствуют.

Прежде всего нам потребуются беззнаковые целочисленные типы. Пакет Interfaces предоставляет все распространенные типы, включая нужный нам для работы с 32-разрядными регистрами Unsigned_32.

Ничего магического в беззнаковых типах нет, и их можно легко определить самим: type My_Unsigned_32 is mod 2**5. Или даже type Strange_Unsigned is mod 19.

Нам также нужно знать, как работать с шестнадцатеричными числами. Ада использует несколько необычный синтаксис: <основание>#<значение>#. Таким образом, число 0x40000000 пишется 16#40000000#. Основание системы счисления может быть вообще любым, хоть троичным — такое нам, к счастью, не потребуется.

Процедуру Asm можно найти в пакете System.Machine_Code. Конкретный интерфейс и реализация стандартом не регламентируются и оставлены на усмотрение разработчиков компилятора. У процедуры два параметра: Inputs и Outputs, которые могут быть либо отдельными значениями, либо массивами. Синтаксис обращений к регистрам напоминает макрос __asm__ из GNU C, но есть значительное отличие: регистры сопоставляются переменным не просто по указателям, а с помощью функций-атрибутов Asm_Input и Asm_Output, например Unsigned_32'Asm_Input.

Как везде в GCC, по умолчанию используется синтаксис AT&T.

Для демонстрации мы напишем простую программу, которая складывает два 32-разрядных числа с помощью addl %eax, %ebx.

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

Вывести ассемблерный листинг можно как обычно в GCC: gcc -S asm_test.adb. В полученном файле asm_test.s мы сможем увидеть что-то вроде

[/crayon]

Двоичная арифметика

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

Все поразрядные логические операции есть в стандартной библиотеке, без импорта дополнительных пакетов доступны not, and, or и xor. Операции сдвига доступны из того же пакета Interfaces и называются Shift_Left и Shift_Right. Благодаря перегрузке функций они доступны для всех определенных там беззнаковых типов под одинаковыми именами.

РЕКОМЕНДУЕМ:
Как сделать свою структуру данных в Python совместимой с фичами

Если мы уже как-то получили значение из регистра ecx, с помощью сдвига на 31 разряд и применения маски 1 мы можем определить, находимся мы в виртуальной машине или нет:

Таким же способом мы можем разобрать 32-разрядное число на байты. Применим маски 16#FF и сделаем сдвиг на восемь двоичных разрядов, пока число не станет равным нулю. Останется только преобразовать байты в символы, для этого мы используем атрибут Val типа Character ( Character'Val), эквивалент функции chr.

Собираем воедино и определяем гипервизор через [crayon-662bad218595f861893929-i/]

[/crayon]
В hvinfo этот подход используется в функциях Hypervisor_Present и Get_Vendor_String из hypervisor_check.adb.

Вызов функций из библиотек на C

Иногда самый простой способ определения гипервизора требует выполнения команд ОС. Например, в FreeBSD информацию о Xen можно получить через sysctl kern.vm_guest. Но для этого нам надо научиться выполнять внешние программы. Это можно было бы сделать встроенными средствами через пакет POSIX, но для простоты и демонстрационной ценности мы используем функцию system из libc.

Ада предоставляет простые и развитые средства FFI и не требует libffi для их работы. Можно импортировать функции из библиотек на C, C++ и Fortran, экспортировать функции из ады и создавать библиотеки для этих языков. Мы рассмотрим самый простой вариант применения.

Для взаимодействия с библиотеками на C используется пакет Interfaces.C. Для преобразования символьных и строковых типов там определены функции To_C и To_Ada. Например, эквивалент char* там называется Char_Array. Поскольку ада не использует нулевой байт как признак конца строки, передача строк без преобразования может привести к самым неожиданным результатам.

Перед тем как импортировать функцию, нужно написать ее объявление. Сам импорт функций производится с помощью директивы компилятора Import. Она принимает три аргумента: конвенцию вызовов, имя функции, каким оно будет видно в аде, и имя функции из библиотеки. В нашем случае для импорта system() из libc потребуется pragma Import (C, System, "system");.

Вот пример простейшей программы, которая вызывает uname -a и выводит код возврата, если он не равен нулю:

В hvinfo этот подход используется в функциях VirtualBox_PCI_Present и Xen_Present из hypervisor_check.adb.

Чтение файлов

В Linux очень много информации можно получить из sysfs, но для этого нам нужно научиться читать файлы. Для демонстрации мы прочитаем название производителя машины из /sys/class/dmi/id/sys_vendor, если каталог dmi/ существует. Многие гипервизоры помещают туда свое название, и некоторые из них можно определить только этим способом.

РЕКОМЕНДУЕМ:
Олимпиады по программированию

Функции для работы с текстовыми файлами можно найти в пакете Ada.Text_IO. Для сериализации и ввода-вывода структурированных типов лучше было бы использовать пакеты потоков ввода-вывода Ada.Streams или Ada.Sequential_IO, но для чтения из sysfs нам это не потребуется. Поскольку интересующие нас файлы всегда состоят из одной строки, нам даже не понадобится искать конец файла, так что просто запомним, что для этого использовалась бы функция End_Of_File. В целом интерфейс Ada.Text_IO привычен: открываем файл, получаем дескриптор, используем его как аргумент в функциях чтения и записи.

Удобный пакет для работы с каталогами появился в Ada2005 и называется Ada.Directories. Для проверки существования каталога с данными из DMI мы используем функцию Ada.Directories.Exists.

Вот наша программа:

В hvinfo этот подход используется для определения VirtualBox и Parallels на Linux.

Условная компиляция и макросы

Условная компиляция в аде используется редко. Компиляторы ады всегда удаляют заведомо недостижимый код, но только после проверки его правильности, так что не нужно тестировать сборку со всеми возможными комбинациями опций. Стандартный способ — определить константы для опций сборки и просто писать что-то вроде if My_Feature then ....

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

В качестве препроцессора GNAT используется программа gnatprep. Синтаксис ее опций мало отличается от препроцессора C в GCC.

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

С помощью команды gnatprep -D VERSION=1.0 config.ads.in config.ads можно заменить $VERSION на 1.0.

Условная компиляция поддерживается самим компилятором, нужные фрагменты заворачиваются в директиву #if <var> then ... #else ... #end if, а переменные хранятся в файле, который можно указать в опции
-gnatep=<some file>.

Например, пусть у нас есть такой код:

[/crayon]
Чтобы собрать его с поддержкой With_Cool_Feature, нужно создать файл (назовем его config.def) с содержанием With_Cool_Feature := True и выполнить команду gnatmake -gnatep=config.def myfile.adb.

В hvinfo используются оба подхода. Для установки констант версии программы и целевой ОС применяется gnatprep, скрипт mkconfig.def вызывает его и генерирует модуль config.ads из файла config.ads.in. По крайней мере техническую возможность сборки на отличных от x86 платформах обеспечивает условная компиляция, файл с определениями переменных для нее генерирует скрипт mkdefs.sh. Пример использования можно увидеть в функции CPUID из hypervisor_check.adb.

Сборка проекта

Поддержка ады в популярных средствах сборки проектов вроде CMake и autotools рудиментарна либо отсутствует вовсе. В случае с autotools это особенно странно, поскольку они оба — проекты GNU и ада поддерживается в GCC еще с начала девяностых. К счастью, в GNAT есть собственный и весьма достойный высокоуровневый инструмент сборки под названием gprbuild.

Сценарии сборки хранятся в файлах с расширением .gpr, и их синтаксис напоминает саму аду. Вот файл из hvinfo:

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

Запустить сборку можно командой gprbuild -P<project file>. Имя файла проекта указывается без расширения. Например, в hvinfo этот файл называется hvinfo.gpr, а команда для сборки будет gprbuild -Phvinfo.

РЕКОМЕНДУЕМ:
Безопасность JavaScript

Нужно отметить, что gprbuild управляет только сборкой, но не конфигурацией, аналог ./configure с его помощью не создать. Поскольку hvinfo не имеет настраиваемых пользователем опций сборки, я ограничился теми скриптами на Bourne shell, которые определяют ОС и архитектуру процессора через uname и Makefile. В силу простоты формата файлов для gnatprep и -gnatep написать свой скрипт настройки можно было бы на любом языке.

Среда разработки GNAT Programming Studio для сборки проектов использует именно gprbuild.

Заключение

Несмотря на некоторые сложности, поставленная задача оказалась вполне выполнимой. Проект работает и достаточно прост в сборке и упаковке в пакеты RPM/DEB.

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

Понравилась статья? Поделиться с друзьями:
Комментарии: 1
  1. Geo

    Интересная статья. Спасибо!

Добавить комментарий