Antarmuka syscall Linux

Nomor syscall pada rax, argumen pada register, instruksi syscall, dan daftar rujukannya.

Daftar isi

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.

SyscallNomor pada raxArgumen
read0rdi = file descriptor, rsi = buffer, rdx = jumlah byte
write1rdi = file descriptor, rsi = buffer, rdx = jumlah byte
open2rdi = alamat nama berkas, rsi = flag akses, rdx = mode izin
close3rdi = file descriptor
exit60rdi = 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.

nasm tulis-halo.asm
; 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
bash
# 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-halo
text Keluaran
Halo dari kernel
exit=17
Breakpoint 1 at 0x401018
Halo dari kernel

Breakpoint 1, 0x0000000000401018 in sesudah ()
$1 = 0
$2 = 0x202
$3 = 17

Kode 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.

Aplikasiring 3, mode penggunasyscallberalih ke ring 0Kernel Linuxring 0, mode intirax = nomor syscallargumen di rdi, rsi, rdx, dan seterusnyanilai balik ditulis ke rax
Alur satu pemanggilan syscall dari aplikasi ke kernel dan kembali

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 pada r10.
  • Data penting disimpan di rcx atau r11. Isi kedua register itu dihapus oleh setiap instruksi syscall. Nilai yang masih diperlukan dipindahkan lebih dahulu ke register lain atau ke memori.
  • Nomor disalin dari tabel 32-bit. Tutorial yang menggunakan int 0x80 menomori layanannya berbeda, sehingga program memanggil layanan yang sama sekali tidak diharapkan. Rujukannya selalu tabel x86-64 pada syscalls(2).
  • rax dibiarkan tanpa nomor. Nilai 0 berarti read, sehingga pemanggilan yang lupa mengisi rax menjalankan layanan lain dengan argumen yang tidak disiapkan. Setiap pemanggilan selalu diawali penulisan nomor layanan pada rax.

Ringkasan#

  • Nomor syscall diletakkan pada rax, sedangkan argumen pertama hingga keenam diletakkan berurutan pada rdi, rsi, rdx, r10, r8, dan r9.
  • Lima layanan inti Linux x86-64 bernomor 0 untuk read, 1 untuk write, 2 untuk open, 3 untuk close, dan 60 untuk exit.
  • Instruksi syscall memindahkan kendali dari ring 3 ke ring 0, mengembalikan kendali dengan nilai balik di rax, dan merusak rcx serta r11.
  • Daftar nomor resmi dirujuk melalui halaman manual syscalls(2) dan berkas tajuk kernel asm/unistd_64.h.

Referensi#