新聞中心
1、 Go 匯編基礎(chǔ)知識
1.1、通用寄存器
不同體系結(jié)構(gòu)的 CPU,其內(nèi)部寄存器的數(shù)量、種類以及名稱可能大不相同,這里我們只介紹 AMD64 的寄存器。AMD64 有 20 多個可以直接在匯編代碼中使用的寄存器,其中有幾個寄存器在操作系統(tǒng)代碼中才會見到,而應(yīng)用層代碼一般只會用到如下三類寄存器。

上述這些寄存器除了段寄存器是 16 位的,其它都是 64 位的,也就是 8 個字節(jié),其中的 16 個通用寄存器還可以作為 32/16/8 位寄存器使用,只是使用時需要換一個名字,比如可以用 EAX 這個名字來表示一個 32 位的寄存器,它使用的是 RAX 寄存器的低 32 位。
AMD64 的通用通用寄存器的名字在 plan9 中的對應(yīng)關(guān)系:
|
AMD64 |
RAX |
RBX |
RCX |
RDX |
RDI |
RSI |
RBP |
RSP |
R8 |
R9 |
R10 |
R11 |
R12 |
R13 |
R14 |
RIP |
|
Plan9 |
AX |
BX |
CX |
DX |
DI |
SI |
BP |
SP |
R8 |
R9 |
R10 |
R11 |
R12 |
R13 |
R14 |
PC |
Go 語言中寄存器一般用途:
1.2、偽寄存器
偽寄存器是 plan9 偽匯編中的一個助記符, 也是 Plan9 比較有個性的語法之一。常見偽寄存器如下表所示:
SB:指向全局符號表。相對于寄存器,SB 更像是一個聲明標(biāo)識,用于標(biāo)識全局變量、函數(shù)等。通過 symbol(SB) 方式使用,symbol<>(SB)表示 symbol 只在當(dāng)前文件可見,跟 C 中的 static 效果類似。此外可以在引用上加偏移量,如 symbol+4(SB) 表示 symbol+4bytes 的地址。
PC:程序計數(shù)器(Program Counter),指向下一條要執(zhí)行的指令的地址,在 AMD64 對應(yīng) rip 寄存器。個人覺得,把他歸為偽寄存器有點令人費解,可能是因為每個平臺對應(yīng)的物理寄存器名字不一樣。
SP:SP 寄存器比較特殊,既可以當(dāng)做物理寄存器也可以當(dāng)做偽寄存器使用,不過這兩種用法的使用語法不同。其中,偽寄存器使用語法是 symbol+offset(SP),此場景下 SP 指向局部變量的起始位置(高地址處);x-8(SP) 表示函數(shù)的第一個本地變量;物理 SP(硬件SP) 的使用語法則是 +offset(SP),此場景下 SP 指向真實棧頂?shù)刂罚畹偷刂诽帲?/p>
FP:用于標(biāo)識函數(shù)參數(shù)、返回值。被調(diào)用者(callee)的 FP 實際上是調(diào)用者(caller)的棧頂,即 callee.SP(物理SP) == caller.FP;x+0(FP) 表示第一個請求參數(shù)(參數(shù)返回值從右到左入棧)。
實際上,生成真正可執(zhí)行代碼時,偽 SP、FP 會由物理 SP 寄存器加上偏移量替換。所以執(zhí)行過程中修改物理 SP,會引起偽 SP、FP 同步變化,比如執(zhí)行 SUBQ $16, SP 指令后,偽 SP 和偽 FP 都會 -16。而且,反匯編二進制而生成的匯編代碼中,只有物理 SP 寄存器。即 go tool objdump/go tool compile -S 輸出的匯編代碼中,沒有偽 SP 和 偽 FP 寄存器,只有物理 SP 寄存器。
另外還有 1 個比較特殊的偽寄存器:TLS:存儲當(dāng)前 goroutine 的 g 結(jié)構(gòu)體的指針。實際上,X86 和 AMD64 下的 TLS 是通過段寄存器 FS 或 GS 實現(xiàn)的線程本地存儲基地址,而當(dāng)前 g 的指針是線程本地存儲的第一個變量。
比如 github.com/petermattis/goid.Get 函數(shù)的匯編實現(xiàn)如下:
// func Get() int64
TEXT ·Get(SB),NOSPLIT,$0-8
MOVQ (TLS), R14
MOVQ g_goid(R14), R13
MOVQ R13, ret+0(FP)
RET
編譯成二進制之后,再通過 go tool objdump 反編譯成匯編(Go 1.18),得到如下代碼:
TEXT github.com/petermattis/goid.Get.abi0(SB) /Users/bytedance/go/pkg/mod/github.com/petermattis/[email protected]/goid_go1.5_amd64.s
goid_go1.5_amd64.s:28 0x108adc0 654c8b342530000000 MOVQ GS:0x30, R14
goid_go1.5_amd64.s:29 0x108adc9 4d8bae98000000 MOVQ 0x98(R14), R13
goid_go1.5_amd64.s:30 0x108add0 4c896c2408 MOVQ R13, 0x8(SP)
goid_go1.5_amd64.s:31 0x108add5 c3 RET
可以知道 MOVQ (TLS), R14 指令最終編譯成了 MOVQ GS:0x30, R14 ,使用了 GS 段寄存器實現(xiàn)相關(guān)功能。
操作系統(tǒng)對內(nèi)存的一般劃分如下圖所示:
高地址 +------------------+
| |
| 內(nèi)核空間 |
| |
--------------------
| |
| 棧 |
| |
--------------------
| |
| ....... |
| |
--------------------
| |
| 堆 |
| |
--------------------
| 全局?jǐn)?shù)據(jù) |
|------------------|
| |
| 靜態(tài)代碼 |
| |
|------------------|
| 系統(tǒng)保留 |
低地址 |------------------|
這里提個疑問,我們知道協(xié)程分為有棧協(xié)程和無棧協(xié)程,go 語言是有棧協(xié)程。那你知道普通 gorutine 的調(diào)用棧是在哪個內(nèi)存區(qū)嗎?
1.3、函數(shù)調(diào)用棧幀
我們先熟悉幾個名詞。
caller:函數(shù)調(diào)用者。callee:函數(shù)被調(diào)用者。比如函數(shù) main 中調(diào)用 sum 函數(shù),那么 main 就是 caller,而 sum 函數(shù)就是 callee。棧幀:stack frame,即執(zhí)行中的函數(shù)所持有的、獨立連續(xù)的棧區(qū)段。一般用來保存函數(shù)參數(shù)、返回值、局部變量、返回 PC 值等信息。golang 的 ABI 規(guī)定,由 caller 管理函數(shù)參數(shù)和返回值。
下圖是 golang 的調(diào)用棧,源于曹春暉老師的 github 文章《匯編 is so easy》 ,做了簡單修改:
caller
+------------------+
| |
+----------------------> +------------------+
| | |
| | caller parent BP |
| BP(pseudo SP) +------------------+
| | |
| | Local Var0 |
| +------------------+
| | |
| | ....... |
| +------------------+
| | |
| | Local VarN |
+------------------+
caller stack frame | |
| callee arg2 |
| +------------------+
| | |
| | callee arg1 |
| +------------------+
| | |
| | callee arg0 |
| SP(Real Register) -> +------------------+--------------------------+ FP(virtual register)
| | | |
| | return addr | parent return address |
+----------------------> +------------------+--------------------------+ <-----------------------+
| caller BP | |
| (caller frame pointer) | |
BP(pseudo SP) +--------------------------+ |
| | |
| Local Var0 | |
+--------------------------+ |
| |
| Local Var1 |
+--------------------------+ callee stack frame
| |
| ..... |
+--------------------------+ |
| | |
| Local VarN | |
High SP(Real Register) +--------------------------+ |
^ | | |
| | | |
| | | |
| | | |
| | | |
| +--------------------------+ <-----------------------+
Low
callee
需要指出的是,上圖中的 CALLER BP 是在編譯期由編譯器在符合條件時自動插入。所以手寫匯編時,計算 framesize 時不應(yīng)包括 CALLER BP 的空間。是否插入 CALLER BP 的主要判斷依據(jù)如下:
// Must agree with internal/buildcfg.FramePointerEnabled.
const framepointer_enabled = GOARCH == "amd64" || GOARCH == "arm64"
以下是 Go 語言函數(shù)棧展開邏輯的一段代碼,它側(cè)面驗證了 BP 插入的條件:
- 函數(shù)的棧幀大小大于 0;
- 常量 framepointer_enabled 值為 true。
// For architectures with frame pointers, if there's
// a frame, then there's a saved frame pointer here.
//
// NOTE: This code is not as general as it looks.
// On x86, the ABI is to save the frame pointer word at the
// top of the stack frame, so we have to back down over it.
// On arm64, the frame pointer should be at the bottom of
// the stack (with R29 (aka FP) = RSP), in which case we would
// not want to do the subtraction here. But we started out without
// any frame pointer, and when we wanted to add it, we didn't
// want to break all the assembly doing direct writes to 8(RSP)
// to set the first parameter to a called function.
// So we decided to write the FP link *below* the stack pointer
// (with R29 = RSP - 8 in Go functions).
// This is technically ABI-compatible but not standard.
// And it happens to end up mimicking the x86 layout.
// Other architectures may make different decisions.
if frame.varp > frame.sp && framepointer_enabled {
frame.varp -= goarch.PtrSize
}
// Must agree with internal/buildcfg.FramePointerEnabled.
const framepointer_enabled = GOARCH == "amd64" || GOARCH == "arm64"
1.4、golang常用匯編指令
參考文檔:
Go 支持的 X86 指令
https://github.com/golang/arch/blob/v0.2.0/x86/x86.csv
Go 支持的 ARM64 指令
https://github.com/golang/arch/blob/v0.2.0/arm64/arm64asm/inst.json
Go 支持的 ARM 指令
https://github.com/golang/arch/blob/v0.2.0/arm/arm.csv?
常用指令:
例如
MOVB $1, DI // 1 byte; 將 DI 的第一個 Byte 的值設(shè)置為 1
MOVW $0x10, BX // 2bytes
MOVD $1, DX // 4 bytes
MOVQ $-10, AX // 8 bytes
SUBQ $0x18, SP //對SP做減法,擴棧
ADDQ $0x18, SP //對SP做加法,縮棧
ADDQ AX, BX // BX += AX
SUBQ AX, BX // BX -= AX
IMULQ AX, BX // BX *= AX
JMP addr // 跳轉(zhuǎn)到地址,地址可為代碼中的地址,不過實際上手寫一般不會出現(xiàn)
JMP label // 跳轉(zhuǎn)到標(biāo)簽,可以跳轉(zhuǎn)到同一函數(shù)內(nèi)的標(biāo)簽位置
JMP 2(PC) // 向前轉(zhuǎn)2行
JMP -2(PC) // 向后跳轉(zhuǎn)2行
JNZ target // 如果zero flag被set過,則跳轉(zhuǎn)
常用標(biāo)志位:
1.5 全局變量
參考文檔:《Go語言高級編程》的章節(jié) 3.3 常量和全局變量
??https://github.com/chai2010/advanced-go-programming-book/blob/master/ch3-asm/ch3-03-const-and-var.md??
1.5.1 使用語法
使用 GLOBL 關(guān)鍵字聲明全局變量,用 DATA 定義指定內(nèi)存的值:
// DATA 匯編指令指定對應(yīng)內(nèi)存中的值; width 必須是 1、2、4、8 幾個寬度之一
DATA symbol+offset(SB)/width, value // symbol+offset 偏移量,width 寬度, value 初始值
// GLOBL 指令聲明一個變量對應(yīng)的符號,以及變量對應(yīng)的內(nèi)存大小
GLOBL symbol(SB), flag, width // 名為 symbol, 內(nèi)存寬度為 width, flag可省略
例子:
DATA age+0x00(SB)/4, $18 // age = 18
GLOBL age(SB), RODATA, $4 // 聲明全局變量 age,占用 4Byte 內(nèi)存空間
DATA pi+0(SB)/8, $3.1415926
GLOBL pi(SB), RODATA, $8
DATA bio<>+0(SB)/8, $"hello wo" // <> 表示只在當(dāng)前文件生效
DATA bio<>+8(SB)/8, $"old !!!!" // bio = "hello world !!!!"
GLOBL bio<>(SB), RODATA, $16
其中 flag 的字面量定義在 Go 標(biāo)準(zhǔn)庫下 src/runtime/textflag.h 文件中,需要在匯編文件中 #include "textflag.h",其類型有有如下幾個:
|
flag |
value |
說明 |
|
NOPROF |
1 |
(TEXT項使用) 不優(yōu)化NOPROF標(biāo)記的函數(shù)。這個標(biāo)志已廢棄。(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 |
(TEXT項使用) 不插入檢測棧分裂(擴張)的前導(dǎo)指令代碼(減少開銷,一般用于葉子節(jié)點函數(shù)(函數(shù)內(nèi)部不調(diào)用其他函數(shù)))。程序的棧幀中,如果調(diào)用其他函數(shù)會增加棧幀的大小,必須在棧頂留出可用空間。用來保護程序,例如堆棧拆分代碼本身。(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. |
|
RODATA |
8 |
(DATA和GLOBAL項使用) 將這個數(shù)據(jù)放在只讀的塊中。(For DATA and GLOBL items.) Put this data in a read-only section. |
|
NOPTR |
16 |
(用于DATA和GLOBL項目)這個數(shù)據(jù)不包含指針?biāo)跃筒恍枰占鱽頀呙琛?For DATA and GLOBL items.) This data contains no pointers and therefore does not need to be scanned by the garbage collector. |
|
WRAPPER |
32 |
(TEXT項使用)這是包裝函數(shù) (For TEXT items.) This is a wrapper function and should not count as disabling recover. |
|
NEEDCTXT |
64 |
(TEXT項使用)此函數(shù)是一個閉包,因此它將使用其傳入的上下文寄存器。(For TEXT items.) This function is a closure so it uses its incoming context register. |
|
TLSBSS |
256 |
(用于DATA和GLOBL項目)將此數(shù)據(jù)放入線程本地存儲中。Allocate a word of thread local storage and store the offset from the thread local base to the thread local storage in this variable. |
|
NOFRAME |
512 |
(TEXT項使用)不要插入指令為此函數(shù)分配棧幀。僅在聲明幀大小為0的函數(shù)上有效。(函數(shù)必須是葉子節(jié)點函數(shù),且以0標(biāo)記堆棧函數(shù),沒有保存幀指針(或link寄存器架構(gòu)上的返回地址))TODO(mwhudson):目前僅針對 ppc64x 實現(xiàn)。Do not insert instructions to allocate a stack frame for this function. Only valid on functions that declare a frame size of 0. TODO(mwhudson): only implemented for ppc64x at present. |
|
REFLECTMETHOD |
1024 |
函數(shù)可以調(diào)用 reflect.Type.Method 或 reflect.Type.MethodByName。Function can call reflect.Type.Method or reflect.Type.MethodByName. |
|
TOPFRAME |
2048 |
(TEXT項使用)函數(shù)是調(diào)用堆棧的頂部。?;厮輵?yīng)在此功能處停止。Function is the outermost frame of the call stack. Call stack unwinders should stop at this function. |
|
ABIWRAPPER |
4096 |
函數(shù)是一個 ABI 包裝器。Function is an ABI wrapper. |
其中 NOSPLIT 需要特別注意,它表示該函數(shù)運行不會導(dǎo)致棧分裂,用戶也可以使用 //go:nosplit 強制給 go 函數(shù)指定 NOSPLIT 屬性。例如:
//go:nosplit
func someFunc() {
}
匯編中直接給函數(shù)標(biāo)記 NOSPLIT 即可:
// 表示someFunc函數(shù)執(zhí)行時最多需要 24 字節(jié)本地變量和 8 字節(jié)參數(shù)空間
TEXT ·someFunc(SB), NOSPLIT, $24-8
RET
鏈接器認(rèn)為標(biāo)記為 NOSPLIT 的函數(shù),最多需要使用 StackLimit 字節(jié)空間,所以不需要插入棧分裂(溢出)檢查,函數(shù)調(diào)用損耗更小。不過,使用該標(biāo)志的時候要特別小心,萬一發(fā)生意外容易導(dǎo)致棧溢出錯誤,溢出時會在執(zhí)行期報 nosplit stack overflow 錯。Go 1.18 標(biāo)準(zhǔn)庫下 go/src/runtime/HACKING.md 中有如下說明:
nosplit functions
Most functions start with a prologue that inspects the stack pointer and the current G's stack bound and calls morestack if the stack needs to grow.
Functions can be marked //go:nosplit (or NOSPLIT in assembly) to indicate that they should not get this prologue. This has several uses:
- Functions that must run on the user stack, but must not call into stack growth, for example because this would cause a deadlock, or because they have untyped words on the stack.
- Functions that must not be preempted on entry.
- Functions that may run without a valid G. For example, functions that run in early runtime start-up, or that may be entered from C code such as cgo callbacks or the signal handler.
Splittable functions ensure there's some amount of space on the stack for nosplit functions to run in and the linker checks that any static chain of nosplit function calls cannot exceed this bound.
Any function with a //go:nosplit annotation should explain why it is nosplit in its documentation comment.
另外,當(dāng)函數(shù)處于調(diào)用鏈的葉子節(jié)點,且棧幀小于 StackSmall(128)字節(jié)時,則自動標(biāo)記為 NOSPLIT。此邏輯的代碼如下:
//const StackSmall = 128
if ctxt.Arch.Family == sys.AMD64 && autoffset < objabi.StackSmall && !p.From.Sym.NoSplit() {
leaf := true
LeafSearch:
for q := p; q != nil; q = q.Link {
switch q.As {
case obj.ACALL:
// Treat common runtime calls that take no arguments
// the same as duffcopy and duffzero.
if !isZeroArgRuntimeCall(q.To.Sym) {
leaf = false
break LeafSearch
}
fallthrough
case obj.ADUFFCOPY, obj.ADUFFZERO:
if autoffset >= objabi.StackSmall-8 {
leaf = false
break LeafSearch
}
}
}
if leaf {
p.From.Sym.Set(obj.AttrNoSplit, true)
}
}
1.5.2 Go 語言中的常用用法
在匯編代碼中使用 go 變量:
#include "textflag.h"
TEXT ·get(SB), NOSPLIT, $0-8
MOVQ ·a(SB), AX // 把 go 代碼定義的全局變量讀到 AX 中
MOVQ AX, ret+0(FP) // 把 AX 的值寫入返回值位置
RET
package main
var a = 999
func get() intfunc main() {
println(get())
}
go 代碼中使用匯編定義的變量:
// string 定義形式 1: 在 String 結(jié)構(gòu)體后多分配一個 [n]byte 數(shù)組存放靜態(tài)字符串
DATA ·Name+0(SB)/8,$·Name+16(SB) // StringHeader.Data
DATA ·Name+8(SB)/8,$6 // StringHeader.Len
DATA ·Name+16(SB)/8,$"gopher" // [6]byte{'g','o','p','h','e','r'}
GLOBL ·Name(SB),NOPTR,$24 // struct{Data uintptr, Len int, str [6]byte}
// string 定義形式 2:獨立分配一個僅當(dāng)前文件可見的 [n]byte 數(shù)組存放靜態(tài)字符串
DATA str<>+0(SB)/8,$"Hello Wo" // str[0:8]={'H','e','l','l','o',' ','W','o'}
DATA str<>+8(SB)/8,$"rld!" // str[9:12]={'r','l','d','!''}
GLOBL str<>(SB),NOPTR,$16 // 定義全局?jǐn)?shù)組 var str<> [16]byte
DATA ·Helloworld+0(SB)/8,$str<>(SB) // StringHeader.Data = &str<>
DATA ·Helloworld+8(SB)/8,$12 // StringHeader.Len = 12
GLOBL ·Helloworld(SB),NOPTR,$16 // struct{Data uintptr, Len int}
var Name,Helloworld string
func doSth() {
fmt.Printf("Name: %s\n", Name) // 讀取匯編中初始化的變量 Name
fmt.Printf("Helloworld: %s\n", Helloworld) // 讀取匯編中初始化的變量 Helloworld
}
// 輸出:
// Name: gopher
// Helloworld: Hello World!
1.6 函數(shù)調(diào)用
1.6.1 使用語法
Go 語言匯編中,函數(shù)聲明格式如下:
告訴匯編器該數(shù)據(jù)放到TEXT區(qū)
^ 靜態(tài)基地址指針(告訴匯編器這是基于靜態(tài)地址的數(shù)據(jù))
| ^
| | 標(biāo)簽 函數(shù)入?yún)?返回值占用空間大小
| | ^ ^
| | | |
TEXT pkgname·funcname(SB),TAG,$16-24
^ ^ ^ ^
| | | |
函數(shù)所屬包名 函數(shù)名 表示ABI類型 函數(shù)棧幀大小(本地變量占用空間大小)
一些說明:
- 棧幀大小包括局部變量和可能需要的額外調(diào)用函數(shù)的參數(shù)空間的總大小,但不不包含調(diào)用其他函數(shù)時的 ret address 的大小。
- 匯編文件中,函數(shù)名以 '·' 開頭或連接 pkgname 是固定格式。
- go 函數(shù)采用的是 caller-save 模式,被調(diào)用者的參數(shù)、返回值、棧位置都由調(diào)用者維護。
go 語言編譯成匯編:
go tool compile -S xxx.go
go build -gcflags -S xxx.go
從二進制反編譯為匯編:
go tool objdump -s "main.main" main.out > main.S
1.6.2 使用例子
Go 函數(shù)調(diào)用匯編函數(shù):
// add.go
package main
import "fmt"
func add(x, y int64) int64
func main() {
fmt.Println(add(2, 3))
}
// add_amd64.s
// add(x,y) -> x+y
TEXT ·add(SB),NOSPLIT,$0
MOVQ x+0(FP), BX
MOVQ y+8(FP), BP
ADDQ BP, BX
MOVQ BX, ret+16(FP)
RET
匯編調(diào)用 go 語言函數(shù):
package main
import "fmt"
func add(x, y int) int {
return x + y
}
func output(a, b int) int
func main() {
s := output(10, 13)
fmt.Println(s)
}
#include "textflag.h"
// func output(a,b int) int
TEXT ·output(SB), NOSPLIT, $24-24
MOVQ a+0(FP), DX // arg a
MOVQ DX, 0(SP) // arg x
MOVQ b+8(FP), CX // arg b
MOVQ CX, 8(SP) // arg y
CALL ·add(SB) // 在調(diào)用 add 之前,已經(jīng)把參數(shù)都通過物理寄存器 SP 搬到了函數(shù)的棧頂
MOVQ 16(SP), AX // add 函數(shù)會把返回值放在這個位置
MOVQ AX, ret+16(FP) // return result
RET
1.6.1 匯編函數(shù)中用到的一些特殊命令(偽指令)
GO_RESULTS_INITIALIZED:如果 Go 匯編函數(shù)返回值含指針,則該指針信息必須由 Go 源文件中的函數(shù)的 Go 原型提供,即使對于未直接從 Go 調(diào)用的匯編函數(shù)也是如此。如果返回值將在調(diào)用指令期間保存實時指針,則該函數(shù)中應(yīng)首先將結(jié)果歸零, 然后執(zhí)行偽指令 GO_RESULTS_INITIALIZED。表明該堆棧位置應(yīng)該執(zhí)行進行 GC 掃描,避免其指向的內(nèi)存地址唄 GC 意外回收。
NO_LOCAL_POINTERS: 就是字面意思,表示函數(shù)沒有指針類型的局部變量。
PCDATA: Go 語言生成的匯編,利用此偽指令表明匯編所在的原始 Go 源碼的位置(file&line&func),用于生成 PC 表格。runtime.FuncForPC 函數(shù)就是通過 PC 表格得到結(jié)果的。一般由編譯器自動插入,手動維護并不現(xiàn)實。
FUNCDATA: 和 PCDATA 的格式類似,用于生成 FUNC 表格。FUNC 表格用于記錄函數(shù)的參數(shù)、局部變量的指針信息,GC 依據(jù)它來跟蹤棧中指針指向內(nèi)存的生命周期,同時棧擴縮容的時候也是依據(jù)它來確認(rèn)是否需要調(diào)整棧指針的值(如果指向的地址在需要擴縮容的棧中,則需要同步修改)。
1.7 條件編譯
Go 語言僅支持有限的條件編譯規(guī)則:
- 根據(jù)文件名編譯。
- 根據(jù) build 注釋編譯。
根據(jù)文件名編譯類似 *_test.go,通過添加平臺后綴區(qū)分,比如: asm_386.s、asm_amd64.s、asm_arm.s、asm_arm64.s、asm_mips64x.s、asm_linux_amd64.s、asm_bsd_arm.s 等.
根據(jù) build 注釋編譯,就是在源碼中加入?yún)^(qū)分平臺和編譯器版本的注釋。比如:
//go:build (darwin || freebsd || netbsd || openbsd) && gc
// +build darwin freebsd netbsd openbsd
// +build gc
Go 1.17 之前,我們可以通過在源碼文件頭部放置 +build 構(gòu)建約束指示符來實現(xiàn)構(gòu)建約束,但這種形式十分易錯,并且它并不支持&&和||這樣的直觀的邏輯操作符,而是用逗號、空格替代,下面是原 +build 形式構(gòu)建約束指示符的用法及含義:
Go 1.17 引入了 //go:build 形式的構(gòu)建約束指示符,支持&&和||邏輯操作符,如下代碼所示:
//go:build linux && (386 || amd64 || arm || arm64 || mips64 || mips64le || ppc64 || ppc64le)
//go:build linux && (mips64 || mips64le)
//go:build linux && (ppc64 || ppc64le)
//go:build linux && !386 && !arm
考慮到兼容性,Go 命令可以識別這兩種形式的構(gòu)建約束指示符,但推薦 Go 1.17 之后都用新引入的這種形式。
gofmt 可以兼容處理兩種形式,處理原則是:如果一個源碼文件只有 // +build 形式的指示符,gofmt 會將與其等價的 //go:build 行加入。否則,如果一個源文件中同時存在這兩種形式的指示符行,那么 //+build 行的信息將被 //go:build 行的信息所覆蓋。
2、 go 語言 ABI
參考文檔:
Go internal ABI specification
https://go.googlesource.com/go/+/refs/heads/dev.regabi/src/cmd/compile/internal-abi.md
Proposal: Create an undefined internal calling convention
https://go.googlesource.com/proposal/+/master/design/27539-internal-abi.md
名詞解釋:ABI: application binary interface, 應(yīng)用程序二進制接口,規(guī)定了程序在機器層面的操作規(guī)范和調(diào)用規(guī)約。調(diào)用規(guī)約: calling convention, 所謂“調(diào)用規(guī)約”是調(diào)用方和被調(diào)用方對于函數(shù)調(diào)用的一個明確的約定,包括:函數(shù)參數(shù)與返回值的傳遞方式、傳遞順序。只有雙方都遵守同樣的約定,函數(shù)才能被正確地調(diào)用和執(zhí)行。如果不遵守這個約定,函數(shù)將無法正確執(zhí)行。
Go 從1.17.1版本開始支持多 ABI:1. 為了兼容性各平臺保持通用性,保留歷史版本 ABI,并更名為 ABI0。2. 為了更好的性能,增加新版本 ABI 取名 ABIInternal。ABI0 遵循平臺通用的函數(shù)調(diào)用約定,實現(xiàn)簡單,不用擔(dān)心底層cpu架構(gòu)寄存器的差異;ABIInternal 可以指定特定的函數(shù)調(diào)用規(guī)范,可以針對特定性能瓶頸進行優(yōu)化,在多個 Go 版本之間可以迭代,靈活性強,支持寄存器傳參提升性能。Go 匯編為了兼容已存在的匯編代碼,保持使用舊的 ABI0。
Go 為什么在有了 ABI0 之后,還要引入 ABIInternal?當(dāng)然是為了性能!據(jù)官方測試,寄存器傳參可以帶來 5% 的性能提升。
我們看一個例子:
package main
import _ "fmt"
func Print(delta string)
func main() {
Print("hello")
}
#include "textflag.h"
TEXT ·Print(SB), NOSPLIT, $8
CALL fmt·Println(SB)
RET
運行上面代碼會報錯:main.Print: relocation target fmt.Println not def
當(dāng)前名稱:Go匯編詳解
文章路徑:http://m.fisionsoft.com.cn/article/ccoceji.html


咨詢
建站咨詢
