Topik ini membahas LEA, instruksi yang menghitung alamat efektif dan meletakkan hasilnya di register tanpa membaca memori. Komponen alamat efektif diuraikan satu per satu dan dibuktikan pada penghitungan alamat elemen array, lalu LEA digunakan sebagai aritmetika yang murah. Pada akhir topik, perbedaan LEA terhadap MOV dan MUL dapat dijelaskan dari perilakunya sendiri.
Komponen alamat efektif#
Alamat efektif (effective address) adalah nilai alamat yang dihitung dari beberapa komponen sebelum benar-benar digunakan untuk menyentuh memori. Pada mode 64-bit, bentuk lengkapnya dirangkum oleh persamaan berikut.
Komponen basis dan indeks berupa register 64-bit, skala hanya boleh bernilai 1, 2, 4, atau 8, sedangkan displacement berupa konstanta bertanda yang tertulis langsung di dalam instruksi. Tidak semua komponen wajib ada, sehingga [rdi], [rdi + rsi*4], dan [rdi + rsi*4 + 8] semuanya merupakan alamat efektif yang sah.
| Komponen | Isinya | Contoh |
|---|---|---|
| basis | satu register 64-bit | rdi |
| indeks | satu register 64-bit yang dikalikan skala | rsi*4 |
| skala | konstanta 1, 2, 4, atau 8 | 4 |
| displacement | konstanta bertanda | 8 |
Skala 1 tetap sah dan lazim ditulis eksplisit, misalnya [rdi + rsi*1], meskipun hasilnya sama dengan [rdi + rsi]. Displacement juga boleh negatif, sehingga [rdi - 8] merujuk 8 byte sebelum alamat yang ditunjuk rdi. Kombinasi lengkap inilah yang membuat satu bentuk pengalamatan mampu menjangkau elemen array, field struct, dan variabel lokal tanpa perhitungan tambahan.
Contoh beranotasi berikut memperlihatkan posisi setiap komponen pada satu instruksi.
lea rax, [rdi + rsi*4 + 8]
; | | |
; | | +-- displacement berupa 8
; | +-------- indeks rsi dengan skala 4
; +--------------- basis rdiHasil perhitungan rdi + rsi*4 + 8 ditaruh di rax. LEA dipilih karena hasil yang dibutuhkan memang berupa alamat, tetapi perhatikan bahwa tidak ada satu byte pun yang dibaca dari memori untuk menghasilkannya.
LEA tidak menyentuh memori#
Perbedaan LEA dengan MOV terletak pada langkah terakhirnya. MOV menghitung alamat efektif, lalu membaca atau menulis memori pada alamat tersebut. LEA berhenti pada langkah perhitungan dan menaruh angka alamatnya di register tujuan, tanpa akses memori dan tanpa memperbarui flag. Instruksi ini tepat digunakan ketika alamat elemen array dibutuhkan sebagai nilai, misalnya untuk disimpan sebagai pointer atau diteruskan ke fungsi.
Program berikut mengisi elemen indeks 5 dari sebuah array berisi delapan bilangan 32-bit. Alamat dasar array dihitung dengan lea rdi, [suhu], sedangkan alamat elemen dihitung dengan lea rax, [rdi + rsi*4] karena setiap elemen berjarak 4 byte.
default rel
section .data
suhu: times 8 dd 0 ; delapan bilangan 32-bit, tiap elemen 4 byte
section .text
global _start
_start:
lea rdi, [suhu] ; alamat dasar array
mov rsi, 5 ; indeks elemen
lea rax, [rdi + rsi*4] ; alamat elemen indeks 5
mov dword [rax], 42 ; tulis melalui alamat hasil LEA
mov eax, [rax] ; baca kembali dari alamat yang sama
mov edi, eax
mov eax, 60
syscall# rakit dan tautkan program
nasm -f elf64 lea-array.asm -o lea-array.o
ld lea-array.o -o lea-array
# alamat elemen indeks 5 pada rax, lalu isi array diperiksa
gdb -batch -ex 'break _start' -ex 'run' -ex 'stepi 4' -ex 'info registers rax' -ex 'x/8xw &suhu' ./lea-arrayBreakpoint 1 at 0x401000
Breakpoint 1, 0x0000000000401000 in _start ()
0x0000000000401016 in _start ()
rax 0x402014 4202516
0x402000: 0x00000000 0x00000000 0x00000000 0x00000000
0x402010: 0x00000000 0x0000002a 0x00000000 0x00000000Register rax menampung 0x402014, yaitu alamat dasar 0x402000 ditambah 5 dikalikan 4 byte. Pemeriksaan delapan kata memori menampilkan 0x2a tepat pada elemen indeks 5, sedangkan elemen lain tetap nol. Penulisan dan pembacaan kembali dilakukan melalui alamat hasil LEA, sehingga seluruh jalur penghitungan alamat terbukti bekerja dari awal sampai akhir.
LEA sebagai aritmetika#
Karena LEA hanya menjumlahkan dan mengalikan dengan skala, instruksi ini dapat digunakan sebagai aritmetika bilangan bulat yang murah. Bentuk [rdi + rdi*8] menghasilkan sembilan kali nilai rdi karena basis dan indeks menggunakan register yang sama. Untuk sepuluh kali nilai, satu LEA menghasilkan lima kali nilai melalui [rdi + rdi*4], lalu satu add menggandakannya. Perlu diingat bahwa skala hanya menyediakan faktor 1, 2, 4, dan 8, sehingga perkalian tiga kali dinyatakan sebagai [rdi + rdi*2].
Program berikut menjalankan kedua bentuk di atas dengan rdi berisi 10, lalu hasilnya diperiksa lewat gdb pada dua titik berbeda.
default rel
section .text
global _start
_start:
mov rdi, 10 ; nilai untuk kedua percobaan
lea rax, [rdi + rdi*8] ; 9 * 10
mov rbx, rax
lea rax, [rdi + rdi*4] ; 5 * 10
add rax, rax ; 10 * 10
mov edi, 0
mov eax, 60
syscall# rakit dan tautkan program
nasm -f elf64 lea-demo.asm -o lea-demo.o
ld lea-demo.o -o lea-demo
# hasil perhitungan 9 * rdi pada rax dan rbx
gdb -batch -ex 'break _start' -ex 'run' -ex 'stepi 3' -ex 'info registers rax rbx' ./lea-demo
# hasil perhitungan 10 * rdi pada rax setelah add
gdb -batch -ex 'break _start' -ex 'run' -ex 'stepi 5' -ex 'info registers rax' ./lea-demoBreakpoint 1 at 0x401000
Breakpoint 1, 0x0000000000401000 in _start ()
0x000000000040100c in _start ()
rax 0x5a 90
rbx 0x5a 90
Breakpoint 1 at 0x401000
Breakpoint 1, 0x0000000000401000 in _start ()
0x0000000000401013 in _start ()
rax 0x64 100Keluaran pertama menampilkan 0x5a atau 90 pada rax dan rbx, persis sembilan kali nilai awal. Keluaran kedua menampilkan 0x64 atau 100 setelah add menggandakan hasil LEA yang bernilai 50. Tidak ada instruksi perkalian yang digunakan di sepanjang program, dan nilai 10 hanya dimuat satu kali di awal.
Sebagai pembanding, MUL mengharuskan satu operannya berada di rax secara implisit, menyimpan hasil 128-bit pada pasangan rdx dan rax, serta memperbarui flag. LEA tidak menyentuh flag sama sekali, dan pada banyak prosesor modern dieksekusi oleh unit penghitung alamat dalam satu siklus. Sifat itulah yang membuat compiler memilih LEA untuk perkalian dengan faktor kecil yang berpola basis ditambah indeks berskala.
Kesalahan umum#
Mengharapkan LEA membaca isi memori. Hasil lea rax, [rdi + rsi*4] berupa alamatnya, bukan isinya, sehingga program yang mencari nilai sebuah elemen harus dilanjutkan dengan MOV dari alamat tersebut. Pola LEA diikuti MOV memang terdiri atas dua instruksi, dan pemisahan itulah yang menjadi ciri x86 antara perhitungan alamat dan akses data.
Menggunakan skala di luar 1, 2, 4, dan 8. Bentuk [rdi + rsi*3] ditolak assembler karena perangkat keras hanya menyediakan empat faktor skala. Perkalian tiga kali terhadap satu nilai dinyatakan sebagai [rdi + rdi*2], dengan basis dan indeks menggunakan register yang sama.
Mengira LEA memperbarui flag. Percabangan yang ditempatkan tepat setelah LEA akan membaca flag sisa instruksi sebelumnya, bukan hasil LEA, sehingga keputusannya tidak mencerminkan perhitungan yang baru dijalankan. Flag hanya diperbarui oleh instruksi aritmetika seperti add, sub, dan cmp.
Ringkasan#
- Alamat efektif x86-64 berbentuk basis ditambah indeks dikalikan skala ditambah displacement, dengan skala 1, 2, 4, atau 8.
- LEA menghitung alamat efektif ke register tanpa akses memori dan tanpa memperbarui flag.
- Penghitungan alamat elemen array menggunakan LEA untuk alamatnya dan MOV untuk isinya.
- Compiler menggunakan LEA sebagai aritmetika murah untuk perkalian dengan faktor kecil, sedangkan MUL menyediakan perkalian penuh dengan efek samping rdx dan flag.
Referensi#
- LEA — x86 and x86-64 Instruction Set Reference — Felix Cloutier — dokumentasi LEA beserta bentuk alamat efektif dan rentang skalanya.
- Intel 64 and IA-32 Architectures Software Developer’s Manual — Intel — Volume 1 membahas perhitungan alamat efektif dan Volume 2 mendokumentasikan LEA.