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
...

輸出結果中的 FUNCDATAPCDATA 指令由編譯器引入,包含 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 $101AX = mov rax, 101

僞寄存器

Go 彙編語言 有 4 個預聲明的符號引用 僞寄存器,是由工具鏈維護的 虛擬寄存器 (不是真正的寄存器),例如幀指針,僞寄存器在所有硬件體系結構下的語義和作用都是相同的。

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

不支持的指令

如果要使用硬件體系結構缺失的彙編指令,有兩種方法:

// 示例: 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

參考資料

[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