Skip to content

在上一章討論了區域變數如何存放在棧中,在零.一版中提及了全域變數如何生成。現在,還剩下一種變數沒有討論——參數

精五真言施術約定

精五有 32 個通用暫存器,施術時 call 術 這個偽指令做的不過是將咒指針(program counter)跳躍到所在位址,把就位址放到 ra 暫存器而已。

施者(caller)如何傳遞資訊給受者(callee)呢?其實怎樣都可以,無論是丟到棧上的某個位置還是丟到特定暫存器都可以。總之講好就好。如果不同編譯器不按照同個約定來傳遞參數,那這些編譯器編譯出來的術就無法互相調用了。

精五的標準施展約定規定在整數情況下,使用 a0 ~ a7 暫存器來傳遞變數。這些暫存器吾人稱之為參數暫存器。(a 即是 argument 的縮寫)

而術結束後,會將其歸值(return value)放在 a0a1,歸值若僅一個字長,只要看 a0 就好了。

貧道用 gcc 編譯個簡單的 C 程式(音界咒當前版本也依賴類似外術),來看看是否按照這個約定就能呼叫到它了。

編譯外術(外部函式)

將以下 C 代碼存入 曰.c 檔案中。

c
#include <stdio.h>
#include <inttypes.h>

int64_t print_int(int64_t number) {
    printf("%" PRId64 "\n", number);
    return 111;
}

執行以下指令來編譯該檔案

sh
riscv64-unknown-elf-gcc -c 曰.c -o 曰.o
riscv64-unknown-elf-objcopy --redefine-sym print_int=曰 曰.o

C 語言不支援非 ASCII 的字符,得先編譯出目的檔 曰.o 之後,再將其符號從 print_int 抽換成

施術範例

再將以下檔案存到 施展.S

assembly
    .section .text
    .global main

main:

    li a0, 17
    call 曰
    call 曰

    li a7, 93           # ecall號碼93表示退出
    li a0, 0
    ecall

用以下指令組譯 施展.S 並鏈結曰.o

riscv64-unknown-elf-gcc 施展.S 曰.o

再以 qemu 執行生成的執行檔 a.out

qemu-riscv64 a.out

會看到終端輸出

17
111

第一行 17 正是吾人放進 a0 的值,而第二行的 111 恰好就是曰的歸值,第一個 call 曰 結束之後,a0 已經被改為 111 ,立刻再施展一次 ,果然輸出了 111

看來 gcc 確實按照這個約定在傳遞參數與回傳值,音界咒編譯器也遵照約定,以在未能完全自舉之前方便與塵界之術互動。

儲存參數

既然在術開始之前,參數就已被施者填入 a0 ~ a7 之中,那能否每次需要讀取參數時就直接使用暫存器呢?不能,因為術有可能還要去調用其他術,這是又得修改 a0 ~ a7 來傳遞參數了。

還是只能老方法全都存到棧裡了。

如果不需要再調用其他術,大可以就放在參數暫存器裡,但零.二版音界咒的實作先不做這個優化。

來看個範例:

音界
術.甲(子、丑)【
    元.天=1
    元.地=1
    元.玄=1
    元.黃=1

執行後,棧應該要是

加入參數的精五棧圖解