Instruksi
Sekarang setelah Anda memahami register dan region memori sBPF, mari kita periksa instruksi yang memanipulasinya.
Instruksi adalah operasi fundamental yang dilakukan program Anda—menambahkan angka, memuat dari memori, atau melompat ke lokasi yang berbeda.
Apa itu Instruksi?
Instruksi adalah blok dasar pembangun program Anda. Anggap saja sebagai perintah yang memberi tahu prosesor persis apa yang harus dilakukan:
add64 r1, r2: "Tambahkan nilai dalam registerr1danr2, simpan hasilnya dir1"ldxdw r0, [r10 - 8]: "Muat 8 byte dari memori stack ke dalam registerr0"jeq r1, 42, +3: "Jikar1sama dengan 42, lompat maju 3 instruksi"
Setiap instruksi melakukan tepat satu operasi dan dikodekan sebagai data 8 byte yang tepat untuk dekoding VM secara instan.
Instruksi sBPF bekerja dengan ukuran data yang berbeda:
byte = 8 bit (1 byte)
halfword = 16 bit (2 byte)
word = 32 bit (4 byte)
doubleword = 64 bit (8 byte)
Sebagian besar operasi sBPF menggunakan nilai 64-bit (doubleword) karena register berukuran 64 bit, tetapi Anda dapat memuat dan menyimpan ukuran yang lebih kecil saat diperlukan untuk efisiensi.
Kategori dan Format Instruksi
Ketika Anda mengompilasi kode Rust, C, atau assembly, toolchain menghasilkan aliran instruksi lebar-tetap 8-byte yang dikemas ke dalam bagian .text ELF Anda.
Setiap instruksi mengikuti struktur yang konsisten yang dapat didekode VM dalam satu kali proses:
1 byte 4 bits 4 bits 2 bytes 4 bytes
┌──────────┬────────┬────────┬──────────────┬──────────────────┐
│ opcode │ dst │ src │ offset │ imm │
└──────────┴────────┴────────┴──────────────┴──────────────────┘opcode: Mendefinisikan jenis operasi. 3 bit teratas memilih kelas instruksi (aritmatika, memori, lompat, panggil, keluar), sementara 5 bit bawah menentukan varian yang tepat (tambah, kalikan, muat, lompat-jika-sama).dst: Nomor register tujuan (r0–r10) tempat hasil disimpan—hasil aritmatika, nilai yang dimuat, atau pengembalian fungsi pembantu.src: Register sumber yang menyediakan input. Untuk aritmatika dua-operand (add r1, r2), ini menyediakan nilai kedua. Untuk operasi memori, ini dapat menyediakan alamat dasar. Untuk varian langsung (add r1, 10), 4 bit ini digabungkan ke dalam opcode.offset: Integer kecil yang memodifikasi perilaku instruksi. Untuk load/store, ini ditambahkan ke alamat sumber untuk mencapai[src + offset]. Untuk lompatan, ini adalah target cabang relatif yang diukur dalam instruksi.imm: Bidang nilai langsung. Operasi aritmatika menggunakannya untuk konstanta (add r1, 42),CALLmenggunakannya untuk nomor syscall (sol_log = 16), dan operasi memori mungkin memperlakukannya sebagai pointer absolut.
Kategori Instruksi
Jenis instruksi yang berbeda menggunakan bidang-bidang ini dengan cara spesifik:
Pergerakan Data: Memindahkan nilai antara register dan memori:
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=unusedAritmatika: Melakukan operasi matematika:
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=100Alur Kontrol: Mengubah urutan eksekusi:
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=42Pengkodean Opcode
Pengkodean opcode menangkap beberapa informasi di luar jenis operasi:
Kelas instruksi: Aritmatika, memori, lompatan, panggilan, dll.
Ukuran operasi: Operasi 32-bit vs 64-bit
Jenis sumber: Register vs nilai langsung
Operasi spesifik: Tambah vs kurang, muat vs simpan, dll.
Ini menciptakan opcode yang berbeda untuk varian instruksi. Misalnya, add64 r1, r2 (sumber register) menggunakan opcode yang berbeda dari add64 r1, 42 (sumber langsung). Demikian juga, add64 dan add32 memiliki opcode berbeda untuk ukuran operasi yang berbeda.
Operasi aritmatika lebih lanjut membedakan antara varian bertanda dan tanpa tanda. udiv64 memperlakukan nilai sebagai tanpa tanda (0 hingga 18 kuintiliun), sementara sdiv64 menangani nilai bertanda (-9 kuintiliun hingga +9 kuintiliun).
Eksekusi Instruksi
Opcode menentukan bagaimana VM menafsirkan bidang-bidang yang tersisa.
Ketika VM menemukan add64 r1, r2, ia membaca opcode dan mengenalinya sebagai operasi aritmatika 64-bit menggunakan dua register:
Bidang dst menunjukkan hasilnya masuk ke r1, bidang src menentukan r2 sebagai operan kedua, dan bidang offset dan immediate diabaikan.
Untuk add64 r1, 42, opcode berubah untuk menunjukkan operasi langsung. Sekarang dst masih menunjuk ke r1, tetapi src menjadi tidak berarti, dan bidang immediate menyediakan operan kedua (42).
Operasi memori menggabungkan beberapa bidang secara bermakna:
Untuk ldxdw r1, [r2+8], opcode menunjukkan pemuatan memori 64-bit, dst menerima nilai yang dimuat, src menyediakan alamat dasar, dan offset (8) ditambahkan untuk membuat alamat akhir r2 + 8.
Instruksi aliran kontrol mengikuti pola yang sama:
Ketika Anda menulis jeq r1, r2, +5, opcode mengenkode lompatan bersyarat yang membandingkan dua register. Jika r1 sama dengan r2, VM akan menambahkan offset (5) ke program counter, melompat maju 5 instruksi.
Pemanggilan Fungsi dan Syscall
Mekanisme pemanggilan sBPF berkembang di berbagai versi untuk kejelasan dan keamanan yang lebih baik. Hingga sBPF v3, call imm memiliki dua tujuan: nilai immediate menentukan apakah Anda memanggil fungsi internal atau menggunakan syscall.
Runtime membedakan antara keduanya berdasarkan rentang nilai immediate, dengan nomor syscall biasanya berupa bilangan bulat positif kecil seperti 16 untuk sol_log.
Dari sBPF v3 dan seterusnya, instruksi dipisahkan untuk perilaku yang eksplisit. call sekarang menangani pemanggilan fungsi internal menggunakan offset relatif, sementara syscall imm secara eksplisit memanggil fungsi runtime. Pemisahan ini membuat intensi bytecode jelas dan memungkinkan verifikasi yang lebih baik.
Pemanggilan tidak langsung melalui callx juga berkembang. Versi sebelumnya mengenkode register target di bidang immediate, tetapi dari v2 dan seterusnya, register target dienkode di bidang register sumber untuk konsistensi dengan format instruksi umum.