Topik ini menjelaskan cara add, sub, dan cmp memperbarui register flag pada x86-64. Empat flag utama diperkenalkan satu per satu, diamati langsung lewat gdb pada empat kasus aritmetika, lalu dibaca dari dalam program melalui stack. Pemahaman ini diperlukan karena seluruh percabangan bersyarat x86-64 mengambil keputusan dari flag yang ditinggalkan instruksi sebelumnya.
Empat flag utama#
Setiap operasi aritmetika pada x86-64 meninggalkan jejak hasilnya pada register EFLAGS. Empat flag yang paling sering digunakan adalah ZF, CF, SF, dan OF. Flag adalah bit tunggal yang bernilai 1 ketika ditetapkan dan 0 ketika dibersihkan, dan nilainya dibaca oleh instruksi lain, bukan sebagai angka.
| Flag | Nama lengkap | Artinya | Contoh kejadiannya |
|---|---|---|---|
| ZF | zero flag | hasil operasi sama dengan nol | 5 - 5 |
| CF | carry flag | ada bit yang keluar dari 64 bit pada pembacaan tak bertanda | 0xffffffffffffffff + 1 |
| SF | sign flag | salinan bit 63 dari hasil | hasil 0x8000000000000000 |
| OF | overflow flag | hasil melampaui rentang bilangan bertanda | 0x7fffffffffffffff + 1 |
CF dan OF menjawab dua pertanyaan yang berbeda atas hasil yang sama. CF melihat hasil sebagai bilangan tak bertanda dan menandai bit keluaran yang tidak muat, sedangkan OF melihat hasil sebagai bilangan bertanda komplemen dua dan menandai hasil yang keluar dari rentang -9223372036854775808 sampai 9223372036854775807. Satu operasi dapat menetapkan keduanya, salah satunya, atau tidak sama sekali.
Mengamati flag lewat gdb#
Program berikut menjalankan empat kasus aritmetika secara berurutan, dan gdb berhenti pada label tepat setelah setiap kasus. gdb memeriksa EFLAGS pada setiap pemberhentian, sehingga flag yang tersisa dari instruksi terakhir dapat dibaca tanpa menambahkan kode apa pun.
default rel
section .text
global _start
_start:
mov rax, 0x7fffffffffffffff
add rax, 1 ; kasus 1, luapan bilangan bertanda
kasus1:
mov rax, 0xffffffffffffffff
add rax, 1 ; kasus 2, carry bilangan tak bertanda
kasus2:
mov rax, 5
sub rax, 7 ; kasus 3, pengurangan dengan pinjaman
kasus3:
mov rax, 42
cmp rax, 42 ; kasus 4, perbandingan nilai sama
kasus4:
mov edi, 0
mov eax, 60
syscall# rakit dan tautkan program
nasm -f elf64 flag-demo.asm -o flag-demo.o
ld flag-demo.o -o flag-demo
# flag diperiksa pada empat pemberhentian, satu per kasus
gdb -batch -ex 'break kasus1' -ex 'break kasus2' -ex 'break kasus3' -ex 'break kasus4' -ex 'run' -ex 'info registers eflags' -ex 'continue' -ex 'info registers eflags' -ex 'continue' -ex 'info registers eflags' -ex 'continue' -ex 'info registers eflags' ./flag-demoBreakpoint 1 at 0x40100e
Breakpoint 2 at 0x401019
Breakpoint 3 at 0x401022
Breakpoint 4 at 0x40102b
Breakpoint 1, 0x000000000040100e in kasus1 ()
eflags 0xa96 [ PF AF SF IF OF ]
Breakpoint 2, 0x0000000000401019 in kasus2 ()
eflags 0x257 [ CF PF AF ZF IF ]
Breakpoint 3, 0x0000000000401022 in kasus3 ()
eflags 0x293 [ CF AF SF IF ]
Breakpoint 4, 0x000000000040102b in kasus4 ()
eflags 0x246 [ PF ZF IF ]Keluaran menampilkan nilai EFLAGS dalam heksadesimal beserta daftar flag yang sedang ditetapkan. Kasus pertama menetapkan OF dan SF, karena penjumlahan 0x7fffffffffffffff dengan 1 melampaui rentang bilangan bertanda dan hasilnya 0x8000000000000000 memiliki bit 63 bernilai 1. Kasus kedua menetapkan CF dan ZF untuk penjumlahan yang membawa bit keluar pada pembacaan tak bertanda. Kasus ketiga menetapkan CF dan SF karena 5 - 7 meminjam dan hasilnya negatif. Kasus keempat menetapkan ZF karena cmp mendapati kedua operannya sama.
Daftar flag pada keluaran juga memuat PF dan AF, yaitu parity flag dan auxiliary carry flag yang jarang digunakan pada kode bilangan bulat sehari-hari. Keduanya diperbarui oleh operasi aritmetika yang sama dan muncul dalam daftar karena sedang ditetapkan. Daftar itu hanya menampilkan flag yang bernilai 1, sehingga keberadaan sebuah nama berarti flag ditetapkan dan ketiadaannya berarti flag dibersihkan.
CF berbeda dari OF#
Perbedaan CF dan OF paling mudah dilihat dengan membandingkan kasus pertama dan kasus kedua. Penjumlahan 0x7fffffffffffffff dengan 1 menghasilkan 0x8000000000000000. Sebagai bilangan bertanda, hasil itu melompat dari nilai maksimum ke nilai minimum, sehingga OF ditetapkan, sedangkan tidak ada bit yang keluar dari 64 bit dan CF tetap 0. Sebaliknya, penjumlahan 0xffffffffffffffff dengan 1 menghasilkan nol yang masih sah bagi bilangan bertanda, tetapi satu bit keluar dari 64 bit, sehingga CF ditetapkan dan OF dibersihkan.
Kaidah praktisnya, CF melaporkan luapan untuk pembacaan bilangan tak bertanda, sedangkan OF melaporkan luapan untuk pembacaan bilangan bertanda. Kode yang membandingkan bilangan tak bertanda membaca CF, sedangkan kode yang membandingkan bilangan bertanda membaca OF bersama SF. Flag yang sesuai dengan jenis bilangan perlu dipilih lebih dahulu agar keputusan tidak terbalik.
cmp dan pembacaan flag dari program#
cmp adalah sub yang membuang hasilnya. Kedua operan dikurangkan semata-mata untuk menghasilkan flag, sedangkan kedua register operan tidak berubah. Nilai yang sama menghasilkan ZF bernilai 1, nilai pertama yang lebih kecil menghasilkan CF bernilai 1 pada pembacaan tak bertanda, dan urutan operan menentukan arah pengurangannya.
Flag juga dapat dibaca dari dalam program dengan menyimpan EFLAGS ke stack terlebih dahulu. Instruksi pushfq mendorong EFLAGS ke stack, lalu pop rax menariknya ke rax, sehingga setiap flag berubah menjadi bit biasa yang dapat diambil dengan and. Bit yang digunakan pada contoh berikut adalah CF pada bit 0, ZF pada bit 6, SF pada bit 7, dan OF pada bit 11.
default rel
section .text
global _start
_start:
mov rax, 5
sub rax, 7 ; 5 - 7 meminjam, sehingga CF = 1
pushfq
pop rax ; salin EFLAGS ke rax
and eax, 0x1 ; ambil bit CF (bit 0) saja
mov edi, eax ; kode keluar = nilai CF
mov eax, 60
syscall# rakit program, jalankan, lalu tampilkan kode keluarnya
nasm -f elf64 flag-exit.asm -o flag-exit.o
ld flag-exit.o -o flag-exit
./flag-exit
echo "exit=$?"exit=1Pengurangan 5 - 7 meminjam, sehingga bit CF pada EFLAGS bernilai 1. Nilai itu lolos penapis and eax, 0x1 dan dibawa keluar sebagai kode keluar program, yang terbaca sebagai exit=1 oleh shell. Pola pushfq dan pop inilah yang digunakan untuk membaca flag tanpa debugger, misalnya sebelum percabangan dibangun secara manual.
Kesalahan umum#
Mencari hasil cmp di dalam register. Instruksi cmp rax, 42 tidak mengubah rax, sehingga kode yang membaca rax setelahnya menemukan nilai lama. Hasil perbandingan hanya tersedia melalui flag, dan flag itu harus dibaca sebelum instruksi aritmetika lain menimpanya.
Menggunakan flag yang keliru untuk jenis bilangannya. Membaca CF pada bilangan bertanda, atau OF pada bilangan tak bertanda, menghasilkan keputusan yang terbalik di sekitar nilai negatif dan nilai besar. Tentukan jenis bilangan yang dibandingkan terlebih dahulu, lalu pilih flag atau gabungan flag yang sesuai.
Membaca flag dari instruksi yang salah. Flag adalah sisa instruksi terakhir yang memperbaruinya, sehingga ZF yang dibaca sebelum cmp mana pun dijalankan adalah jejak instruksi lain yang tidak berhubungan. Kebiasaan yang aman adalah menempatkan cmp dan pembacaan flagnya berdampingan tanpa instruksi aritmetika di antaranya.
Ringkasan#
add,sub, dancmpmemperbarui ZF, CF, SF, dan OF sesuai hasil operasinya, sedangkancmpmembuang hasil pengurangannya.- CF melaporkan bit keluaran untuk pembacaan tak bertanda, sedangkan OF melaporkan luapan untuk pembacaan bertanda.
- EFLAGS dapat diselamatkan ke register dengan pushfq dan pop, lalu flag individual diambil dengan
andpada bit 0, 6, 7, dan 11. - gdb menampilkan daftar flag yang sedang ditetapkan melalui
info registers eflags.
Referensi#
- Intel 64 and IA-32 Architectures Software Developer’s Manual — Intel — Volume 1 mendefinisikan EFLAGS dan perilaku flag pada ADD, SUB, dan CMP.
- ADD — x86 and x86-64 Instruction Set Reference — Felix Cloutier — daftar flag yang diperbarui ADD beserta aturan perhitungannya.
- gdb(1) — Linux manual page — opsi -batch dan -ex yang dipakai untuk memeriksa EFLAGS tanpa sesi interaktif.