Jump dan cabang bersyarat

Percabangan berdasarkan kombinasi flag individual dengan je, jne, jl, dan sejenisnya.

Daftar isi

Tidak ada pernyataan if pada tingkat mesin. Setiap keputusan yang dibuat program x86-64 berujung pada instruksi jump, yaitu instruksi yang memindahkan kendali eksekusi ke label yang ditunjuknya. Topik ini membahas keluarga jump bersyarat yang membaca flag individual, perbedaan tafsir signed dan unsigned atas bit yang sama, jmp yang selalu diambil, serta program klasifikasi bilangan yang menunjukkan cara kerja seluruh mekanisme tersebut.

Flag sebagai dasar keputusan#

Percabangan tidak membandingkan nilai secara langsung. Yang dibaca adalah flag, yaitu bit-bit status yang ditinggalkan instruksi sebelumnya. Instruksi cmp memperbarui flag dengan cara yang sama seperti sub, tetapi hasil pengurangannya tidak disimpan ke register maupun memori, sehingga perbandingan tidak mengubah data apa pun. Setelah cmp dijalankan, ZF menyala bila kedua operan sama, SF menyalin bit tertinggi hasil pengurangan, OF menyala bila hasil keluar dari jangkauan signed, dan CF menyala bila pengurangan unsigned meminjam.

Karena keputusan hanya bergantung pada flag, pola percabangannya selalu sama, yaitu perbandingan lebih dahulu, lalu satu jump bersyarat, lalu dua jalur program yang mungkin dilalui. Fragmen berikut memeriksa kesamaan rax dengan 42 dan menyimpan jawabannya di ebx.

nasm Pola percabangan dengan cmp dan je
    cmp rax, 42             ; hitung rax - 42, buang hasilnya
    je sama                 ; ZF menyala berarti rax sama dengan 42
    mov ebx, 1              ; jalur ketika tidak sama
    jmp selesai
sama:
    mov ebx, 0              ; jalur ketika sama
selesai:

Perbandingan dan keputusan adalah dua instruksi terpisah yang dihubungkan oleh flag. Pemisahan inilah yang membuat satu perbandingan dapat melayani beberapa jump berturut-turut dengan kondisi berbeda, sebab flag tetap berlaku sampai ada instruksi lain yang menimpanya.

Keluarga jcc dan tabel kondisinya#

Seluruh jump bersyarat dikenal sebagai keluarga jcc, dengan cc sebagai singkatan condition code. Setiap anggota memeriksa kombinasi flag yang berbeda, sedangkan perilaku yang lain identik, yaitu melompat ke label bila kondisi terpenuhi dan berlanjut ke instruksi berikutnya bila tidak. Tabel berikut merangkum anggota yang paling sering digunakan, dengan a sebagai operan pertama dan b sebagai operan kedua pada cmp.

InstruksiFlag yang diperiksaArti setelah cmp a, b
je, jneZFa sama atau tidak sama dengan b
js, jnsSFhasil negatif atau tidak negatif
jo, jnoOFoverflow signed atau tidak
jb, jbe, ja, jaeCF, dan ZF untuk jbe dan jaeperbandingan unsigned
jl, jle, jg, jgeSF dan OF, dan ZF untuk jle dan jgeperbandingan signed

Nama anggotanya mengikuti sudut pandang perbandingan. Huruf b dan a berasal dari below dan above untuk tafsir unsigned, sedangkan l dan g berasal dari less dan greater untuk tafsir signed. Huruf e menilai kesamaan melalui ZF saja, dan bentuk negatifnya ditandai huruf n, misalnya jne untuk jump if not equal. Pasangan berhuruf n juga melengkapi setiap kelompok, sehingga ada jns dan jno di samping js dan jo. Dua tafsir itu memungkinkan satu perbandingan menghasilkan dua keputusan berbeda atas bit yang sama, dan kasusnya diperlihatkan oleh program berikut.

jl dan jb pada bit yang sama#

Satu pola bit dapat berarti dua bilangan yang berbeda: pola 0xffffffff berarti -1 bila dibaca signed dan 4294967295 bila dibaca unsigned. cmp menghitung hasil pengurangan yang sama untuk kedua tafsir, sehingga flag yang dihasilkannya juga sama, dan yang berbeda hanyalah pasangan flag yang dibaca. Untuk cmp edi, 1 ketika edi berisi 0xffffffff, hasil 0xfffffffe membuat SF menyala, sedangkan OF dan CF padam. jl memeriksa apakah SF berbeda dengan OF, menemukan keduanya berbeda, dan melompat, sesuai fakta bahwa -1 lebih kecil daripada 1. jb memeriksa CF, menemukan CF padam, dan tidak melompat, sesuai fakta bahwa 4294967295 tidak lebih kecil daripada 1.

Program berikut menjalankan kedua jump tersebut terhadap perbandingan yang sama, lalu melaporkan keputusannya melalui kode keluar (exit code), yaitu angka kecil yang diserahkan program pada saat berhenti. Bit bernilai 1 menandai bahwa cabang signed diambil, dan bit bernilai 4 menandai bahwa cabang unsigned diambil.

nasm jl-jb.asm
; jl-jb.asm
; Perbandingan 0xffffffff dengan 1 dilihat dari dua tafsir.
; jl diambil karena -1 lebih kecil daripada 1 secara signed,
; sedangkan jb tidak diambil karena 4294967295 tidak lebih
; kecil daripada 1 secara unsigned.
; Kode keluar 1 berarti hanya cabang signed yang diambil.
default rel
section .text
global _start
_start:
    mov edi, 0xffffffff
    cmp edi, 1
uji:
    jl terambil_signed      ; SF berbeda dengan OF, yaitu -1 < 1
    xor ebx, ebx            ; cabang signed tidak diambil
    jmp lanjut
terambil_signed:
    mov ebx, 1              ; cabang signed diambil
lanjut:
    mov edi, 0xffffffff
    cmp edi, 1
    jb terambil_unsigned    ; CF menyala berarti lebih kecil secara unsigned
    jmp selesai
terambil_unsigned:
    or ebx, 4               ; cabang unsigned diambil
selesai:
    mov edi, ebx
    mov eax, 60
    syscall
bash
# rakit, tautkan, dan jalankan
nasm -f elf64 jl-jb.asm -o jl-jb.o && ld jl-jb.o -o jl-jb && ./jl-jb; echo "exit=$?"
# amati flag sesudah cmp dan lompatan yang diambil
gdb -batch -ex 'break uji' -ex 'run' -ex 'info registers eflags' -ex 'nexti' ./jl-jb
text Keluaran
exit=1
Breakpoint 1 at 0x401008

Breakpoint 1, 0x0000000000401008 in uji ()
eflags         0x282               [ SF IF ]
0x000000000040100e in terambil_signed ()

Keluaran menunjukkan dua hal. Kode keluar 1 berarti hanya jalur signed yang dilalui. Baris eflags memuat tanda [ SF IF ], yang berarti SF menyala pada saat jl dievaluasi, sedangkan ZF, OF, dan CF tidak tercantum karena padam. Instruksi nexti yang mengikuti mendarat di terambil_signed, sehingga keputusan jump terbaca langsung dari debugger, bukan dari dugaan.

Program klasifikasi bilangan#

Perbedaan antar anggota jcc memungkinkan satu perbandingan melayani beberapa keputusan sekaligus. Program berikut membaca satu nilai dari memori, lalu menggolongkannya sebagai negatif, nol, atau positif. cmp terhadap nol menetapkan SF dan ZF sekaligus, sehingga js menangkap kasus negatif, jz menangkap nol, dan kasus positif jatuh ke jalur tanpa jump apa pun. Hasil penggolongan dikirim sebagai kode keluar, yaitu 2 untuk negatif, 1 untuk positif, dan 0 untuk nol.

nasm klasifikasi.asm
; klasifikasi.asm
; Mengklasifikasikan satu nilai sebagai negatif, nol, atau positif.
; Hasilnya dikirim melalui kode keluar, yaitu 2 untuk negatif,
; 1 untuk positif, dan 0 untuk nol.
default rel
section .data
nilai: dq -12

section .text
global _start
_start:
    mov rax, [nilai]
    cmp rax, 0              ; perbandingan terhadap nol
    js negatif              ; SF menyala berarti negatif
    jz nol                  ; ZF menyala berarti nol
    mov edi, 1              ; selain keduanya berarti positif
    jmp selesai
negatif:
    mov edi, 2
    jmp selesai
nol:
    mov edi, 0
selesai:
    mov eax, 60             ; syscall exit dengan status edi
    syscall
bash
# rakit, tautkan, dan jalankan dengan nilai -12
nasm -f elf64 klasifikasi.asm -o klasifikasi.o && ld klasifikasi.o -o klasifikasi && ./klasifikasi; echo "exit=$?"
# ulangi dengan nilai positif
sed 's/dq -12/dq 7/' klasifikasi.asm > klasifikasi-positif.asm
nasm -f elf64 klasifikasi-positif.asm -o klasifikasi-positif.o && ld klasifikasi-positif.o -o klasifikasi-positif && ./klasifikasi-positif; echo "exit=$?"
# ulangi sekali lagi dengan nol
sed 's/dq -12/dq 0/' klasifikasi.asm > klasifikasi-nol.asm
nasm -f elf64 klasifikasi-nol.asm -o klasifikasi-nol.o && ld klasifikasi-nol.o -o klasifikasi-nol && ./klasifikasi-nol; echo "exit=$?"
text Keluaran
exit=2
exit=1
exit=0

Tiga eksekusi di atas menggunakan berkas program yang sama, sedangkan nilainya diubah dengan sed sebelum dirakit ulang. Nilai -12 memenuhi js karena SF menyala, dan programnya berakhir dengan kode keluar 2. Nilai 7 tidak memenuhi js maupun jz, sehingga eksekusinya jatuh ke jalur positif dengan kode keluar 1. Nilai 0 memenuhi jz, dan programnya berhenti dengan kode keluar 0. Susunan seperti ini, yaitu beberapa jump berurutan atas satu perbandingan, adalah bentuk percabangan tiga arah yang paling ringkas di tingkat mesin, dan setiap jalurnya diakhiri jmp selesai supaya eksekusi tidak jatuh ke label berikutnya secara tidak sengaja.

jmp tak bersyarat#

jmp tidak membaca flag dan selalu diambil. Perannya bukan memutuskan, melainkan mengatur jalur eksekusi. Pada program klasifikasi, jmp selesai mencegah jalur positif jatuh ke label negatif. Pada struktur perulangan, jmp mengembalikan kendali ke label kondisi di atas tubuh. jmp juga dapat menunjuk labelnya sendiri, sehingga program berputar di satu instruksi tanpa pernah berakhir.

nasm Jump ke labelnya sendiri
tunggu:
    jmp tunggu              ; tidak pernah berakhir tanpa gangguan dari luar

Perputaran seperti pada fragmen itu hanya berhenti bila ada gangguan dari luar, misalnya sinyal dari sistem operasi. Di dalam program nyata, bentuk yang sama menjadi kerangka perulangan begitu ada kondisi yang menghentikannya, hanya dengan menambah pemeriksaan flag di dalam tubuhnya. Sifat tanpa syarat jmp juga tampak pada encoding-nya, karena instruksi ini tidak membawa nomor kondisi seperti anggota jcc. Dengan demikian, pemilihan bentuk mengikuti kebutuhan, yaitu cabang bersyarat untuk keputusan dan jmp untuk pengaturan jalur tanpa syarat.

Ringkasan#

  • Percabangan assembly membaca flag yang ditinggalkan cmp atau instruksi lain, bukan membandingkan nilai secara langsung.
  • Setiap anggota jcc menilai kombinasi flag tertentu, misalnya ZF untuk je dan jne, SF bersama OF untuk keluarga signed jl, jle, jg, dan jge, serta CF untuk keluarga unsigned jb, jbe, ja, dan jae.
  • Bit yang sama dapat menghasilkan keputusan berbeda pada jl dan jb karena tafsir signed dan unsigned atas pola bit tersebut berbeda.
  • jmp tidak membaca flag dan selalu diambil, sehingga digunakan untuk mengatur jalur, bukan untuk memutuskan.

Referensi#