
Как писать программы Solana на ассемблере SBPF
Содержание
- Архитектура виртуальной машины sBPF
- Архитектура системы команд sBPF
- Модель памяти sBPF
- Системные вызовы Solana в sBPF
- Настройте среду
- Настройте проект
- Пример memo-программы на ассемблере sBPF
- Соберите и разверните программу
- Взаимодействуйте с программой
- Проверка входных данных
- Проверка границ памяти
- Управление регистрами
- Безопасная арифметика
- Проверка параметров системных вызовов
Сейчас в экосистеме Solana параллельно идет гонка за предельной оптимизацией программ.
На высоком уровне библиотеки вроде Pinocchio меняют подход к разработке на Rust и обеспечивают многократное повышение вычислительной эффективности. Тем временем на самом низком уровне группа преданных разработчиков, объединенных общим недоверием к компилятору, идет еще дальше. Вместо написания программ Solana на компилируемых языках вроде Rust или C они тщательно создают байт-код вручную, чтобы добиться максимальной производительности от каждой инструкции.
Такой прирост на низком уровне возможен, только если напрямую давать VM команды на ее родном языке: ассемблере sBPF, собственной версии расширенного Berkeley Packet Filter (eBPF) от Solana. Именно этот байт-код используется и исполняется в каждой ончейн-программе.
Ассемблер sBPF дает разработчикам прямой доступ к самому низкоуровневому интерфейсу Solana Virtual Machine. Компилятор Rust и LLVM пытаются оптимизировать код, но из-за недостаточной выразительности синтаксиса языка или нехватки контекста для выбора лучшего варианта компиляции они часто генерируют неоптимальный байт-код. Опытный разработчик, получив полный контроль над инструкциями благодаря ассемблеру, может добиться лучшего результата.
Дополнительный контроль ухудшает удобство разработки, но значительно сокращает расход вычислительных единиц и размер бинарного файла, а значит, и ренту. Такая экономия особенно важна в условиях высокой конкуренции и для критичных к производительности операций.
В то же время есть веские причины не писать все программы на ассемблере.
Хотя ситуация значительно улучшилась, исторически инструменты были ограничены. Что еще важнее, прирост производительности часто требует вручную проверять корректность и повышает стоимость аудита. Причины — нехватка автоматизированных инструментов и более сложный для чтения, написания и понимания синтаксис.
С другой стороны, компилируемые языки можно считать черным ящиком, который скрывает решения компилятора. Поэтому дополнительная прозрачность и контроль ассемблера нередко позволяют увидеть то, что сложно заметить при работе с компилируемыми языками. Более того, большинство недавних прорывов в производительности наших SDK для Rust произошло именно благодаря пониманию, что мы можем вручную создавать более эффективный байт-код, чем компилятор.
Из этой статьи вы узнаете:
- Что такое ассемблер sBPF и как он обеспечивает прямой контроль над виртуальной машиной
- Как Berkeley Packet Filter развился до eBPF и почему Solana выбрала его
- Как устроены архитектура виртуальной машины sBPF, система команд и модель памяти
- Как настроить среду разработки и собирать программы sBPF
- Как пошагово программировать на ассемблере на практическом примере memo-программы
- Какие ключевые аспекты безопасности нужно учитывать при написании низкоуровневого кода
Что такое ассемблер?
Ассемблер — это понятная человеку форма машинного кода, то есть низкоуровневый язык программирования, который напрямую соответствует системе команд CPU или VM.
Вместо переменных и функций ассемблер работает с регистрами (быстрыми временными хранилищами в CPU), адресами памяти (физическими расположениями в RAM или на диске) и базовыми операциями: загрузкой (чтением из памяти), сохранением (записью в память), арифметическими действиями и переходами (управлением потоком выполнения).
Каждая ассемблерная инструкция однозначно соответствует эквивалентной инструкции машинного кода.
Благодаря такому однозначному соответствию программист точно контролирует действия процессора: какие регистры хранят данные, как осуществляется доступ к памяти и в какой последовательности выполняются операции.
В отличие от высокоуровневых языков, где одна функция может порождать десятки инструкций, ассемблер обеспечивает полную прозрачность и контроль над поведением машины без непрозрачных абстракций.
Что такое Berkeley Packet Filter (BPF) и eBPF?
Berkeley Packet Filter (BPF) появился в 1992 году как виртуальная машина для эффективной фильтрации сетевых пакетов в ядрах Unix. Исходная версия BPF использовала простую систему команд и регистровую архитектуру, которые позволяли безопасно исполнять изолированный код внутри ядра.
Extended Berkeley Packet Filter (eBPF) модернизировал эту концепцию и превратился из пакетного фильтра в виртуальную машину общего назначения. eBPF получил 64-разрядную архитектуру, больше регистров и более богатую систему команд. Это позволило безопасно запускать в пространстве ядра сложные программы для работы с сетями, обеспечения безопасности и мониторинга систем.
Solana выбрала eBPF, поскольку он предоставлял проверенную и безопасную среду исполнения со встроенной изоляцией. Изоляция не дает программам обращаться к системным ресурсам, вызывать сбои нод или мешать другим программам. Детерминированное исполнение при этом гарантирует, что все валидаторы получают одинаковые результаты.
Кроме того, регистровая архитектура и зрелый набор инструментов хорошо подходили для высокопроизводительного ончейн-исполнения, а существующий бэкенд LLVM позволял разработчикам компилировать код с высокоуровневых языков вроде Rust.
Архитектура виртуальной машины sBPF
При исполнении программы Solana среда выполнения загружает байт-код sBPF в память, выполняет статическую проверку безопасности — ищет бесконечные циклы, недопустимый доступ к памяти и неправильное использование инструкций — а затем исполняет его в виртуальной машине.
VM предоставляет контролируемую 64-разрядную среду исполнения. В ней программы полностью изолированы от хост-системы и других программ, а весь доступ к ресурсам осуществляется через среду выполнения.
Архитектура системы команд sBPF
sBPF использует одиннадцать 64-разрядных регистров (r0-r10). r10 служит указателем кадра только для чтения, а r0 — регистром возврата.
Инструкции имеют единый формат: коды операций определяют действие (арифметика, логика, доступ к памяти, переходы), а операнды указывают исходные и целевые регистры, смещения и/или непосредственные значения.
Основные категории инструкций включают операции ALU (сложение, вычитание, побитовые операции), операции с памятью (загрузка и сохранение) и управление потоком выполнения (условные и безусловные переходы).
Модель памяти sBPF
Программы sBPF работают со структурированной схемой памяти: стеком размером 4 КБ для локальных переменных и вызовов функций, кучей для динамического выделения памяти, доступными только для чтения данными программы с байт-кодом и константами, а также областями данных аккаунтов, которые соответствуют аккаунтам Solana и доступны программе во время исполнения.
Весь доступ к памяти проверяется на выход за границы. Программы не могут обращаться к памяти за пределами выделенных им областей.
Системные вызовы Solana в sBPF
Программы sBPF не могут напрямую обращаться к системным ресурсам или выполнять операции ввода-вывода. Вместо этого они запрашивают сервисы через системные вызовы — специальные инструкции, которые передают управление среде выполнения Solana.
В ассемблере sBPF системные вызовы выполняются с помощью инструкции call и символа вызова, который компилятор при сборке преобразует в цель вызова. Сейчас системные вызовы используют текстовые динамические релокации — сложную систему таблиц поиска по строкам, которая при JIT-компиляции сопоставляет символы с 32-разрядным хешем Mumur3. Однако существует активное предложение заменить их статическими системными вызовами, что значительно упростит соглашения о вызовах. При выполнении системного вызова аргументы передаются через регистры с 1-го по 5-й, причем регистр 5 иногда используется для временного сохранения данных из стека, а возвращаемые значения записываются в r0.
К распространенным системным вызовам относятся операции с памятью (sol_memcpy, sol_memcmp), криптографические функции (хеширование, проверка подписей), журналирование и межпрограммные вызовы.
Хотя все обычные инструкции sBPF имеют стоимость 1 CU, вычисление расхода вычислительных единиц для системных вызовов устроено совсем иначе. При добавлении каждого системного вызова в протокол проводится бенчмарк, по результатам которого определяется базовая стоимость вызова (например, для вызова CPI она составляет 1000). В некоторых случаях также начисляется дополнительная переменная стоимость, зависящая от объема обрабатываемых данных.
Руководство по ассемблеру SBPF
Настройте среду
Раньше для написания кода на ассемблере sBPF требовался полный набор инструментов Solana. Это был громоздкий и сложный процесс, зависевший от платформы.
Поэтому Dean Little создал sBPF SDK — комплексное решение для инициализации, сборки, компиляции, тестирования и развертывания программ sBPF.
SDK можно установить в любой операционной системе с помощью Cargo:
cargo install --git https://github.com/blueshift-gg/sbpf.gitПеред началом работы с кодом также рекомендуем установить расширение sBPF Assembly для VS Code, которое поддерживает подсветку синтаксиса, автодополнение и обнаружение ошибок.
Настройте проект
Создайте шаблон нового проекта:
sbpf init <name_of_the_project>Так вы создадите проект с тестами Rust на базе Mollusk.
Если вам нужен шаблон с тестами TypeScript, инициализируйте его так:
sbpf init <name_of_the_project> --ts-tests Пример memo-программы на ассемблере sBPF
Сложно представить программу проще memo-программы. Именно поэтому она идеально подходит для знакомства с ассемблером sBPF.
Программа выполняет одну задачу: принимает любые отправленные ей данные инструкции и записывает их в журнал блокчейна. Никаких аккаунтов и сложной логики — только полный контроль над Solana Virtual Machine на уровне инструкций.
Сначала рассмотрим программу целиком:
.equ NUM_ACCOUNTS, 0x00
.equ DATA_LEN, 0x08
.equ DATA, 0x10
.globl entrypoint
entrypoint:
ldxdw r0, [r1+NUM_ACCOUNTS]
ldxdw r2, [r1+DATA_LEN]
add64 r1, DATA
call sol_log_
exitОпределите константы
В начале программы определяются три константы, которые соответствуют структуре сериализованной области входных данных среды выполнения Solana:
.equ NUM_ACCOUNTS, 0x00 // Account count offset
.equ DATA_LEN, 0x08 // Data length offset
.equ DATA, 0x10 // Data start offsetЭти смещения указывают, где среда выполнения Solana размещает данные инструкции в памяти.
Когда VM вызывает нашу программу, она передает ей структурированный буфер в регистре r1. Эти константы позволяют перемещаться по его структуре.
Инструменты вроде sbpf.xyz могут автоматически вычислять эти смещения на основе структуры ваших аккаунтов и данных инструкции.
Создайте точку входа
Затем создадим точку входа и проверку для программы.
.globl entrypoint
entrypoint:
ldxdw r0, [r1+NUM_ACCOUNTS]Директива точки входа .globl указывает компоновщику сделать символ точки входа глобально видимым. Среда выполнения Solana ищет этот символ, чтобы определить, откуда начинать исполнение программы. Интересный факт: обычно мы называем его entrypoint, но на самом деле имя может быть любым.
Первая инструкция использует хитрый прием проверки, характерный для ассемблера. r0 — наш регистр возврата, а любое возвращаемое значение, отличное от 0, служит кодом ошибки. Поэтому загрузка количества аккаунтов непосредственно в r0 заставляет программу завершиться с ненулевым кодом ошибки, если ей передали хотя бы один аккаунт.
Так нам не нужно вручную пропускать все входные аккаунты, чтобы проверить смещение данных инструкции в VM.
Выполните системный вызов sol_log
Теперь можно выполнить системный вызов sol_log_:
ldxdw r2, [r1+DATA_LEN] ; Load memo length
add64 r1, DATA ; Point r1 to memo data
call sol_log_ ; Log the memo
exit ; Exit with r0 valueСистемный вызов sol_log_ ожидает длину сообщения в r2 и указатель на записываемое в журнал сообщение в r1. К счастью, в сериализованной области входных данных среда выполнения автоматически добавляет перед данными инструкции 64-разрядный счетчик длины. Поэтому для выполнения системного вызова sol_log_ достаточно:
- Загрузить значение по смещению DATA_LEN в r2
- Направить r1 на смещение начала данных инструкции
Когда оба регистра указывают на правильные значения, мы просто выполняем системный вызов, а затем завершаем работу со значением из r0.
Соберите и разверните программу
Программу можно собрать с помощью собственного встроенного ассемблера SBPF, написанного Claire Fan. Это бинарный файл Rust размером 5 МБ, который заменяет набор инструментов LLVM объемом более 2 ГБ, необходимый при использовании инструментов платформы Solana.
Чтобы запустить сборку, выполните:
sbpf buildПосле сборки программу можно развернуть с помощью:
sbpf deployВзаимодействуйте с программой
Чтобы протестировать программу с помощью созданных вместе с шаблоном тестов, выполните sbpf test. Также можно запустить весь конвейер командой sbpf e2e, чтобы собрать, развернуть и протестировать программу за один раз.
Безопасность ассемблера sBPF
При написании ассемблерного кода вы полностью отвечаете за безопасность: компилятор не обнаружит ваши ошибки. Каждая инструкция напрямую влияет на безопасность программы, поэтому важно соблюдать следующие основные принципы.
Проверка входных данных
Ассемблерные программы должны вручную проверять все входные данные. Перед использованием всегда проверяйте количество аккаунтов, длину данных и размеры буферов.
Например, в нашей memo-программе загрузка количества аккаунтов в r0 обеспечила автоматическую проверку: при ошибочной передаче аккаунтов программа завершится с ошибкой. Перед доступом к памяти при обработке данных проверяйте, что длина входит в ожидаемый диапазон.
Проверка границ памяти
sBPF не выполняет автоматическую проверку границ. Перед доступом к массивам или буферам вручную убедитесь, что операции чтения и записи не выходят за пределы выделенной памяти. Простая проверка границ перед обращением к памяти предотвращает сбои и повреждение данных:
jgt r2, MAX_LENGTH, error # Check if length exceeds limit
ldxb r3, [r1+r2] # Safe to load if check passesУправление регистрами
Регистры хранят критически важное состояние программы. При вызове функций или системных вызовов сохраняйте важные значения в стеке или других регистрах.
Указатель кадра (r10) и возвращаемые значения в r0 требуют особого внимания: их повреждение может привести к сбою программы или создать уязвимости.
Безопасная арифметика
Для арифметических операций критически важно вручную обнаруживать переполнение. Перед сложением больших значений проверяйте, не превысит ли результат 64-разрядный предел.
Перед делением необходимо явно проверять делитель на ноль, чтобы предотвратить ошибки среды выполнения.
Проверка параметров системных вызовов
Системные вызовы ожидают корректные параметры и завершаются с ошибкой при недопустимых входных данных. Перед их выполнением убедитесь, что регистры содержат правильные указатели, длины и значения. Недопустимые параметры не только вызывают ошибки, но и напрасно расходуют вычислительные единицы.
Заключение
Ассемблер sBPF подходит не всем — и в этом его смысл.
Большинству разработчиков стоит использовать Rust и доверить оптимизацию компилятору. Но тем, кто оптимизирует расход до последней вычислительной единицы или создает критически важную для производительности инфраструктуру, ассемблер дает то, чего не может предложить ни один высокоуровневый язык: полный контроль.
Мы рассмотрели основы: от роли sBPF в архитектуре Solana до создания первой memo-программы.
Пример с memo-программой может показаться элементарным, но он демонстрирует ключевые принципы, которые пригодятся в более сложных программах:
- Прямое управление регистрами
- Ручное управление памятью
- Явная обработка системных вызовов
Такой прирост имеет свою цену: вы меняете защитные механизмы на скорость, а абстракции — на контроль. Выбирайте ассемблер, если уже оптимизировали код Rust, но вам все еще нужна более высокая производительность; если создаете инфраструктуру, где важна каждая микросекунда; или если вам нужно сделать то, что компилятор просто не способен эффективно оптимизировать.
Во всех остальных случаях избавьте себя от лишних трудностей и используйте Rust.
Инструменты становятся лучше, сообщество растет, а преимущества в производительности говорят сами за себя. Но помните: «большая сила означает большую ответственность за то, чтобы ничего не сломать».
Если вы хотите узнать больше об использовании ассемблера sBPF, пройдите вводный курс по ассемблеру на Blueshift и проверьте свои навыки с помощью представленных там задач!
Похожие статьи
Подпишитесь на Helius
Следите за новостями разработки на Solana и получайте уведомления о наших публикациях


