蛮荆

Go 汇编

2023-04-17

为什么要学习汇编语言

  • 更加接近硬件底层,性能极致优化
  • 降维打击所有 “高级编程语言”

如果读者对汇编语言零基础,建议直接跳转到文末 Reference 列表,最后两个链接可以作为入门读物。

概述

Go 汇编语言 并不是一个独立的语言,因为其无法独立编译和使用。Go 汇编代码必须以 Go 包的方式组织,同时包中至少要有一个 Go 语言文件用于指明当前包名等基本包信息。 如果 Go 汇编代码中定义的变量和函数要被其它 Go 语言代码引用,还需要通过 Go 语言代码将汇编中定义的符号声明出来,用于变量的定义和函数的定义。 Go 汇编文件类似于 C 语言中的 .c 文件,而用于导出汇编中定义符号的 Go 源文件类似于 C 语言的.h 文件。

查看 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 $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

不支持的指令

如果要使用硬件体系结构缺失的汇编指令,有两种方法:

  • 更新汇编程序以支持该指令
  • 使用 BYTEWORD 指令将数据显式放到 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

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。