Sebagian besar pekerjaan sebuah program pada akhirnya berupa permintaan layanan kepada kernel, mulai dari menulis teks ke terminal sampai membaca isi berkas. Topik ini membahas antarmuka yang digunakan untuk mengajukan permintaan itu secara langsung, yaitu instruksi syscall pada Linux x86-64. Pembahasannya mencakup penomoran syscall yang dibawa rax, penempatan argumen pada register, perubahan hak akses yang terjadi saat instruksi dijalankan, serta rujukan resmi daftar nomornya. Setelah topik ini, baris kode yang memanggil kernel tanpa pustaka standar dapat dibaca dan ditulis dengan letak register yang tepat.
Nomor syscall dan register argumen#
Kernel mengenali layanan yang diminta melalui sebuah angka, bukan melalui nama. Sebelum instruksi syscall dijalankan, nomor layanan diletakkan pada rax, sedangkan argumennya diletakkan pada register berurutan, yaitu rdi untuk argumen pertama, rsi untuk kedua, rdx untuk ketiga, r10 untuk keempat, r8 untuk kelima, dan r9 untuk keenam. Hingga enam argumen dapat diserahkan dengan cara ini, sedangkan argumen yang lebih banyak diletakkan di stack. Lima layanan yang menjadi tulang punggung program tanpa libc tidak memerlukan lebih dari tiga argumen, sehingga rangkaian register di atas sudah mencukupi.
| Syscall | Nomor pada rax | Argumen |
|---|---|---|
read | 0 | rdi = file descriptor, rsi = buffer, rdx = jumlah byte |
write | 1 | rdi = file descriptor, rsi = buffer, rdx = jumlah byte |
open | 2 | rdi = alamat nama berkas, rsi = flag akses, rdx = mode izin |
close | 3 | rdi = file descriptor |
exit | 60 | rdi = kode keluar |
Nomor pada tabel hanya berlaku untuk Linux x86-64. Arsitektur lain menomori layanannya sendiri, misalnya Linux i386 mengenal exit sebagai nomor 1, sehingga tabel dari arsitektur tersebut tidak boleh disalin begitu saja.
Peralihan ring 3 ke ring 0#
Program pengguna berjalan pada ring 3, yaitu tingkat hak akses yang dilarang menyentuh perangkat keras secara langsung, sedangkan kernel Linux berjalan pada ring 0 dengan hak penuh. Instruksi syscall memindahkan kendali dari ring 3 ke ring 0 dalam satu langkah. Kernel kemudian melayani permintaan sesuai nomor di rax, lalu kendali kembali ke instruksi tepat sesudah syscall dengan membawa nilai balik layanan pada rax. Untuk write, nilai balik itu berupa jumlah byte yang berhasil ditulis.
Peralihan ini meninggalkan jejak pada dua register. Instruksi syscall menulis alamat instruksi sesudahnya ke rcx dan menyalin register flag ke r11, sehingga isi keduanya sebelum pemanggilan hilang tanpa pemberitahuan. Nilai penting pada rcx dan r11 perlu dipindahkan ke register lain sebelum syscall dijalankan. Program berikut memanggil write untuk menulis satu baris teks ke stdout, lalu memanggil exit dengan kode keluar yang diambil dari nilai balik write.
; tulis-halo.asm
; Menulis satu baris teks ke stdout melalui syscall write,
; lalu mengakhiri proses melalui syscall exit.
default rel
section .rodata
pesan: db "Halo dari kernel", 10
panjang equ $ - pesan
section .text
global _start
_start:
mov eax, 1 ; nomor syscall write
mov edi, 1 ; file descriptor 1, yaitu stdout
lea rsi, [pesan] ; alamat teks yang akan ditulis
mov edx, panjang ; jumlah byte yang akan ditulis
syscall
sesudah:
mov edi, eax ; nilai balik write menjadi kode keluar
mov eax, 60 ; nomor syscall exit
syscall# rakit, tautkan, dan jalankan
nasm -f elf64 tulis-halo.asm -o tulis-halo.o && ld tulis-halo.o -o tulis-halo && ./tulis-halo; echo "exit=$?"
# amati rcx, r11, dan rax tepat sesudah syscall write
gdb -batch -ex 'break sesudah' -ex 'run' -ex 'p (long)$rcx - (long)$rip' -ex 'p/x $r11' -ex 'p/d $rax' ./tulis-haloHalo dari kernel
exit=17
Breakpoint 1 at 0x401018
Halo dari kernel
Breakpoint 1, 0x0000000000401018 in sesudah ()
$1 = 0
$2 = 0x202
$3 = 17Kode keluar 17 sama dengan panjang teks Halo dari kernel ditambah satu byte baris baru, dan angka itu adalah nilai balik write yang dibawa keluar melalui exit. Debugger berhenti pada label sesudah, yaitu instruksi tepat sesudah syscall pertama, sehingga keadaan register belum tersentuh oleh instruksi lain. Selisih rcx terhadap rip bernilai 0 pada baris $1 = 0, yang membuktikan rcx berisi alamat instruksi sesudah syscall sebagaimana ditinggalkan oleh instruksi itu. Baris $2 = 0x202 memperlihatkan r11 berisi salinan register flag dari saat sebelum pemanggilan, dan baris $3 = 17 memperlihatkan nilai balik write pada rax. Diagram berikut merangkum alur satu pemanggilan secara utuh.
Merujuk daftar nomor syscall#
Nomor syscall bukan aturan yang berdiri sendiri, melainkan bagian dari kontrak antara kernel dan seluruh program pengguna. Halaman manual syscalls(2) memuat daftar seluruh syscall Linux dan menjelaskan konvensi nilai baliknya, sedangkan halaman tiap layanan, misalnya open(2) dan read(2), merinci argumen, flag, dan perilakunya. Di dalam pohon kode kernel, tabel nomor untuk x86-64 tertulis pada berkas tajuk asm/unistd_64.h, dan tabel yang sama juga menjadi sumber bagi pustaka standar. Pustaka standar sendiri hanya membungkus instruksi yang sama dengan fungsi yang lebih nyaman digunakan, sehingga nomor yang dirujuk header libc adalah nomor yang persis sama dengan tabel di atas. Kontrak ini stabil di seluruh versi kernel yang beredar, dan justru karena stabil, program tanpa libc dapat mengandalkannya tanpa menanyakan versi kernel saat berjalan.
Kesalahan umum#
Kesalahan pada pemanggilan syscall jarang menghasilkan pesan error, karena kernel hanya mengembalikan sebuah angka dan program yang membaca angka tersebut dengan keliru tetap berlanjut. Kesalahan berikut paling sering muncul pada tahap ini.
- Argumen keempat ditaruh di
rcx. Penyebabnya adalah kebiasaan konvensi pemanggilan fungsi, dan akibatnya layanan menerima argumen yang salah. Perbaikannya adalah menempatkan argumen keempat padar10. - Data penting disimpan di
rcxataur11. Isi kedua register itu dihapus oleh setiap instruksisyscall. Nilai yang masih diperlukan dipindahkan lebih dahulu ke register lain atau ke memori. - Nomor disalin dari tabel 32-bit. Tutorial yang menggunakan
int 0x80menomori layanannya berbeda, sehingga program memanggil layanan yang sama sekali tidak diharapkan. Rujukannya selalu tabel x86-64 pada syscalls(2). raxdibiarkan tanpa nomor. Nilai 0 berartiread, sehingga pemanggilan yang lupa mengisiraxmenjalankan layanan lain dengan argumen yang tidak disiapkan. Setiap pemanggilan selalu diawali penulisan nomor layanan padarax.
Ringkasan#
- Nomor syscall diletakkan pada
rax, sedangkan argumen pertama hingga keenam diletakkan berurutan padardi,rsi,rdx,r10,r8, danr9. - Lima layanan inti Linux x86-64 bernomor 0 untuk
read, 1 untukwrite, 2 untukopen, 3 untukclose, dan 60 untukexit. - Instruksi
syscallmemindahkan kendali dari ring 3 ke ring 0, mengembalikan kendali dengan nilai balik dirax, dan merusakrcxsertar11. - Daftar nomor resmi dirujuk melalui halaman manual syscalls(2) dan berkas tajuk kernel
asm/unistd_64.h.
Referensi#
- System calls(2) — Linux man-pages project — Daftar seluruh syscall Linux per arsitektur beserta konvensi nilai baliknya.
- NASM 2.16.03 Documentation — NASM
— Rujukan sintaks perakit NASM, termasuk direktif
section,equ, dan penulisan instruksi pada topik ini.