Setiap fungsi yang membutuhkan penyimpanan lokal membangun stack frame, yaitu wilayah stack miliknya sendiri selama fungsi berjalan. Seluruh pemanggilan fungsi di Linux x86-64 tunduk pada satu janji penyelarasan, yaitu rsp selalu kelipatan 16 pada setiap instruksi call. Topik ini membahas prolog dan epilog berbasis rbp, aturan penyelarasan beserta pembuktiannya, dan akibat yang nyata ketika janji itu dilanggar. Janji penyelarasan itu mengikat kedua pihak: pemanggil menata stack sebelum call, sedangkan yang dipanggil menjaganya selama berjalan.
Prolog dan epilog berbasis rbp#
Prolog standar terdiri atas dua instruksi, push rbp lalu mov rbp, rsp. Instruksi pertama menyimpan rbp milik pemanggil, instruksi kedua menyalin posisi stack saat itu ke rbp, sehingga rbp menjadi acuan tetap untuk seluruh akses lokal selama fungsi berjalan, misalnya [rbp-8] untuk variabel lokal pertama. Adapun rsp terus bergerak setiap kali ada push, pop, sub, atau call, sehingga akses melalui rsp harus mengikuti setiap pergeseran itu. Epilog membatalkan prolog dengan urutan terbalik, lalu ret mengembalikan kendali kepada pemanggil.
Acuan tetap tersebut juga dimanfaatkan perkakas di luar program itu sendiri. Setiap frame menyimpan rbp pemanggil tepat pada alamat yang ditunjuk rbp aktif, sehingga debugger dapat menyusuri rantai ini untuk berjalan mundur dari satu frame ke frame pemanggilnya. Fungsi tanpa frame pointer masih sah menurut ABI, tetapi kerangka rbp membuat penelusuran mundur dan inspeksi variabel lokal menjadi mudah.
Contoh berikut membangun frame dengan dua variabel lokal sekaligus memeriksa penyelarasan stack pada dua titik berbeda.
// tidak-dijalankan
#include <stdio.h>
long cek_entry(void);
long cek_setelah_prolog(void);
long jumlah_lokal(void);
int main(void) {
printf("rsp mod 16 saat masuk fungsi = %ld\n", cek_entry());
printf("rsp mod 16 setelah prolog push rbp = %ld\n", cek_setelah_prolog());
printf("jumlah_lokal() dengan dua variabel = %ld\n", jumlah_lokal());
return 0;
}default rel
global cek_entry
global cek_setelah_prolog
global jumlah_lokal
section .text
cek_entry:
mov rax, rsp ; rsp pada instruksi pertama fungsi
and rax, 15 ; sisa pembagian rsp oleh 16
ret
cek_setelah_prolog:
push rbp
mov rbp, rsp
mov rax, rsp ; rsp setelah prolog
and rax, 15
pop rbp
ret
jumlah_lokal:
push rbp ; prolog: simpan rbp pemanggil
mov rbp, rsp ; rbp menjadi acuan tetap frame
sub rsp, 16 ; ruang dua variabel lokal 8 byte
mov qword [rbp-8], 10
mov qword [rbp-16], 32
mov rax, [rbp-8] ; baca variabel lokal pertama
add rax, [rbp-16] ; + variabel lokal kedua
mov rsp, rbp ; lepaskan ruang lokal
pop rbp ; epilog: pulihkan rbp pemanggil
retgcc -c utama.c -o utama.o
nasm -f elf64 bingkai.asm -o bingkai.o
gcc utama.o bingkai.o -o prog
./progrsp mod 16 saat masuk fungsi = 8
rsp mod 16 setelah prolog push rbp = 0
jumlah_lokal() dengan dua variabel = 42Fungsi jumlah_lokal memesan 16 byte dengan sub rsp, 16, mengisi dua variabel pada [rbp-8] dan [rbp-16], menjumlahkannya, lalu memulihkan rsp dengan mov rsp, rbp sebelum pop rbp. Keluaran 42 berasal dari 10 + 32 yang tersimpan di frame, bukan dari register argumen. Gambar berikut menunjukkan posisi setiap bagian frame pada saat fungsi berjalan setelah prolognya.
Janji penyelarasan 16 byte#
Kontrak menetapkan bahwa rsp selalu kelipatan 16 pada saat instruksi call dieksekusi. Instruksi call sendiri mendorong alamat kembali sebesar 8 byte, sehingga pada instruksi pertama fungsi nilai rsp selalu bersisa 8 terhadap 16. Angka 8 itulah yang dicetak cek_entry pada baris pertama keluaran di atas. Setelah prolog mendorong rbp, sisanya kembali menjadi 0, dan keadaan itulah yang wajib dipenuhi sebelum fungsi memanggil fungsi lain, sebagaimana dicetak oleh cek_setelah_prolog.
Alasan janjinya ada pada instruksi SSE yang memuat atau menyimpan data 16 byte dari memori. Misalnya, movaps menuntut alamat kelipatan 16 dan memicu kesalahan bila tidak. Compiler menghasilkan instruksi semacam itu untuk slot stack milik fungsi yang dipanggil dengan keyakinan bahwa stack selaras sesuai kontrak, sehingga kepatuhan pemanggil menjadi syarat berjalannya kode yang bahkan tidak ditulisnya sendiri.
Akibat stack tidak selaras#
Contoh berikut memperagakan mekanisme kegagalan secara langsung. Fungsi simpan_paket menggunakan movaps untuk menyimpan xmm0 ke slot lokalnya, dan operasi itu sah hanya bila pemanggil selaras. Pemanggil yang benar memanggilnya tanpa masalah, sedangkan panggil_salah menggeser rsp sebesar 8 byte sebelum call, sehingga stack tidak selaras pada instruksi call dan movaps di dalam simpan_paket menerima alamat yang bersisa 8.
// tidak-dijalankan
#include <stdio.h>
#include <string.h>
int simpan_paket(void);
int panggil_salah(void);
int main(int argc, char **argv) {
if (argc > 1 && strcmp(argv[1], "salah") == 0) {
return panggil_salah();
}
printf("movaps berhasil, simpan_paket balik %d\n", simpan_paket());
return 0;
}default rel
global simpan_paket
global panggil_salah
section .text
simpan_paket:
push rbp
mov rbp, rsp
sub rsp, 16
movaps [rsp], xmm0 ; butuh rsp selaras 16 byte, sesuai janji ABI
mov eax, 1
mov rsp, rbp
pop rbp
ret
panggil_salah:
push rbp
sub rsp, 8 ; stack kini tidak selaras saat call (rsp mod 16 = 8)
pxor xmm0, xmm0
call simpan_paket
add rsp, 8
pop rbp
retgcc -c utama_crash.c -o utama_crash.o
nasm -f elf64 salah_selaras.asm -o salah_selaras.o
gcc utama_crash.o salah_selaras.o -o prog-selaras
# pemanggilan dengan stack selaras
./prog-selaras
echo "exit=$?"
# pemanggilan tidak selaras: pesan shell disenyapkan, kode keluar tetap ditampilkan
sh -c './prog-selaras salah; echo "exit=$?"' 2>/dev/nullmovaps berhasil, simpan_paket balik 1
exit=0
exit=139Keluaran menunjukkan panggilan selaras berjalan normal, sedangkan panggilan tak selaras diakhiri sinyal SIGSEGV yang tampak sebagai kode keluar 139. Baris Segmentation fault yang biasanya ditambahkan shell memuat nomor proses yang berbeda antar eksekusi, sehingga contoh menjalankan pemanggilan itu lewat sh -c dengan pesan shell disenyapkan dan hanya kode keluarnya yang ditampilkan. Pada program nyata, kegagalan semacam ini sering muncul jauh dari pemanggil yang salah, misalnya di dalam fungsi pustaka yang menggunakan instruksi SSE selaras pada slot stack-nya.
Karena kegagalannya melompat jauh dari sebabnya, pemeriksaan penyelarasan dilakukan di titik yang dicurigai dengan pola yang sama seperti cek_entry, yaitu mengambil sisa rsp terhadap 16 dan mengembalikannya sebagai nilai balik. Pola ini menempatkan pembuktian tepat pada batas antara pemanggil dan yang dipanggil, sehingga pihak yang melanggar janji dapat ditunjuk secara langsung.
Kesalahan umum#
Tiga kekeliruan berikut paling sering muncul pada pengelolaan frame dan penyelarasan.
- Mendorong jumlah register genap sebelum
calltanpa koreksi. Setiappushmenggeserrspsebesar 8 byte, dan karena stack bersisa 8 pada titik masuk fungsi, duapushsetelah masuk fungsi, misalnyapush rbplalupush rbx, membuatrspbersisa 8 pada instruksicall. Tambahkansub rsp, 8atau dorong satu register cadangan untuk mengembalikan kelipatan 16. - Menulis epilog yang tidak mencerminkan prolog.
sub rsp, 16pada prolog tanpamov rsp, rbpatauadd rsp, 16pada epilog membuatretmenarik alamat kembali dari posisi yang keliru, dan program melompat ke alamat sembarangan. Pasanganmov rsp, rbpdanpop rbpdapat digantikan satu instruksileaveyang melakukan keduanya. - Mengandalkan red zone setelah ada panggilan fungsi. Wilayah 128 byte di bawah
rsphanya aman untuk fungsi daun, sehingga data yang dititipkan di sana hilang karena tertimpa begitu fungsi lain dipanggil.
Ringkasan#
- Prolog
push rbpdanmov rbp, rspmembangun acuan tetap frame, dan epilog memulihkannya sebelumret. rspwajib kelipatan 16 pada setiap instruksicall, sehingga selalu bersisa 8 pada instruksi pertama fungsi dan kembali bersisa 0 setelah prolog.- Pelanggaran penyelarasan memicu SIGSEGV pada instruksi SSE selaras seperti
movaps, sering kali jauh dari pemanggil yang salah. - Red zone 128 byte di bawah
rsphanya aman untuk fungsi daun yang tidak memanggil fungsi lain.
Referensi#
- System V Application Binary Interface — AMD64 Architecture Processor Supplement — Spesifikasi penyelarasan stack 16 byte, red zone, dan bentuk frame.
- NASM 2.16.03 documentation — NASM Project
— Manual NASM untuk
push,pop,sub, danleaveyang menyusun prolog serta epilog.