Go的汇编器继承自Plan9的汇编器,但与Plan9汇编器仍有很多不同之处。
Plan9并不是Go语言中特有的东西,而是指贝尔实验室中开发的一个操作系统。
贝尔实验室九号项目(英语:Plan 9 from Bell Labs,常简称为Plan 9)是一个分布式操作系统,由贝尔实验室的计算科学研究中心在1980年代中期至2002年开发,以作为UNIX的后继者。它现在仍然被操作系统的研究者和爱好者开发使用。
Go汇编并不是对底层机器的直接表示,相反,它是一种半抽象的汇编语言,是Go编译器的输出,并作为Go汇编器的输入,Go汇编器会将Go汇编表示的指令翻译成具体平台的机器码。所以使用Go汇编编写的程序,在编译完成后可能和原来并不相同。例如Go汇编的MOV
指令,在编译完成后可能会变成清除指令或加载指令,再例如,在arm中,为了方便编写汇编,可能使用一条DIV
或MOD
指令来做算数操作,但实际上其对应的arm指令可能不止一条。 另外,Go汇编也不是个独立的语言,无法独立使用,必须结合Go代码。Go汇编必须以Go包的方式组织,同时包中至少要有一个Go源文件用于指明当前包名等基本包信息。
Go使用Plan9汇编意在提供更好的跨平台特性,让汇编与机器无关,但是并没能实现。对于一些通用的,例如内存操作指令和过程调用指令,由于这些指令在许多平台上基本相同,所以可以进行抽象,但是对一些平*有的指令,Go汇编却并不能进行抽象。
在命名上,Go汇编必须以CPU的体系结构作为文件名的后缀,如_amd64
、arm
等。(?)
在Go源码中,src/cmd
目录下是一些与Go相关的命令实现,如go fmt
、go doc
等,而在src/cmd/compile
目录下,便是Go编译器的实现。 对于一个Go语言文件,可以使用该目录下的一些工具来查看生成的汇编。如可以使用下面的命令查看一个Go源文件生成的Go汇编。
$ GOOS=linux GOARCH=amd64 go tool compile -S main.go
或
$ go build -gcflags -S main.go
要看最终生成的机器相关的汇编,可以使用go tool objdump
:
$ go build -o a.out main.go
$ go tool objdump -s main.main a.out
伪寄存器
在Go汇编中,预定义了4个伪寄存器。它们并不是真正的寄存器,只是为了适配多平台而提供的一种抽象,会在生成机器码的时候映射到具体平台的真正的寄存器上:
-
FP
: Frame pointer -
PC
: Program counter -
SB
: Static base pointer -
SP
: Stack pointer
FP寄存器是一个指向函数参数的栈帧指针,通常用FP加上一个偏移来访问函数的参数和返回值。例如用0(FP)
访问第一个参数,8(FP)
访问第二个参数(在64位机器上)。但是实际上以这种方式访问函数参数时,还需要再前面加上参数的名称:first_arg+0(FP)
、second_arg+8(FP)
,其中+
并没有任何意义,只用作分隔符。
PC为程序计数器,指向当前程序的执行位置,在x86中它是eip
或rip
寄存器。
SB寄存器的作用是声明某个符号是内存中的一个地址,例如foo(SB)
表示符号foo
指代内存中的一个地址,类似x86汇编中的label。SB寄存器通常在全局函数或全局变量的命名中使用。如果在符号后边加一个<>
,即foo<>(SB)
,表示foo
仅在当前文件中可见。SB还可以加上一个偏移使用,如foo+4(SB)
表示从foo
开始的第4个字节处。注意该处的+
与FP的不同,该处确实有“加“的含义。
SP是栈指针,指向当前函数的栈帧,用来访问函数的局部变量或参数。在x86中它是esp
或rsp
所有用户定义的符号都会被写成SB和FP加偏移的形式。SP寄存器包含了FP的功能。 其实在我看到的x86下的Go汇编中,编译器一般不使用FP寄存器, 而是使用SP来访问局部变量和函数参数。
函数与数据定义
Go汇编中,函数的定义和调用必须要包含包名,即使用fmt.Printf
或math/rand.Int
这种形式,但是在Go汇编中,一些符号有特殊的含义,例如.
,所以不能使用这些符号。但是又中点·
和除号/
,所以可以写成 fmt·Printf
和 math∕rand·Int
这种形式。在当前包中,为了简便,可以只写一个中点,而不必写完整的包名,如·Int
,但是在某些复杂的情况下,必须要写成这种简便的形式。
与其他风格的汇编类似,Go汇编也需要为函数或数据指明它们所在的节,如TEXT节或DATA节。但是Go汇编在定义每个函数或变量时都要明确指定:
TEXT runtime·profileloop(SB),NOSPLIT,$8
MOVQ $runtime·profileloop1(SB), CX
MOVQ CX, 0(SP)
CALL runtime·externalthreadhandler(SB)
RET
TEXT指令后面分别是函数名、函数标记(flags)和帧大小。通常情况下,帧大小后面跟着一个参数大小,并用负号分隔,如$24-8
,其中帧大小为24,参数大小为8。由于这里使用了NOSPLIT
标示,可以不提供参数大小。有时候函数没有栈帧,可以将帧大小设置为0。同样的,有些函数也没有参数和返回值,可以将参数大小设置为0。在函数的最后,必须是短跳转指令或RET
指令,如果不是,Go链接器会在函数后面添加一个跳转到该函数自身的指令。
数据的定义可以使用DATA指令:
DATA symbol+offset(SB)/width, value
其中offset
为相对于symbol
的偏移,width
为数据宽度,value
为数据的值。
go_asm.h
在调用go build
时,如果当前包下有.s
文件,那么Go编译器会生成一个特殊的go_asm.h
文件。该文件中使用#define
定义了一些常量,其中包含了当前包中const
的大部分定义、Go结构体的大小和字段偏移。
常量的形式为const_name
,例如对于Go定义const bufSize = 1024
,在Go汇编中可以使用const_bufSize
来访问。
结构体大小的形式为type_size
,结构体字段的偏移的形式为type_field
。例如对于下面的结构体:
type reader struct {
buf [bufSize]byte
r int
}
可以在Go汇编中使用reader_size
获取该结构体的大小,使用reader_buf
和reader_r
访问该结构体的字段。
如果Go编译器在生成go_asm.h
文件时出现命名冲突的话,将会触发”宏重定义“错误。
关于更多Go汇编的介绍可以查看:
https://chai2010.cn/advanced-go-programming-book/ch3-asm/readme.html
关于Plan9汇编器的详细语法可以查看:
http://doc.cat-v.org/plan_9/4th_edition/papers/asm