Setiap panggilan fungsi membagi register serbaguna menjadi dua kelas dengan janji yang berbeda. Register caller-saved boleh ditimpa oleh fungsi yang dipanggil, sedangkan register callee-saved wajib bernilai sama sebelum dan sesudah panggilan. Topik ini menetapkan pembagian tersebut lalu mengamatinya secara langsung dengan program yang membandingkan isi rbx dan r12 sebelum dan sesudah memanggil dua fungsi berbeda, satu yang taat kontrak dan satu yang sengaja melanggarnya.
Dua kelas register pada setiap panggilan#
Pembagian ini lahir dari kebutuhan kedua belah pihak yang sama-sama membutuhkan register. Bila semua register wajib dipertahankan, fungsi yang dipanggil harus menyelamatkan banyak register padahal mungkin tidak menggunakannya, dan bila tidak ada yang wajib dipertahankan, pemanggil harus menyelamatkan semuanya sebelum setiap call. Komprominya adalah membagi register menjadi dua kelas dengan tanggung jawab yang jelas.
| Kelas | Register | Janjinya |
|---|---|---|
| Caller-saved (volatile) | rax, rcx, rdx, rsi, rdi, r8 sampai r11 | Isinya boleh berubah setelah call, pemanggil menyelamatkannya sendiri bila masih diperlukan |
| Callee-saved (dipertahankan) | rbx, rbp, r12, r13, r14, r15 | Wajib utuh setelah ret, yang dipanggil menyelamatkannya bila ingin menimpanya |
| Kasus khusus | rsp | Stack pointer, wajib kembali ke nilai semula sebelum ret |
Istilahnya merangkum siapa yang bekerja. Caller-saved berarti register diselamatkan pemanggil bila isinya masih dibutuhkan setelah panggilan, sedangkan callee-saved berarti register dipulihkan oleh fungsi yang dipanggil. Mekanisme penyelamatannya sama, yaitu push sebelum register ditimpa dan pop sebelum kembali. Pembagian ini memberi ruang bagi fungsi untuk menyimpan nilai lintas panggilan di register callee-saved tanpa biaya apa pun di sisi pemanggil, sekaligus membebankan pengurusan register kerja sementara kepada sisi pemanggil.
Membuktikan pemulihan rbx dan r12#
Program berikut menempatkan dua nilai penanda di rbx dan r12, lalu memanggil fungsi lain dan memeriksa kedua register itu setelah panggilan. Dua fungsi uji disiapkan. uji_baik memanggil fungsi_c tulisan C yang dikompilasi compiler, sedangkan uji_jahat memanggil perusak tulisan assembly yang menimpa kedua register tanpa menyelamatkannya. Hasil pemeriksaan dikembalikan sebagai 1 bila utuh dan 0 bila berubah.
// tidak-dijalankan
#include <stdio.h>
int fungsi_c(void);
int uji_baik(void);
int uji_jahat(void);
int main(void) {
printf("panggil fungsi_c (taat ABI) : rbx dan r12 utuh = %d\n", uji_baik());
printf("panggil perusak (langgar ABI): rbx dan r12 utuh = %d\n", uji_jahat());
return 0;
}
int fungsi_c(void) {
return 40 + 2;
}default rel
global uji_baik
global uji_jahat
global perusak
extern fungsi_c
section .text
uji_baik:
push rbp ; penyelaras stack sebelum call
push rbx ; selamatkan isi rbx milik pemanggil
push r12 ; selamatkan isi r12 milik pemanggil
mov rbx, 0x1111111122222222
mov r12, 0x3333333344444444
call fungsi_c wrt ..plt
mov rax, 0x1111111122222222
cmp rbx, rax ; apakah rbx masih utuh?
jne .rusak
mov rax, 0x3333333344444444
cmp r12, rax ; apakah r12 masih utuh?
jne .rusak
mov eax, 1 ; utuh
jmp .pulih
.rusak:
xor eax, eax ; ada yang berubah
.pulih:
pop r12
pop rbx
pop rbp
ret
uji_jahat:
push rbp
push rbx
push r12
mov rbx, 0x1111111122222222
mov r12, 0x3333333344444444
call perusak
mov rax, 0x1111111122222222
cmp rbx, rax
jne .rusak2
mov rax, 0x3333333344444444
cmp r12, rax
jne .rusak2
mov eax, 1
jmp .pulih2
.rusak2:
xor eax, eax
.pulih2:
pop r12
pop rbx
pop rbp
ret
perusak:
xor ebx, ebx ; melanggar kontrak: rbx ditimpa tanpa diselamatkan
xor r12d, r12d ; melanggar kontrak: r12 ditimpa tanpa diselamatkan
retgcc -c utama.c -o utama.o
nasm -f elf64 uji-register.asm -o uji-register.o
gcc utama.o uji-register.o -o prog
./progpanggil fungsi_c (taat ABI) : rbx dan r12 utuh = 1
panggil perusak (langgar ABI): rbx dan r12 utuh = 0Kedua fungsi uji menyelamatkan isi asli rbx dan r12 milik pemanggilnya dengan push di awal dan pop di akhir, sehingga kedua fungsi uji itu tetap taat kontrak meskipun sedang menguji pelanggaran. Tiga push juga memulihkan penyelarasan stack, karena stack yang bersisa 8 pada titik masuk kembali menjadi kelipatan 16 sebelum call. Akhiran wrt ..plt pada panggilan fungsi_c mengikuti kebiasaan program yang ditautkan secara dinamis, yaitu melintasi PLT terlebih dahulu. Adapun fungsi_c yang dikompilasi gcc tidak menyentuh rbx dan r12 sama sekali, dan bila compiler memang memerlukannya, compiler itu menyisipkan push dan pop otomatis sesuai kontrak yang sama. perusak tidak memiliki disiplin itu, sehingga penanda di kedua register berubah menjadi nol dan pemeriksaan mengembalikan 0.
Pelanggaran kontrak dan akibatnya#
Fungsi perusak bukan sekadar contoh buruk, melainkan model dari bug yang nyata. Compiler C bebas menyimpan variabel yang masih hidup di rbx atau di r12 sampai r15 lintas panggilan, misalnya penghitung perulangan yang harus bertahan sementara badan perulangannya memanggil fungsi lain, dan compiler hanya berani melakukannya karena kontrak menjanjikan register itu utuh. Fungsi assembly yang menimpanya tanpa menyelamatkan tidak memicu satu pun error pada waktu bangun, tetapi nilai variabel yang dititipkan compiler berubah tanpa sebab, dan gejalanya dapat muncul di fungsi mana pun yang memanggil fungsi assembly tersebut. Karena itu aturannya tegas, yaitu register callee-saved boleh digunakan sebebasnya di dalam fungsi sendiri, asalkan diselamatkan di awal dan dipulihkan sebelum kembali.
Kesalahan umum#
Empat kekeliruan berikut paling sering muncul pada pengelolaan register lintas panggilan.
- Lupa memulihkan register pada satu jalur kembali saja. Fungsi dengan beberapa titik
retyang salah satunya melompatipopmembuat pemanggil menerima register yang sudah ditimpa, dan kegagalannya muncul hanya untuk jalur eksekusi tersebut. - Menyelamatkan register setelah ditimpa.
push rbxharus dilakukan sebelummov rbx, ..., karena yang diselamatkan adalah isi milik pemanggil, bukan isi baru hasil fungsi sendiri. Penyelamatan yang terlambat membuat isi asli tidak dapat dipulihkan dengan cara apa pun. - Menganggap
raxtermasuk yang dipertahankan.raxadalah register balikan sekaligus caller-saved, sehingga isinya praktis berubah pada setiap panggilan fungsi. - Menimpa register callee-saved tanpa penyelamatan karena program terlihat berjalan benar. Pemanggil yang saat itu tidak menggunakan
rbxmembuat bug tidak tampak, lalu bug yang sama baru muncul pada pemanggil lain yang menitipkan nilai dirbx.
Ringkasan#
- Register caller-saved, yaitu
rax,rcx,rdx,rsi,rdi, danr8sampair11, boleh ditimpa oleh fungsi yang dipanggil. - Register callee-saved, yaitu
rbx,rbp, danr12sampair15, wajib utuh setelah panggilan dan dipulihkan oleh fungsi yang menimpanya. - Fungsi menyelamatkan register callee-saved dengan
pushdi awal danpopdi akhir, sedangkan pemanggil menyelamatkan sendiri nilai caller-saved yang masih digunakan. - Pelanggaran kontrak ini tidak terdeteksi pada waktu bangun, dan gejalanya muncul jauh dari sebabnya.
Referensi#
- System V Application Binary Interface — AMD64 Architecture Processor Supplement — Daftar resmi register volatile dan callee-saved pada kontrak System V.
- NASM 2.16.03 documentation — NASM Project
— Manual NASM untuk
push,pop, danretyang menyusun pola penyelamatan register.