Array dan aritmetika pointer

Penelusuran array menggunakan pengalamatan indeks berskala dan perhitungan offset elemennya.

Daftar isi

Array dalam memori hanyalah deretan byte berurutan tanpa tanda batas antarelemen, sehingga seluruh penelusuran array pada assembly bertumpu pada satu perhitungan alamat. Topik ini membahas perhitungan offset elemen array, penjumlahan seluruh elemen dengan pengalamatan indeks berskala, dan pola penelusuran kedua yang memajukan pointer satu elemen setiap putaran. Hasil akhirnya adalah dua pola penelusuran yang dijalankan pada data yang sama dan memberikan hasil yang sama, lengkap dengan pemeriksaan nilai akhirnya di memori lewat gdb.

Offset elemen array#

Alamat elemen dengan indeks i dihitung dari alamat awal array ditambah i dikalikan ukuran elemennya. Array yang dideklarasikan dengan dq berukuran 8 byte per elemen, sedangkan array dd berukuran 4 byte per elemen, sehingga rumusnya sebagai berikut.

alamat elemen i=basis+i×ukuran elemen \text{alamat elemen } i = \text{basis} + i \times \text{ukuran elemen}

Pada array dq dengan delapan elemen, elemen dengan indeks 3 berada pada offset byte 24, dan elemen terakhir berada pada offset byte 56. Rumus itu diterjemahkan langsung ke dalam bentuk pengalamatan berskala yang dibahas pada mode pengalamatan, dengan skala mengambil peran ukuran elemen.

nasm
; tidak-dijalankan
; alamat elemen ke-i pada array dq
mov rax, [suhu + rsi*8]     ; baca isi elemen ke-i
lea rdx, [suhu + rsi*8]     ; ambil alamat elemen ke-i tanpa membacanya

Skala 8 pada array dq dan skala 4 pada array dd memungkinkan indeks ditulis sebagai nomor elemen, bukan sebagai offset byte. Perbedaan ini penting karena bentuk [suhu + rsi] tanpa skala tetap sah bagi assembler, tetapi maknanya berbeda, yaitu offset byte ke-rsi dan bukan elemen ke-rsi. Pada array byte yang dideklarasikan dengan db, ukuran elemennya 1 byte sehingga indeks dan offset byte menjadi angka yang sama, dan skala 1 dapat ditulis maupun dihilangkan.

Offset yang dihitung dengan rumus ini juga menjadi dasar perhitungan alamat batas, yaitu alamat satu langkah setelah elemen terakhir yang digunakan pola pointer pada bagian berikutnya.

Penjumlahan dengan dua pola penelusuran#

Program berikut menggunakan satu array berisi delapan pembacaan sensor suhu dan menjumlahkannya dengan dua pola berbeda. Pola pertama menggunakan indeks berskala [suhu + r13*8] yang bertambah satu setiap putaran, sedangkan pola kedua memajukan pointer berjalan delapan byte setiap putaran hingga mencapai alamat satu langkah di luar array.

nasm jumlah-array.asm
; jumlah-array.asm, penjumlahan array dq dengan indeks berskala dan pointer maju
default rel

JUM     equ 8                      ; jumlah elemen
UKURAN  equ 8                      ; ukuran satu elemen dq dalam byte

section .data
    suhu    dq 27, 29, 31, 30, 28, 26, 27, 30    ; delapan pembacaan sensor

    label1  db "elemen[3] = "
    len1    equ $ - label1
    label2  db "jumlah (indeks) = "
    len2    equ $ - label2
    label3  db "jumlah (pointer) = "
    len3    equ $ - label3

section .bss
    hasil   resq 1
    digit   resb 25

section .text
global _start

_start:
    mov  rbx, 3                   ; indeks elemen yang dibaca
    mov  r8,  [suhu + rbx*8]      ; alamat = suhu + 3*8, elemen keempat

    lea  r14, [suhu]              ; pointer berjalan, di awal array
    lea  r15, [suhu + JUM*8]      ; batas berhenti, satu langkah di luar array
    xor  r12, r12                 ; penjumlah versi pointer
.pointer_loop:
    add  r12, [r14]
    add  r14, 8                   ; maju satu elemen, bukan satu byte
    cmp  r14, r15
    jb   .pointer_loop
    push r12

    xor  r12, r12                 ; penjumlah versi indeks
    xor  r13, r13                 ; indeks elemen
.indeks_loop:
    add  r12, [suhu + r13*8]      ; alamat = suhu + r13*8
    inc  r13
    cmp  r13, JUM
    jb   .indeks_loop
    mov  [hasil], r12             ; simpan jumlah versi indeks ke .bss
    push r12

    lea  rsi, [label1]
    mov  rdx, len1
    mov  rax, r8
    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

selesai:
    mov  rdi, [hasil]             ; kode keluar = jumlah seluruh elemen
    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
bash
# rakit dan tautkan program
nasm -f elf64 jumlah-array.asm -o jumlah-array.o
ld jumlah-array.o -o jumlah-array

# jalankan program
./jumlah-array
echo "exit=$?"
text Keluaran
elemen[3] = 30
jumlah (indeks) = 228
jumlah (pointer) = 228
exit=228

Kedua pola menghasilkan jumlah 228, yaitu penjumlahan 27, 29, 31, 30, 28, 26, 27, dan 30, dan nilai itu juga muncul sebagai kode keluar program. Batas berhenti pada pola pointer dihitung dengan lea r15, [suhu + JUM*8], yaitu alamat satu langkah di luar elemen terakhir. Perbandingan jb berhenti tepat sebelum pointer mencapai alamat itu, sehingga tidak ada byte di luar array yang tersentuh.

Pemeriksaan hasil di debugger#

Nilai penjumlahan versi indeks disimpan ke label hasil di section .bss sebelum dicetak, sehingga nilai penuhnya dapat diperiksa langsung di memori. Label selesai tepat sebelum syscall exit menjadi titik henti yang pasti, karena kedua perulangan sudah selesai dijalankan pada saat eksekusi mencapainya. Pada titik henti itu, isi hasil dibaca dengan x/1gx, yaitu satu qword dalam heksadesimal.

bash
# hentikan program tepat sebelum exit dan periksa hasil di memori
gdb -batch -ex 'break selesai' -ex 'run' -ex 'x/1gx &hasil' ./jumlah-array
text Keluaran
Breakpoint 1 at 0x401084
elemen[3] = 30
jumlah (indeks) = 228
jumlah (pointer) = 228

Breakpoint 1, 0x0000000000401084 in selesai ()
0x402074:	0x00000000000000e4

Keluaran gdb menampilkan isi hasil sebagai satu qword, yaitu 0x00000000000000e4. Nilai heksadesimal 0xe4 sama dengan 228 desimal, sehingga isi memori cocok dengan yang dicetak oleh program dan dengan perhitungan manual. Pemeriksaan semacam ini memisahkan dua kemungkinan kesalahan, yaitu perulangan yang salah menghitung dan pencetakan yang salah mengonversi, karena isi memori selalu mencerminkan apa yang benar-benar ditulis perulangan.

Kesalahan umum#

Menggunakan perbandingan yang melewati batas. Perulangan indeks dengan cmp r13, JUM dan jle tetap memasuki putaran ketika r13 bernilai 8, sehingga [suhu + 8*8] membaca byte di luar array. Bandingkan dengan jb, atau dengan jl pada bilangan bertanda, agar indeks berhenti sebelum mencapai jumlah elemen.

Melupakan ukuran elemen pada batas pointer. Alamat berhenti untuk array dq dengan delapan elemen adalah suhu + JUM*8 atau byte ke-64, sedangkan suhu + JUM hanya melompat delapan byte sehingga perulangan hanya memproses elemen pertama. Selalu kalikan jumlah elemen dengan ukuran elemen ketika batas dinyatakan sebagai alamat.

Membiarkan penjumlah tidak dinolkan. Register r12 pada program ini dinolkan dengan xor r12, r12 sebelum setiap perulangan dimulai, karena register menyimpan nilai apa pun yang ditinggalkan instruksi sebelumnya. Tanpa penolanan, hasil penjumlahan terakumulasi dengan nilai lama dan kedua pola langsung menunjukkan angka yang berbeda meskipun keduanya menelusuri array yang sama.

Ringkasan#

  • Alamat elemen dengan indeks i dihitung sebagai basis ditambah i dikalikan ukuran elemen, dan skala pada pengalamatan berskala mengambil peran ukuran elemen tersebut.
  • Penelusuran dengan indeks menggunakan bentuk [array + rsi*8] untuk array dq, sedangkan penelusuran dengan pointer memajukan register alamat sebesar ukuran elemen setiap putaran.
  • Batas berhenti pointer adalah alamat satu langkah di luar elemen terakhir, dihitung dengan lea dari jumlah elemen dikalikan ukuran elemen.
  • Menyimpan hasil ke label di section .bss memungkinkan pemeriksaan nilai penuh lewat gdb dengan x/1gx untuk membandingkannya dengan keluaran program.

Referensi#