Stack

Perilaku push dan pop, pertumbuhan stack ke bawah melalui rsp, dan penyelarasan 16 byte pada titik masuk.

Daftar isi

Setiap program memerlukan tempat meletakkan nilai yang hanya diperlukan sesaat. Stack adalah area memori yang dikelola melalui register rsp dan tunduk pada aturan yang ketat. Topik ini membahas pasangan push dan pop, arah pertumbuhan stack yang menurun, penyelarasan 16 byte yang berlaku pada titik masuk proses, serta dua penggunaannya yang paling lazim, yaitu menitipkan nilai dan menyerahkan argumen.

Push, pop, dan sifat LIFO#

Stack bekerja dengan prinsip terakhir masuk pertama keluar (last in, first out, LIFO). Instruksi push menyimpan satu nilai ke puncak stack, sedangkan pop menariknya kembali. Keduanya menggerakkan rsp sebagai penunjuk puncak. push mengurangi rsp 8 byte lebih dahulu, lalu menulis nilainya ke alamat yang baru, sedangkan pop membaca nilai di puncak, lalu menaikkan rsp 8 byte. Pada mode 64-bit, satu push selalu memindahkan 8 byte.

Arah pertumbuhannya menurun. Alamat puncak justru semakin kecil setiap kali push dijalankan, kebalikan dari arah penataan data yang umum, yaitu dari alamat rendah menuju tinggi. Ruang di bawah puncak adalah ruang yang belum digunakan, sedangkan ruang di atasnya berisi nilai yang sudah tersimpan. Karena penunjuknya bergerak turun, stack tumbuh menuju alamat rendah dan menyusut menuju alamat tinggi.

Mengamati pergerakan rsp dan isi stack#

Program berikut mendorong dua nilai, yaitu 111 dan 222, lalu menariknya kembali. Label satu dan dua ditempatkan di antara kedua push agar debugger dapat berhenti tepat sesudah push pertama dan sesudah push kedua.

nasm push-demo.asm
; push-demo.asm
; Mendorong dua nilai ke stack lalu menariknya kembali.
; Kode keluar 222 menunjukkan nilai yang terakhir didorong
; adalah nilai pertama yang ditarik, sesuai sifat LIFO.
default rel
section .text
global _start
_start:
    mov rax, 111
    mov rbx, 222
    push rax                ; rsp turun 8, 111 tersimpan
satu:
    push rbx                ; rsp turun 8 lagi, 222 tersimpan
dua:
    pop rcx                 ; tarik puncak, yaitu 222
    pop rdx                 ; tarik berikutnya, yaitu 111
    mov edi, ecx
    mov eax, 60
    syscall
bash
# rakit, tautkan, dan jalankan
nasm -f elf64 push-demo.asm -o push-demo.o && ld push-demo.o -o push-demo && ./push-demo; echo "exit=$?"
# lacak pergerakan rsp dan isi puncak stack di debugger
gdb -batch -ex 'break _start' -ex 'break satu' -ex 'break dua' -ex 'run' -ex 'p/x (unsigned long) $rsp % 16' -ex 'set $r0 = $rsp' -ex 'continue' -ex 'p $rsp - $r0' -ex 'p/x (unsigned long) $rsp % 16' -ex 'continue' -ex 'p $rsp - $r0' -ex 'p/x (unsigned long) $rsp % 16' -ex 'p/x *(unsigned long *) $rsp' -ex 'p/x *(unsigned long *) ($rsp + 8)' ./push-demo
text Keluaran
exit=222
Breakpoint 1 at 0x401000
Breakpoint 2 at 0x40100b
Breakpoint 3 at 0x40100c

Breakpoint 1, 0x0000000000401000 in _start ()
$1 = 0x0

Breakpoint 2, 0x000000000040100b in satu ()
$2 = -8
$3 = 0x8

Breakpoint 3, 0x000000000040100c in dua ()
$4 = -16
$5 = 0x0
$6 = 0xde
$7 = 0x6f

Keluaran debugger diatur untuk menampilkan sisa pembagian rsp terhadap 16 dan selisihnya terhadap titik masuk, karena alamat absolut berbeda antar lingkungan. Baris $1 = 0x0 menunjukkan rsp pada titik masuk habis dibagi 16. Baris $2 = -8 menunjukkan push pertama menurunkan rsp 8 byte, dan baris $3 = 0x8 menunjukkan penyelarasan 16 byte hilang sesudahnya. Push kedua membawa selisih menjadi -16 pada baris $4 = -16, dan sisa pembagiannya kembali ke 0x0 pada baris $5 = 0x0. Baris $6 dan $7 memperlihatkan isi puncak, yaitu 0xde atau 222, diikuti 0x6f atau 111 di bawahnya.

Urutan penarikannya membuktikan sifat LIFO. pop rcx yang pertama menarik 222, yaitu nilai yang terakhir didorong, dan pop rdx menarik 111. Kode keluar 222 pada ./push-demo menegaskan hal yang sama. Diagram berikut merangkum gerakan rsp dan isi stack pada contoh yang sama.

…e738…e730…e728…e720…e718(kosong)(kosong)111222(belum dipakai)← rsp pada titik masuk program← rsp sesudah dua pushalamat menurunSetiap push menurunkan rsp 8 byte, dari …e730 ke …e720.
Pertumbuhan stack ke bawah setiap push dijalankan

Penyelarasan 16 byte pada titik masuk#

Kontrak lingkungan Linux menetapkan bahwa pada instruksi pertama proses, rsp habis dibagi 16, dan kondisi ini disebut terselaras 16 byte (16-byte aligned). Penyelarasan ini tidak tampak sampai diperiksa. Program berikut mengambil rsp, menghitung sisanya terhadap 16 dengan and, dan mengirim sisanya sebagai kode keluar.

nasm cek-penyelarasan.asm
; cek-penyelarasan.asm
; Memeriksa penyelarasan rsp pada titik masuk proses.
; Kode keluar 0 berarti rsp habis dibagi 16 pada instruksi pertama.
default rel
section .text
global _start
_start:
    mov rax, rsp
    and rax, 15             ; sisa rsp terhadap 16
    mov edi, eax
    mov eax, 60
    syscall
bash
# rakit, tautkan, dan jalankan
nasm -f elf64 cek-penyelarasan.asm -o cek-penyelarasan.o && ld cek-penyelarasan.o -o cek-penyelarasan && ./cek-penyelarasan; echo "exit=$?"
text Keluaran
exit=0

Kode keluar 0 berarti sisa rsp terhadap 16 adalah nol pada instruksi pertama. Konsekuensinya berlaku pada setiap push: satu push menurunkan rsp 8 byte sehingga sisanya menjadi 8 dan stack tidak terselaras lagi, sedangkan dua push mengembalikan penyelarasan itu. Kedua hal ini persis seperti pada penelusuran debugger sebelumnya, melalui baris $3 = 0x8 dan $5 = 0x0. Inilah alasan keluaran debugger pada bagian sebelumnya sengaja menampilkan sisa pembagian tersebut. Sejumlah instruksi menuntut operand terselaras, dan konvensi pemanggilan menuntut stack terselaras pada saat prosedur dipanggil, sehingga jumlah push di antara dua titik terselaras selalu diperhitungkan.

Menitipkan nilai dan menyerahkan argumen#

Dua penggunaan stack yang paling lazim adalah menyelamatkan nilai dan menyerahkan data kepada prosedur. Register berjumlah terbatas, sehingga nilai yang masih dibutuhkan nanti perlu dititipkan selama register itu digunakan untuk keperluan lain.

nasm Menitipkan isi register sementara
    push rbx                ; selamatkan isi rbx milik pemanggil
    mov rbx, 7              ; timpa rbx untuk keperluan sendiri
    add rbx, 3              ; kerjakan perhitungan dengan rbx
    pop rbx                 ; pulihkan isi semula

Pasangan push dan pop harus seimbang. Nilai yang dititipkan ditarik kembali dengan urutan terbalik, karena stack hanya menyerahkan elemen puncaknya, dan pola yang sama berlaku bila yang dititipkan lebih dari satu register, yaitu didorong berurutan lalu ditarik dari yang terakhir. Stack juga menjadi saluran penyerahan argumen apabila register yang tersedia tidak cukup, dan data itu diletakkan di stack sebelum kendali berpindah ke prosedurnya. Mekanisme dasarnya tetap push dan pop yang sama, sedangkan aturan lengkapnya ditetapkan oleh konvensi pemanggilan.

Ringkasan#

  • push mengurangi rsp 8 byte lalu menulis nilai ke alamat baru, sedangkan pop membaca puncak lalu menaikkan rsp 8 byte, dan keduanya beroperasi sesuai LIFO.
  • Stack tumbuh ke arah alamat rendah, sehingga puncaknya bergerak turun setiap push dijalankan.
  • Pada titik masuk proses Linux, rsp terselaras 16 byte, satu push menghilangkan penyelarasan itu, dan dua push mengembalikannya.
  • Stack digunakan untuk menitipkan nilai sementara dan menyerahkan argumen, dengan syarat pasangan push dan pop selalu seimbang.

Referensi#