Skip to content

Commit 1425e5f

Browse files
committed
術的施展
1 parent 588043d commit 1425e5f

File tree

5 files changed

+113
-12
lines changed

5 files changed

+113
-12
lines changed

book/.vitepress/config.mts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,12 @@ export default defineConfig({
9090
link: "/零.二版/語義分析:類型檢查.md",
9191
},
9292
{
93-
text: "精五真言生成(一)施術",
94-
link: "/零.二版/精五真言生成(一)施術.md",
93+
text: "精五真言生成(一)棧與區域變數",
94+
link: "/零.二版/精五真言生成(一)棧與區域變數.md",
95+
},
96+
{
97+
text: "精五真言生成(二)術的施展",
98+
link: "/零.二版/精五真言生成(二)術的施展.md",
9599
},
96100
],
97101
},
44.6 KB
Loading

book/用語對照.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
2-
3-
## 零.二版引入
4-
1+
##
52
###
63
函式
74

85
### 外術
96
外部函式
107

8+
### 施者
9+
caller
10+
11+
### 受者
12+
callee
13+
14+
## 符號
1115
### 基括號
1216
〖〗

book/零.二版/精五真言生成(一)施術.md renamed to book/零.二版/精五真言生成(一)棧與區域變數.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
但零.二版引入了術之後,一切都變了,術內宣告的變數都是區域變數;而程式碼在施術後會跳到另一個術裡執行,完成後又回到原本的術裡。
66

7-
## 棧與術
7+
## 區域變數
88

9-
由於在咒執行的過程中,一個術可能被施展多次,最簡單的例子就是遞迴術,術中有術,層層嵌套。由於執行次數可能取決於法咒的輸入,編譯器無法知曉究竟一個術在法咒執行過程中究竟會執行幾次,因此不可能在編譯期就將記憶體分配完
9+
咒執行的過程中,一個術可能被施展多次,最簡單的例子就是遞迴術,術中有術,層層嵌套。由於執行次數可能取決於法咒的輸入,編譯器無法知曉究竟一個術在法咒執行過程中究竟會執行幾次,因此不可能在編譯期就將樹中區域變數的記憶體分配完
1010

1111
那只好在執行期動態分配記憶體了,分配到哪裡呢?[老樣子](../零.一版/精五真言生成.md),放到棧上,畢竟術的施展天然就像棧這種資料結構。
1212

@@ -41,14 +41,14 @@
4141

4242
當計算機執行術時,會將計算機的咒指針(program counter)指向術的開頭,隨著咒指針遞增,術就一行行執行下去了,當甲術執行到要施展乙術後,乙必須先記錄當下的施者(caller)——也就是甲——施術時的咒指針,在乙術結束時,才能夠再跳回甲施乙術後的下一條真言執行。
4343

44-
### 臨時變數
44+
## 臨時變數
4545
注意到``術的最後一行 `曰(宇+宙+洪+荒)` ,回憶在[零.一版](../零.一版/精五真言生成.md)中,計算算式時,吾人會將計算機當成一個堆疊機來用,在棧上開出更多空間以存放計算的中間結果。
4646

4747
這就意味著單純採用堆疊機,且不進行優化,是沒法僅在棧中開四個變數的空間就完成計算。
4848

4949
這有幾種不同的作法:
5050

51-
#### 編譯器計算出臨時變數需要的空間
51+
### 編譯器計算出臨時變數需要的空間
5252
例如,編譯器能夠直接計算出需要幾個臨時變數,例如可以將原``術在中間碼就轉換成如下邏輯
5353

5454
```
@@ -68,7 +68,7 @@
6868
```
6969
則可以配出七個整數的空間。(此非最優解)
7070

71-
#### 棧動態增長
71+
### 棧動態增長
7272
也可以如零.一版一樣,一邊計算一邊把臨時結果壓入棧,實作可能簡單一些。但最終仍要把棧恢復到舊貌,也就是說仍然要記錄棧到底增長了多少,或者採用 `fp` 暫存器來記錄當下的棧底為多少。
7373

7474
臨時增長棧也會導致用 `sp` 來索引區域變數的位址會不太穩定,以 `fp` 來索引實作會比較容易。(棧臨時增長時,`sp`動,但`fp`不動)
@@ -194,7 +194,6 @@ gcc 可以用 `-fomit-frame-pointer` 來調控是否採用 `fp` 的棧形式,`
194194
ret # jr ra
195195
196196
```
197-
全在暫存器就算完了。
198197

199198
再來看看乙那段將宇、宙、洪、荒相加的算式,若編譯器好好優化,會怎麽計算:
200199

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
在上一章討論了區域變數如何存放在棧中,在[零.一版](../零.一版/精五真言生成.md)中提及了全域變數如何生成。現在,還剩下一種變數沒有討論——**參數**
2+
3+
## 精五真言施術約定
4+
精五有 32 個通用暫存器,施術時 `call 術` 這個偽指令做的不過是將咒指針(program counter)跳躍到``所在位址,把就位址放到 `ra` 暫存器而已。
5+
6+
施者(caller)如何傳遞資訊給受者(callee)呢?其實怎樣都可以,無論是丟到棧上的某個位置還是丟到特定暫存器都可以。總之**講好就好**。如果不同編譯器不按照同個約定來傳遞參數,那這些編譯器編譯出來的術就無法互相調用了。
7+
8+
精五的[標準施展約定](https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf)規定在整數情況下,使用 `a0` ~ `a7` 暫存器來傳遞變數。這些暫存器吾人稱之為參數暫存器。(a 即是 argument 的縮寫)
9+
10+
而術結束後,會將其歸值(return value)放在 `a0``a1`,歸值若僅一個字長,只要看 `a0` 就好了。
11+
12+
貧道用 gcc 編譯個簡單的 C 程式(音界咒當前版本也依賴類似外術),來看看是否按照這個約定就能呼叫到它了。
13+
14+
## 編譯外術(外部函式)
15+
16+
將以下 C 代碼存入 `曰.c` 檔案中。
17+
18+
```c
19+
#include <stdio.h>
20+
#include <inttypes.h>
21+
22+
int64_t print_int(int64_t number) {
23+
printf("%" PRId64 "\n", number);
24+
return 111;
25+
}
26+
```
27+
28+
執行以下指令來編譯該檔案
29+
```sh
30+
riscv64-unknown-elf-gcc -c 曰.c -o 曰.o
31+
riscv64-unknown-elf-objcopy --redefine-sym print_int=曰 曰.o
32+
```
33+
34+
C 語言不支援非 ASCII 的字符,得先編譯出目的檔 `曰.o` 之後,再將其符號從 `print_int` 抽換成 ``
35+
36+
## 施術範例
37+
38+
再將以下檔案存到 `施展.S`
39+
40+
```assembly
41+
.section .text
42+
.global main
43+
44+
main:
45+
46+
li a0, 17
47+
call 曰
48+
call 曰
49+
50+
li a7, 93 # ecall號碼93表示退出
51+
li a0, 0
52+
ecall
53+
```
54+
55+
用以下指令組譯 `施展.S` 並鏈結`曰.o`
56+
```
57+
riscv64-unknown-elf-gcc 施展.S 曰.o
58+
```
59+
再以 qemu 執行生成的執行檔 `a.out`
60+
61+
```
62+
qemu-riscv64 a.out
63+
```
64+
會看到終端輸出
65+
```
66+
17
67+
111
68+
```
69+
第一行 `17` 正是吾人放進 `a0` 的值,而第二行的 `111` 恰好就是曰的歸值,第一個 `call 曰` 結束之後,`a0` 已經被改為 `111` ,立刻再施展一次 `` ,果然輸出了 `111`
70+
71+
看來 gcc 確實按照這個約定在傳遞參數與回傳值,音界咒編譯器也遵照約定,以在未能完全自舉之前方便與塵界之術互動。
72+
73+
## 儲存參數
74+
75+
既然在術開始之前,參數就已被施者填入 `a0` ~ `a7` 之中,那能否每次需要讀取參數時就直接使用暫存器呢?不能,因為術有可能還要去調用其他術,這是又得修改 `a0` ~ `a7` 來傳遞參數了。
76+
77+
還是只能老方法全都存到棧裡了。
78+
79+
如果不需要再調用其他術,大可以就放在參數暫存器裡,但零.二版音界咒的實作先不做這個優化。
80+
81+
來看個範例:
82+
83+
```音界
84+
術.甲(子、丑)【
85+
元.天=1
86+
元.地=1
87+
元.玄=1
88+
元.黃=1
89+
90+
```
91+
92+
執行後,棧應該要是
93+
94+
![加入參數的精五棧圖解](../image/參數精五棧圖解.png)

0 commit comments

Comments
 (0)