Go汇编语言

Go的汇编器继承自Plan9的汇编器,但与Plan9汇编器仍有很多不同之处。

Plan9并不是Go语言中特有的东西,而是指贝尔实验室中开发的一个操作系统。

贝尔实验室九号项目(英语:Plan 9 from Bell Labs,常简称为Plan 9)是一个分布式操作系统,由贝尔实验室的计算科学研究中心在1980年代中期至2002年开发,以作为UNIX的后继者。它现在仍然被操作系统的研究者和爱好者开发使用。

Go汇编并不是对底层机器的直接表示,相反,它是一种半抽象的汇编语言,是Go编译器的输出,并作为Go汇编器的输入,Go汇编器会将Go汇编表示的指令翻译成具体平台的机器码。所以使用Go汇编编写的程序,在编译完成后可能和原来并不相同。例如Go汇编的MOV指令,在编译完成后可能会变成清除指令或加载指令,再例如,在arm中,为了方便编写汇编,可能使用一条DIVMOD指令来做算数操作,但实际上其对应的arm指令可能不止一条。 另外,Go汇编也不是个独立的语言,无法独立使用,必须结合Go代码。Go汇编必须以Go包的方式组织,同时包中至少要有一个Go源文件用于指明当前包名等基本包信息。

Go使用Plan9汇编意在提供更好的跨平台特性,让汇编与机器无关,但是并没能实现。对于一些通用的,例如内存操作指令和过程调用指令,由于这些指令在许多平台上基本相同,所以可以进行抽象,但是对一些平台独有的指令,Go汇编却并不能进行抽象。

在命名上,Go汇编必须以CPU的体系结构作为文件名的后缀,如_amd64arm等。(?)

在Go源码中,src/cmd目录下是一些与Go相关的命令实现,如go fmtgo 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中它是eiprip寄存器。

SB寄存器的作用是声明某个符号是内存中的一个地址,例如foo(SB)表示符号foo指代内存中的一个地址,类似x86汇编中的label。SB寄存器通常在全局函数或全局变量的命名中使用。如果在符号后边加一个<>,即foo<>(SB),表示foo仅在当前文件中可见。SB还可以加上一个偏移使用,如foo+4(SB)表示从foo开始的第4个字节处。注意该处的+与FP的不同,该处确实有“加“的含义。

SP是栈指针,指向当前函数的栈帧,用来访问函数的局部变量或参数。在x86中它是esprsp

所有用户定义的符号都会被写成SB和FP加偏移的形式。SP寄存器包含了FP的功能。 其实在我看到的x86下的Go汇编中,编译器一般不使用FP寄存器, 而是使用SP来访问局部变量和函数参数。

函数与数据定义

Go汇编中,函数的定义和调用必须要包含包名,即使用fmt.Printfmath/rand.Int这种形式,但是在Go汇编中,一些符号有特殊的含义,例如.,所以不能使用这些符号。但是又中点·和除号/,所以可以写成 fmt·Printfmath∕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_bufreader_r访问该结构体的字段。

如果Go编译器在生成go_asm.h文件时出现命名冲突的话,将会触发”宏重定义“错误。


关于更多Go汇编的介绍可以查看:

https://golang.org/doc/asm

https://chai2010.cn/advanced-go-programming-book/ch3-asm/readme.html

关于Plan9汇编器的详细语法可以查看:

http://doc.cat-v.org/plan_9/4th_edition/papers/asm

posted @ 2021-07-03 01:05  一川official  阅读(363)  评论(1编辑  收藏  举报