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.
; 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# 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-demoexit=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 = 0x6fKeluaran 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.
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.
; 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# 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=$?"exit=0Kode 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.
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 semulaPasangan 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#
pushmengurangi rsp 8 byte lalu menulis nilai ke alamat baru, sedangkanpopmembaca puncak lalu menaikkan rsp 8 byte, dan keduanya beroperasi sesuai LIFO.- Stack tumbuh ke arah alamat rendah, sehingga puncaknya bergerak turun setiap
pushdijalankan. - Pada titik masuk proses Linux, rsp terselaras 16 byte, satu
pushmenghilangkan penyelarasan itu, dan dua push mengembalikannya. - Stack digunakan untuk menitipkan nilai sementara dan menyerahkan argumen, dengan syarat pasangan
pushdanpopselalu seimbang.
Referensi#
- System V Application Binary Interface, AMD64 Architecture — x86-64 psABI maintainers — Menetapkan penyelarasan 16 byte stack pada titik masuk proses beserta aturan pemakaiannya.
- Intel 64 and IA-32 Architectures Software Developer Manuals — Intel — Volume 1 menjelaskan instruksi PUSH dan POP serta arah pertumbuhan stack.