
Виртуальная машина на Rust и JIT-компилятор для eBPF-программ
Виртуальная машина на Rust (пользовательское пространство) для eBPF
Этот crate содержит виртуальную машину для выполнения eBPF-программ. BPF, то есть Berkeley Packet Filter, — это ассемблероподобный язык, изначально разработанный для систем BSD, чтобы фильтровать пакеты в ядре с помощью таких инструментов, как tcpdump, и тем самым избегать бесполезного копирования в пользовательское пространство. Он был портирован в Linux, где эволюционировал в eBPF (extended BPF) — более быструю версию с большим количеством возможностей. Хотя BPF-программы изначально предназначены для выполнения в ядре, виртуальная машина из этого crate позволяет запускать их в пользовательских приложениях; она содержит интерпретатор, x86_64 JIT-компилятор для eBPF-программ, а также дизассемблер.
Она основана на программном обеспечении uBPF Рича Лейна, которое делает почти то же самое, но написано на C.
Предполагается, что crate компилируется и работает на Linux, MacOS X и Windows, хотя JIT-компилятор в настоящее время не работает с Windows.
Этот crate доступен на crates.io, поэтому он
должен работать из коробки при добавлении его как зависимости в ваш файл Cargo.toml:
[dependencies]
rbpf = "0.4.1"
Вы также можете использовать версию для разработки из этого репозитория GitHub. Для этого достаточно добавить следующее в ваш Cargo.toml:
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }
Конечно, если предпочитаете, вы можете клонировать его локально, возможно, модифицировать крейт,
а затем указать путь к вашей локальной версии в Cargo.toml:
[dependencies]
rbpf = { path = "path/to/rbpf" }
Затем укажите в исходном коде, что вы хотите использовать этот crate:
extern crate rbpf;
API довольно хорошо документирован внутри исходного кода. Вы также должны иметь возможность получить доступ к онлайн-версии документации здесь, автоматически сгенерированной из версии на crates.io (может быть не актуальной по сравнению с основной веткой). Примеры и модульные тесты также должны оказаться полезными. Ниже приведено краткое описание того, как использовать этот крейт.
Вот шаги, которые нужно выполнить для запуска программы eBPF с помощью rbpf:
eBPF изначально был разработан для фильтрации пакетов (сейчас у него есть некоторые другие хуки
в ядре Linux, такие как kprobes, но это не покрывается rbpf). Как следствие,
большинство инструкций загрузки и сохранения программы выполняются
над областью памяти, представляющей данные пакета. Однако в ядре
Linux программа eBPF не обращается к этой области данных напрямую: изначально
она имеет доступ к C-структуре struct sk_buff, которая представляет собой буфер, содержащий
метаданные о пакете — включая адреса памяти начала и
конца области данных пакета. Таким образом, программа сначала загружает эти указатели из
sk_buff, а затем может получить доступ к данным пакета.
Это поведение можно воспроизвести с помощью rbpf, но это не обязательно. По этой причине у нас есть несколько структур, представляющих разные виды виртуальных машин:
struct EbpfVmMbuffer имитирует ядро. Когда программа запускается,
адрес, переданный в её первый регистр eBPF, будет адресом буфера метаданных,
предоставленного пользователем, и ожидается, что он содержит указатели на
начало и конец области памяти данных пакета.
struct EbpfVmFixedMbuff имеет одну цель: обеспечить выполнение программ,
созданных для совместимости с ядром, при этом избавляя пользователя от необходимости вручную
обрабатывать буфер метаданных. Фактически, эта структура имеет статический
внутренний буфер, который передаётся программе. Пользователь должен указать
значения смещений, по которым программа eBPF ожидает найти начало и конец
данных пакета в буфере. При вызове функции, запускающей программу
(JIT-скомпилированную или нет), структура автоматически обновляет адреса в этом
статическом буфере, по указанным смещениям, для начала и конца
данных пакета, для которого вызывается программа.
struct EbpfVmRaw предназначена для программ, которые хотят работать непосредственно с данными пакета.
Буфер метаданных не задействован, программа eBPF напрямую получает
адрес данных пакета в своём первом регистре. Это поведение
uBPF.
struct EbpfVmNoData не принимает никаких данных. Программа eBPF не принимает
никаких аргументов, и её возвращаемое значение детерминировано. Не уверен, что
для этого есть valid use case, но, по крайней мере, это очень полезно для
модульных тестов.
Все эти структуры реализуют одни и те же публичные функции:
// called with EbpfVmMbuff:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmMbuff<'a>, Error>
// called with EbpfVmFixedMbuff:: prefix
pub fn new(prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> Result<EbpfVmFixedMbuff<'a>, Error>
// called with EbpfVmRaw:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmRaw<'a>, Error>
// called with EbpfVmNoData:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmNoData<'a>, Error>
Это используется для создания нового экземпляра виртуальной машины. Возвращаемый тип зависит от структуры, из которой вызывается функция. Например,
rbpf::EbpfVmRaw::new(Some(my_program)) вернёт экземпляр struct rbpf::EbpfVmRaw (обёрнутый в Result). При загрузке программы она
проверяется с помощью очень простого верификатора (ничего близкого к верификатору для ядра
Linux). Пользователи также могут заменить его на пользовательский верификатор.
Для struct EbpfVmFixedMbuff в конструктор необходимо передать два дополнительных аргумента:
data_offset и data_end_offset. Это смещения (в байтах), по которым указатели на начало и на конец, соответственно,
области памяти данных пакета должны быть сохранены во внутреннем буфере метаданных
каждый раз при выполнении программы. Другие структуры не используют этот механизм и
не нуждаются в этих смещениях.