Mode pengalamatan

Pola pengalamatan basis ditambah indeks dikalikan skala ditambah displacement pada bentuknya yang lengkap.

Daftar isi

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.

nasm
; 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
KomponenContohPeran
displacement+16konstanta yang dijumlahkan ke alamat, boleh negatif
basisrbxregister yang menyimpan alamat awal
indeksr12register yang nilainya dikalikan skala
skala*4pengali 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.

nasm akses-pengalamatan.asm
; 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 progbits

Perhatikan 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.

bash
# 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
text Keluaran
[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                 3

Keluaran 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.

bash
# 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=$?"
text Keluaran
skala-3.asm:5: error: invalid 64-bit effective address
exit=1

NASM 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 rel bentuk [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#