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

Однако дела на фронте Rust BPF были заняты! В конце октября я начал работать над блогом о текущем положении вещей, ровно через год после того, как начал этим заниматься. Делая это, я, наконец, почувствовал себя достаточно вдохновленным, чтобы попытаться добавить цель BPF в rustc, что-то, что было в моем списке задач в течение очень долгого времени, но никогда не находил времени для работы. ( Ааа… если бы кто-то захотел спонсировать всю эту работу… подмигивает!)

Пару недель назад я, наконец, отправил pull request, чтобы слить новые цели. Изменения были довольно простыми, единственной неожиданностью было то, что мне пришлось написать https://github.com/alessandrod/bpf-linker — частичный компоновщик, необходимый для включения rustc для вывода байт-кода BPF.

Я собираюсь рассказать вам, почему мне пришлось написать компоновщик, но сначала давайте начнем с рассмотрения того, как clang — де-факто стандартный компилятор BPF — компилирует код C в BPF.

Как проекты BPF компилируются с clang

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

Из-за этого, а также из-за того, что в течение длительного времени вызовы функций были запрещены, поэтому все приходилось встраивать, программы BPF, написанные на C, обычно компилируются как единая единица компиляции с библиотечным кодом, написанным в заголовке files и включены в директивы#include.

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

Как компилируются проекты Rust

Rust использует другую модель компиляции. Код разбит на ящики. Крейты нельзя объединять вместе с директивами #include, они всегда компилируются независимо как одна или несколько единиц компиляции.

Рассмотрим следующий пример:

alessandro@ubvm:~/src/app$ cargo tree
app v0.1.0 (/home/alessandro/src/app) 
└── dep v0.1.0 (/home/alessandro/src/dep)
alessandro@ubvm:~/src/app$ cargo build
Compiling dep v0.1.0 (/home/alessandro/src/dep)
Compiling app v0.1.0 (/home/alessandro/src/app)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s

app — это ящик приложения, зависящий от библиотеки dep. При построении происходит следующее:

Компилятор rust вызывается дважды: сначала для компиляции крейта dep как библиотеки ржавчины, а затем для компиляции крейта app как исполняемого файла. Когда крейт app компилируется, предварительно скомпилированный крейт dep предоставляется в качестве входных данных для компилятора с помощью параметра --extern.

Эта модель компиляции всегда создает несколько объектных файлов, которые затем должны быть связаны вместе для получения окончательного результата. Компилятор rust использует внутреннюю абстракцию компоновщика, реализации которой порождаются внешними компоновщиками, такими как ld, lld, link.exe и другие.

Поэтому, чтобы добавить новую цель с этой моделью, нам нужен компоновщик для цели. Поскольку clang никогда ничего не связывает при нацеливании на BPF, оказывается, что lld — компоновщик LLVM — вообще не может связывать BPF. Поэтому я написал новый компоновщик.

Новый (частичный) линкер BPF

bpf-linker принимает битовый код LLVM в качестве входных данных, дополнительно применяет целевые оптимизации и выводит один объектный файл BPF. Входными данными могут быть файлы с битовым кодом (.bc), объектные файлы со встроенным битовым кодом (например, файлы .o, созданные при компиляции с -C embed-bitcode=yes), или архивные файлы (.a или .rlib).

Компоновщик работает со всем, что может выводить биткод LLVM, включая clang. Есть несколько причин использовать в качестве входных данных биткод вместо объектных файлов.

Только подмножество Rust (как и только подмножество C) может быть скомпилировано в байт-код BPF. Поэтому bpf-linker пытается отодвинуть генерацию кода как можно позже в процессе компиляции, после того как были применены оптимизации времени компоновки и устранен мертвый код. Это позволяет избежать потенциальных сбоев, генерирующих байт-код для неподдерживаемого кода Rust, который на самом деле не используется (например, части крейта core, которые никогда не используются в контексте BPF).

Другая причина заключается в том, что компоновщику может потребоваться применить дополнительные оптимизации, такие как — unroll-loops и — ignore-inline-never при работе со старыми версиями ядра, которые не поддерживают циклы и вызовы.

Не одна, а две мишени БНФ!

Форк rustc по адресу https://github.com/alessandrod/rust/tree/bpf включает две новые цели, bpfel-unknown-none и bpfeb-unknown-none, которые генерируют BPF с прямым и обратным порядком байтов соответственно. Цели автоматически вызывают bpf-linker, поэтому с помощью этого форка компиляция проекта BPF с помощью Rust, наконец, так же проста, как:

alessandro@ubvm:~/src/app$ cargo build --target=bpfel-unknown-none Compiling dep v0.1.0 (/home/alessandro/src/dep)
Compiling app v0.1.0 (/home/alessandro/src/app)
Finished dev [unoptimized + debuginfo] target(s) in 1.98s
alessandro@ubvm:~/src/app$ file target/bpfel-unknown-none/debug/app
target/bpfel-unknown-none/debug/app: ELF 64-bit LSB relocatable, eBPF, version 1 (SYSV), not stripped

Объединение целей, вероятно, займет некоторое время, но не беспокойтесь! С небольшой хитростью вы можете использовать компоновщик для компиляции кода BPF со стабильной версией Rust уже сегодня!

Я заставил bpf-linker реализовать wasm-ld совместимую командную строку. Поскольку rustc уже знает, как вызывать wasm-ld при работе с веб-сборкой, его можно заставить использовать bpf-linker со следующими параметрами:

alessandro@ubvm:~/src/app$ cargo rustc -- \
        -C linker-flavor=wasm-ld \
        -C linker=bpf-linker \
        -C linker-plugin-lto
Compiling dep v0.1.0 (/home/alessandro/src/dep)
Compiling app v0.1.0 (/home/alessandro/src/app)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s 
alessandro@ubvm:~/src/app$ file target/debug/app
target/debug/app: ELF 64-bit LSB relocatable, eBPF, version 1 (SYSV), not stripped

Давайте посмотрим, что делают эти опции:

  • -C linker-flavor=wasm-ld сообщает компилятору, что компоновщик поддерживает те же параметры командной строки, что и wasm-ld
  • -C linker=bpf-linker настраивает bpf-linker как компоновщик для порождения
  • -C linker-plugin-lto указывает rustc передать битовый код LLVM компоновщику.

И вуаля! Скомпилируйте немного BPF со стабильной ржавчиной прямо сейчас 🎉

Что дальше

bpf-linker явно новый и нуждается в дальнейшем тестировании. В течение следующих нескольких недель я собираюсь добавить больше модульных тестов и попробовать их на большем количестве кода Rust. Я также подумываю связать с ним весь код Cilium BPF, просто чтобы протестировать большую и сложную кодовую базу.

Работая над целью rustc, в какой-то момент я немного отклонился от темы и закончил тем, что внес некоторые изменения в LLVM и ядро, так что я попытаюсь закончить их. Они необходимы для реализации встроенной функции llvm.trap, чтобы panic!() можно было реализовать общим способом, вместо того, чтобы прибегать к специфичным для программы хакам, таким как переход к пустой программе с помощью bpf_tail_call(). Я, пожалуй, сделаю об этом отдельный пост.

Наконец-то после моего последнего поста я получил по-настоящему отличный отзыв! Мне было особенно приятно услышать от нескольких компаний, которые используют BPF и рассматривают возможность его использования с Rust.

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

Первоначально опубликовано на https://confused.ai.