Perulangan secara manual

Perulangan yang dibangun dari label, perbandingan, dan jump mundur tanpa instruksi loop.

Daftar isi

Kata kunci while dan for tidak memiliki padanan berupa satu instruksi di tingkat mesin. Perulangan tetap tersusun dari bagian yang sama seperti pada bahasa tingkat tinggi, yaitu inisialisasi, kondisi, tubuh, dan kenaikan, tetapi setiap bagian ditulis sebagai instruksi biasa dengan label sebagai penanda titik ulangnya. Topik ini membangun perulangan secara manual dari label, perbandingan, dan jump mundur, menampilkan penjumlahan bilangan 1 sampai 100 dengan hasil yang dibaca dari debugger, serta menimbang instruksi loop sebagai alternatif yang jarang dipilih compiler modern.

Empat bagian perulangan manual#

Perulangan manual memerlukan empat bagian, yaitu inisialisasi yang menetapkan nilai awal, kondisi yang menentukan apakah perulangan berlanjut atau berhenti, tubuh yang menjalankan pekerjaan, dan kenaikan yang menggerakkan penghitung menuju kondisi berakhir. Label memegang peran kurung kurawal pada bahasa tingkat tinggi. Jump mundur, yaitu jump yang menunjuk label dengan alamat lebih rendah, mengembalikan kendali ke atas tubuh, sedangkan jump ke label selesai membawa kendali keluar dari perulangan.

Kerangka perulangan naik berikut menempatkan kondisi di awal tubuh, sehingga nilai yang melewati batas tidak pernah ikut diproses.

nasm Kerangka perulangan naik
    mov rcx, 1              ; inisialisasi
.ulang:
    cmp rcx, 100            ; kondisi
    ja selesai              ; melewati batas berarti berhenti
    add rax, rcx            ; tubuh
    add rcx, 1              ; kenaikan
    jmp .ulang              ; kembali ke kondisi
selesai:

Label yang diawali titik seperti .ulang bersifat lokal terhadap label global terdekat, sehingga kerangka yang sama dapat digunakan di beberapa prosedur tanpa bentrok nama. Keempat bagiannya juga terbaca jelas pada fragmen itu, yaitu mov sebagai inisialisasi, cmp dan ja sebagai kondisi, add rax, rcx sebagai tubuh, dan add rcx, 1 sebagai kenaikan. Dalam kasus lain, bagian kenaikan dapat berupa penurunan, dan kondisinya menyesuaikan arah gerak penghitung.

Menjumlahkan 1 sampai 100#

Contoh berikut menggunakan arah yang berlawanan dengan kerangka naik, yaitu penghitung menurun dari 100 menuju nol. Keuntungannya, kondisi berakhir sudah tersedia tanpa perbandingan tambahan, karena sub menetapkan ZF pada saat penghitung mencapai nol, sehingga jne cukup membaca flag itu. Keempat bagian tetap lengkap, tetapi kondisinya berpindah ke akhir tubuh, tepat pada jump mundurnya.

nasm jumlah.asm
; jumlah.asm
; Menjumlahkan bilangan 1 sampai 100 dengan perulangan manual.
; Penghitung menurun dari 100 menuju nol sehingga jne
; cukup membaca ZF yang ditetapkan sub.
default rel
section .text
global _start
_start:
    mov rax, 0              ; inisialisasi, akumulator nol
    mov rcx, 100            ; inisialisasi, penghitung 100
.ulang:
    add rax, rcx            ; tubuh, tambahkan penghitung ke akumulator
    sub rcx, 1              ; penurunan, kurangi penghitung satu
    jne .ulang              ; kondisi, ulangi selama rcx belum nol
selesai:
    mov edi, eax
    mov eax, 60
    syscall
bash
# rakit, tautkan, dan jalankan
nasm -f elf64 jumlah.asm -o jumlah.o && ld jumlah.o -o jumlah && ./jumlah; echo "exit=$?"
# baca nilai penuh akumulator di debugger
gdb -batch -ex 'break selesai' -ex 'run' -ex 'info registers rax rcx' ./jumlah
text Keluaran
exit=186
Breakpoint 1 at 0x401013

Breakpoint 1, 0x0000000000401013 in selesai ()
rax            0x13ba              5050
rcx            0x0                 0

Akumulator rax benar berisi 5050, yaitu 0x13ba, pada saat kendali tiba di selesai, dan rcx sudah mencapai nol sesuai kondisi berakhirnya. Menjalankan ./jumlah secara langsung hanya menampilkan exit=186 karena kode keluar hanya berukuran 8 bit, dan 5050 dibagi 256 menyisakan 186. Nilai penuhnya tetap utuh di register sampai syscall exit dijalankan, dan debugger memperlihatkannya sebagai pasangan 0x13ba dan 5050 pada baris rax. Angka pada baris rcx, yaitu 0x0 dan 0, menegaskan bahwa perulangan berhenti tepat setelah penghitung habis. Pola membaca hasil dari register melalui debugger menjadi kebiasaan yang berguna karena kode keluar memang sempit.

Instruksi loop sebagai alternatif#

Instruksi loop memadatkan dua langkah versi manual menjadi satu, yaitu menurunkan rcx sebanyak satu dan melompat mundur selama rcx belum nol. Program penjumlahan yang sama dapat ditulis ulang sebagai berikut.

nasm jumlah-loop.asm
; jumlah-loop.asm
; Versi penjumlahan yang sama dengan memakai instruksi loop.
; loop menurunkan rcx dan melompat mundur selama rcx belum nol.
default rel
section .text
global _start
_start:
    mov rax, 0              ; akumulator nol
    mov rcx, 100            ; penghitung 100
.ulang:
    add rax, rcx            ; tubuh
    loop .ulang             ; turunkan rcx, ulangi selama belum nol
selesai:
    mov edi, eax
    mov eax, 60
    syscall
bash
# rakit, tautkan, dan jalankan versi loop
nasm -f elf64 jumlah-loop.asm -o jumlah-loop.o && ld jumlah-loop.o -o jumlah-loop && ./jumlah-loop; echo "exit=$?"
text Keluaran
exit=186

Hasilnya identik dengan versi manual. Meskipun benar dan lebih ringkas, loop jarang dipilih compiler modern. Alasannya praktis, yaitu pada banyak prosesor x86-64 modern instruksi ini berjalan lebih lambat daripada pasangan sub dan jne yang terpisah, sehingga compiler cenderung menghasilkan bentuk manual untuk perulangan dengan tubuh yang pendek. Instruksi loop tetap perlu dikenal karena muncul pada kode lama dan pada hasil disassembly program yang sudah jadi. Strukturnya juga mengikat penghitung pada rcx secara khusus, sedangkan bentuk manual bebas memilih register mana pun, misalnya ketika rcx sudah terpakai oleh perulangan luar pada struktur bersarang.

Kesalahan umum#

Dua kesalahan paling lazim pada perulangan manual menyangkut penghitungnya.

Penghitung yang tidak bergerak. Menghapus sub rcx, 1 pada jumlah.asm membuat rcx selalu 100, sehingga jne selalu terpenuhi dan program tidak pernah tiba di syscall exit. Program yang dijalankan di terminal tampak membeku, dan satu-satunya jalan untuk berhenti adalah sinyal interrupt dari luar. Perbaikannya adalah memastikan setiap jalur di dalam tubuh menggerakkan penghitung menuju kondisi berakhir.

Batas yang meleset satu. Menulis cmp rcx, 99 pada kerangka naik membuat angka 100 tidak pernah dijumlahkan, sedangkan batas 101 memasukkan angka 101. Hasilnya menjadi 4950 atau 5151 alih-alih 5050. Perbaikannya adalah menguji tepi batas dengan memastikan nilai pertama dan nilai terakhir yang diinginkan benar-benar masuk ke tubuh, satu demi satu.

Ringkasan#

  • Perulangan manual tersusun dari inisialisasi, kondisi, tubuh, dan kenaikan, dengan label sebagai titik ulang dan jump mundur sebagai pengembali kendali.
  • Penghitung menurun memanfaatkan ZF dari sub, sehingga kondisi berakhirnya cukup diperiksa oleh jne tanpa cmp tambahan, dan jumlah 1 sampai 100 tampak sebagai 0x13ba atau 5050 di debugger.
  • Kode keluar hanya berukuran 8 bit, sehingga 5050 tampil sebagai 186 pada exit=, dan nilai penuhnya harus dibaca dari register.
  • Instruksi loop memadatkan penurunan dan lompatan menjadi satu, tetapi compiler modern cenderung memilih bentuk manual karena latensi pasangan sub dan jne lebih rendah.

Referensi#