這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
最近升級 go1.9,發現一個擷取 goroutine id 的依賴沒有支援1.9,於是手動寫了一個,順便學習一下 go assembly。希望你看完這篇文章後,對go彙編有一定的瞭解。
Go Assembly
首先安利一個擷取當前goroutine id 的library, gid,支援 go1.7 - go1.9, 可能是目前最小的庫了,使用也很簡單: id := gid.Get()
。
Go彙編文法類似 Plan 9,它不是對機器語言的直接表達,擁有半抽象的指令集。總體來說, machine-specific 操作一般就是它們的本意,其他概念例如 memory move, subroutine call, return 是抽象的表達。
常量
evaluation 優先順序和 C 不同,例如 3&1<<2 == 4, 解釋為 (3&1) << 2。
常量被認為是 unsigned 64-bit int, 因此 -2 不是負數,而是被作為 uint64 解讀。
符號
4個預定義的符號,表示 pseudo-registers, 偽寄存器(虛擬寄存器?)。
- FP: frame pointer, 參數和本地變數
- PC: program counter: 跳轉,分支
- SB: static base pointer: 全域符號
- SP: stack pointer: 棧頂
使用者定義的符號都是通過位移(offset)來表示的。
SB寄存器表示全域記憶體起點,foo(SB) 表示 符號foo作為記憶體位址使用。這種形式用於命名 全域函數,資料。<>
限制符號只能在當前源檔案使用,類似 C 中的 static。foo+4(SB)
表示foo 往後 4位元組的地址。
FP寄存器指向函數參數。0(FP)是第一個參數,8(FP)是第二個參數(64-bit machine). first_arg+0(FP)
表示把第一個參數地址綁定到符號 first_arg, 這個與SB的含義不同。
SP寄存器表示棧指標,指向 top of local stack frame, 所以 offset 都是負數,範圍在 [ -framesize, 0 ), 例如 x-8(SP). 對於硬體寄存器名稱為SP的架構,x-8(SP)
表示虛擬棧指標寄存器, -8(SP)
表示硬體 SP 寄存器.
跳轉和分支是針對PC的offset,或者 label, 例如:
label: MOVW $0, R1 JMP label
label 範圍是函數層級的,不同函數可以定義相同名稱的label。
指令
例如:
TEXT runtime·profileloop(SB),NOSPLIT,$8 MOVQ $runtime·profileloop1(SB), CX MOVQ CX, 0(SP) CALL runtime·externalthreadhandler(SB) RET
TEXT 指令定義符號 runtime·profileloop
, RET 表示結尾,如果沒聲明,linker會添加 jump-to-self 指令。
$8 表示 frame size,一般後面需要加上參數大小。這裡因為有 NOSPLIT,可以不加。
全域資料符號用 DATA 聲明,方式為 DATA symbol+offset(SB)/width, value
GLOBL 定義資料為全域。例如:
DATA divtab<>+0x00(SB)/4, $0xf4f8fcffDATA divtab<>+0x04(SB)/4, $0xe6eaedf0...DATA divtab<>+0x3c(SB)/4, $0x81828384GLOBL divtab<>(SB), RODATA, $64GLOBL runtime·tlsoffset(SB), NOPTR, $4
定義並初始化了 divtab<>, 一個 唯讀 64位元組 表,每一項4位元組。定義了 runtime·tlsoffset, 4位元組空值,非指標。
指令有一個或兩個參數。如果有兩個,第一個是 bit mask, 可以為數字運算式。值的定義如下:
- NOPROF = 1 ; (For TEXT items.) Don't profile the marked function. This flag is deprecated. 廢棄
- DUPOK = 2 ; It is legal to have multiple instances of this symbol in a single binary. The linker will choose one of the duplicates to use. 此符號允許存在多個,連結器選擇其一使用。
- NOSPLIT = 4 ; (For TEXT items.) Don't insert the preamble to check if the stack must be split. The frame for the routine, plus anything it calls, must fit in the spare space at the top of the stack segment. Used to protect routines such as the stack splitting code itself. 不插入代碼,不檢查是否需要 stack split. (疑問,高版本go使用連續棧,這個指令還有作用嗎?)
- RODATA = 8 ; (For DATA and GLOBL items.) Put this data in a read-only section. 資料存入唯讀區
- NOPTR = 16 ; (For DATA and GLOBL items.) This data contains no pointers and therefore does not need to be scanned by the garbage collector. 表示非指標,不需要 GC。
- WRAPPER = 32 ; (For TEXT items.) This is a wrapper function and should not count as disabling recover.
- NEEDCTXT = 64 ; (For TEXT items.) This function is a closure so it uses its incoming context register.
Example: Add
//main.gopackage mainimport "fmt"func add(x, y int64) int64func main() { fmt.Println(add(2, 3))}
// add.sTEXT ·add(SB),$0-24 MOVQ x+0(FP), BX MOVQ y+8(FP), BP ADDQ BP, BX MOVQ BX, ret+16(FP) RET
定義一個函數的方式為: TEXT package_name·function_name(SB),$frame_size-arguments_size
例子中 package_name 是空,表示當前package。 之後是一個 middle point(U+00B7) 和 函數名稱。
frame_size 是 $0, 表示了需要 stack 的空間大小,這裡是0, 表示不需要stack,只使用 寄存器。函數的參數和傳回值的大小為 3 * 8 = 24
bytes。
MOVQ
表示移動一個 64bit 的值(Q 代表 quadword)。這裡是從 FP(frame pointer, 指向 函數參數的起始位置) 移動到 BX
和 BP
. 文法 symbol+offset(register)
中的 offset, 代表了從 register 為起點,移動 offset後的地址。這裡的 x, y 是在函數定義中的參數符號。
ADDQ
那一行指令 表示把兩個 64bit register的值相加,存到 BX。
最後的 MOVQ
把 BX 中的值,移動到 FP+16的位置, 這裡的 ret
符號是編譯器預設的傳回值符號。
Example: Hello
package mainimport _ "fmt"func hello()func main(){ hello()}
#include "textflag.h"DATA world<>+0(SB)/8, $"hello wo"DATA world<>+8(SB)/4, $"rld "GLOBL world<>+0(SB), RODATA, $12// 需要 stack空間 88位元組,沒有參數和傳回值TEXT ·hello(SB),$88-0 SUBQ $88, SP MOVQ BP, 80(SP) LEAQ 80(SP), BP // 建立字元,存在 my_string LEAQ world<>+0(SB), AX MOVQ AX, my_string+48(SP) MOVQ $11, my_string+56(SP) MOVQ $0, autotmp_0+64(SP) MOVQ $0, autotmp_0+72(SP) LEAQ type·string(SB), AX MOVQ AX, (SP) LEAQ my_string+48(SP), AX MOVQ AX, 8(SP) // 建立一個 interface CALL runtime·convT2E(SB) MOVQ 24(SP), AX MOVQ 16(SP), CX MOVQ CX, autotmp_0+64(SP) MOVQ AX, autotmp_0+72(SP) LEAQ autotmp_0+64(SP), AX MOVQ AX, (SP) MOVQ $1, 8(SP) MOVQ $1, 16(SP) // 調用 fmt.Println CALL fmt·Println(SB) MOVQ 80(SP), BP ADDQ $88, SP RET
第一行的 #include
載入一些常量,這裡我們將用到 RODATA
.
DATA
用於在記憶體中儲存字串,一次可以儲存 1,2,4或8 位元組。在符號後的<>
作用是限制資料在當前檔案使用。
GLOBL
將資料設為全域,唯讀,相對位置12.
Example: gid
gid 庫中用到的函數
#include "go_asm.h"#include "go_tls.h"#include "textflag.h"// 傳回值 8 bytes, 符號為 getgTEXT ·getg(SB), NOSPLIT, $0-8 // get_tls 的宏為: #define get_tls(r) MOVQ TLS, r // 等價於 MOVQ TLS, CX // 從 TLS(Thread Local Storage) 起始移動 8 byte 值 到 CX 寄存器 get_tls(CX) // g的宏為: g(r) 0(r)(TLS*1) // 等價於 0(CX)(TLS*1), AX // 查到意義為 indexed with offset, 這裡 offset=0, 索引是什麼意思不清楚 MOVQ g(CX), AX // 從AX起始移動 8 byte 值,到ret符號的位置 MOVQ AX, ret+0(FP) RET
Example: SwapInt32
一個原子交換 int32 的函數
package atomicimport ( "unsafe")func SwapInt32(addr *int32, new int32) (old int32)
#include "textflag.h"// 參數大小 = 8 + 4 + 4 , + 4 (預設的 ret符號?)TEXT ·SwapInt32(SB),NOSPLIT,$0-20 JMP ·SwapUint32(SB)TEXT ·SwapUint32(SB),NOSPLIT,$0-20 // 第一個參數 移動 8 byte 到 BP MOVQ addr+0(FP), BP // 第二個參數 移動 4 byte 到 AX MOVL new+8(FP), AX // 原子操作, write-after-read, 把 (AX, offset=0) 與 (BP, offset=0) 交換 4 byte 資料 XCHGL AX, 0(BP) // 移動 AX 到 old 符號 MOVL AX, old+16(FP) RET