Berkas register x86-64

Enam belas register serbaguna 64-bit, ukuran 32/16/8-bit-nya, dan peran khusus rsp serta rbp.

Daftar isi

Topik ini memetakan enam belas register serbaguna pada x86-64 beserta seluruh nama alias untuk ukuran 32, 16, dan 8 bit. Pemahaman atas pemecahan nama ini menjadi bekal untuk membaca hampir semua kode assembly, karena satu register yang sama dapat disentuh melalui beberapa nama dengan ukuran berbeda. Peran khusus rsp dan rbp serta perilaku zero-extension juga dibuktikan langsung dengan program kecil yang diperiksa lewat gdb.

Enam belas register serbaguna#

Arsitektur x86-64 menyediakan enam belas register serbaguna (general purpose register), yaitu register 64-bit yang dapat digunakan untuk perhitungan umum. Delapan di antaranya diwarisi dari arsitektur sebelumnya, yaitu rax, rbx, rcx, rdx, rsi, rdi, rbp, dan rsp. Delapan lainnya adalah register baru yang diperkenalkan x86-64 dan dinamai berurutan dari r8 sampai r15. Hampir semua instruksi aritmetika dan perpindahan data menerima keenam belasnya secara setara, meskipun sebagian register tetap memiliki peran khusus yang digunakan instruksi tertentu tanpa disebut sebagai operan.

Tabel berikut merangkum keenam belas register beserta nama aliasnya. Nama pada setiap kolom bukan register yang berbeda, melainkan nama lain untuk menyentuh sebagian dari 64 bit penyimpanan yang sama. Menulis ke eax berarti menulis 32 bit bawah dari rax, dan menulis ke al berarti menulis 8 bit terendahnya.

64 bit32 bit16 bit8 bit8 bit tinggiPeran khusus
raxeaxaxalahregister balikan nilai syscall
rbxebxbxblbhwarisan register basis
rcxecxcxclchwarisan register penghitung
rdxedxdxdldhpasangan rax pada perkalian dan pembagian
rsiesisisil-indeks sumber pada operasi string
rdiedididil-indeks tujuan pada operasi string
rbpebpbpbpl-frame pointer
rspespspspl-stack pointer
r8 sampai r15r8d sampai r15dr8w sampai r15wr8b sampai r15b-register tambahan x86-64

Kolom 8 bit tinggi hanya terisi untuk empat register pertama, karena hanya rax, rbx, rcx, dan rdx yang memiliki byte atas dengan nama sendiri, yaitu ah, bh, ch, dan dh. Register r8 sampai r15 tidak memiliki padanan byte tinggi, sehingga byte atasnya hanya dapat diakses melalui nama seperti r8b. Nama sil, dil, spl, dan bpl juga baru muncul pada mode 64-bit, dan tidak ada sih, dih, sph, maupun bph. Dua register dengan peran paling khusus, yaitu rsp dan rbp, dijelaskan pada bagian akhir topik ini.

Pecahan nama register rax#

Nama-nama alias paling mudah diamati pada rax, dan diagram berikut merangkum susunannya. Register rax dipecah menjadi eax yang mencakup 32 bit bawah, lalu ax yang mencakup 16 bit bawah dari eax, dan akhirnya dua byte bernama ah serta al. Byte ah menempati bit 8 sampai 15, sedangkan al menempati bit 0 sampai 7. Pola yang sama berlaku untuk rbx, rcx, dan rdx dengan nama aliasnya masing-masing.

Pecahan register rax6347311570rax (64 bit)32 bit atas, tak bernamaeax (32 bit)16 bit atasax (16 bit)ah8 bital8 bit
Pecahan register rax menjadi eax, ax, ah, dan al

Diagram juga menunjukkan bagian yang tidak bernama. Bit 32 sampai 63 dari rax tidak memiliki alias sama sekali, begitu pula bit 16 sampai 31 di dalam eax. Satu-satunya cara menyentuh bagian tersebut adalah menulis ke register yang melingkupinya secara utuh. Ketika kode sumber menggunakan eax atau ax, assembler memilih encoding instruksi yang operasinya dibatasi pada bagian yang disebut.

Zero-extension pada penulisan 32 bit#

Menulis ke nama 32 bit seperti eax memiliki perilaku khusus yang tidak dimiliki ukuran lain. Instruksi yang menulis 32 bit bawah mengosongkan 32 bit atas register yang sama, sehingga 32 bit atas rax selalu bernilai nol setelah penulisan. Perilaku ini disebut zero-extension, yakni pengisian nol pada bit atas yang tidak ditulis. Sebaliknya, penulisan berukuran 8 bit atau 16 bit membiarkan bit di atasnya apa adanya, sehingga nilai lama tetap tersimpan.

Program berikut mengisi rax dengan sebuah pola, menulis ke eax, lalu menulis ke al. gdb menghentikan program di dua titik berbeda agar nilai rax terlihat setelah setiap penulisan.

nasm pecahan-register.asm
default rel

section .text
global _start

_start:
    mov  rax, 0x1122334455667788  ; isi seluruh 64 bit dengan pola
    mov  eax, 0xffffffff          ; tulis 32 bit bawah saja
    nop
    mov  al, 0x12                 ; tulis 8 bit bawah saja
    nop
    mov  edi, eax                 ; kode keluar dari 32 bit bawah
    mov  eax, 60
    syscall
bash
# rakit dan tautkan program
nasm -f elf64 pecahan-register.asm -o pecahan-register.o
ld pecahan-register.o -o pecahan-register

# hentikan program setelah mov eax, lalu periksa rax
gdb -batch -ex 'break _start' -ex 'run' -ex 'stepi 2' -ex 'info registers rax' ./pecahan-register

# hentikan program setelah mov al, lalu periksa rax lagi
gdb -batch -ex 'break _start' -ex 'run' -ex 'stepi 4' -ex 'info registers rax' ./pecahan-register
text Keluaran
Breakpoint 1 at 0x401000

Breakpoint 1, 0x0000000000401000 in _start ()
0x000000000040100f in _start ()
rax            0xffffffff          4294967295
Breakpoint 1 at 0x401000

Breakpoint 1, 0x0000000000401000 in _start ()
0x0000000000401012 in _start ()
rax            0xffffff12          4294967058

Nilai rax berubah menjadi 0xffffffff setelah mov eax dijalankan, dan bukan 0x11223344ffffffff. Pola 0x11223344 pada 32 bit atas dibersihkan menjadi nol, dan itulah zero-extension. Setelah mov al dijalankan, hanya byte terendah yang berubah menjadi 0x12, sedangkan bit 8 ke atas masih memuat sisa 0xffffff dari langkah sebelumnya. gdb menampilkan nilai register dalam heksadesimal sekaligus desimal, sehingga kedua bentuknya dapat dibaca langsung dari keluaran.

Peran khusus rsp dan rbp#

rsp adalah stack pointer, yaitu register yang menyimpan alamat puncak stack. Instruksi push dan pop menggunakan rsp tanpa menyebutnya sebagai operan, begitu pula call dan ret yang menyimpan serta mengambil alamat kembali. Karena stack tumbuh ke alamat yang lebih rendah, setiap push menurunkan rsp sebesar ukuran datanya, yaitu 8 byte pada program 64-bit. rsp tidak dirancang sebagai tempat data umum karena terlalu banyak mekanisme yang mengandalkan nilainya.

rbp adalah frame pointer, yaitu register yang biasanya menunjuk dasar frame stack sebuah fungsi. Alamat variabel lokal dan argumen sering dihitung sebagai offset dari rbp karena nilainya tetap selama fungsi berjalan, sedangkan rsp naik turun mengikuti push dan pop. Menyalin rsp ke rbp di awal fungsi, lalu mengembalikan nilai rbp semula di akhir, merupakan pola yang lazim pada kode yang tidak dioptimalkan.

Program berikut menyalin rsp ke rbp pada titik masuk, mendorong satu nilai dengan push, lalu menariknya kembali. gdb membuktikan tiga hal sekaligus, yaitu kesamaan rsp dan rbp, selisih 8 byte setelah push, serta nilai 0x41 yang tersimpan di puncak stack.

nasm peran-rsp-rbp.asm
default rel

section .text
global _start

_start:
    mov  rbp, rsp        ; rbp meniru rsp pada titik masuk
    push 0x41            ; dorong satu nilai ke stack
    pop  rax             ; tarik kembali nilainya
    mov  edi, eax
    mov  eax, 60
    syscall
bash
# rakit dan tautkan program
nasm -f elf64 peran-rsp-rbp.asm -o peran-rsp-rbp.o
ld peran-rsp-rbp.o -o peran-rsp-rbp

# rsp dan rbp sama persis setelah mov rbp, rsp
gdb -batch -ex 'break _start' -ex 'run' -ex 'stepi' -ex 'p $rsp == $rbp' ./peran-rsp-rbp

# push menurunkan rsp 8 byte dan menaruh 0x41 di alamat baru
gdb -batch -ex 'break _start' -ex 'run' -ex 'stepi 2' -ex 'p $rbp - $rsp' -ex 'p/x *(unsigned long *)$rsp' ./peran-rsp-rbp
text Keluaran
Breakpoint 1 at 0x401000

Breakpoint 1, 0x0000000000401000 in _start ()
0x0000000000401003 in _start ()
$1 = 1
Breakpoint 1 at 0x401000

Breakpoint 1, 0x0000000000401000 in _start ()
0x0000000000401005 in _start ()
$1 = 8
$2 = 0x41

Hasil pemeriksaan pertama menampilkan $1 = 1, artinya rsp dan rbp memuat alamat yang sama persis setelah mov rbp, rsp. Hasil kedua menampilkan selisih $1 = 8, sehingga satu push benar-benar menurunkan rsp sebesar 8 byte. Nilai $2 = 0x41 yang dibaca pada alamat rsp yang baru membuktikan bahwa data yang didorong tersimpan tepat di puncak stack.

Kesalahan umum#

Menganggap alias sebagai register terpisah. Menulis ke eax lalu membaca rax tidak memberikan dua nilai yang berdiri sendiri, karena keduanya menunjuk penyimpanan yang sama. Kebingungan ini biasanya muncul saat debug, ketika nilai rax tidak sesuai perkiraan karena sebuah mov eax sebelumnya mengosongkan 32 bit atasnya. Perbaikannya ada pada kebiasaan memilih nama sesuai ukuran yang benar-benar dimaksud.

Menggunakan rsp sebagai register serbaguna. Menyimpan data ke rsp mengubah alamat puncak stack, sehingga push, pop, atau ret berikutnya membaca alamat yang salah dan program biasanya berakhir dengan Segmentation fault. Tempat data yang aman adalah register lain, terutama rbx dan r12 sampai r15 yang nilainya lazim dipertahankan antar pemanggilan fungsi.

Lupa bahwa byte tinggi tidak tersedia untuk semua register. Nama seperti ah hanya ada untuk rax sampai rdx, sehingga kode yang menyebut r8h tidak akan pernah dirakit oleh NASM. Apabila byte tinggi register r8 dibutuhkan, nilainya diperoleh dengan menggeser bit terlebih dahulu, misalnya melalui shr sebelum dibaca sebagai r8b.

Ringkasan#

  • x86-64 menyediakan enam belas register serbaguna 64-bit, yaitu rax sampai rdi, rbp, rsp, dan r8 sampai r15.
  • Nama alias seperti eax, ax, al, dan ah menyentuh sebagian penyimpanan yang sama, sehingga menulis ke satu nama mengubah nilai yang dibaca lewat nama lain.
  • Menulis ke nama 32 bit mengosongkan 32 bit atas register, sedangkan penulisan 8 bit dan 16 bit membiarkan bit atasnya tetap.
  • rsp menyimpan alamat puncak stack dan bergeser otomatis pada push serta pop, sedangkan rbp menjadi jangkar tetap untuk frame stack.

Referensi#