Memahami stack x86-64 secara penuh: push/pop, manipulasi rsp, dan stack frame dengan rbp (prologue/epilogue). Kemudian subroutine: call yang mendorong alamat kembali, ret yang mem-pop-nya, serta variabel lokal yang hidup di atas stack — fondasi ABI di episode 9 dan buffer overflow di episode 17.

Di episode 6, kalian diperkenalkan pada call dan ret — instruksi yang mendorong dan mem-pop alamat kembali lewat stack. Episode 8 membedah mekanisme ini secara menyeluruh: bagaimana stack, register rsp/rbp, dan subroutine bekerja sama membentuk struktur program. Ini mungkin episode paling penting di seluruh series, karena hampir semua topik lanjutan — ABI (9), syscall (10), debug (16), dan exploit (17) — berdiri di atasnya.
Bayangkan stack sebagai papan catatan tempel: setiap fungsi menempelkan catatannya sendiri (variabel lokal, alamat kembali), dan wajib melepasnya sebelum selesai. Jika salah satu fungsi lupa melepas, catatan-catatan itu menumpuk dan program berantakan. Itulah yang terjadi pada stack overflow dan stack corruption.
Dari episode 2 kita tahu: stack tumbuh ke bawah, dikelola register rsp. push mengurangi rsp lalu menyimpan; pop memuat lalu menambah rsp.
push rax ; rsp -= 8; *(rsp) = rax
push rbx ; rsp -= 8; *(rsp) = rbx
pop rcx ; rcx = *(rsp); rsp += 8
pop rdx ; rdx = *(rsp); rsp += 8Perhatikan urutan LIFO: rdx menerima nilai terakhir yang di-push (rbx), bukan rax. Inilah alasan compiler menyimpan register dalam urutan tertentu.
Warning
Push/pop harus seimbang. Setiap push butuh pop (atau add rsp, n). Jika tidak, ret akan membaca "alamat kembali" yang salah — dan program akan melompat ke tempat yang tidak diketahui. Inilah akar banyak crash misterius dan juga target buffer overflow di episode 17.
Ketika call func dieksekusi, terjadi dua hal berurutan:
call didorong ke stack (8 byte).func.Ketika ret dieksekusi: alamat tersebut di-pop dari stack dan dimuat ke RIP. Pemanggilan nested bekerja dengan menumpuk alamat kembali:
_start:
call a ; push addr1; lompat ke a
a:
call b ; push addr2; lompat ke b
b:
ret ; pop addr2 → kembali ke setelah call b
ret ; pop addr1 → kembali ke setelah call aSetiap call "menyimpan" satu alamat di stack, dan ret "mengambil" yang paling atas. Ini membuat pemanggilan fungsi bertingkat — dan rekursi — menjadi mungkin.
Untuk menyimpan variabel lokal dengan rapi, fungsi klasik membuat stack frame menggunakan register rbp sebagai penanda. Polanya:
my_func:
push rbp ; simpan rbp milik pemanggil
mov rbp, rsp ; rbp = marka frame baru
sub rsp, 16 ; alokasikan 16 byte untuk variabel lokal
; ... badan fungsi ...
mov rsp, rbp ; lepaskan frame (kembalikan rsp)
pop rbp ; pulihkan rbp pemanggil
ret ; pop alamat kembaliTahap prologue (push rbp; mov rbp, rsp; sub rsp, N) mempersiapkan panggung; tahap epilogue (mov rsp, rbp; pop rbp; ret) membongkarnya. Visualisasi frame:
+--------------------+
| alamat kembali | <- di-push oleh call
+--------------------+ rbp + 8 (argumen akses di sini)
| rbp pemanggil | <- di-push prologue
+--------------------+ rbp (marka frame)
| variabel lokal 2 | rbp - 8
| variabel lokal 1 | rbp - 16
+--------------------+ rsp (setelah sub rsp, 16)[rbp - 8], [rbp - 16], dst.[rbp + 8] ke atas.my_func:
push rbp
mov rbp, rsp
sub rsp, 8
mov qword [rbp - 8], 42 ; local var int x = 42
mov rax, [rbp - 8] ; return x;
mov rsp, rbp
pop rbp
retFungsi yang tidak memanggil fungsi lain (leaf function) dan tidak butuh variabel lokal bisa melewati frame sama sekali — cukup jaga register rbp tetap utuh:
double_it:
add rax, rax
retCompiler modern sering mengoptimalkan frame ini (omission of frame pointer) demi performa — salah satu alasan kenapa disassembly "aneh" di episode 16 dan 20. Jangan terkejut jika melihat kode yang tidak memakai rbp sama sekali.
Mari gabungkan semuanya — subroutine yang mencetak string yang diberi rsi (buffer) dan rdx (panjang):
section .data
hello db "Hello from function!", 0x0a
hello_len equ $-hello
section .text
global _start
_start:
mov rsi, hello ; argumen 1: buffer
mov rdx, hello_len ; argumen 2: panjang
call print ; panggil subroutine
call print ; panggil lagi — tes reusability
jmp exit
print:
push rbp
mov rbp, rsp
mov rax, 1 ; syscall write
mov rdi, 1 ; stdout
syscall
mov rsp, rbp
pop rbp
ret
exit:
mov rax, 60
mov rdi, 0
syscallTip
Perhatikan pola: _start menyimpan argumen (rsi, rdx) sebelum call, dan print tidak mengubah register yang dipakai pemanggil kecuali rax. Inilah cikal bakal aturan caller-saved vs callee-saved yang kita formalisasi di episode 9 (ABI System V).
push tanpa pop → ret melompat ke alamat sampah.rbp tanpa menyimpan — kalian kehilangan jejak frame pemanggil.rsp di tengah fungsi lalu mengakses [rbp-8] yang seharusnya masih valid.sub rsp, N) lalu menulis ke area di bawah rsp — rawan tertimpa oleh interrupt/exception.Caution
Menulis ke memori di bawah rsp tanpa mengalokasikannya dulu (sub rsp, N) sangat berbahaya: sinyal dan interrupt handler kernel memakai area tersebut. Selalu alokasikan ruang secara eksplisit sebelum menulis variabel lokal ke stack.
Pada episode 8 ini, kalian telah menguasai mekanisme fungsi di level mesin.
Inti yang harus dibawa pulang:
push/pop mengelola stack LIFO yang tumbuh ke bawah; harus selalu seimbang.call mendorong alamat kembali; ret mem-pop-nya — inilah dasar pemanggilan fungsi.push rbp; mov rbp, rsp; sub rsp, N (prologue) dan kebalikannya (epilogue).[rbp - N], argumen lewat stack di [rbp + 8].Di episode 9 selanjutnya, kita formalisasi semua aturan tak tertulis ini menjadi ABI & Calling Convention (System V x86-64): di mana argumen diletakkan (rdi, rsi, rdx, rcx, r8, r9 lalu stack), ke mana hasil kembali (rax), register mana yang wajib disimpan pemanggil vs pemanggil, serta perbedaan dengan ABI Windows x64. Siapkan stack frame kalian — mulai sekarang kita menulis kode yang bisa bekerja sama dengan compiler dan libc.