Go 彙編語言快速入門
爲什麼要學習彙編語言
-
更加接近硬件底層,性能極致優化
-
降維打擊所有 "高級編程語言"
如果讀者對彙編語言零基礎,建議直接跳轉到文末 Reference 列表,最後兩個鏈接可以作爲入門讀物。
概述
Go 彙編語言
並不是一個獨立的語言,因爲其無法獨立編譯和使用。Go 彙編代碼必須以 Go 包的方式組織,同時包中至少要有一個 Go 語言文件用於指明當前包名等基本包信息。
如果 Go 彙編代碼中定義的變量和函數要被其它 Go 語言代碼引用,還需要通過 Go 語言代碼將彙編中定義的符號聲明出來,用於變量的定義和函數的定義。
查看 Go 程序彙編代碼
// main.go
package main
func main() {
println(3)
}
例如,我們想查看 Linux/amd64
架構體系下的彙編代碼,可以使用下面的指令:
$ GOOS=linux GOARCH=amd64 go tool compile -S main.go
# 或者
$ GOOS=linux GOARCH=amd64 go build -gcflags -S main.go
# 輸出結果如下
main.main STEXT size=66 args=0x0 locals=0x10 funcid=0x0 align=0x0
0x0000 00000 (main.go:50) TEXT main.main(SB), ABIInternal, $16-0
0x0000 00000 (main.go:50) CMPQ SP, 16(R14)
0x0004 00004 (main.go:50) PCDATA $0, $-2
0x0004 00004 (main.go:50) JLS 57
0x0006 00006 (main.go:50) PCDATA $0, $-1
0x0006 00006 (main.go:50) SUBQ $16, SP
0x000a 00010 (main.go:50) MOVQ BP, 8(SP)
0x000f 00015 (main.go:50) LEAQ 8(SP), BP
0x0014 00020 (main.go:50) FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x0014 00020 (main.go:50) FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x0014 00020 (main.go:51) PCDATA $1, $0
0x0014 00020 (main.go:51) CALL runtime.printlock(SB)
0x0019 00025 (main.go:51) MOVL $3, AX
0x001e 00030 (main.go:51) NOP
0x0020 00032 (main.go:51) CALL runtime.printint(SB)
0x0025 00037 (main.go:51) CALL runtime.printnl(SB)
0x002a 00042 (main.go:51) CALL runtime.printunlock(SB)
0x002f 00047 (main.go:52) MOVQ 8(SP), BP
0x0034 00052 (main.go:52) ADDQ $16, SP
0x0038 00056 (main.go:52) RET
0x0039 00057 (main.go:52) NOP
0x0039 00057 (main.go:50) PCDATA $1, $-1
0x0039 00057 (main.go:50) PCDATA $0, $-2
0x0039 00057 (main.go:50) CALL runtime.morestack_noctxt(SB)
0x003e 00062 (main.go:50) PCDATA $0, $-1
0x003e 00062 (main.go:50) NOP
0x0040 00064 (main.go:50) JMP 0
...
輸出結果中的 FUNCDATA
和 PCDATA
指令由編譯器引入,包含 GC 用到的信息。
查看鏈接後放入二進制文件的內容,使用 go tool objdump
# 編譯 main.go 文件
$ go build -o main main.go
# 匹配編譯後文件中的 "main.main" 符號
$ go tool objdump -s main.main main
# 輸出如下
main.go:50 0x457c00 493b6610 CMPQ 0x10(R14), SP
main.go:50 0x457c04 7633 JBE 0x457c39
main.go:50 0x457c06 4883ec10 SUBQ $0x10, SP
main.go:50 0x457c0a 48896c2408 MOVQ BP, 0x8(SP)
main.go:50 0x457c0f 488d6c2408 LEAQ 0x8(SP), BP
main.go:51 0x457c14 e84776fdff CALL runtime.printlock(SB)
main.go:51 0x457c19 b803000000 MOVL $0x3, AX
main.go:51 0x457c1e 6690 NOPW
main.go:51 0x457c20 e83b7dfdff CALL runtime.printint(SB)
main.go:51 0x457c25 e89678fdff CALL runtime.printnl(SB)
main.go:51 0x457c2a e8b176fdff CALL runtime.printunlock(SB)
main.go:52 0x457c2f 488b6c2408 MOVQ 0x8(SP), BP
main.go:52 0x457c34 4883c410 ADDQ $0x10, SP
main.go:52 0x457c38 c3 RET
main.go:50 0x457c39 e802cdffff CALL runtime.morestack_noctxt.abi0(SB)
main.go:50 0x457c3e 6690 NOPW
main.go:50 0x457c40 ebbe JMP main.main(SB)
寄存器
通用寄存器
Go 彙編語言
中使用寄存器不需要帶通用寄存器的前綴,例如 rax,只要寫 AX 即可。
MOVQ $101, AX = mov rax, 101
僞寄存器
Go 彙編語言
有 4 個預聲明的符號引用 僞寄存器
,是由工具鏈維護的 虛擬寄存器
(不是真正的寄存器),例如幀指針,僞寄存器在所有硬件體系結構下的語義和作用都是相同的。
-
FP: 棧基,棧幀(函數的棧叫棧幀)的開始位置 (一般用來訪問函數的參數和返回值)
-
PC: 存放 CPU 下一條執行指令的位置地址 (amd64 環境中就是 IP 指令計數寄存器的別名)
-
SB: 靜態內存的開始地址 (內存是通過 SB 僞寄存器定位,所有的靜態全局符號通常可以通過 SB 加一個偏移量獲得)
-
SP: 棧底,棧幀的結束位置 (不包括參數和返回值,一般用於定位局部變量)
SP
寄存器比較特殊,因爲存在一個 真 SP
寄存器。真 SP
寄存器對應的是棧頂,一般用於定位調用其它函數的參數和返回值。
內存區域模型
如何區分真 · 僞寄存器?
僞寄存器
需要一個標識符和偏移量爲前綴,真寄存器
則不需要。比如 (SP)、+8(SP) 沒有標識符前綴表示 真 SP
,而 a(SP)、b+8(SP) 有標識符前綴表示 僞 SP
。
基礎指令
變量聲明
使用 DATA + GLOBL
定義一個變量,GLOBL 必須在 DATA 指令之後,語法如下。
DATA symbol+offset(SB)/width, value
# GLOBL 指令將變量聲明爲 global
# RODATA 表示變量位於只讀區
# $64 表示數據大小爲 64 字節
GLOBL divtab(SB), RODATA, $64
# ·count 以 · 開頭表示變量屬於當前包
GLOBL ·count(SB),$4
示例
# 逐個字節初始化變量
DATA ·count+0(SB)/1,$1
DATA ·count+1(SB)/1,$2
DATA ·count+2(SB)/1,$3
DATA ·count+3(SB)/1,$4
# 一次性初始化變量
DATA ·count+0(SB)/4,$0x04030201
# 定義一個字符串
GLOBL ·helloworld(SB),$16
# 字符串內容爲 Hello World
GLOBL text<>(SB),NOPTR,$16
DATA text<>+0(SB)/8,$"Hello Wo"
DATA text<>+8(SB)/8,$"rld!"
# 初始化 Go 中的字符串對應的 StringHeader 結構體
DATA ·helloworld+0(SB)/8,$text<>(SB) # StringHeader.Data
DATA ·helloworld+8(SB)/8,$12 # StringHeader.Len
# 聲明數據不含指針
GLOBL ·NameData(SB),NOPTR,$8
DATA ·NameData+0(SB)/8,$"gopher"
賦值操作
字節數由 MOV
指令的後綴決定。
MOVB $1, DI # 1 byte B => Byte
MOVW $0x10, BX # 2 bytes W => Word
MOVD $1, DX # 4 bytes L => Long
MOVQ $-10, AX # 8 bytes Q => Quadword
數值計算
類似賦值操作指令,可以通過修改指令後綴來對應不同長度的操作數。例如 ADDQ/ADDW/ADDL/ADDB。
ADDQ AX, BX # BX += AX
SUBQ AX, BX # BX -= AX
IMULQ AX, BX # BX *= AX
MOVL g(CX), AX # 將 g 賦值到寄存器 AX
MOVL g_m(AX), BX # 將 g.m 賦值到寄存器 BX
條件跳轉 / 無條件跳轉
# 無條件跳轉
JMP addr # 跳轉到指定地址
JMP label # 跳轉到指定標籤
JMP 2(PC) # 以當前指令爲基礎,向前/後跳轉指定行數
JMP -2(PC) # 同上
# 有條件跳轉
CMPQ CX, $0 # 如果寄存器 CX 的值等於 0,跳轉到 L 標籤
JZ L
棧調整
SUBQ $0x18, SP # 對 SP 做減法,爲函數分配函數棧幀
SUBQ $40, SP # 分配 40 字節棧空間
ADDQ $0x18, SP # 對 SP 做加法,清除函數棧幀
函數聲明
彙編函數語法
示例
add 函數
func add(a, b int) int
對應的彙編代碼如下:
# pkgname 名稱直接省略即可
TEXT pkgname·add(SB), NOSPLIT, $0-8
MOVQ a+0(FP), AX
MOVQ a+8(FP), BX
ADDQ AX, BX
MOVQ BX, ret+16(FP)
RET
Say 函數
func Say()
對應的彙編代碼如下:
TEXT ·Say(SB), NOSPLIT, $16-0
MOVQ ·helloWorld+0(SB), AX
MOVQ AX, 0(SP)
MOVQ ·helloWorld+8(SB), BX
MOVQ BX, 8(SP)
CALL ·output(SB) # 在調用 output 之前,已經把參數通過真寄存器 SP 傳遞到了函數棧頂
RET
函數調用
調用函數時,被調用函數的參數和返回值內存空間都必須由調用者提供。
.s 彙編文件和 .go 文件重定向
// Go 文件變量
var version float32 = 1.0
// Go 文件函數
func getVersion() float32
# 彙編文件函數
# ·version(SB),表示該符號需要鏈接器來幫我們進行重定向(relocation)
# 這裏表示使用 Go 程序中的 version 變量
TEXT ·getVersion(SB),NOSPLIT,$0-4
MOVQ ·version(SB), AX
MOVQ AX, ret+0(FP)
RET
不支持的指令
如果要使用硬件體系結構缺失的彙編指令,有兩種方法:
-
更新彙編程序以支持該指令
-
使用
BYTE
和WORD
指令將數據顯式放到TEXT
中的指令流中
// 示例: 386 如何定義 atomic.LoadUint64 函數
TEXT runtime·atomicload64(SB), NOSPLIT, $0-12
MOVL ptr+0(FP), AX
TESTL $7, AX
JZ 2(PC)
MOVL 0, AX // crash with nil ptr deref
LEAL ret_lo+4(FP), BX
// MOVQ (%EAX), %MM0
BYTE $0x0f; BYTE $0x6f; BYTE $0x00
// MOVQ %MM0, 0(%EBX)
BYTE $0x0f; BYTE $0x7f; BYTE $0x03
// EMMS
BYTE $0x0F; BYTE $0x77
RET
綜合示例
main.go
文件中的函數都是聲明未實現,全部使用匯編語言實現。
package main
import (
"fmt"
"unsafe"
)
type Text struct {
Language string
_ uint8
Length int
}
var version float32 = 1.0
func add(a, b int) int
func addX(a, b int) int
func sub(a, b int) int
func mul(a, b int) int
// 獲取 Text 的 Length 字段的值
func length(text *Text) int
// 獲取 Text 結構體的大小
func sizeOfTextStruct() int
func getAge() int32
func getPI() float64
func getBirthYear() int32
func getVersion() float32
func main() {
println(add(1, 2))
println(addX(1, 2))
println(sub(10, 5))
println(mul(10, 5))
text := &Text{
Language: "Go",
Length: 1024,
}
fmt.Println(text)
println(length(text))
println(sizeOfTextStruct())
println(unsafe.Sizeof(*text))
println(getAge())
println(getPI())
println(getBirthYear())
println(getVersion())
}
main.s
彙編程序文件中使用了 Go 文件中聲明的部分變量,並且實現了 Go 文件中聲明的所有函數。
#include "textflag.h"
#include "go_asm.h" // 該文件編譯時自動生成
# 實現了 add 函數
TEXT ·add(SB),NOSPLIT,$0-24
MOVQ a+0(FP), AX # 讀取第一個參數
MOVQ b+8(FP), BX # 讀取第二個參數
ADDQ BX, AX
MOVQ AX, ret+16(FP) # 保存結果
RET
# 實現了 addX 函數
TEXT ·addX(SB),NOSPLIT,$0-24
MOVQ a+0(FP), AX
MOVQ b+8(FP), BX
ADDQ BX, AX
MOVQ $x(SB), BX # 讀取全局變量 x 的地址
MOVQ 0(BX), BX # 讀取全局變量 x 的值
ADDQ BX, AX
MOVQ AX, ret+16(FP)
RET
# 實現了 sub 函數
TEXT ·sub(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX
MOVQ b+8(FP), BX
SUBQ BX, AX // AX -= BX
MOVQ AX, ret+16(FP)
RET
# 實現了 mul 函數
TEXT ·mul(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX
MOVQ b+8(FP), BX
IMULQ BX, AX # AX *= BX
MOVQ AX, ret+16(FP)
RET
# 實現了 length 函數
TEXT ·length(SB),NOSPLIT,$0-16
MOVQ text+0(FP), AX
MOVQ Text_Length(AX), AX # 通過字段在結構體中的偏移量讀取字段值
MOVQ AX, ret+8(FP)
RET
# 實現了 sizeOfTextStruct 函數
TEXT ·sizeOfTextStruct(SB),NOSPLIT,$0-8
MOVQ $Text__size, AX // 保存結構體的大小到 AX 寄存器
MOVQ AX, ret+0(FP)
RET
# 實現了 getAge 函數
TEXT ·getAge(SB),NOSPLIT,$0-4
MOVQ age(SB), AX
MOVQ AX, ret+0(FP)
RET
# 實現了 getPI 函數
TEXT ·getPI(SB),NOSPLIT,$0-8
MOVQ pi(SB), AX
MOVQ AX, ret+0(FP)
RET
# 實現了 getBirthYear 函數
TEXT ·getBirthYear(SB),NOSPLIT,$0-4
MOVQ birthYear(SB), AX
MOVQ AX, ret+0(FP)
RET
# 實現了 getVersion 函數
TEXT ·getVersion(SB),NOSPLIT,$0-4
MOVQ ·version(SB), AX
MOVQ AX, ret+0(FP)
RET
DATA x+0(SB)/8, $10 # 初始化全局變量 x, 賦值爲 10
GLOBL x(SB), RODATA, $8 # 聲明全局變量 x, GLOBL 必須跟在 DATA 指令之後
DATA age+0x00(SB)/4, $18
GLOBL age(SB), RODATA, $4
DATA pi+0(SB)/8, $3.1415926
GLOBL pi(SB), RODATA, $8
DATA birthYear+0(SB)/4, $1992
GLOBL birthYear(SB), RODATA, $4
# 最後一行的空行是必須的,否則報錯 unexpected EOF; 或者在最後一行代碼末尾加分號 ;
運行輸出
# 注意這裏要以當前包爲運行路徑,因爲要包含 main.s 彙編文件
$ go run ./
# 輸出結果如下
3
13
5
50
&{Go 0 1024}
1024
32
32
18
+3.141593e+000
1992
+1.000000e+000
小結
本文介紹了 Go 彙編語言
的基礎,包括變量 / 常量、分支 / 循環、函數,讀者可以根據 綜合示例 的代碼來編寫一些簡單的小程序。 如果讀者希望深入學習或者通過彙編來優化已有的 Go
語言程序,可以參考下面的鏈接列表。
Reference
-
A Quick Guide to Go's Assembler[1]
-
A Manual for the Plan 9 assembler[2]
-
Optimizing GoLang for High Performance with ARM64 Assembly[3]
-
Go Assembly by Example[4]
-
Chapter I: A Primer on Go Assembly[5]
-
Go 彙編 [6]
-
Go 語言高級編程 [7]
-
Go 彙編優化入門. pdf[8]
-
函數調用 [9]
-
Go Assembly 示例 [10]
-
Linux 彙編語言開發指南 [11]
-
Some Assembly Required[12]
-
深入理解程序設計 - Linux 彙編語言 [13]
-
NASM 程序設計 [14]
參考資料
[1]
A Quick Guide to Go's Assembler: https://go.dev/doc/asm
[2]
A Manual for the Plan 9 assembler: https://9p.io/sys/doc/asm.html
[3]
Optimizing GoLang for High Performance with ARM64 Assembly: https://www.slideshare.net/linaroorg/optimizing-golang-for-high-performance-with-arm64-assembly-sfo17314
[4]
Go Assembly by Example: https://davidwong.fr/goasm/
[5]
Chapter I: A Primer on Go Assembly: https://github.com/teh-cmc/go-internals/blob/master/chapter1_assembly_primer/README.md
[6]
Go 彙編: https://go.xargin.com/docs/assembly/assembly
[7]
Go 語言高級編程: https://chai2010.cn/advanced-go-programming-book
[8]
Go 彙編優化入門. pdf: https://mzh.io/2018/05/Go%E6%B1%87%E7%BC%96%E4%BC%98%E5%8C%96%E5%85%A5%E9%97%A8.pdf
[9]
函數調用: https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-function-call/
[10]
Go Assembly 示例: https://colobu.com/goasm/
[11]
Linux 彙編語言開發指南: https://www.cnblogs.com/xmphoenix/p/3702503.html
[12]
Some Assembly Required: https://github.com/hackclub/some-assembly-required/tree/main
[13]
深入理解程序設計 - Linux 彙編語言: https://book.douban.com/subject/25789594/
[14]
NASM 程序設計: https://www.yuque.com/qyuhen/asm
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/orGo3KW0Y1784dTX-hyO4A