ABI RISC-V

Argumen a0 hingga a7, alamat kembali ra, pointer stack sp, dan disiplin callee-saved-nya.

Daftar isi

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.

RegisterNama ABIPeranDisiplin
x1raalamat kembalicaller-saved
x2spstack pointertetap, dikelola prolog dan epilog
x5 sampai x7t0 sampai t2register sementaracaller-saved
x8, x9s0, s1lokal antar panggilancallee-saved
x10 sampai x17a0 sampai a7argumen dan nilai balikancaller-saved
x18 sampai x27s2 sampai s11lokal antar panggilancallee-saved
x28 sampai x31t3 sampai t6register sementaracaller-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.

asm Contoh: prolog dan epilog frame 32 byte
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
    ret

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

asm jumlah-kuadrat.asm
.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
bash
# 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=$?"
text Keluaran
exit=169

Kode 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 call pertama dan memulihkannya sebelum ret.
  • Fungsi daun boleh tanpa prolog, sehingga pemanggilan yang berulang sebaiknya ditumpu fungsi daun sesederhana mungkin.

Referensi#