Інструкції
Тепер, коли ви розумієте регістри та області пам'яті sBPF, давайте розглянемо інструкції, які ними маніпулюють.
Інструкції — це фундаментальні операції, які виконує ваша програма: додавання чисел, завантаження з пам'яті або перехід до різних місць.
Що таке інструкції?
Інструкції — це основні будівельні блоки вашої програми. Уявіть їх як команди, які точно вказують процесору, що робити:
add64 r1, r2
: "Додати значення в регістрахr1
таr2
, зберегти результат вr1
"ldxdw r0, [r10 - 8]
: "Завантажити 8 байтів зі стекової пам'яті в регістрr0
"jeq r1, 42, +3
: "Якщоr1
дорівнює 42, перейти вперед на 3 інструкції"
Кожна інструкція виконує рівно одну операцію і кодується як рівно 8 байтів даних для миттєвого декодування віртуальною машиною.
Інструкції sBPF працюють з різними розмірами даних:
- байт = 8 бітів (1 байт)
- півслово = 16 бітів (2 байти)
- слово = 32 біти (4 байти)
- подвійне слово = 64 біти (8 байтів)
Більшість операцій sBPF використовують 64-бітні значення (подвійні слова), оскільки регістри мають розмір 64 біти, але за потреби ви можете завантажувати та зберігати менші розміри для підвищення ефективності.
Категорії інструкцій та формат
Коли ви компілюєте код на Rust, C або асемблері, інструментарій видає потік інструкцій фіксованої ширини по 8 байтів, упакованих у секцію .text
вашого ELF-файлу.
Кожна інструкція має послідовну структуру, яку віртуальна машина може декодувати за один прохід:
1 byte 4 bits 4 bits 2 bytes 4 bytes
┌──────────┬────────┬────────┬──────────────┬──────────────────┐
│ opcode │ dst │ src │ offset │ imm │
└──────────┴────────┴────────┴──────────────┴──────────────────┘
opcode
: Визначає тип операції. Верхні 3 біти вибирають клас інструкції (арифметична, пам'ять, перехід, виклик, вихід), тоді як нижні 5 бітів визначають конкретний варіант (додавання, множення, завантаження, перехід-якщо-дорівнює).dst
: Номер регістра призначення (r0–r10
), де зберігаються результати — результати арифметичних операцій, завантажені значення або повернені значення допоміжних функцій.src
: Вихідний регістр, що надає вхідні дані. Для двооперандної арифметики (add r1, r2
) він надає друге значення. Для операцій з пам'яттю він може надавати базову адресу. Для варіантів з безпосереднім значенням (add r1, 10
) ці 4 біти включаються в код операції.offset
: Невелике ціле число, яке змінює поведінку інструкції. Для завантаження/збереження воно додається до вихідної адреси, щоб досягти[src + offset]
. Для переходів це відносна ціль переходу, виміряна в інструкціях.imm
: Поле безпосереднього значення. Арифметичні операції використовують його для констант (add r1, 42
),CALL
використовує його для номерів системних викликів (sol_log = 16
), а операції з пам'яттю можуть розглядати його як абсолютний вказівник.
Категорії інструкцій
Різні типи інструкцій використовують ці поля особливим чином:
- Переміщення даних: Переміщення значень між регістрами та пам'яттю:
mov64 r1, 42 // Put immediate value 42 into r1
// opcode=move_imm, dst=1, src=unused, imm=42
ldxdw r0, [r10 - 8] // Load 8 bytes from stack into r0
// opcode=load64, dst=0, src=10, offset=-8, imm=unused
stxdw [r1 + 16], r0 // Store r0 to memory at [r1 + 16]
// opcode=store64, dst=1, src=0, offset=16, imm=unused
- Арифметика: Виконання математичних операцій:
add64 r1, r2 // r1 = r1 + r2
// opcode=add_reg, dst=1, src=2, offset=unused, imm=unused
add64 r1, 100 // r1 = r1 + 100
// opcode=add_imm, dst=1, src=unused, offset=unused, imm=100
- Керування потоком: Зміна послідовності виконання:
ja +5 // Jump forward 5 instructions unconditionally
// opcode=jump, dst=unused, src=unused, offset=5, imm=unused
jeq r1, r2, +3 // If r1 == r2, jump forward 3 instructions
// opcode=jump_eq_reg, dst=1, src=2, offset=3, imm=unused
jeq r1, 42, +3 // If r1 == 42, jump forward 3 instructions
// opcode=jump_eq_imm, dst=1, src=unused, offset=3, imm=42
Кодування опкодів
Кодування опкодів містить багато інформації, окрім типу операції:
- Клас інструкції: арифметична, пам'ять, перехід, виклик тощо.
- Розмір операції: 32-бітні та 64-бітні операції
- Тип джерела: регістр чи безпосереднє значення
- Конкретна операція: додавання чи віднімання, завантаження чи збереження тощо.
Це створює окремі опкоди для варіантів інструкцій. Наприклад, add64 r1, r2
(джерело - регістр) використовує інший опкод, ніж add64 r1, 42
(безпосереднє джерело). Аналогічно, add64
та add32
мають різні опкоди для різних розмірів операцій.
Арифметичні операції додатково розрізняють знакові та беззнакові варіанти. udiv64
обробляє значення як беззнакові (від 0 до 18 квінтильйонів), тоді як sdiv64
обробляє знакові значення (від -9 квінтильйонів до +9 квінтильйонів).
Instruction Execution
Опкод визначає, як віртуальна машина інтерпретує решту полів.
Коли віртуальна машина зустрічає add64 r1, r2
, вона зчитує опкод і розпізнає це як 64-бітну арифметичну операцію з використанням двох регістрів:
Поле dst
вказує, що результат записується в r1
, поле src
визначає r2
як другий операнд, а поля offset
та immediate
ігноруються.
Для add64 r1, 42
, опкод змінюється, щоб вказати на операцію з безпосереднім значенням. Тепер dst
все ще вказує на r1
, але src
стає беззмістовним, а поле immediate
надає другий операнд (42).
Операції з пам'яттю змістовно поєднують кілька полів:
Для ldxdw r1, [r2+8]
, опкод вказує на 64-бітне завантаження з пам'яті, dst
отримує завантажене значення, src
надає базову адресу, а offset
(8) додається для створення кінцевої адреси r2 + 8
.
Інструкції потоку керування слідують тому ж шаблону:
Коли ви пишете jeq r1, r2, +5
, опкод кодує умовний перехід, порівнюючи два регістри. Якщо r1
дорівнює r2
, віртуальна машина додає offset
(5) до лічильника програми, переходячи вперед на 5 інструкцій.
Виклики функцій та системні виклики
Механізм виклику sBPF еволюціонував у різних версіях для кращої ясності та безпеки. До sBPF v3, call imm
служив подвійним цілям: безпосереднє значення визначало, чи викликаєте ви внутрішню функцію, чи системний виклик.
Середовище виконання розрізняло їх на основі діапазону безпосередніх значень, при цьому номери системних викликів зазвичай були малими додатними цілими числами, як-от 16 для sol_log
.
Починаючи з sBPF v3, інструкції розділилися для явної поведінки. call
тепер обробляє внутрішні виклики функцій, використовуючи відносні зміщення, тоді як syscall imm
явно викликає функції середовища виконання. Це розділення робить наміри байткоду зрозумілими та забезпечує кращу перевірку.
Непрямі виклики через callx
також еволюціонували. У ранніх версіях цільовий регістр кодувався в полі безпосереднього значення, але починаючи з v2, він кодується в полі регістра джерела для узгодженості із загальним форматом інструкції.
Таблиця довідкових опкодів
Операції завантаження пам'яті
опкод | Мнемоніка | Опис |
---|---|---|
lddw | lddw dst, imm | Завантаження 64-бітного безпосереднього значення (перший слот) |
lddw | lddw dst, imm | Завантаження 64-бітного безпосереднього значення (другий слот) |
ldxw | ldxw dst, [src + off] | Завантаження слова з пам'яті |
ldxh | ldxh dst, [src + off] | Завантаження півслова з пам'яті |
ldxb | ldxb dst, [src + off] | Завантаження байта з пам'яті |
ldxdw | ldxdw dst, [src + off] | Завантаження подвійного слова з пам'яті |
Операції зберігання в пам'яті
opcode | Мнемоніка | Опис |
---|---|---|
stw | stw [dst + off], imm | Зберегти слово безпосередньо |
sth | sth [dst + off], imm | Зберегти півслово безпосередньо |
stb | stb [dst + off], imm | Зберегти байт безпосередньо |
stdw | stdw [dst + off], imm | Зберегти подвійне слово безпосередньо |
stxw | stxw [dst + off], src | Зберегти слово з регістра |
stxh | stxh [dst + off], src | Зберегти півслово з регістра |
stxb | stxb [dst + off], src | Зберегти байт з регістра |
stxdw | stxdw [dst + off], src | Зберегти подвійне слово з регістра |
Арифметичні операції (64-біт)
opcode | Мнемоніка | Опис |
---|---|---|
add64 | add64 dst, imm | Додати безпосереднє значення |
add64 | add64 dst, src | Додати з регістра |
sub64 | sub64 dst, imm | Відняти безпосереднє значення |
sub64 | sub64 dst, src | Відняти з регістра |
mul64 | mul64 dst, imm | Помножити на безпосереднє значення |
mul64 | mul64 dst, src | Помножити на значення з регістра |
div64 | div64 dst, imm | Поділити на безпосереднє значення (беззнакове) |
div64 | div64 dst, src | Поділити на значення з регістра (беззнакове) |
sdiv64 | sdiv64 dst, imm | Поділити на безпосереднє значення (знакове) |
sdiv64 | sdiv64 dst, src | Поділити на значення з регістра (знакове) |
mod64 | mod64 dst, imm | Залишок від ділення на безпосереднє значення (беззнакове) |
mod64 | mod64 dst, src | Залишок від ділення на значення з регістра (беззнакове) |
smod64 | smod64 dst, imm | Залишок від ділення на безпосереднє значення (знакове) |
smod64 | smod64 dst, src | Залишок від ділення на значення з регістра (знакове) |
neg64 | neg64 dst | Заперечення |
Арифметичні операції (32-біт)
opcode | Мнемоніка | Опис |
---|---|---|
add32 | add32 dst, imm | Додати безпосереднє значення (32-біт) |
add32 | add32 dst, src | Додати з регістра (32-біт) |
sub32 | sub32 dst, imm | Відняти безпосереднє значення (32-біт) |
sub32 | sub32 dst, src | Відняти з регістра (32-біт) |
mul32 | mul32 dst, imm | Помножити на безпосереднє значення (32-біт) |
mul32 | mul32 dst, src | Помножити на значення з регістра (32-біт) |
div32 | div32 dst, imm | Поділити на безпосереднє значення (32-біт) |
div32 | div32 dst, src | Поділити на значення з регістра (32-біт) |
sdiv32 | sdiv32 dst, imm | Поділити на безпосереднє значення (знакове 32-біт) |
sdiv32 | sdiv32 dst, src | Поділити на значення з регістра (знакове 32-біт) |
mod32 | mod32 dst, imm | Залишок від ділення на безпосереднє значення (32-біт) |
mod32 | mod32 dst, src | Залишок від ділення на значення з регістра (32-біт) |
smod32 | smod32 dst, imm | Залишок від ділення на безпосереднє значення (знакове 32-біт) |
smod32 | smod32 dst, src | Залишок від ділення на значення з регістра (знакове 32-біт) |
Логічні операції (64-біт)
opcode | Мнемоніка | Опис |
---|---|---|
or64 | or64 dst, imm | Побітове АБО з константою |
or64 | or64 dst, src | Побітове АБО з регістром |
and64 | and64 dst, imm | Побітове І з константою |
and64 | and64 dst, src | Побітове І з регістром |
lsh64 | lsh64 dst, imm | Зсув вліво на константу |
lsh64 | lsh64 dst, src | Зсув вліво на регістр |
rsh64 | rsh64 dst, imm | Зсув вправо на константу |
rsh64 | rsh64 dst, src | Зсув вправо на регістр |
xor64 | xor64 dst, imm | Побітове виключне АБО з константою |
xor64 | xor64 dst, src | Побітове виключне АБО з регістром |
mov64 | mov64 dst, imm | Переміщення константи |
mov64 | mov64 dst, src | Переміщення регістра |
arsh64 | arsh64 dst, imm | Арифметичний зсув вправо на константу |
arsh64 | arsh64 dst, src | Арифметичний зсув вправо на регістр |
Логічні операції (32-біт)
opcode | Мнемоніка | Опис |
---|---|---|
or32 | or32 dst, imm | Побітове АБО з константою (32-біт) |
or32 | or32 dst, src | Побітове АБО з регістром (32-біт) |
and32 | and32 dst, imm | Побітове І з константою (32-біт) |
and32 | and32 dst, src | Побітове І з регістром (32-біт) |
lsh32 | lsh32 dst, imm | Зсув вліво на константу (32-біт) |
lsh32 | lsh32 dst, src | Зсув вліво на регістр (32-біт) |
rsh32 | rsh32 dst, imm | Зсув вправо на константу (32-біт) |
rsh32 | rsh32 dst, src | Зсув вправо на регістр (32-біт) |
xor32 | xor32 dst, imm | Побітове виключне АБО з константою (32-біт) |
xor32 | xor32 dst, src | Побітове виключне АБО з регістром (32-біт) |
mov32 | mov32 dst, imm | Переміщення константи (32-біт) |
mov32 | mov32 dst, src | Переміщення регістра (32-біт) |
arsh32 | arsh32 dst, imm | Арифм. зсув вправо на константу (32-біт) |
arsh32 | arsh32 dst, src | Арифм. зсув вправо на регістр (32-біт) |
Операції керування потоком
opcode | Мнемоніка | Опис |
---|---|---|
ja | ja off | Безумовний перехід (jump 0 = перехід до наступного) |
jeq | jeq dst, imm, off | Перехід, якщо дорівнює безпосередньому значенню |
jeq | jeq dst, src, off | Перехід, якщо дорівнює регістру |
jgt | jgt dst, imm, off | Перехід, якщо більше безпосереднього значення (беззнакове) |
jgt | jgt dst, src, off | Перехід, якщо більше регістра (беззнакове) |
jge | jge dst, imm, off | Перехід, якщо більше або дорівнює безпосередньому значенню (беззнакове) |
jge | jge dst, src, off | Перехід, якщо більше або дорівнює регістру (беззнакове) |
jset | jset dst, imm, off | Перехід, якщо біт встановлено (безпосередня маска) |
jset | jset dst, src, off | Перехід, якщо біт встановлено (маска регістра) |
jne | jne dst, imm, off | Перехід, якщо не дорівнює безпосередньому значенню |
jne | jne dst, src, off | Перехід, якщо не дорівнює регістру |
jsgt | jsgt dst, imm, off | Перехід, якщо більше безпосереднього значення (знакове) |
jsgt | jsgt dst, src, off | Перехід, якщо більше регістра (знакове) |
jsge | jsge dst, imm, off | Перехід, якщо більше або дорівнює безпосередньому значенню (знакове) |
jsge | jsge dst, src, off | Перехід, якщо більше або дорівнює регістру (знакове) |
jlt | jlt dst, imm, off | Перехід, якщо менше безпосереднього значення (беззнакове) |
jlt | jlt dst, src, off | Перехід, якщо менше регістра (беззнакове) |
jle | jle dst, imm, off | Перехід, якщо менше або дорівнює безпосередньому значенню (беззнакове) |
jle | jle dst, src, off | Перехід, якщо менше або дорівнює регістру (беззнакове) |
jslt | jslt dst, imm, off | Перехід, якщо менше безпосереднього значення (знакове) |
jslt | jslt dst, src, off | Перехід, якщо менше регістра (знакове) |
jsle | jsle dst, imm, off | Перехід, якщо менше або дорівнює безпосередньому значенню (знакове) |
jsle | jsle dst, src, off | Перехід, якщо менше або дорівнює регістру (знакове) |
Операції виклику функцій
опкод | Мнемоніка | Опис |
---|---|---|
call | call imm або syscall imm | Виклик функції або системний виклик |
callx | callx imm | Непрямий виклик (регістр у полі imm) |
exit | exit або return | Повернення з функції |
Операції обміну байтів
опкод | Мнемоніка | Опис |
---|---|---|
be | be dst, imm | Обмін байтів (16, 32 або 64 біт) |
le | le dst, imm | Перетворення у порядок little endian (застаріло) |