Calling convention adalah aturan yang menentukan bagaimana argumen fungsi diteruskan ke callee, register mana yang dipakai, siapa yang bertanggung jawab membersihkan stack, dan bagaimana nilai balik dikembalikan. Calling convention adalah bagian paling kentara dari C ABI. Tiga convention klasik yang sering disinggung adalah cdecl (default C, caller cleanup), stdcall (Windows API, callee cleanup), dan fastcall (register-passing pertama). Pada platform modern x86_64 Linux/macOS, semuanya disatukan menjadi System V AMD64 ABI; pada Windows x86_64 dipakai Microsoft x64 ABI. Pada chapter ini kita akan mengamati bagaimana GCC meletakkan argumen di register pada System V AMD64, lalu mencoba __attribute__((stdcall)) untuk melihat reaksinya.

Tiga Convention Klasik #

Pada arsitektur 32-bit x86, perbedaan convention sangat kentara:

  • cdecl (default C): argumen didorong ke stack dari kanan ke kiri. Caller membersihkan stack setelah call. Konsekuensi: variadic function (printf) bisa bekerja karena caller tahu berapa argumen diberi.
  • stdcall (Windows API, WINAPI): argumen didorong ke stack dari kanan ke kiri. Callee membersihkan stack. Konsekuensi: kode sedikit lebih efisien (cleanup satu tempat) tetapi tidak mendukung variadic function.
  • fastcall (Microsoft, Borland): sebagian argumen pertama lewat register (ECX, EDX), sisanya lewat stack. Bertujuan mempercepat call untuk fungsi pendek.

GCC menyediakan attribute untuk memilih convention: __attribute__((cdecl)), __attribute__((stdcall)), __attribute__((fastcall)), __attribute__((regparm(N))). Pada x86_64, attribute tersebut diabaikan dengan warning karena platform hanya punya satu ABI.

c
/* File: cc_attributes.c - coba attribute calling convention */
#include <stdio.h>

__attribute__((cdecl)) int tambah_cdecl(int a, int b) {
    return a + b;
}

__attribute__((stdcall)) int tambah_stdcall(int a, int b) {
    return a + b;
}

int main(void) {
    int a = 17, b = 25;
    printf("cdecl:   %d + %d = %d\n", a, b, tambah_cdecl(a, b));
    printf("stdcall: %d + %d = %d\n", a, b, tambah_stdcall(a, b));
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o cc_attributes cc_attributes.c
./cc_attributes

Output yang Dihasilkan:

text
cc_attributes.c:5:1: warning: ‘cdecl’ attribute ignored [-Wattributes]
    5 | __attribute__((cdecl)) int tambah_cdecl(int a, int b) {
      | ^~~~~~~~~~~~~
cc_attributes.c:11:1: warning: ‘stdcall’ attribute ignored [-Wattributes]
   11 | __attribute__((stdcall)) int tambah_stdcall(int a, int b) {
      | ^~~~~~~~~~~~~
cdecl:   17 + 25 = 42
stdcall: 17 + 25 = 42

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (__attribute__((cdecl))): GCC memancangkan attribute ini pada symbol, tetapi pada x86_64 ABI hanya ada satu convention, sehingga GCC mencetak warning dan mengabaikannya. Pada x86 32-bit (-m32), attribute ini benar-benar mengubah kode mesin yang dihasilkan.
  2. Baris 11 (__attribute__((stdcall))): Sama—diabaikan dengan warning pada x86_64. Bila Anda menulis library yang harus kompatibel dengan Windows 32-bit (misalnya plugin Winamp lama), attribute ini penting. Pada Linux x86_64 modern, abaikan saja.
  3. Baris 16-17 (pemanggilan): Kedua fungsi menghasilkan output yang sama (42) karena pada x86_64 keduanya dikompilasi dengan convention identik—System V AMD64 ABI.

Mengamati System V AMD64 ABI #

Pada x86_64 Linux, 6 argumen integer pertama lewat register berurutan: RDI, RSI, RDX, RCX, R8, R9. Argumen ke-7 dan seterusnya didorong ke stack. Nilai balik integer lewat RAX (atau EAX untuk 32-bit). Float lewat register XMM terpisah. Kita dapat memverifikasi aturan ini dengan meng-inspeksi assembly yang dihasilkan GCC.

c
/* File: cc_7args.c - fungsi dengan 7 argumen, 6 pertama lewat register */
#include <stdio.h>

int hitung_biasa(int a, int b, int c, int d, int e, int f, int g) {
    return a + b + c + d + e + f + g;
}

int main(void) {
    int total = hitung_biasa(10, 20, 30, 40, 50, 60, 70);
    printf("Total 7 angka = %d\n", total);
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -O0 -S -o cc_7args.s cc_7args.c
gcc -std=c11 -Wall -Wextra -o cc_7args cc_7args.c
./cc_7args

Output yang Dihasilkan:

text
Total 7 angka = 280

Untuk memverifikasi register yang dipakai, kita mengintip assembly fungsi main yang dihasilkan (dengan -S):

text
main:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $16, %rsp
        pushq   $70                  ; argumen ke-7 (g) lewat stack
        movl    $60, %r9d            ; argumen ke-6 (f) lewat R9D
        movl    $50, %r8d            ; argumen ke-5 (e) lewat R8D
        movl    $40, %ecx            ; argumen ke-4 (d) lewat ECX
        movl    $30, %edx            ; argumen ke-3 (c) lewat EDX
        movl    $20, %esi            ; argumen ke-2 (b) lewat ESI
        movl    $10, %edi            ; argumen ke-1 (a) lewat EDI
        call    hitung_biasa
        addq    $8, %rsp
        movl    %eax, -4(%rbp)       ; return value diambil dari EAX
        ...

Mari kita telaah baris-baris kode di atas:

  1. Baris pushq $70: Argumen ketujuh (g = 70) didorong ke stack karena sudah tidak ada slot register. Pemanggil bertanggung jawab membersihkannya (lihat addq $8, %rsp setelah call).
  2. Baris movl $60, %r9d: Argumen keenam (f) diletakkan di register R9 (32-bit: R9D). Inilah register terakhir dari slot 6 yang disediakan System V AMD64.
  3. Baris movl $10, %edi: Argumen pertama (a) di register RDI. Urutannya: RDI, RSI, RDX, RCX, R8, R9. Sama persis dengan dokumentasi System V ABI.
  4. Baris call hitung_biasa: Instruksi call melompat ke fungsi callee. Pada saat fungsi dijalankan, keenam argumen pertama sudah tersedia di register tanpa perlu akses memori.
  5. Baris movl %eax, -4(%rbp): Setelah call, nilai balik fungsi (di EAX/RAX) disimpan ke local variable total di stack frame main.

Implikasi untuk FFI #

Mengapa semua ini penting? Saat kita memanggil C dari bahasa lain (Python, Rust, Go), bahasa tersebut harus menggunakan convention yang sama. Kabar baiknya: hampir semua bahasa modern sudah mendukung System V AMD64 secara default pada Linux x86_64, jadi FFI umumnya langsung bekerja. Jebakan muncul di edge case:

  • Variadic function: System V AMD64 punya aturan khusus untuk variadic—al register harus berisi jumlah register vektor (XMM) yang dipakai. Bahasa yang memanggil printf lewat FFI harus menangani aturan ini (misalnya Rust extern "C-unwind").
  • Struct return: Struct kecil (≤16 byte) dikembalikan via register; struct besar dikembalikan lewat pointer tersembunyi argumen pertama. Saat FFI me-return struct, pastikan konfigurasi layout cocok.
  • Cross-platform: Windows x86_64 memakai RCX, RDX, R8, R9 (urutan berbeda dari Linux). Library yang ditulis untuk Linux bisa jadi tidak kompatibel dengan Windows bila mengandalkan asumsi register.