Topik ini membedah jalur argumen dan jalur balikan pada System V AMD64 ABI secara langsung, mulai dari enam register pertama, argumen yang melebihi enam lewat stack, sampai bentuk balikan yang lebih besar dari satu register. Setiap aturan dibuktikan dengan fungsi assembly yang dipanggil dari C, sehingga pembacaan offset stack dan isi register dapat diperiksa ulang dari keluaran program. Enam register tersebut melayani argumen integer dan pointer, sedangkan argumen pecahan memiliki jalur xmm sendiri yang tidak menggunakan register daftar ini.
Enam argumen pertama menumpang register#
Kontrak menetapkan rdi, rsi, rdx, rcx, r8, dan r9 sebagai penampung enam argumen bilangan bulat pertama secara berurutan. Fungsi yang membutuhkan lebih dari enam argumen tidak memperpanjang daftar register, melainkan memindahkan sisanya ke stack. Contoh berikut menghitung penjumlahan enam nilai untuk memperlihatkan urutan register tersebut dari sisi yang dipanggil.
// tidak-dijalankan
#include <stdio.h>
long jumlah_enam(long a, long b, long c, long d, long e, long f);
int main(void) {
long hasil = jumlah_enam(10, 20, 30, 40, 50, 60);
printf("jumlah_enam(10, 20, 30, 40, 50, 60) = %ld\n", hasil);
return 0;
}default rel
global jumlah_enam
section .text
jumlah_enam:
mov rax, rdi ; argumen 1
add rax, rsi ; + argumen 2
add rax, rdx ; + argumen 3
add rax, rcx ; + argumen 4
add rax, r8 ; + argumen 5
add rax, r9 ; + argumen 6
retgcc -c utama.c -o utama.o
nasm -f elf64 jumlah_enam.asm -o jumlah_enam.o
gcc utama.o jumlah_enam.o -o prog
./progjumlah_enam(10, 20, 30, 40, 50, 60) = 210Setiap add membaca satu register argumen, dan urutannya mengikuti urutan argumen pada deklarasi C. Untuk fungsi yang seluruh argumennya berupa bilangan bulat, penempatannya selalu persis seperti kontrak, yaitu argumen ke-n berada di register ke-n. Register r10 dan r11 sengaja tidak tersentuh karena keduanya bukan register argumen, melainkan register kerja bebas yang juga berada dalam kelompok caller-saved.
Ukuran register mengikuti ukuran argumennya. Argumen int selebar 32 bit dikirim lewat edi, dan karena penulisan ke register 32-bit otomatis mengosongkan 32 bit atasnya, membaca rdi tetap menghasilkan nilai yang benar. Adapun argumen long selebar 64 bit menggunakan register penuhnya. Contoh-contoh pada topik ini menggunakan long sehingga setiap register terisi penuh dan tidak ada tafsir tambahan pada pembacaannya.
Argumen ketujuh dan seterusnya lewat stack#
Argumen setelah yang keenam diletakkan di stack. Pemanggil mendorongnya sebelum instruksi call, dan kontrak menetapkan arah pendorongannya dari kanan ke kiri, sehingga argumen terakhir berada pada alamat terendah. Pada titik masuk fungsi, [rsp] memuat alamat kembali yang didorong instruksi call, sehingga argumen ketujuh berada di [rsp+8] dan argumen kedelapan di [rsp+16]. Contoh berikut membaca kedua slot itu langsung pada titik masuk fungsi.
// tidak-dijalankan
#include <stdio.h>
long delapan_arg(long a, long b, long c, long d, long e, long f, long g, long h);
long dua_arg_stack(long a, long b, long c, long d, long e, long f, long g, long h);
int main(void) {
long sisa = dua_arg_stack(10, 20, 30, 40, 50, 60, 70, 80);
long total = delapan_arg(10, 20, 30, 40, 50, 60, 70, 80);
printf("dua_arg_stack membaca [rsp+8] + [rsp+16] = %ld\n", sisa);
printf("delapan_arg(10, 20, 30, 40, 50, 60, 70, 80) = %ld\n", total);
return 0;
}default rel
global delapan_arg
global dua_arg_stack
section .text
delapan_arg:
mov r11, [rsp+8] ; argumen ke-7 dibaca langsung dari stack
mov r10, [rsp+16] ; argumen ke-8 dibaca langsung dari stack
mov rax, rdi ; enam argumen pertama dari register
add rax, rsi
add rax, rdx
add rax, rcx
add rax, r8
add rax, r9
add rax, r11
add rax, r10
ret
dua_arg_stack:
mov rax, [rsp+8] ; argumen ke-7: slot stack pertama
add rax, [rsp+16] ; argumen ke-8: slot stack kedua
retgcc -c utama_stack.c -o utama_stack.o
nasm -f elf64 delapan_arg.asm -o delapan_arg.o
gcc utama_stack.o delapan_arg.o -o prog-stack
./prog-stackdua_arg_stack membaca [rsp+8] + [rsp+16] = 150
delapan_arg(10, 20, 30, 40, 50, 60, 70, 80) = 360Fungsi dua_arg_stack membaca kedua slot stack langsung pada titik masuknya, lalu menjumlahkannya menjadi 150, yakni 70 + 80. Fungsi delapan_arg menjumlahkan keenam register argumen beserta kedua slot stack menjadi 360. Kedua angka itu membuktikan bahwa argumen ketujuh dan kedelapan benar-benar tiba di stack pada offset yang diramalkan kontrak, bukan di register.
Susunan memori di belakang angka-angka tersebut dapat ditelusuri dari urutan pendorongan. Pemanggil mendorong 80 lebih dahulu, lalu 70, sehingga 70 mendarat pada alamat yang lebih rendah dan berhenti tepat di [rsp+8] pada titik masuk fungsi, sedangkan 80 berada di [rsp+16]. Posisi inilah yang dibaca kedua fungsi assembly tanpa bantuan apa pun dari compiler.
Balikan satu register dan lebih#
Nilai balik bilangan bulat dan pointer dibawa rax dengan lebar sampai 64 bit, dan untuk balikan yang lebih besar kontrak menyediakan dua jalur. Balikan selebar 128 bit dibagi ke pasangan rax dan rdx, masing-masing 8 byte, dan pemanggil membaca keduanya secara berurutan. Adapun struktur yang lebih besar dari 16 byte tidak melewati register sama sekali. Pemanggil menyediakan memorinya, lalu menyelipkan alamatnya sebagai argumen tersembunyi di rdi, dan fungsi mengembalikan alamat yang sama di rax. Pada fungsi yang memiliki frame, bentuk balikan 128 bit terlihat seperti berikut.
mov rax, [rbp-16] ; 8 byte pertama
mov rdx, [rbp-8] ; 8 byte berikutnya
retPasangan rax dan rdx cocok untuk hasil ganda yang saling terkait, misalnya hasil bagi beserta sisanya, atau struct kecil berisi dua anggota 8 byte. Adapun jalur alamat tersembunyi adalah jalur yang dipilih compiler C untuk struct yang lebih besar, sehingga fungsi assembly yang memanggil maupun dipanggil C perlu menyadari kemungkinan rdi digunakan untuk itu. Rincian lengkap klasifikasi tipe balikan terdapat pada bagian nilai balik dokumen ABI yang dirujuk di akhir topik, sedangkan untuk fungsi bilangan bulat biasa cukup diingat satu aturannya, yaitu rax sudah harus memuat hasil pada saat ret dieksekusi.
Kesalahan umum#
Empat kekeliruan berikut paling sering muncul pada pembacaan argumen dan penulisan balikan.
- Membaca argumen ketujuh dari
[rsp]alih-alih[rsp+8]. Slot[rsp]memuat alamat kembali, sehingga keliru satu slot membuat nilai yang dibaca adalah alamat, bukan argumen. - Kehilangan isi register argumen setelah panggilan lain. Register argumen termasuk caller-saved, sehingga isinya tidak dijamin utuh setelah
call. Salin nilai yang masih diperlukan ke register callee-saved atau ke stack sebelum memanggil fungsi lain. - Menaruh balikan di register yang salah kelompok. Pemanggil membaca balikan dari register yang ditetapkan kontrak untuk tipe balikannya, yaitu
raxuntuk bilangan bulat danxmm0untuk pecahan, sehingga hasil di register lain tidak pernah terbaca. - Memanggil fungsi variadik tanpa mengatur
al. Registeralmenyatakan banyaknya register vektor yang digunakan argumen fungsi variadik, sehingga register itu tidak boleh dibiarkan berisi nilai sembarangan meskipun gejalanya sering tidak terlihat pada panggilan sederhana.
Ringkasan#
- Enam argumen bilangan bulat pertama menumpang
rdi,rsi,rdx,rcx,r8, danr9secara berurutan, dan balikan dibawarax. - Argumen ketujuh dan seterusnya diletakkan pemanggil di stack serta dibaca dari
[rsp+8],[rsp+16], dan seterusnya pada titik masuk fungsi. - Balikan 128 bit dibagi ke
raxdanrdx, sedangkan struktur yang lebih besar dikembalikan lewat alamat yang diselipkan dirdi. - Register argumen termasuk kelompok caller-saved, sehingga isinya tidak dijamin bertahan melewati panggilan fungsi lain.
Referensi#
- System V Application Binary Interface — AMD64 Architecture Processor Supplement — Spesifikasi penempatan argumen, urutan pendorongan stack, dan klasifikasi nilai balik.
- NASM 2.16.03 documentation — NASM Project
— Manual NASM untuk sintaks register 64-bit dan pengalamatan
[rsp+offset]yang dipakai contoh.