System V AMD64 ABI

Kontrak pemanggilan fungsi Linux: register argumen, balikan, register yang dipertahankan, dan stack.

Daftar isi

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.

KelompokRegisterPeran
Argumen bilangan bulat dan pointerrdi, rsi, rdx, rcx, r8, r9Menampung enam argumen pertama secara berurutan
Argumen pecahanxmm0 sampai xmm7Menampung delapan argumen pecahan pertama
Balikanrax, xmm0Membawa nilai balik bilangan bulat, pointer, dan pecahan
Caller-savedrax, rcx, rdx, rsi, rdi, r8 sampai r11Boleh ditimpa oleh yang dipanggil, diselamatkan pemanggil bila masih diperlukan
Callee-savedrbx, rbp, r12 sampai r15Wajib utuh setelah panggilan, diselamatkan oleh yang dipanggil
StackrspMenampung 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.

c utama.c
// 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;
}
nasm fungsi.asm
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 rax
bash
gcc -c utama.c -o utama.o
nasm -f elf64 fungsi.asm -o fungsi.o
gcc utama.o fungsi.o -o prog
./prog
text Keluaran
jumlah_tiga(25, 17, 8) = 50

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

utama.cpemanggil (C)jumlah_tigafungsi (assembly)rdi = 25rsi = 17rdx = 8rax = 50 (balikan)
Alur argumen dan balikan pada pemanggilan jumlah_tiga

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 r9 lalu kembali tanpa menyalinnya ke rax membuat pemanggil C membaca nilai lama yang tidak pernah diisi. Pastikan rax memuat balikan sebelum ret.
  • Mengandalkan register argumen setelah memanggil fungsi lain. rdi dan kawan-kawannya termasuk caller-saved, sehingga isinya tidak dijamin utuh setelah call, 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 dari xmm0, sehingga hasil di rax tidak 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, dan r9, argumen pecahan menumpang xmm0 sampai xmm7, dan balikan dibawa rax atau xmm0.
  • rax, rcx, rdx, rsi, rdi, serta r8 sampai r11 boleh ditimpa oleh yang dipanggil, sedangkan rbx, rbp, dan r12 sampai r15 wajib utuh setelah panggilan.
  • Tidak ada pemeriksaan otomatis atas kontrak ini, sehingga pelanggarannya baru terasa sebagai kegagalan saat program berjalan.

Referensi#