Program yang memanggil syscall tidak menerima penjelasan apa pun apabila permintaannya ditolak oleh kernel. Yang dikembalikan hanyalah sebuah angka negatif pada rax yang menyatakan penyebab penolakan, sedangkan program yang tidak memeriksanya melanjutkan proses dengan keadaan yang sudah salah. Topik ini membahas kode error mentah pada syscall Linux x86-64, perbandingannya dengan konvensi pustaka standar, pembacaannya di debugger dan di kode keluar shell, serta kebiasaan pemeriksaan tanda yang memastikan error selalu terdeteksi.
Kegagalan berupa angka negatif pada rax#
Kernel menandai syscall yang gagal dengan mengembalikan angka negatif pada rax, dan angka tersebut sekaligus menyatakan penyebabnya. Nilai -2 berarti ENOENT, yaitu berkas atau direktori yang diminta tidak ada, sedangkan -9 berarti EBADF, yaitu file descriptor yang digunakan tidak sah. Tidak ada pesan yang ditampilkan ke layar dan tidak ada mekanisme pelemparan pengecualian. Proses berlanjut persis seperti biasa, sehingga satu-satunya petunjuk adalah tanda negatif pada rax.
Konvensi ini berbeda dari pustaka standar. Halaman manual syscalls(2) menjelaskan bahwa fungsi pembungkus libc mengubah angka negatif itu menjadi positif, menyimpannya pada variabel errno, lalu mengembalikan -1 kepada pemanggilnya. Pada syscall mentah tidak ada errno dan tidak ada -1, karena angka negatifnya sudah berada langsung pada rax. Tabel berikut merangkum kode yang paling sering ditemui beserta nama dan maknanya.
Nilai rax | Nama | Makna |
|---|---|---|
| -2 | ENOENT | berkas atau direktori tidak ada |
| -9 | EBADF | file descriptor tidak sah |
| -13 | EACCES | izin akses ditolak |
| -14 | EFAULT | alamat buffer berada di luar jangkauan proses |
Angka negatif ini juga terbawa ke kode keluar proses, tetapi hanya delapan bit terendahnya. Syscall exit mengambil byte terendah dari rdi, sehingga -2 yang hampir seluruh bitnya bernilai 1 menjadi 254 pada shell, -9 menjadi 247, dan -13 menjadi 243. Kode keluar yang besar dan dekat dengan 256 ini sering merupakan petunjuk pertama bahwa sebuah program tanpa libc gagal pada syscall-nya.
Peluang kemunculan setiap kode tidak sama. ENOENT paling sering muncul karena jalur berkas salah ketik atau berkas memang belum dibuat, sedangkan EBADF biasanya berasal dari kesalahan program sendiri, misalnya descriptor yang sudah ditutup lebih awal lalu digunakan lagi. EACCES bergantung pada lingkungan, yaitu izin berkas dan pengguna yang menjalankan program, sehingga kasus yang sama bisa berhasil pada satu lingkungan dan gagal pada lingkungan lain. Mengenali penyebab lazim tiap kode mempercepat pencarian akar masalahnya.
Mengamati ENOENT pada open yang gagal#
Program berikut membuka arsip.txt yang sengaja tidak dibuat, lalu mengakhiri proses dengan kode keluar yang diambil langsung dari nilai balik open.
; uji-enoent.asm
; Membuka berkas yang tidak ada, lalu mengakhiri proses
; dengan kode keluar yang berasal dari error mentahnya.
default rel
section .rodata
nama: db "arsip.txt", 0
section .text
global _start
_start:
mov eax, 2 ; nomor syscall open
lea rdi, [nama]
xor esi, esi ; flag O_RDONLY
xor edx, edx
syscall
sesudah_open:
mov edi, eax ; rax = -2, dipotong menjadi 254 oleh exit
mov eax, 60 ; nomor syscall exit
syscall# pastikan berkas yang akan dibuka memang tidak ada
rm -f arsip.txt
# rakit, tautkan, dan jalankan
nasm -f elf64 uji-enoent.asm -o uji-enoent.o && ld uji-enoent.o -o uji-enoent && ./uji-enoent; echo "exit=$?"
# amati nilai rax mentah tepat sesudah open yang gagal
gdb -batch -ex 'break sesudah_open' -ex 'run' -ex 'p $rax' -ex 'p/x $rax' ./uji-enoentexit=254
Breakpoint 1 at 0x401012
Breakpoint 1, 0x0000000000401012 in sesudah_open ()
$1 = -2
$2 = 0xfffffffffffffffeKode keluar 254 pada baris pertama adalah -2 yang dipotong menjadi delapan bit oleh exit. Di dalam debugger, $1 = -2 menampilkan rax sebagai bilangan bertanda, sedangkan $2 = 0xfffffffffffffffe menampilkan pola bit yang sesungguhnya, yaitu 64 bit yang hampir seluruhnya bernilai 1. Pola inilah yang berada di rax tanpa pengecualian maupun pesan, dan program yang tidak memeriksanya akan memperlakukan angka tersebut sebagai data biasa.
Perintah rm -f pada awal blok menjaga kondisi percobaan tetap konsisten, karena berkas sisa dari percobaan sebelumnya akan membuat open berhasil dan contoh tidak lagi memperlihatkan kegagalan yang dimaksud. Tanpa debugger, petunjuk pertamanya adalah kode keluar 254 pada shell, sedangkan petunjuk berikutnya bisa diperoleh dari strace yang menampilkan baris open dengan nilai balik -2 ENOENT. Kedua jalan itu menunjukkan hal yang sama, yaitu kegagalan hanya terlihat oleh program yang sengaja membacanya.
Membedakan error dari nilai balik normal#
Nilai balik syscall yang sah juga berupa angka pada rax, sehingga tanda negatif menjadi satu-satunya pembeda. open mengembalikan descriptor berupa bilangan positif yang kecil, read dan write mengembalikan jumlah byte berupa bilangan positif atau 0 sebagai tanda masukan habis, sedangkan kegagalan selalu berupa bilangan negatif. Karena itu, kebiasaan yang benar adalah memeriksa tanda rax tepat setelah setiap syscall sebelum nilainya digunakan.
syscall
test rax, rax ; periksa nilai balik
js gagal ; loncat ke label gagal bila negatif
; di sini rax aman dipakai
gagal:
neg rax ; ubah -2 menjadi 2 agar mudah dicocokkanInstruksi test rax, rax menetapkan flag tanpa mengubah isi rax, dan js melompat hanya apabila nilai rax negatif. Pada label gagal, instruksi neg rax membalik tandanya sehingga angka 2 mudah dicocokkan dengan daftar errno pada halaman manual. Pola serupa sudah terlihat pada program penyalin berkas yang menggunakan test rax, rax diikuti jle untuk sekaligus menangani 0 sebagai tanda habis dan angka negatif sebagai tanda error.
Program berikut mempraktikkan pola itu pada kasus EBADF, yaitu membaca dari descriptor 42 yang tidak pernah dibuka.
; uji-ebadf.asm
; Membaca dari file descriptor yang tidak pernah dibuka.
default rel
section .bss
buffer: resb 16
section .text
global _start
_start:
mov eax, 0 ; nomor syscall read
mov edi, 42 ; descriptor 42 tidak pernah dibuka
lea rsi, [buffer]
mov edx, 16
syscall
sesudah_read:
mov edi, eax ; rax = -9, dipotong menjadi 247 oleh exit
mov eax, 60 ; nomor syscall exit
syscall# rakit dan jalankan uji EBADF
nasm -f elf64 uji-ebadf.asm -o uji-ebadf.o && ld uji-ebadf.o -o uji-ebadf && ./uji-ebadf; echo "exit=$?"exit=247Kode keluar 247 adalah -9 yang dipotong menjadi delapan bit. Kasus ini tidak melibatkan berkas sama sekali, sehingga bisa terjadi pada program apa pun yang descriptor-nya tertukar atau sudah ditutup lebih awal.
Ringkasan#
- Syscall yang gagal tidak melempar apa pun, melainkan mengembalikan angka negatif pada
rax, misalnya -2 untuk ENOENT, -9 untuk EBADF, dan -13 untuk EACCES. - Pustaka standar mengubah angka itu menjadi errno positif dan mengembalikan -1, sedangkan pada syscall mentah angka negatifnya sudah langsung berada di
rax. exithanya membawa delapan bit terendah, sehingga -2 terlihat sebagai kode keluar 254, -9 sebagai 247, dan -13 sebagai 243.- Nilai balik yang sah selalu positif atau 0, sehingga
test rax, raxdiikutijssetelah setiap syscall menjadi kebiasaan wajib sebelum nilai balik digunakan.
Referensi#
- System calls(2) — Linux man-pages project — Menjelaskan kegagalan syscall mentah berupa angka negatif serta cara pustaka standar mengubahnya menjadi errno dan -1.
- open(2) — Linux man-pages project
— Memuat daftar kode error
open, termasuk ENOENT dan EACCES beserta penyebabnya. - read(2) — Linux man-pages project
— Memuat daftar kode error
read, termasuk EBADF dan EFAULT beserta penyebabnya.