Assembly
Вступ до асемблера

Вступ до асемблера

Приклад програми

Тепер давайте подивимося, як регістри (r0-r10), області пам'яті та інструкції працюють разом у реальній програмі. Ми почнемо з найпростішої можливої програми sBPF, щоб зрозуміти фундаментальний процес виконання.

NoOp-program

Програма "NoOp" (No Operation, без операцій) ідеально підходить для навчання, оскільки демонструє основну структуру програми без зайвої складності:

  • Як програми отримують вхідні дані (у регістрі r1)
  • Як вони повертають результати (у регістрі r0)
  • Базовий потік входу/виходу, якому слідує кожна програма sBPF
  • Як Rust компілюється в асемблер sBPF

Хоча вона "нічого не робить", вона показує вам основу, на якій будується кожна програма Solana.

Тепер, коли ми знаємо основні операції sBPF, давайте подивимося, як вони виглядають у реальній (навіть якщо крихітній) програмі.

Pinocchio NoOp

Нижче наведено високопродуктивний "noop", написаний за допомогою Pinocchio. Все, що він робить, це повертає успішний результат:

rust
#![no_std]
use pinocchio::{entrypoint::InstructionContext, lazy_program_entrypoint, no_allocator, nostd_panic_handler, ProgramResult};
 
lazy_program_entrypoint!(process_instruction);
 
nostd_panic_handler!();
no_allocator!();
 
fn process_instruction(
    _context: InstructionContext, // wrapper around the input buffer
 ) -> ProgramResult {
    Ok(())
}

Якщо ми скомпілюємо цей код за допомогою cargo build-sbf --dump, ми отримаємо дамп ELF, який надасть нам інформацію про наш бінарний файл у директорії /target/deploy/.

Потім ми захочемо знайти секцію .text — частину нашого бінарного файлу, де зберігається виконуваний код.

pinocchio_noop-dump.txt
Disassembly of section .text
0000000000000120 <entrypoint>
     120 b7 00 00 00 00 00 00 00      	mov64 r0, 0x0
     128 95 00 00 00 00 00 00 00      	exit

Давайте розберемо, що насправді означають ці шістнадцяткові байти, використовуючи формат інструкцій, який ми вивчили:

Це перша інструкція: 120 b7 00 00 00 00 00 00 00

  • Адреса: 0x120 (в текстовій області, що починається з 0x100000000)
  • Опкод: 0xb7 = mov64 з безпосереднім значенням
  • dst: 0x00 = регістр r0
  • src: 0x00 = не використовується (операція з безпосереднім значенням)
  • offset: 0x0000 = не використовується
  • imm: 0x00000000 = безпосереднє значення 0

І це друга інструкція: 128 95 00 00 00 00 00 00 00

  • Адреса: 0x128 (8 байтів після першої інструкції)
  • Опкод: 0x95 = вихід/повернення
  • Усі інші поля: 0x00 = не використовуються для виходу

Зміщення 0x120 - це місце, де компонувальник вирішив розмістити функцію точки входу в секції .text файлу ELF. Файл ELF починається з заголовків, таблиць секцій та інших метаданих, які займають перші ~0x120 байтів (288 байтів). Фактичний виконуваний код розміщується після всієї цієї службової інформації.

Assembly NoOp

Якщо ми дизасемблюємо бінарний файл, щоб перетворити його назад у компільований sBPF Assembly, код виглядатиме так:

sbpf
.globl entrypoint
entrypoint:
    mov64 r0, 0x00   // r0 <- success
    exit             // finish, return r0

Розберемо код:

  • .globl entrypoint: Це директива асемблера, яка вказує компонувальнику зробити символ точки входу глобально видимим. Середовище виконання Solana шукає цей символ, щоб знати, з якого місця почати виконання вашої програми.
  • entrypoint: Це мітка, яка позначає адресу пам'яті, з якої починається виконання програми. Коли середовище виконання завантажує вашу програму, воно переходить на цю адресу.
  • mov64 r0, 0x00: Переміщення безпосереднього значення 0 в регістр r0. Оскільки r0 є регістром повернення, це встановлює код успішного повернення.
  • exit: Завершення виконання програми та повернення значення з r0 до середовища виконання.

повернення 0 означає, що ми повертаємо успішне виконання програми.

Це надзвичайно маленька програма, з лише 2 інструкціями, які споживають тільки 2 обчислювальні одиниці (CU) для виконання, що ідеально відповідає нашому коду Rust:

  • Ми визначили функцію точки входу
  • Ми повертаємо Ok(()) що обчислюється як 0
  • Компілятор згенерував відповідні інструкції mov64 та exit

Оптимізована збірка NoOp

Однак ми можемо оптимізувати це ще більше. Оскільки середовище виконання Solana ініціалізує r0 значенням 0 за замовчуванням, ми можемо усунути надлишкову інструкцію mov64:

sbpf
.globl entrypoint
entrypoint:
    exit

Ця оптимізована версія:

  • Коштує лише 1 обчислювальну одиницю (зменшення на 50%!)
  • Дає ідентичні результати (r0 все ще містить 0)

Ось чому розуміння асемблера допомагає з оптимізацією!

Ця оптимізація можлива, оскільки ми знаємо початкові стани регістрів — щось, чим компілятор Rust не завжди користується. Розуміння асемблера sBPF дозволяє виявляти та усувати такі неефективності в критичному для продуктивності коді.

Blueshift © 2025Commit: 6d01265
Blueshift | Вступ до асемблера | Приклад програми