Setiap pembacaan dan penulisan memori pada x86-64 menghitung alamatnya menurut satu pola perhitungan yang sama. Topik ini membahas pola tersebut pada bentuk lengkapnya, yaitu displacement ditambah basis ditambah indeks dikalikan skala, kemudian menunjukkan enam variasi yang sah di dalam satu program nyata. Hasil akhirnya adalah kemampuan menuliskan operan memori dalam bentuk apa pun yang diterima assembler, sekaligus mengetahui batasan skala yang ditoleransi encoding instruksi.
Bentuk lengkap operan memori#
Operan memori pada sintaks Intel ditulis di dalam kurung siku, dan alamatnya dihitung dengan menjumlahkan hingga empat komponen. Bentuk lengkapnya adalah [displacement + basis + indeks*skala], dan setiap program yang membaca memori hanyalah variasi dari bentuk itu.
; tidak-dijalankan
; bentuk umum operan memori pada sintaks Intel
[displacement + basis + indeks*skala]
mov rax, [rbx + r12*4 + 16] ; seluruh komponen dipakai sekaligus
mov rax, [rbx] ; hanya basis
mov rax, [rbx + r12*4] ; basis dan indeks berskala| Komponen | Contoh | Peran |
|---|---|---|
| displacement | +16 | konstanta yang dijumlahkan ke alamat, boleh negatif |
| basis | rbx | register yang menyimpan alamat awal |
| indeks | r12 | register yang nilainya dikalikan skala |
| skala | *4 | pengali indeks, hanya bernilai 1, 2, 4, atau 8 |
Keempat komponen bersifat opsional selama minimal satu komponen tersisa, sehingga [elemen], [rax], [rax+8], [rax+rbx], dan [rax+rbx*4] semuanya sah. Label pada NASM berperan sebagai displacement, karena nama label pada dasarnya adalah konstanta alamat yang diketahui assembler. Skala dibatasi pada nilai 1, 2, 4, dan 8 karena byte SIB, yaitu byte pengalamatan yang menyertai banyak instruksi memori, hanya menyediakan dua bit untuk medan skala. Dua bit hanya dapat menyandikan empat pilihan pengali itu, sehingga pembatasan bukan kebijakan NASM, melainkan sifat encoding instruksi x86.
Satu varian perhitungan alamat berada di luar bentuk di atas, yaitu pengalamatan RIP-relative yang menghitung alamat dari jaraknya terhadap instruksi yang sedang berjalan. Varian ini banyak digunakan compiler untuk kode position independent dan tidak memuat basis, indeks, maupun skala.
Enam variasi dalam satu program#
Program berikut membaca isi satu array delapan elemen dengan enam variasi pengalamatan yang berbeda, mulai dari [label] yang paling sederhana sampai [basis+indeks*skala+displacement] yang lengkap. Hasil setiap pembacaan dicetak ke terminal, lalu dijumlahkan menjadi satu nilai pemeriksa yang juga dijadikan kode keluar program.
; akses-pengalamatan.asm, enam variasi mode pengalamatan membaca satu array
default rel
section .data
elemen dq 11, 22, 33, 44, 55, 66, 77, 88
label1 db "[elemen] = "
len1 equ $ - label1
label2 db "[rax] = "
len2 equ $ - label2
label3 db "[rax+8] = "
len3 equ $ - label3
label4 db "[rax+rbx] = "
len4 equ $ - label4
label5 db "[rax+r12*8] = "
len5 equ $ - label5
label6 db "[rax+r12*8+16] = "
len6 equ $ - label6
label7 db "jumlah = "
len7 equ $ - label7
section .bss
digit resb 25
section .text
global _start
_start:
lea rax, [elemen] ; basis: alamat elemen pertama
mov rbx, 16 ; offset byte 16, yaitu elemen ketiga
mov r12, 3 ; indeks elemen untuk pembacaan berskala
xor r13, r13 ; penjumlah seluruh hasil pembacaan
mov rdi, [elemen] ; variasi 1: label
mov rsi, [rax] ; variasi 2: basis
mov rdx, [rax+8] ; variasi 3: basis + displacement
mov rcx, [rax+rbx] ; variasi 4: basis + indeks
mov r8, [rax+r12*8] ; variasi 5: basis + indeks*skala
mov r9, [rax+r12*8+16] ; variasi 6: bentuk lengkap
add r13, rdi
add r13, rsi
add r13, rdx
add r13, rcx
add r13, r8
add r13, r9
push r9 ; hasil disimpan ke stack agar urutan cetak bebas
push r8
push rcx
push rdx
push rsi
push rdi
lea rsi, [label1]
mov rdx, len1
pop rax
call cetak_baris
lea rsi, [label2]
mov rdx, len2
pop rax
call cetak_baris
lea rsi, [label3]
mov rdx, len3
pop rax
call cetak_baris
lea rsi, [label4]
mov rdx, len4
pop rax
call cetak_baris
lea rsi, [label5]
mov rdx, len5
pop rax
call cetak_baris
lea rsi, [label6]
mov rdx, len6
pop rax
call cetak_baris
mov rax, r13
lea rsi, [label7]
mov rdx, len7
call cetak_baris
mov rdi, r13 ; kode keluar = jumlah seluruh hasil
mov rax, 60
syscall
cetak_baris: ; rsi = alamat label, rdx = panjang label, rax = nilai
push rax
call cetak_teks
pop rax
call cetak_angka
ret
cetak_teks: ; tulis rsi sepanjang rdx byte ke stdout
mov rax, 1
mov rdi, 1
syscall
ret
cetak_angka: ; tulis rax sebagai desimal tanpa tanda diikuti baris baru
mov rbx, 10 ; pembagi
lea r12, [digit + 24]
mov byte [r12], 10 ; byte newline di ujung buffer
lea r12, [digit + 24]
.isi:
xor edx, edx
div rbx
add dl, 48 ; 48 adalah kode ASCII angka 0
dec r12
mov [r12], dl
test rax, rax
jnz .isi
lea rdx, [digit + 25]
sub rdx, r12 ; panjang digit ditambah newline
mov rsi, r12
mov rax, 1
mov rdi, 1
syscall
ret
section .note.GNU-stack noalloc noexec nowrite progbitsPerhatikan pemetaan register sebelum enam pembacaan dilakukan. Register rax menampung alamat elemen hasil lea, rbx diisi 16 yang berarti offset byte ke-16, dan r12 diisi 3 yang berarti indeks elemen. Dengan susunan itu, [rax+rbx] membaca elemen ketiga melalui offset byte 16. Bentuk [rax+r12*8] menghitung offset 3*8 atau byte ke-24, yaitu elemen keempat, sedangkan [rax+r12*8+16] mencapai offset byte ke-40, yaitu elemen keenam. Dua variasi pertama, [elemen] dan [rax], menghitung alamat yang sama persis, yakni alamat elemen pertama.
# rakit dan tautkan program
nasm -f elf64 akses-pengalamatan.asm -o akses-pengalamatan.o
ld akses-pengalamatan.o -o akses-pengalamatan
# jalankan program
./akses-pengalamatan
echo "exit=$?"
# isi register setelah enam pembacaan selesai
gdb -batch -ex 'break _start' -ex 'run' -ex 'stepi 10' -ex 'info registers rax rbx r12' ./akses-pengalamatan[elemen] = 11
[rax] = 11
[rax+8] = 22
[rax+rbx] = 33
[rax+r12*8] = 44
[rax+r12*8+16] = 66
jumlah = 187
exit=187
Breakpoint 1 at 0x401000
Breakpoint 1, 0x0000000000401000 in _start ()
0x0000000000401030 in _start ()
rax 0x402000 4202496
rbx 0x10 16
r12 0x3 3Keluaran memperlihatkan keenam variasi membaca nilai yang tepat sesuai perhitungan offsetnya. Bagian gdb mengonfirmasi keadaan register setelah sepuluh instruksi pertama dijalankan, yaitu rax berisi 0x402000 sebagai alamat array, rbx berisi 0x10 atau 16, dan r12 berisi 3. Jumlah 187 pada baris terakhir adalah penjumlahan 11, 11, 22, 33, 44, dan 66, dan nilai yang sama muncul sebagai kode keluar karena jumlah itu dimuat ke rdi sebelum syscall exit.
Skala yang sah dan yang ditolak#
Nilai skala selain 1, 2, 4, dan 8 tidak memiliki encoding, sehingga assembler menolaknya sebelum program sempat dirakit. Perilaku ini dapat dibuktikan dengan berkas kecil yang menggunakan skala 3.
# skala 3 tidak didukung encoding pengalamatan x86
cat > skala-3.asm <<'EOF'
section .text
global _start
_start:
mov rax, [rbx + rcx*3]
EOF
nasm -f elf64 skala-3.asm -o skala-3.o
echo "exit=$?"skala-3.asm:5: error: invalid 64-bit effective address
exit=1NASM berhenti pada baris 5 dengan pesan error invalid 64-bit effective address dan kode keluar 1, tanpa menghasilkan berkas objek. Pesan itu menunjuk alamat efektif secara keseluruhan karena komponen yang tidak sah membuat alamatnya tidak dapat disandikan. Perbaikannya adalah memilih skala yang tersedia, misalnya *2 atau *4, atau memindahkan pengali ke instruksi aritmetika terpisah bila pengali yang dimaksud memang bukan kelipatan pangkat dua.
Kesalahan umum#
Memperlakukan indeks elemen sebagai offset byte. Pada array dq, elemen kedua berada pada offset byte ke-8, sehingga [array+rsi] dengan rsi bernilai 2 membaca byte ketiga terhitung dari elemen pertama dan bukan elemen kedua. Perbaikannya adalah menyatakan skala secara eksplisit dalam bentuk [array+rsi*8], atau mengalikan indeks dengan ukuran elemen di dalam register terlebih dahulu bila bentuk tanpa skala memang diperlukan.
Mengabaikan ukuran operan pada pembacaan array. Ekspresi [array+rsi*8] hanya menyatakan alamat, sedangkan jumlah byte yang dibaca ditentukan oleh register tujuan. Instruksi mov al, [array+rsi*8] hanya memuat satu byte pertama dari elemen yang ditunjuk, sehingga nilainya salah tanpa pesan error apa pun. Gunakan register seukuran elemen, misalnya rax untuk array dq, agar pembacaannya utuh.
Membaca indeks yang melewati batas array. Array dengan delapan elemen memiliki indeks sah 0 sampai 7, sehingga [array+rcx*8] dengan rcx bernilai 8 membaca byte di luar array tanpa ditolak assembler maupun prosesor. Kebiasaan yang mencegahnya adalah menguji batas indeks sebelum perulangan membaca elemen, sebagaimana dilakukan perbandingan cmp pada perulangan array.
Ringkasan#
- Alamat operan memori x86-64 dihitung dari bentuk
[displacement + basis + indeks*skala], dan setiap variasi sah adalah versi dari bentuk itu dengan sebagian komponen dihilangkan. - Skala hanya menerima 1, 2, 4, dan 8 karena byte SIB menyediakan dua bit untuk medan skala, sehingga skala lain ditolak assembler.
- Label pada NASM berperan sebagai displacement, dan pada berkas dengan
default relbentuk[label]dirakit sebagai pembacaan RIP-relative. - Ukuran pembacaan ditentukan register tujuan, sedangkan skala mengalikan indeks menjadi offset byte, sehingga keduanya harus disesuaikan dengan ukuran elemen array.
Referensi#
- NASM 2.16.03 documentation — NASM Project — bab effective address memuat bentuk basis, indeks, skala, dan displacement pada sintaks Intel.
- Intel 64 and IA-32 Architectures Software Developer’s Manual — Intel — Volume 2 mendokumentasikan byte SIB beserta medan skala dua bitnya.