Membangun input dengan buffer

Buffer baca yang dapat digunakan kembali di atas syscall mentah sampai data terbaca penuh.

Daftar isi

Pustaka standar memberikan fungsi input yang tampak sederhana, tetapi di baliknya selalu ada pola yang sama, yaitu sebuah buffer memori yang diisi berulang kali melalui syscall read sampai masukan habis. Topik ini membangun pola itu secara langsung tanpa libc, mulai dari pemesanan buffer di section .bss, perulangan read sampai nilai balik 0, hingga penanganan sebagian terbaca yang dibuktikan dengan strace. Hasil akhirnya adalah program penyalin masukan ke stdout yang bekerja untuk berkas dengan ukuran apa pun.

Buffer baca di section .bss#

Buffer adalah area memori sementara yang menampung data hasil pembacaan sebelum diproses. Pada program tanpa libc, buffer dipesan di section .bss melalui direktif resb, yaitu direktif yang memesan sejumlah byte tanpa mengisinya, sehingga tidak menambah ukuran berkas program. Section ini dipilih karena isi buffer memang tidak memerlukan nilai awal, mengingat setiap putaran perulangan menimpanya dengan data baru dari read. Kapasitas yang dipilih pada contoh berikut adalah 64 byte, dan angka itu dikirim ke kernel melalui rdx pada setiap pemanggilan read. Isi buffer sendiri tidak dijamin kosong, sehingga jumlah byte yang sah selalu ditentukan oleh nilai balik read, bukan oleh isi memori.

Buffer yang sama digunakan kembali pada setiap putaran perulangan. Tidak ada pemesanan memori baru di tengah proses, karena alamat buffer tetap dan hanya isinya yang berganti. Pola inilah yang dilakukan fungsi input pada pustaka standar, yaitu mengisi buffer berulang kali, memotongnya sesuai jumlah byte yang sah, lalu menyerahkannya kepada pemanggil.

Perulangan read sampai masukan habis#

Program berikut membaca descriptor 0 yang sudah tersambung oleh shell ke sebuah berkas melalui pengalihan <, lalu menyalin seluruh isinya ke stdout. Syscall open tidak diperlukan karena shell menyiapkan descriptor 0 sebelum program dimulai.

nasm salin-stdin.asm
; salin-stdin.asm
; Menyalin seluruh masukan ke stdout dengan satu buffer yang dipakai ulang.
default rel
section .bss
buffer: resb 64
section .text
global _start
_start:
ulang:
    mov eax, 0              ; nomor syscall read
    mov edi, 0              ; file descriptor 0, yaitu stdin
    lea rsi, [buffer]       ; tempat data ditampung
    mov edx, 64             ; kapasitas buffer
    syscall
    test rax, rax
    jle selesai             ; 0 berarti masukan habis
    mov r12, rax            ; jumlah byte yang benar-benar terbaca

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

selesai:
    xor edi, edi
    mov eax, 60             ; nomor syscall exit
    syscall
bash
# buat berkas masukan dua baris
printf 'suhu pagi: 24\nsuhu siang: 27\n' > data.txt
# rakit, tautkan, dan jalankan dengan pengalihan stdin
nasm -f elf64 salin-stdin.asm -o salin-stdin.o && ld salin-stdin.o -o salin-stdin && ./salin-stdin < data.txt; echo "exit=$?"
text Keluaran
suhu pagi: 24
suhu siang: 27
exit=0

Program menyalin kedua baris data.txt lalu berhenti dengan kode keluar 0. Perulangan mengikuti aturan yang sederhana, yaitu read dipanggil, nilainya diperiksa dengan test rax, rax, dan perulangan berhenti hanya bila nilai itu 0 atau negatif. Selama nilainya positif, byte sebanyak nilai itulah yang dikirim ke write melalui mov edx, r12d, kemudian jmp ulang mengulang pembacaan pada buffer yang sama.

Struktur ini adalah bentuk dasar dari pembaca berkas di mana pun. Fungsi input pada pustaka standar mengulang langkah yang sama, yaitu mengisi buffer, memotongnya sesuai nilai balik, dan berhenti pada 0. Perbedaannya hanya pada letak langkah-langkah itu, karena versi assembly menempatkannya pada register dan perintah yang dapat diamati satu per satu tanpa lapisan pembungkus.

Sebagian terbaca dan pembacaan yang nyata#

Pembacaan yang mengembalikan lebih sedikit dari jumlah yang diminta disebut partial read, dan perilaku ini bukan keanehan melainkan konsekuensi dari definisi read. Kernel hanya wajib mengembalikan apa yang tersedia saat itu. Pada berkas biasa, pembacaan biasanya penuh sampai batas akhir berkas, sedangkan pada terminal, pipe, dan sambungan jaringan, data sering tiba sedikit demi sedikit sehingga nilai baliknya lebih kecil dari kapasitas buffer. Program yang menganggap buffer selalu penuh akan menyalin byte sisa dari putaran sebelumnya.

Perilaku nyata ini dapat diamati melalui strace, terlebih dahulu pada masukan yang kecil.

bash
# lacak syscall read dan write pada masukan kecil
strace -o trace.log ./salin-stdin < data.txt > /dev/null
# tampilkan baris read, write, dan akhir proses dari catatan itu
grep -E '^(read|write)|exit' trace.log
text Keluaran
read(0, "suhu pagi: 24\nsuhu siang: 27\n", 64) = 29
write(1, "suhu pagi: 24\nsuhu siang: 27\n", 29) = 29
read(0, "", 64)                         = 0
exit(0)                                 = ?
+++ exited with 0 +++

Masukan berukuran 29 byte diterima dalam satu pemanggilan read dan disalin penuh oleh satu write, lalu read berikutnya mengembalikan 0 sebagai tanda masukan habis. Urutan inilah yang menggerakkan perulangan, yaitu pembacaan, penyalinan, dan pembacaan terakhir yang menghasilkan 0.

Masukan yang lebih besar daripada buffer memperlihatkan sisa bagiannya. Berkas riwayat.txt berisi 20 baris pengukuran, yaitu 20 kali 11 byte atau 220 byte, sedangkan buffer hanya 64 byte.

bash
# buat masukan yang lebih besar daripada buffer
yes pengukuran | head -n 20 > riwayat.txt
wc -c < riwayat.txt
# jalankan lagi dan amati pemecahan pembacaannya
strace -o trace.log ./salin-stdin < riwayat.txt > /dev/null
grep -E '^(read|write)|exit' trace.log
text Keluaran
220
read(0, "pengukuran\npengukuran\npengukuran"..., 64) = 64
write(1, "pengukuran\npengukuran\npengukuran"..., 64) = 64
read(0, "n\npengukuran\npengukuran\npengukur"..., 64) = 64
write(1, "n\npengukuran\npengukuran\npengukur"..., 64) = 64
read(0, "ran\npengukuran\npengukuran\npenguk"..., 64) = 64
write(1, "ran\npengukuran\npengukuran\npenguk"..., 64) = 64
read(0, "kuran\npengukuran\npengukuran\n", 64) = 28
write(1, "kuran\npengukuran\npengukuran\n", 28) = 28
read(0, "", 64)                         = 0
exit(0)                                 = ?
+++ exited with 0 +++

Tiga pembacaan pertama mengembalikan 64 byte penuh, pembacaan keempat mengembalikan sisa 28 byte, dan pembacaan kelima mengembalikan 0. Jumlahnya cocok dengan ukuran berkas, yaitu 192 byte dari tiga pembacaan penuh ditambah 28 byte sisa. Setiap write mengikuti nilai balik read yang berada tepat di atasnya, termasuk write terakhir yang hanya mengirim 28 byte. Apabila write dipanggil dengan 64 byte tanpa membaca nilai balik, keluaran akan berisi byte sisa dari buffer putaran sebelumnya.

Ringkasan#

  • Buffer baca dipesan di section .bss melalui resb dan digunakan kembali pada setiap putaran tanpa pemesanan memori baru.
  • Perulangan input berhenti ketika read mengembalikan 0, sedangkan nilai positif digunakan sebagai jumlah byte yang dikirim ke write.
  • Partial read adalah perilaku sah dari read yang mengembalikan lebih sedikit dari yang diminta, sehingga nilai baliknya wajib digunakan dan pembacaan tidak boleh dianggap penuh.
  • strace memperlihatkan urutan nyatanya, yaitu pembacaan 64 byte yang berulang, satu pembacaan sisa 28 byte, dan pembacaan 0 sebagai penutup.

Referensi#