ABI menetapkan kontrak yang membuat fungsi yang ditulis terpisah dapat saling memanggil, yaitu di register mana argumen diletakkan, di mana nilai balikan diambil, dan register mana yang wajib dipulihkan. Topik ini membahas calling convention RISC-V untuk Linux riscv64, pola prolog dan epilog yang menyelamatkan ra, serta sebuah fungsi bersarang yang membuktikan seluruh kontraknya di qemu. Seluruh pembuktiannya dijalankan pada qemu-riscv64, sehingga perbandingan langsung dengan kontrak System V AMD64 dapat dilakukan tanpa perangkat keras RISC-V.
Pembagian register menurut ABI#
Konvensi pemanggilan RISC-V menempatkan hingga delapan argumen integer pada a0 sampai a7, jauh lebih banyak daripada enam register argumen pada System V AMD64 milik x86-64. Nilai balikan diteruskan lewat a0, dan a1 mengikutinya untuk nilai 128 bit. Instruksi call menuliskan alamat kembali ke ra, sedangkan ret melompat ke alamat yang tersimpan di ra, sehingga keduanya tidak mendorong atau menarik alamat dari stack seperti pasangan call dan ret milik x86-64.
Tabel berikut merangkum pembagian seluruh register beserta disiplin penyelamatannya. Disiplin itu terbagi menjadi dua, yaitu caller-saved yang boleh dirusak oleh fungsi terpanggil sehingga pemanggil menyelamatkannya sendiri, dan callee-saved yang wajib dipulihkan oleh fungsi terpanggil sebelum kembali.
| Register | Nama ABI | Peran | Disiplin |
|---|---|---|---|
x1 | ra | alamat kembali | caller-saved |
x2 | sp | stack pointer | tetap, dikelola prolog dan epilog |
x5 sampai x7 | t0 sampai t2 | register sementara | caller-saved |
x8, x9 | s0, s1 | lokal antar panggilan | callee-saved |
x10 sampai x17 | a0 sampai a7 | argumen dan nilai balikan | caller-saved |
x18 sampai x27 | s2 sampai s11 | lokal antar panggilan | callee-saved |
x28 sampai x31 | t3 sampai t6 | register sementara | caller-saved |
Pembacaan tabelnya sederhana, yaitu nilai pada register t dan a hanya dapat diandalkan sampai instruksi call berikutnya, sedangkan nilai pada register s tetap utuh melewati panggilan apa pun. Aturan ini menjelaskan mengapa register s adalah pilihan yang tepat untuk menyimpan data yang harus hidup melewati pemanggilan fungsi lain.
Register x0, gp, dan tp tidak masuk tabel disiplin karena perannya tidak terikat kontrak pemanggilan. x0 adalah konstanta nol yang tidak menyimpan apa pun, sedangkan gp dan tp diinisialisasi sekali di awal program dan tidak diubah oleh fungsi biasa. Dengan pengecualian itu, seluruh register lain menuruti salah satu dari dua disiplin pada tabel.
Prolog, epilog, dan penyelamatan ra#
Setiap fungsi yang memanggil fungsi lain wajib menyelamatkan ra, karena call menimpa ra dengan alamat kembali barunya dan ra lama hilang begitu saja. Pola standarnya adalah prolog yang mengalokasikan frame stack lalu menyimpan ra ke dalamnya, diikuti epilog yang memulihkannya sebelum ret. Ukuran frame harus kelipatan 16 byte karena ABI menuntut sp sejajar 16 byte pada setiap titik panggilan, dan sd serta ld memang membutuhkan alamat yang sejajar.
Prolog dan epilog berikut memuat semua elemen pola tersebut untuk sebuah fungsi yang menggunakan ra, s0, dan s1.
jumlahkuadrat:
addi sp, sp, -32 # alokasikan frame 32 byte, tetap 16-byte aligned
sd ra, 24(sp) # simpan alamat kembali
sd s0, 16(sp) # simpan s0 yang akan dipakai
sd s1, 8(sp) # simpan s1 yang akan dipakai
# ... isi fungsi ...
ld s1, 8(sp) # pulihkan s1
ld s0, 16(sp) # pulihkan s0
ld ra, 24(sp) # pulihkan alamat kembali
addi sp, sp, 32 # bebaskan frame
retUrutan penyimpanan dan pemulihannya harus simetris, sehingga setiap sd di prolog memiliki ld pasangannya di epilog dengan offset yang sama. Fungsi yang tidak memanggil fungsi lain, sering disebut fungsi daun, boleh melewatkan penyelamatan ra karena ra tidak pernah ditimpa. Fungsi daun seperti itu justru paling sering muncul sebagai target pemanggilan yang berulang, dan kehematan prolognya menjadi salah satu optimasi kecil yang nyata.
Demo fungsi bersarang#
Program berikut memuat fungsi jumlahkuadrat yang mengembalikan a0 dikalikan a0 ditambah a1 dikalikan a1. Fungsi ini memanggil kuadrat dua kali, sehingga ra wajib diselamatkan, dan kedua nilai antara disimpan di s0 serta s1 agar tetap hidup melewati panggilan.
.global _start
.text
# jumlahkuadrat: mengembalikan a0*a0 + a1*a1
# memanggil kuadrat dua kali sehingga ra wajib diselamatkan
jumlahkuadrat:
addi sp, sp, -32 # alokasikan frame 32 byte, tetap 16-byte aligned
sd ra, 24(sp) # simpan alamat kembali
sd s0, 16(sp) # simpan s0 yang akan dipakai
sd s1, 8(sp) # simpan s1 yang akan dipakai
mv s0, a1 # simpan b di register callee-saved
call kuadrat # a0 = a * a
mv s1, a0 # simpan a*a sebelum panggilan berikutnya
mv a0, s0 # a0 = b
call kuadrat # a0 = b * b
add a0, s1, a0 # a0 = a*a + b*b
ld s1, 8(sp) # pulihkan s1
ld s0, 16(sp) # pulihkan s0
ld ra, 24(sp) # pulihkan alamat kembali
addi sp, sp, 32 # bebaskan frame
ret
# kuadrat: mengembalikan a0*a0, fungsi daun tanpa prolog
kuadrat:
mul a0, a0, a0
ret
_start:
li a0, 3
li a1, 4
call jumlahkuadrat # 9 + 16 = 25
li a0, 5
li a1, 12
call jumlahkuadrat # 25 + 144 = 169
li a7, 93 # syscall exit
ecall # kode keluar = nilai balikan terakhir# rakit dan tautkan program
riscv64-linux-gnu-as -o jumlah-kuadrat.o jumlah-kuadrat.asm
riscv64-linux-gnu-ld -o jumlah-kuadrat jumlah-kuadrat.o
# jalankan di qemu, kode keluarnya nilai balikan panggilan kedua
qemu-riscv64 ./jumlah-kuadrat; echo "exit=$?"exit=169Kode keluar 169 adalah 25 + 144, yang berarti panggilan kedua mengembalikan nilai dengan benar setelah panggilan pertama selesai. Nilai itu hanya mungkin apabila ra dipulihkan dengan benar, karena _start mengandalkan ra yang sama untuk kembali dari kedua panggilan. Alasan penggunaan s0 dan s1 juga terbaca dari kontraknya, yaitu a1 dan register t tidak boleh diandalkan setelah call, sehingga b dan hasil kuadrat pertama ditempatkan pada register yang dijamin utuh oleh fungsi terpanggil.
Tata letak frame-nya sengaja disusun berurutan, yaitu ra pada offset 24, s0 pada 16, dan s1 pada 8, sehingga semua penyimpanan berada di dalam 32 byte yang dialokasikan. Wilayah di bawah sp boleh ditimpa kapan saja oleh fungsi yang dipanggil, dan itulah alasan data yang harus hidup melewati call tidak pernah diletakkan di sana. Panggilan kedua dari _start menegaskan seluruh kontrak, karena ra yang dipulihkan pada akhir panggilan pertama adalah milik _start dan tidak ada jejak frame yang tertinggal.
Fungsi kuadrat berperan sebagai pembanding, karena tanpa prolog apa pun fungsi ini hanya mengeksekusi mul lalu ret. Fungsi daun semacam ini menunjukkan batas bawah biaya pemanggilan, sedangkan jumlahkuadrat menunjukkan biaya penuh beserta penyelamatannya.
Ringkasan#
- Konvensi RISC-V melewatkan hingga delapan argumen lewat a0 sampai a7, mengembalikan nilai lewat a0, dan menyimpan alamat kembali di ra.
- Register t dan a bersifat caller-saved, register s bersifat callee-saved, dan sp dikelola prolog serta epilog dengan sejajar 16 byte.
- Fungsi yang memanggil fungsi lain wajib menyelamatkan ra ke frame stack sebelum
callpertama dan memulihkannya sebelumret. - Fungsi daun boleh tanpa prolog, sehingga pemanggilan yang berulang sebaiknya ditumpu fungsi daun sesederhana mungkin.
Referensi#
- RISC-V ELF psABI Document — RISC-V International — Mendefinisikan konvensi pemanggilan riscv64 beserta pembagian register caller-saved dan callee-saved.
- QEMU User Mode Documentation — QEMU — Dokumentasi qemu-riscv64 yang menjalankan demo fungsi bersarang topik ini.