Error syscall mentah

Kode errno negatif yang dikembalikan dalam rax dan cara membedakannya dari nilai balik normal.

Daftar isi

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 raxNamaMakna
-2ENOENTberkas atau direktori tidak ada
-9EBADFfile descriptor tidak sah
-13EACCESizin akses ditolak
-14EFAULTalamat 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.

nasm uji-enoent.asm
; 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
bash
# 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-enoent
text Keluaran
exit=254
Breakpoint 1 at 0x401012

Breakpoint 1, 0x0000000000401012 in sesudah_open ()
$1 = -2
$2 = 0xfffffffffffffffe

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

nasm Pola pemeriksaan tanda setelah syscall
    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 dicocokkan

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

nasm uji-ebadf.asm
; 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
bash
# 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=$?"
text Keluaran
exit=247

Kode 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.
  • exit hanya 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, rax diikuti js setelah setiap syscall menjadi kebiasaan wajib sebelum nilai balik digunakan.

Referensi#