Go 中的 wasm 組合語言
Go1.11已經正式釋出,最大的一個亮點是增加了對WebAssembly的實驗性支援。對於Go組合語言愛好者來說,WebAssembly平臺是一個新的挑戰。本文嘗試從最簡單的memclr函式入手,簡要了解WebAssembly組合語言。
## runtime·memclrNoHeapPointers 函式
改函式原始檔在:
https://github.com/golang/go/blob/master/src/runtime/memclr_wasm.s
函式的實現如下:
```s
// func memclrNoHeapPointers ( ptr unsafe.Pointer , n uintptr )
TEXT runtime · memclrNoHeapPointers ( SB ), NOSPLIT , $ 0 - 16
MOVD ptr + 0 ( FP ), R0
MOVD n + 8 ( FP ), R1
loop :
Loop
Get R1
I64Eqz
If
RET
End
Get R0
I32WrapI64
I64Const $ 0
I64Store8 $ 0
Get R0
I64Const $ 1
I64Add
Set R0
Get R1
I64Const $ 1
I64Sub
Set R1
Br loop
End
UNDEF
```
## 函式簽名
函式的簽名如下:
```go
func memclrNoHeapPointers(ptr unsafe.Pointer, n uintptr )
```
對應C語言的簽名如下:
```c
void memclrNoHeapPointers (int32_t ptr, int32_t n);
```
對應WebAssembly的函式簽名如下:
```lisp
(func $memclrNoHeapPointers (param $ptr i32) (param $n f32)
...
)
```
## 讀取函式引數
因為Go語言是動態棧,和WebAssembly的記憶體模型並不一樣。我們先忽略這些問題的細節,看看如何讀取引數的:
```s
MOVD ptr + 0 ( FP ), R0
MOVD n + 8 ( FP ), R1
```
熟悉Go組合語言的同學肯定很容易理解上述程式碼。其中第一行指令是將Go函式的第一個引數載入到R0暫存器,第二行指令是將第二個引數載入到R1暫存器。FP是偽暫存器,表示當前函式呼叫的幀暫存器,每個引數分別使用引數名作為字首+引數相對於FP的地址偏移量確定。
不過WebAssembly是基於棧式的虛擬機器結構,並不存在暫存器的概念。不過我們可以將R0和R1看作是函式的區域性變數。因此在memclrNoHeapPointers函式的定義中再增加2個區域性變數:
```lisp
(func $memclrNoHeapPointers (param $ptr i32) (param $n f32)
(local i32) (local f32) ;; R0 R1 暫存器
...
)
```
## WebAssembly組合語言
現在將函式的主體指令改為WebAssembly組合語言,大概是如下的寫法:
```lisp
(func $memclrNoHeapPointers (param $ptr i32) (param $n f32)
(local i32) (local f32) ;; R0 R1 暫存器
loop:
Loop
Get R1
I64Eqz
If
RET
End
Get R0
I32WrapI64
I64Const $0
I64Store8 $0
Get R0
I64Const $1
I64Add
Set R0
Get R1
I64Const $1
I64Sub
Set R1
Br loop
End
UNDEF
)
```
具體的演算法類似以下的Go語言程式碼:
```go
func memclrNoHeapPointers(ptr, n int32 ) {
R0 := ptr
R1 := n
loop: for {
if R1 == 0 {
return
}
Memort[R0] = 0
R0++
R1--
continue loop
}
}
```
在迴圈中,第一組指令是R1表示的未清0的元素個數是否未0,如果未0則返回。對應程式碼如下:
```s
Get R1
I64Eqz
If
RET
End
```
其中Get對應WebAssembly的get_local指令,用於根據區域性變數的索引標號獲取一個值,放到棧中。I64Eqz對應i64.eqz指令,從棧中取出一個值,判斷是否為0,並將結果從新放入棧中。而If則對應br_if控制流指令,首先從棧取出一個值,如果非0則執行分支內的指令。RET返回函式,和WebAssembly的return指令不一定完全等價。
第二組指令是強R0表示的記憶體地址對應的空間清0:
```s
Get R0
I32WrapI64
I64Const $ 0
I64Store8 $ 0
```
Get對應get_local指令,取出一個i64型別的值。I32WrapI64對應i32.wrap/i64指令,將i64型別強制轉型為i32型別,重新入棧。I64Const則是生成一個常數0,入棧。I64Store8對應i32.store8指令,從棧取出記憶體地址,第二個引數是0表示地址採用預設的對其方式。簡而言之就是將R0對應的地址設定為0。
第三組是將R0加一後存回R0區域性變數:
```s
Get R0
I64Const $ 1
I64Add
Set R0
```
第四組是將R1減一後存回R1區域性變數:
```s
Get R1
I64Const $ 1
I64Sub
Set R1
```
迴圈內的最後一個 `Br loop` 指令是繼續從loop標號開始的迴圈。
函式最後的UNDEF並不是WebAssembly彙編指令。
## 總結
因為Go語言序言支援棧的分裂,Go語言對WebAssembly的組合語言是一個變異的版本。Go語言使用區域性或者是全域性變數來模擬暫存器,在函式的內部在依然基於WebAssembly棧虛擬機器的方式工作。
因為WebAssembly也是剛剛支援的平臺,很多技術細節還需要進一步確認。想深入瞭解WebAssembly組合語言的同學,本人寫的《Go語言高階程式設計》(開源圖書)和 《WebAssembly標準入門》(人民郵電即將出版)中的組合語言章節部分的內容。
https://github.com/chai2010/advanced-go-programming-book
https://github.com/chai2010/awesome-wasm-zh/blob/master/webassembly-primer.md)