Belajar Assembly - Stack & Subroutine (Function Calls)
Episode 8 of 23

Belajar Assembly - Stack & Subroutine (Function Calls)

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.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

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.

Stack Revisited

Dari episode 2 kita tahu: stack tumbuh ke bawah, dikelola register rsp. push mengurangi rsp lalu menyimpan; pop memuat lalu menambah rsp.

push dan pop
push rax        ; rsp -= 8; *(rsp) = rax
push rbx        ; rsp -= 8; *(rsp) = rbx
pop rcx         ; rcx = *(rsp); rsp += 8
pop rdx         ; rdx = *(rsp); rsp += 8

Perhatikan 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.

Mekanisme call dan ret (Detail)

Ketika call func dieksekusi, terjadi dua hal berurutan:

  1. Alamat instruksi setelah call didorong ke stack (8 byte).
  2. RIP di-set ke func.

Ketika ret dieksekusi: alamat tersebut di-pop dari stack dan dimuat ke RIP. Pemanggilan nested bekerja dengan menumpuk alamat kembali:

Nested call
_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 a

Setiap call "menyimpan" satu alamat di stack, dan ret "mengambil" yang paling atas. Ini membuat pemanggilan fungsi bertingkat — dan rekursi — menjadi mungkin.

Stack Frame: Prologue dan Epilogue

Untuk menyimpan variabel lokal dengan rapi, fungsi klasik membuat stack frame menggunakan register rbp sebagai penanda. Polanya:

Prologue dan epilogue
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 kembali

Tahap prologue (push rbp; mov rbp, rsp; sub rsp, N) mempersiapkan panggung; tahap epilogue (mov rsp, rbp; pop rbp; ret) membongkarnya. Visualisasi frame:

plaintext
+--------------------+
|  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)

Akses Variabel Lokal dan Argumen

  • Variabel lokal: [rbp - 8], [rbp - 16], dst.
  • Argumen fungsi (jika masuk lewat stack): [rbp + 8] ke atas.
Fungsi dengan variabel lokal
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
    ret

Leaf Function: Tanpa Frame

Fungsi yang tidak memanggil fungsi lain (leaf function) dan tidak butuh variabel lokal bisa melewati frame sama sekali — cukup jaga register rbp tetap utuh:

Leaf function (no frame)
double_it:
    add rax, rax
    ret

Compiler 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.

Contoh Lengkap: Fungsi Cetak

Mari gabungkan semuanya — subroutine yang mencetak string yang diberi rsi (buffer) dan rdx (panjang):

print subroutine
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
    syscall

Tip

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).

Common Pitfalls

  • Stack tidak seimbangpush tanpa popret melompat ke alamat sampah.
  • Menimpa rbp tanpa menyimpan — kalian kehilangan jejak frame pemanggil.
  • Mengubah rsp di tengah fungsi lalu mengakses [rbp-8] yang seharusnya masih valid.
  • Lupa mengalokasikan ruang untuk variabel lokal (sub rsp, N) lalu menulis ke area di bawah rsp — rawan tertimpa oleh interrupt/exception.
  • Asumsi stack tumbuh ke atas — selalu ke bawah di x86-64.

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.

Penutup

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.
  • Stack frame: push rbp; mov rbp, rsp; sub rsp, N (prologue) dan kebalikannya (epilogue).
  • Variabel lokal di [rbp - N], argumen lewat stack di [rbp + 8].
  • Leaf function bisa melewati frame; compiler modern sering meng-omisikan frame pointer.

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.

Belajar Assembly - Stack & Subroutine (Function Calls) | Belajar Assembly