本文档简要介绍了gc
Go编译器使用的非常规形式的汇编语言。该文件不全面。
汇编程序基于Plan 9汇编程序的输入样式,在其他地方详细介绍了该样式 。如果您打算编写汇编语言,则尽管其中大部分是特定于Plan 9的,但您仍应阅读该文档。当前文档提供了语法摘要以及与该文档中所解释内容的区别,并描述了编写汇编代码以与Go交互时适用的特性。
关于Go的汇编器,最重要的事情是它不是底层机器的直接表示。一些细节正好映射到机器,但有些则不然。这是因为编译器套件(请参见此 描述)不需要在常规管道中传递任何汇编程序。取而代之的是,编译器对一种半抽象的指令集进行操作,并且指令选择部分发生在代码生成之后。汇编程序以半抽象形式工作,因此当您看到类似MOV
工具链实际上为该操作生成的内容可能根本不是移动指令,可能是清除指令或加载指令。或者它可能与该名称的机器指令完全对应。通常,特定于机器的操作倾向于自己出现,而更通用的概念(如内存移动和子例程调用与返回)则更为抽象。具体细节因架构而异,我们对此不严谨深表歉意。情况尚不明确。
汇编程序是解析该半抽象指令集的描述并将其转换为要输入到链接器的指令的一种方式。如果要查看给定体系结构(例如amd64)的汇编指令的外观,则标准库的源代码中有许多示例,例如 runtime
和 math/big
。您还可以检查编译器作为汇编代码发出的内容(实际输出可能与您在此处看到的有所不同):
$ cat x.go
package main
func main() {
println(3)
}
$ GOOS=linux GOARCH=amd64 go tool compile -S x.go
# or: go build -gcflags -S x.go
"".main STEXT size=74 args=0x0 locals=0x10
0x0000 00000 (x.go:3) TEXT "".main(SB), $16-0
0x0000 00000 (x.go:3) MOVQ (TLS), CX
0x0009 00009 (x.go:3) CMPQ SP, 16(CX)
0x000d 00013 (x.go:3) JLS 67
0x000f 00015 (x.go:3) SUBQ $16, SP
0x0013 00019 (x.go:3) MOVQ BP, 8(SP)
0x0018 00024 (x.go:3) LEAQ 8(SP), BP
0x001d 00029 (x.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (x.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (x.go:3) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (x.go:4) PCDATA $0, $0
0x001d 00029 (x.go:4) PCDATA $1, $0
0x001d 00029 (x.go:4) CALL runtime.printlock(SB)
0x0022 00034 (x.go:4) MOVQ $3, (SP)
0x002a 00042 (x.go:4) CALL runtime.printint(SB)
0x002f 00047 (x.go:4) CALL runtime.printnl(SB)
0x0034 00052 (x.go:4) CALL runtime.printunlock(SB)
0x0039 00057 (x.go:5) MOVQ 8(SP), BP
0x003e 00062 (x.go:5) ADDQ $16, SP
0x0042 00066 (x.go:5) RET
0x0043 00067 (x.go:5) NOP
0x0043 00067 (x.go:3) PCDATA $1, $-1
0x0043 00067 (x.go:3) PCDATA $0, $-1
0x0043 00067 (x.go:3) CALL runtime.morestack_noctxt(SB)
0x0048 00072 (x.go:3) JMP 0
...
FUNCDATA
和PCDATA
指令包含用于通过垃圾收集器的使用的信息; 它们由编译器引入。
要查看链接后放入二进制文件的内容,请使用go tool objdump
:
$ go build -o x.exe x.go
$ go tool objdump -s main.main x.exe
TEXT main.main(SB) /tmp/x.go
x.go:3 0x10501c0 65488b0c2530000000 MOVQ GS:0x30, CX
x.go:3 0x10501c9 483b6110 CMPQ 0x10(CX), SP
x.go:3 0x10501cd 7634 JBE 0x1050203
x.go:3 0x10501cf 4883ec10 SUBQ $0x10, SP
x.go:3 0x10501d3 48896c2408 MOVQ BP, 0x8(SP)
x.go:3 0x10501d8 488d6c2408 LEAQ 0x8(SP), BP
x.go:4 0x10501dd e86e45fdff CALL runtime.printlock(SB)
x.go:4 0x10501e2 48c7042403000000 MOVQ $0x3, 0(SP)
x.go:4 0x10501ea e8e14cfdff CALL runtime.printint(SB)
x.go:4 0x10501ef e8ec47fdff CALL runtime.printnl(SB)
x.go:4 0x10501f4 e8d745fdff CALL runtime.printunlock(SB)
x.go:5 0x10501f9 488b6c2408 MOVQ 0x8(SP), BP
x.go:5 0x10501fe 4883c410 ADDQ $0x10, SP
x.go:5 0x1050202 c3 RET
x.go:3 0x1050203 e83882ffff CALL runtime.morestack_noctxt(SB)
x.go:3 0x1050208 ebb6 JMP main.main(SB)
常数
尽管汇编程序从Plan 9汇编程序获得指导,但这是一个独立的程序,因此存在一些差异。一种是不断评估。汇编器中的常量表达式是使用Go的运算符优先级解析的,而不是原始的类似于C的优先级。因此3&1<<2
为4,而不是0-解析为(3&1)<<2
not 3&(1<<2)
。同样,常量始终被评估为64位无符号整数。因此-2
,不是整数值减去2,而是具有相同位模式的无符号64位整数。区分很少有关系,但要避免在设置右操作数的高位时避免模棱两可,除法或右移。
符号
一些符号(例如R1
或LR
)是预定义的,并引用寄存器。确切的设置取决于体系结构。
有四个预先声明的符号引用伪寄存器。这些不是真正的寄存器,而是由工具链维护的虚拟寄存器,例如帧指针。伪寄存器的集合对于所有体系结构都是相同的:
FP
:框架指针:参数和局部变量。PC
:程序计数器:跳转和分支。SB
:静态基本指针:全局符号。SP
:堆栈指针:堆栈顶部。
所有用户定义的符号均作为伪寄存器FP
(参数和局部变量)和SB
(全局变量)的偏移量写入 。
该SB
伪寄存器可以被认为是记忆的原点上,所以符号foo(SB)
的名称是foo
在内存中的地址。该表格用于命名全局功能和数据。将<>
名称添加到中(如中)foo<>(SB)
,使名称仅在当前源文件中可见,例如static
C文件中的顶级声明。在名称中添加偏移量是指距符号地址的偏移量,因此 foo+4(SB)
距的开头四个字节。
FP
伪寄存器是用来指函数参数的虚拟帧指针。编译器维护一个虚拟帧指针,并将堆栈上的参数引用为该伪寄存器的偏移量。因此,0(FP)
是该函数的第一个参数, 8(FP)
是第二个参数(在64位计算机上),依此类推。但是,以这种方式引用函数自变量时,必须在first_arg+0(FP)
和开头放置一个名称second_arg+8(FP)
。(偏移量的含义(与帧指针的偏移量不同)与它与with的用法不同SB
,此处偏移量是符号的偏移量。)汇编程序强制执行此约定,拒绝plain0(FP)
和8(FP)
。实际名称在语义上无关紧要,但应用于记录自变量名称。值得强调的是FP
即使在具有硬件帧指针的体系结构上,始终是伪寄存器,而不是硬件寄存器。
对于带有Go原型的汇编函数,go
vet
将检查参数名称和偏移量是否匹配。在32位系统上,通过在名称中添加a_lo
或_hi
后缀来区分64位值的低32位和高32位,如arg_lo+0(FP)
或中所示arg_hi+4(FP)
。如果Go原型未命名其结果,则预期的程序集名称为ret
。
SP
伪寄存器是用来指帧局部变量的虚拟堆栈指针和函数调用正在编写的参数。它指向本地堆栈帧的顶,所以引用应在范围[-framesize,0)使用负偏移: x-8(SP)
,y-4(SP)
,等。
在具有名为的硬件寄存器的体系结构上SP
,名称前缀将对虚拟堆栈指针的引用与对体系结构SP
寄存器的引用区分开 。也就是说,x-8(SP)
并且-8(SP)
是不同的内存位置:第一个引用虚拟堆栈指针伪寄存器,而第二个引用硬件的SP
寄存器。
在一些机器上SP
,并PC
在传统的物理,地址寄存器中的别名,在围棋汇编的名称SP
和PC
仍然特殊处理; 例如,引用SP
需要一个符号,就像FP
。要访问实际的硬件寄存器,请使用真实R
名称。例如,ARM架构的硬件 SP
和PC
是可访问 R13
和R15
。
分支和直接跳转总是写为PC的偏移量或标签的跳转:
label:
MOVW $0, R1
JMP label
每个标签仅在定义它的函数中可见。因此,允许文件中的多个功能定义和使用相同的标签名称。直接跳转和调用指令可以定位文本符号,例如name(SB)
,但不能定位符号的偏移量,例如name+4(SB)
。
指令,寄存器和汇编器指令始终位于大写字母中,以提醒您汇编编程是一项艰巨的工作。(例外:g
ARM上的寄存器重命名。)
在Go目标文件和二进制文件中,符号的全名是程序包路径,后跟一个句点和符号名称: fmt.Printf
或math/rand.Int
。由于汇编程序的解析器将句点和斜杠视为标点符号,因此这些字符串不能直接用作标识符名称。相反,汇编器允许在标识符中使用中间点字符U + 00B7和分隔斜杠U + 2215,并将它们重写为纯句点和斜杠。在汇编程序源文件中,以上符号表示为 fmt·Printf
和math∕rand·Int
。编译器在使用-S
标志时生成的汇编清单直接显示了句点和斜杠,而不是汇编程序要求的Unicode替换。
大多数手写的汇编文件都没有在符号名称中包含完整的程序包路径,因为链接器会在句点开始的任何源名称的开头插入当前对象文件的程序包路径:在math / rand中的汇编源文件中包实现中,包的Int函数可以称为·Int
。这种约定避免了在自己的源代码中对包的导入路径进行硬编码的需要,从而使将代码从一个位置移动到另一个位置变得更加容易。
指令
汇编器使用各种指令将文本和数据绑定到符号名称。例如,这是一个简单的完整函数定义。该TEXT
伪指令声明符号runtime·profileloop
和后面的指令构成函数的主体。TEXT
块中的最后一条指令必须是某种形式的跳转,通常是RET
(伪)指令。(如果不是,则链接器将追加一个跳转至自身的指令;中不存在任何穿透TEXTs
。)在符号之后,参数是标志(请参见下文)和帧大小,常数(但请参见下文):
文本运行时·profileloop(SB),NOSPLIT,$ 8 MOVQ $ runtime·profileloop1(SB),CX MOVQ CX,0(SP) CALL运行时·外部线程处理程序(SB) RET
在一般情况下,帧大小后跟参数大小,并用减号分隔。(这不是减法,只是特有的语法。)框架大小$24-8
指出该函数具有24字节的框架,并使用8个字节的参数进行调用,该参数位于调用方的框架上。如果NOSPLIT
未为指定TEXT
,则必须提供参数大小。对于带有Go原型的汇编函数,go
vet
将检查参数大小是否正确。
请注意,符号名称使用中间的点分隔组件,并被指定为与静态基本伪寄存器的偏移量SB
。该函数将runtime
使用简单名称从Go源代码中调用以进行打包profileloop
。
全局数据符号由一系列初始化DATA
指令及其 后的GLOBL
指令定义。每个DATA
指令都会初始化相应内存的一部分。未显式初始化的内存将清零。DATA
指令的一般形式是
数据符号+偏移量(SB)/宽度,值
它以给定的偏移量和宽度使用给定的值初始化符号存储。DATA
给定符号的指令必须以增加的偏移量编写。
该GLOBL
指令将符号声明为全局符号。参数是可选标志,数据的大小声明为全局,除非DATA
指令已将其初始化,否则其初始值为全零。该GLOBL
指令必须遵循任何相应的DATA
指令。
例如,
数据divtab <> + 0x00(SB)/ 4,$ 0xf4f8fcff 数据divtab <> + 0x04(SB)/ 4,$ 0xe6eaedf0 ... 数据divtab <> + 0x3c(SB)/ 4,$ 0x81828384 GLOBL divtab <>(SB),RODATA,64美元
GLOBL运行时·tlsoffset(SB),NOPTR,$ 4
声明并初始化divtab<>
一个4字节整数值的只读64字节表,并声明runtime·tlsoffset
一个不包含指针的4字节隐式清零变量。
指令可能有一个或两个参数。如果有两个,则第一个是标志的位掩码,可以将这些标志写为数字表达式,或者加在一起或累加起来,或者可以进行符号设置以方便人类吸收。在标准#include
文件中定义的它们的值textflag.h
是:
NOPROF
= 1
(对于TEXT
项目。)不要分析标记的功能。不推荐使用此标志。DUPOK
= 2
在单个二进制文件中具有此符号的多个实例是合法的。链接器将选择要使用的重复项之一。NOSPLIT
= 4
(对于TEXT
项。)请勿插入序言以检查是否必须拆分堆栈。例程的框架及其所调用的所有内容必须适合堆栈段顶部的备用空间。用于保护例程,例如堆栈拆分代码本身。RODATA
= 8
(用于DATA
和GLOBL
。)将此数据放在只读部分中。NOPTR
= 16
(用于DATA
和GLOBL
项目。)此数据不包含指针,因此不需要由垃圾收集器进行扫描。WRAPPER
= 32
(对于TEXT
项。)这是包装函数,不应算作禁用recover
。NEEDCTXT
= 64
(对于TEXT
项。)此函数是一个闭包,因此它将使用其传入的上下文寄存器。LOCAL
= 128
此符号位于动态共享库的本地。TLSBSS
= 256
(用于DATA
和GLOBL
项目。)将此数据放入线程本地存储中。NOFRAME
= 512
(对于TEXT
项。)即使这不是叶函数,也不要插入指令来分配堆栈帧并保存/恢复返回地址。仅在声明帧大小为0的函数上有效。TOPFRAME
= 2048
(对于TEXT
项。)函数是调用堆栈的顶部。回溯应在此功能处停止。