Dasar file descriptor

open, read, write, dan close pada level mentah beserta nilai balik descriptor-nya.

Daftar isi

Seluruh akses berkas pada Linux bermuara pada empat syscall, yaitu open untuk membuka, read untuk membaca, write untuk menulis, dan close untuk menutup. Pustaka standar membungkus keempat operasi itu di balik fungsi seperti fopen dan fclose, sehingga nomor syscall dan register pembawanya tidak pernah terlihat. Topik ini menjalankan keempatnya secara langsung tanpa libc, mulai dari nilai balik open berupa file descriptor, perulangan read dan write untuk menyalin isi berkas, sampai penutupan descriptor yang menandai akhir penggunaannya.

open dan file descriptor yang dihasilkan#

Syscall open meminta kernel membuka sebuah berkas dan mengembalikan file descriptor, yaitu nomor yang merujuk pada sumber data terbuka dan menjadi pegangan pada seluruh operasi berikutnya. Argumennya ada tiga, yaitu alamat nama berkas pada rdi, flag akses pada rsi, dan mode izin pada rdx. Flag O_RDONLY bernilai 0 dan berarti berkas dibuka hanya untuk dibaca, sedangkan mode izin hanya berperan apabila berkas baru ikut dibuat. Kernel mengembalikan descriptor pada rax berupa bilangan bulat kecil yang tidak negatif, dan nilai inilah yang digunakan read, write, maupun close berikutnya.

Tiga descriptor terendah sudah terpakai sejak proses dimulai.

DescriptorNamaPeran
0stdinmasukan standar program
1stdoutkeluaran standar program
2stderrsaluran pesan error

Karena tiga nomor pertama sudah terisi, descriptor untuk berkas yang baru dibuka biasanya bernilai 3. Kernel selalu memberikan nomor terkecil yang masih bebas, sehingga urutan pembukaan berkas menentukan urutan nomornya. Program yang membuka dua berkas secara berurutan menerima 3 kemudian 4, sedangkan descriptor yang sudah ditutup melalui close dapat diberikan kembali kepada berkas berikutnya.

Menyalin berkas dengan perulangan read dan write#

Program berikut membuka data.txt, menyalin seluruh isinya ke stdout, lalu menutupnya. Data ditampung pada sebuah buffer di section .bss yang dialokasikan melalui direktif resb, yaitu direktif pemesanan byte yang isinya tidak perlu diisi saat program dirakit. Perulangan membaca dan menulis berlangsung sampai read mengembalikan 0 sebagai tanda isi berkas sudah habis.

nasm salin-berkas.asm
; salin-berkas.asm
; Membuka data.txt, menyalin seluruh isinya ke stdout, lalu menutupnya.
default rel
section .bss
buffer: resb 64
section .rodata
nama: db "data.txt", 0
section .text
global _start
_start:
    mov eax, 2              ; nomor syscall open
    lea rdi, [nama]         ; alamat nama berkas
    xor esi, esi            ; flag O_RDONLY berarti hanya membaca
    xor edx, edx            ; mode izin hanya dipakai saat membuat berkas
    syscall
sesudah_open:
    mov r12d, eax           ; simpan file descriptor hasil open
baca_ulang:
    mov eax, 0              ; nomor syscall read
    mov edi, r12d           ; descriptor hasil open
    lea rsi, [buffer]       ; tempat data ditampung
    mov edx, 64             ; kapasitas buffer
    syscall
    test rax, rax
    jle selesai             ; 0 berarti habis, negatif berarti error
    mov r13, rax            ; jumlah byte yang benar-benar terbaca

    mov eax, 1              ; nomor syscall write
    mov edi, 1              ; stdout
    lea rsi, [buffer]
    mov edx, r13d           ; tulis hanya sebanyak byte terbaca
    syscall
    jmp baca_ulang

selesai:
    mov eax, 3              ; nomor syscall close
    mov edi, r12d           ; descriptor yang akan ditutup
    syscall
    xor edi, edi
    mov eax, 60             ; nomor syscall exit
    syscall
bash
# buat berkas contoh yang akan disalin
printf 'suhu pagi: 24\nsuhu siang: 27\n' > data.txt
# rakit, tautkan, dan jalankan
nasm -f elf64 salin-berkas.asm -o salin-berkas.o && ld salin-berkas.o -o salin-berkas && ./salin-berkas; echo "exit=$?"
text Keluaran
suhu pagi: 24
suhu siang: 27
exit=0

Isi data.txt tampil dua baris dan kode keluarnya 0, yang berarti perulangan read dan write berjalan sampai isi berkas habis, lalu close dan exit mengakhiri urutannya. Descriptor hasil open disimpan di r12d sebelum digunakan, karena rax akan digunakan kembali oleh syscall berikutnya. Pernyataan test rax, rax memeriksa nilai balik read sebelum digunakan, dan mov edx, r13d memastikan write hanya mengirim sebanyak byte yang benar-benar terbaca, bukan sebanyak kapasitas buffer.

Nilai descriptor itu sendiri dapat diamati langsung pada debugger. Breakpoint dipasang pada label sesudah_open, yaitu instruksi pertama setelah open kembali.

bash
# amati nilai balik open di dalam debugger
gdb -batch -ex 'break sesudah_open' -ex 'run' -ex 'p/d $rax' ./salin-berkas
text Keluaran
Breakpoint 1 at 0x401012

Breakpoint 1, 0x0000000000401012 in sesudah_open ()
$1 = 3

Baris $1 = 3 menunjukkan bahwa kernel memberikan nomor terkecil yang masih bebas. Angka 0, 1, dan 2 sudah digunakan stdin, stdout, dan stderr sejak proses dimulai, sehingga berkas pertama yang dibuka menerima 3. Apabila program membuka berkas kedua sebelum menutup yang pertama, descriptor berikutnya adalah 4, dan pola ini berlanjut sepanjang proses berjalan.

close dan masa hidup descriptor#

Syscall close bernomor 3 menerima satu argumen pada rdi, yaitu descriptor yang akan ditutup, dan mengembalikan 0 pada rax apabila berhasil. Penutupan membebaskan nomor tersebut untuk diberikan kepada pembukaan berikutnya, sehingga jumlah berkas yang terbuka bersamaan tetap terkendali. Pada program di atas, close dijalankan tepat sebelum exit, tepat setelah perulangan menemukan akhir berkas. Tanpa libc, tidak ada pembersihan otomatis dari fungsi semacam fclose, sehingga panggilan close harus ditulis secara eksplisit.

Nilai balik close juga perlu diperiksa karena penutupan bisa gagal. Menutup descriptor yang sama dua kali mengembalikan -9 EBADF pada rax, dan kesalahan semacam ini sering berasal dari variabel yang tidak diperbarui setelah descriptor ditutup. Nomor bekas juga cepat digunakan kembali, karena descriptor yang baru saja ditutup menjadi nomor terkecil yang bebas dan langsung diberikan kepada open berikutnya. Program yang mengandalkan urutan nomor tanpa membaca nilai balik akan salah membedakan berkas lama dan berkas baru.

Kesalahan umum#

  • Nama berkas tanpa byte nol. String data.txt diakhiri 0 karena kernel membaca argumen open sampai menemukan byte nol sebagai penanda ujungnya. Nama tanpa penutup itu membuat kernel terus membaca memori di sebelahnya dan open gagal dengan cara yang sulit dilacak.
  • Menganggap descriptor berurutan. Nomor 3 hanya muncul karena berkas itu yang pertama dibuka. Apabila program membuka berkas lain lebih dahulu, angkanya bergeser, sehingga descriptor selalu diambil dari nilai balik open dan tidak pernah ditebak.
  • Buffer dianggap penuh. read dapat kembali dengan byte lebih sedikit daripada yang diminta, sehingga jumlah yang dikirim ke write harus diambil dari nilai balik read, bukan dari kapasitas buffer.
  • Descriptor tertimpa nilai balik berikutnya. Descriptor disimpan di rax yang digunakan kembali oleh setiap syscall, sehingga pemanggilan read tanpa menyimpan descriptor terlebih dahulu membuat operasi berikutnya bekerja pada descriptor yang salah. Program di atas menyimpannya pada r12d tepat setelah open kembali.

Ringkasan#

  • open menerima nama berkas, flag akses, dan mode izin, lalu mengembalikan file descriptor kecil yang tidak negatif pada rax.
  • Descriptor 0, 1, dan 2 sudah digunakan stdin, stdout, dan stderr, sedangkan berkas yang baru dibuka biasanya menerima 3 karena kernel memberikan nomor terkecil yang bebas.
  • Penyalinan isi berkas berupa perulangan read dan write yang berhenti ketika read mengembalikan 0, dengan jumlah byte yang ditulis diambil dari nilai balik read.
  • close menerima satu descriptor dan mengembalikan 0, lalu nomor itu dapat digunakan kembali oleh pembukaan berikutnya.

Referensi#