3.4.1 基本语法
函数标识符通过TEXT汇编指令定义,表示该行开始的指令定义在TEXT内存段。TEXT语句后的指令一般对应函数的实现,但是对于TEXT指令本身来说并不关心后面是否有指令。因此TEXT和LABEL定义的符号是类似的,区别只是LABEL是用于跳转标号,但是本质上他们都是通过标识符映射一个内存地址。
函数的定义的语法如下:
TEXT symbol(SB), [flags,] $framesize[-argsize]
函数的定义部分由5个部分组成:TEXT指令、函数名、可选的flags标志、函数帧大小和可选的函数参数大小。
其中TEXT用于定义函数符号,函数名中当前包的路径可以省略。函数的名字后面是(SB)
,表示是函数名符号相对于SB伪寄存器的偏移量,二者组合在一起最终是绝对地址。作为全局的标识符的全局变量和全局函数的名字一般都是基于SB伪寄存器的相对地址。标志部分用于指示函数的一些特殊行为,标志在textlags.h
文件中定义,常见的NOSPLIT
主要用于指示叶子函数不进行栈分裂。framesize部分表示函数的局部变量需要多少栈空间,其中包含调用其它函数时准备调用参数的隐式栈空间。最后是可以省略的参数大小,之所以可以省略是因为编译器可以从Go语言的函数声明中推导出函数参数的大小。
我们首先从一个简单的Swap函数开始。Swap函数用于交互输入的两个参数的顺序,然后通过返回值返回交换了顺序的结果。如果用Go语言中声明Swap函数,大概这样的:
package main
//go:nosplit
func Swap(a, b int) (int, int)
下面是main包中Swap函数在汇编中两种定义方式:
// func Swap(a, b int) (int, int)
TEXT ·Swap(SB), NOSPLIT, $0-32
// func Swap(a, b int) (int, int)
TEXT ·Swap(SB), NOSPLIT, $0
下图是Swap函数几种不同写法的对比关系图:
第一种是最完整的写法:函数名部分包含了当前包的路径,同时指明了函数的参数大小为32个字节(对应参数和返回值的4个int类型)。第二种写法则比较简洁,省略了当前包的路径和参数的大小。如果有NOSPLIT标注,会禁止汇编器为汇编函数插入栈分裂的代码。NOSPLIT对应Go语言中的//go:nosplit
注释。
目前可能遇到的函数标志有NOSPLIT、WRAPPER和NEEDCTXT几个。其中NOSPLIT不会生成或包含栈分裂代码,这一般用于没有任何其它函数调用的叶子函数,这样可以适当提高性能。WRAPPER标志则表示这个是一个包装函数,在panic或runtime.caller等某些处理函数帧的地方不会增加函数帧计数。最后的NEEDCTXT表示需要一个上下文参数,一般用于闭包函数。
需要注意的是函数也没有类型,上面定义的Swap函数签名可以下面任意一种格式:
func Swap(a, b, c int) int
func Swap(a, b, c, d int)
func Swap() (a, b, c, d int)
func Swap() (a []int, d int)
// ...
对于汇编函数来说,只要是函数的名字和参数大小一致就可以是相同的函数了。而且在Go汇编语言中,输入参数和返回值参数是没有任何的区别的。
学员评价