System V AMD64 ABI adalah kontrak pemanggilan fungsi yang digunakan Linux pada arsitektur x86-64. Kontrak ini menetapkan register mana yang menampung argumen, register mana yang membawa balikan, register mana yang wajib utuh setelah panggilan, dan bagaimana stack dipersiapkan. Topik ini merangkum keseluruhan kontrak tersebut lalu membuktikannya dengan program C yang memanggil fungsi assembly tulisan tangan, sehingga setiap aturan dapat diamati langsung dari keluaran program.
Kontrak biner antar berkas mesin#
ABI (application binary interface) adalah kesepakatan tingkat mesin tentang bagaimana potongan-potongan program yang sudah dikompilasi saling memanggil. Kesepakatan itu mencakup penempatan argumen, penyimpanan balikan, status register setelah panggilan, dan tata letak stack. Batasnya berbeda dengan API yang berlaku pada tingkat kode sumber: API dijaga oleh compiler pada waktu kompilasi, sedangkan ABI dijaga oleh disiplin penulisan kode pada waktu program berjalan.
Kontrak ini menjadi penting begitu dua berkas objek yang berasal dari alat berbeda ditautkan menjadi satu program. Fungsi printf milik libc tidak mengetahui apa pun tentang kode yang memanggilnya, tetapi panggilan tetap berhasil karena keduanya mengikuti aturan yang sama. Compiler C menghasilkan kode yang mematuhi System V AMD64 ABI untuk setiap fungsi yang dibuatnya, sehingga fungsi assembly tulisan tangan yang mematuhi kontrak yang sama dapat dipanggil dari C dan dapat pula memanggil fungsi C. Seluruh contoh pada topik ini dibangun dengan pola tersebut, yaitu program C sebagai pemanggil (caller) dan fungsi assembly sebagai yang dipanggil (callee).
Ringkasan kontrak System V AMD64#
Tabel berikut merangkum pembagian register pada System V AMD64 ABI. Enam register serbaguna pertama menampung argumen bilangan bulat dan pointer, delapan register xmm menampung argumen bilangan pecahan, dan rax membawa balikan. Register yang tersisa dibagi menjadi dua kelas dengan janji yang berbeda, yaitu register yang boleh ditimpa oleh yang dipanggil dan register yang wajib dipertahankan.
| Kelompok | Register | Peran |
|---|---|---|
| Argumen bilangan bulat dan pointer | rdi, rsi, rdx, rcx, r8, r9 | Menampung enam argumen pertama secara berurutan |
| Argumen pecahan | xmm0 sampai xmm7 | Menampung delapan argumen pecahan pertama |
| Balikan | rax, xmm0 | Membawa nilai balik bilangan bulat, pointer, dan pecahan |
| Caller-saved | rax, rcx, rdx, rsi, rdi, r8 sampai r11 | Boleh ditimpa oleh yang dipanggil, diselamatkan pemanggil bila masih diperlukan |
| Callee-saved | rbx, rbp, r12 sampai r15 | Wajib utuh setelah panggilan, diselamatkan oleh yang dipanggil |
| Stack | rsp | Menampung argumen ketujuh dan seterusnya, tumbuh ke alamat rendah |
Dua ketentuan pelengkap sering diperlukan. Argumen yang tidak muat lagi di register didorong ke stack oleh pemanggil, dan urutan pendorongannya diatur sedemikian rupa sehingga slot teratas stack selalu menjadi argumen ketujuh. Untuk fungsi variadik seperti printf, register al memuat jumlah register vektor yang digunakan argumennya, itulah sebabnya xor eax, eax muncul sebelum hampir setiap panggilan printf dari assembly.
Fungsi assembly yang dipanggil dari C#
Demonstrasi pertama memanggil fungsi assembly jumlah_tiga dari program C. Compiler C wajib menaruh argumen pertama di rdi, argumen kedua di rsi, dan argumen ketiga di rdx karena kontraknya demikian, sedangkan fungsi assembly cukup menjumlahkan ketiga register itu dan menaruh hasilnya di rax. Berkas C-nya hanya berisi kerangka pemanggil dan pencetakan hasil.
// tidak-dijalankan
#include <stdio.h>
long jumlah_tiga(long a, long b, long c);
int main(void) {
printf("jumlah_tiga(25, 17, 8) = %ld\n", jumlah_tiga(25, 17, 8));
return 0;
}default rel
global jumlah_tiga
section .text
jumlah_tiga:
mov rax, rdi ; argumen pertama
add rax, rsi ; + argumen kedua
add rax, rdx ; + argumen ketiga
ret ; balikan sudah berada di raxgcc -c utama.c -o utama.o
nasm -f elf64 fungsi.asm -o fungsi.o
gcc utama.o fungsi.o -o prog
./progjumlah_tiga(25, 17, 8) = 50Blok bash membangun program dalam tiga tahap, yaitu kompilasi utama.c menjadi berkas objek oleh gcc, perakitan fungsi.asm menjadi berkas objek oleh nasm, dan penautan keduanya menjadi satu program lengkap dengan runtime libc. Keluaran menunjukkan 50, yaitu 25 + 17 + 8. Bagian terpenting justru yang tidak terlihat pada keluaran: compiler C yang menerjemahkan ekspresi jumlah_tiga(25, 17, 8) memuat 25 ke rdi, 17 ke rsi, dan 8 ke rdx sebelum instruksi call, kemudian membaca rax setelah fungsi kembali. Fungsi assembly tidak mengetahui nama argumennya di kode C, karena yang dilihatnya hanya register yang sudah terisi. Diagram berikut merangkum alur argumen dan balikan tersebut.
Akibat kontrak dilanggar#
ABI tidak memiliki mekanisme pemeriksaan pada waktu bangun maupun pada waktu berjalan. Assembler dan penaut menerima kode yang melanggar kontrak tanpa satu pun pesan error, dan kegagalannya muncul jauh dari sebabnya, misalnya variabel yang nilainya berubah tanpa sebab atau program yang berhenti di dalam fungsi pustaka. Kegagalan semacam ini sulit dilacak karena penurunannya terjadi melalui register yang nilainya tidak dicatat ke mana pun.
Karena itu, pemeriksaan kontrak secara manual menjadi bagian dari pekerjaan menulis assembly. Tiga aturan yang paling sering dilanggar adalah penempatan argumen, penyelamatan register callee-saved, dan penyelarasan stack. Ketiganya menjadi sumber kegagalan paling lazim pada kode campuran C dan assembly, dan masing-masing dibuktikan dengan program yang dapat dijalankan ulang pada topik-topik modul ini.
Kesalahan umum#
Empat kekeliruan berikut paling sering muncul saat kontrak ini baru dipraktikkan.
- Menaruh balikan di register lain. Fungsi yang menghitung hasilnya ke
r9lalu kembali tanpa menyalinnya keraxmembuat pemanggil C membaca nilai lama yang tidak pernah diisi. Pastikanraxmemuat balikan sebelumret. - Mengandalkan register argumen setelah memanggil fungsi lain.
rdidan kawan-kawannya termasuk caller-saved, sehingga isinya tidak dijamin utuh setelahcall, bahkan saat fungsi yang dipanggil tampak tidak menyentuhnya. Nilai yang masih diperlukan diselamatkan lebih dahulu, misalnya ke register callee-saved atau ke stack. - Menulis balikan pecahan ke
rax. Nilai pecahan dibaca pemanggil darixmm0, sehingga hasil diraxtidak pernah terlihat oleh pemanggil. - Menganggap register sebagai saluran keluaran tambahan. Register adalah salinan yang dikirim pemanggil, dan pemanggil tidak membaca ulang register argumen untuk melihat perubahan yang dilakukan fungsi, sehingga hasil hanya boleh dikirim lewat jalur balikan yang ditetapkan kontrak.
Ringkasan#
- System V AMD64 ABI mengatur penempatan argumen, balikan, kepemilikan register, dan tata letak stack pada tingkat mesin, sehingga kode C dan assembly yang dikompilasi terpisah dapat saling memanggil.
- Enam argumen bilangan bulat pertama menumpang
rdi,rsi,rdx,rcx,r8, danr9, argumen pecahan menumpangxmm0sampaixmm7, dan balikan dibawaraxatauxmm0. rax,rcx,rdx,rsi,rdi, sertar8sampair11boleh ditimpa oleh yang dipanggil, sedangkanrbx,rbp, danr12sampair15wajib utuh setelah panggilan.- Tidak ada pemeriksaan otomatis atas kontrak ini, sehingga pelanggarannya baru terasa sebagai kegagalan saat program berjalan.
Referensi#
- System V Application Binary Interface — AMD64 Architecture Processor Supplement — Dokumen normatif kontrak pemanggilan yang dirangkum topik ini.
- NASM 2.16.03 documentation — NASM Project
— Manual NASM untuk direktif
global,extern, dansectionyang dipakai contoh kode.