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.
| Descriptor | Nama | Peran |
|---|---|---|
| 0 | stdin | masukan standar program |
| 1 | stdout | keluaran standar program |
| 2 | stderr | saluran 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.
; 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# 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=$?"suhu pagi: 24
suhu siang: 27
exit=0Isi 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.
# amati nilai balik open di dalam debugger
gdb -batch -ex 'break sesudah_open' -ex 'run' -ex 'p/d $rax' ./salin-berkasBreakpoint 1 at 0x401012
Breakpoint 1, 0x0000000000401012 in sesudah_open ()
$1 = 3Baris $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.txtdiakhiri0karena kernel membaca argumenopensampai menemukan byte nol sebagai penanda ujungnya. Nama tanpa penutup itu membuat kernel terus membaca memori di sebelahnya danopengagal 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
opendan tidak pernah ditebak. - Buffer dianggap penuh.
readdapat kembali dengan byte lebih sedikit daripada yang diminta, sehingga jumlah yang dikirim kewriteharus diambil dari nilai balikread, bukan dari kapasitas buffer. - Descriptor tertimpa nilai balik berikutnya. Descriptor disimpan di
raxyang digunakan kembali oleh setiap syscall, sehingga pemanggilanreadtanpa menyimpan descriptor terlebih dahulu membuat operasi berikutnya bekerja pada descriptor yang salah. Program di atas menyimpannya padar12dtepat setelahopenkembali.
Ringkasan#
openmenerima nama berkas, flag akses, dan mode izin, lalu mengembalikan file descriptor kecil yang tidak negatif padarax.- 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
readdanwriteyang berhenti ketikareadmengembalikan 0, dengan jumlah byte yang ditulis diambil dari nilai balikread. closemenerima satu descriptor dan mengembalikan 0, lalu nomor itu dapat digunakan kembali oleh pembukaan berikutnya.
Referensi#
- open(2) — Linux man-pages project
— Merinci argumen
open, flag akses sepertiO_RDONLY, dan aturan pemilihan descriptor. - read(2) — Linux man-pages project
— Menjelaskan nilai balik
read, termasuk kondisi akhir masukan dan pembacaan yang lebih sedikit dari yang diminta. - write(2) — Linux man-pages project
— Menjelaskan argumen dan nilai balik
writepada descriptor.