A Quick Guide to Go's Assembler

A Quick Guide to Go's Assembler

本文档简要概述了gc Go编译器使用的非常规形式的汇编语言. 该文件不全面.

汇编程序基于Plan 9汇编程序的输入样式,在其他地方详细介绍了该样式. 如果您打算编写汇编语言,则尽管其中大部分是特定于Plan 9的,但您仍应阅读该文档. 当前文档提供了语法摘要以及与该文档中所解释内容的区别,并描述了编写汇编代码以与Go交互时适用的特性.

The most important thing to know about Go's assembler is that it is not a direct representation of the underlying machine. Some of the details map precisely to the machine, but some do not. This is because the compiler suite (see this description) needs no assembler pass in the usual pipeline. Instead, the compiler operates on a kind of semi-abstract instruction set, and instruction selection occurs partly after code generation. The assembler works on the semi-abstract form, so when you see an instruction like MOV what the toolchain actually generates for that operation might not be a move instruction at all, perhaps a clear or load. Or it might correspond exactly to the machine instruction with that name. In general, machine-specific operations tend to appear as themselves, while more general concepts like memory move and subroutine call and return are more abstract. The details vary with architecture, and we apologize for the imprecision; the situation is not well-defined.

汇编程序是一种解析该半抽象指令集的描述并将其转换为要输入到链接器的指令的方法. 如果要查看给定体系结构(例如amd64)的组装指令说明,标准库源代码中有很多示例,例如runtimemath/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
...

FUNCDATAPCDATA指令包含供垃圾收集器使用的信息. 它们由编译器引入.

要查看链接后放入二进制文件的内容,请使用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)

Constants

尽管汇编程序从Plan 9汇编程序获得指导,但这是一个独立的程序,因此存在一些差异. 一种是不断评估. 汇编器中的常量表达式是使用Go的运算符优先级解析的,而不是原始的C类优先级. 因此3&1<<2是4,而不是0,它解析为(3&1)<<2而不是3&(1<<2) . 同样,常量始终被评估为64位无符号整数. 因此, -2不是整数值减去2,而是具有相同位模式的无符号64位整数. 区分很少有关系,但要避免在设置右操作数的高位时避免歧义,除法或右移.

Symbols

一些符号(例如R1LR )已预定义并引用了寄存器. 确切的设置取决于体系结构.

有四个预先声明的符号引用伪寄存器. 这些不是真正的寄存器,而是由工具链维护的虚拟寄存器,例如帧指针. 伪寄存器的集合对于所有体系结构都是相同的:

所有用户定义的符号都作为偏移量写入伪寄存器FP (参数和局部变量)和SB (全局变量).

可以将SB伪寄存器视为内存的起源,因此符号foo(SB)是名称foo ,它是内存中的地址. 该表格用于命名全局功能和数据. 与foo<>(SB) ,在名称中添加<>可使名称仅在当前源文件中可见,例如C文件中的顶级static声明. 在名称中添加偏移量是指与符号地址的偏移量,因此foo+4(SB)foo的开头之后四个字节.

FP伪寄存器是用于引用函数参数的虚拟帧指针. 编译器维护一个虚拟帧指针,并将堆栈上的参数引用为该伪寄存器的偏移量. 因此, 0(FP)是该函数的第一个参数, 8(FP)是第二个(在64位计算机上),依此类推. 但是,以这种方式引用函数自变量时,必须像first_arg+0(FP)second_arg+8(FP)一样在开头放置一个名称. (偏移量的含义(与帧指针的偏移量不同)与它与SB用法不同,在这里它是与符号的偏移量.)汇编器强制执行此约定,拒绝纯0(FP)8(FP) . 实际名称在语义上无关紧要,但应用于记录自变量名称. 值得强调的是,即使在具有硬件帧指针的体系结构上, FP始终是伪寄存器,而不是硬件寄存器.

对于具有Go原型的汇编函数, go vet将检查参数名称和偏移量是否匹配. 在32位系统上,通过在名称中添加_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寄存器.

SPPC传统上是物理编号寄存器的别名的机器上,在Go汇编器中,名称SPPC仍被特殊处理; 例如,对SP引用需要一个符号,就像FP一样. 要访问实际的硬件寄存器,请使用真实的R名称. 例如,在ARM体系结构上,硬件SPPC可作为R13R15访问.

分支和直接跳转始终被写入PC的偏移量或标签的跳转:

label:
	MOVW $0, R1
	JMP label

每个标签仅在定义它的函数中可见. 因此,允许文件中的多个功能定义和使用相同的标签名称. 直接跳转和调用指令可以以文本符号(例如name(SB)为目标,但不能以符号(例如name+4(SB) .

指令,寄存器和汇编器指令始终位于大写字母中,以提醒您汇编编程是一项艰巨的工作. (例外: g寄存器在ARM上重命名.)

在Go目标文件和二进制文件中,符号的全名是程序包路径,后跟一个句点和符号名称: fmt.Printfmath/rand.Int . 因为汇编程序的解析器将句点和斜杠视为标点符号,所以这些字符串不能直接用作标识符名称. 相反,汇编器允许在标识符中使用中间点字符U + 00B7和分隔斜杠U + 2215,并将它们重写为纯句点和斜杠. 在汇编源文件中,以上符号写为fmt·Printfmath∕rand·Int . 编译器在使用-S标志时生成的汇编列表直接显示了句点和斜杠,而不是汇编程序要求的Unicode替换.

大多数手写的汇编文件都没有在符号名称中包含完整的程序包路径,因为链接程序会在以句号开头的任何名称的开头插入当前目标文件的程序包路径:在math / rand中的汇编源文件中在包实现中,包的Int函数可以称为·Int . 这种约定避免了在自己的源代码中对包的导入路径进行硬编码的需要,从而使将代码从一个位置移动到另一个位置变得更加容易.

Directives

汇编器使用各种指令将文本和数据绑定到符号名称. 例如,这是一个简单的完整函数定义. TEXT指令声明符号runtime·profileloop及其runtime·profileloop的指令构成函数的主体. TEXT块中的最后一条指令必须是某种形式的跳转,通常是RET (伪)指令. (如果不是,则链接器将追加一个跳转至自身的指令; TEXTs不会TEXTs .)在符号之后,参数是标志(请参见下文)和帧大小,常数(但请参见下文):

TEXT runtime·profileloop(SB),NOSPLIT,$8
	MOVQ	$runtime·profileloop1(SB), CX
	MOVQ	CX, 0(SP)
	CALL	runtime·externalthreadhandler(SB)
	RET

在一般情况下,帧大小后跟参数大小,并用减号分隔. (这不是减法,只是特殊语法.)帧大小$24-8声明该函数具有24字节的帧,并使用8字节的参数调用,该参数位于调用方的帧上. 如果未为TEXT指定NOSPLIT ,则必须提供参数大小. 对于具有Go原型的汇编函数, go vet将检查参数大小是否正确.

注意,符号名使用中间的点来分隔各个组成部分,并被指定为与静态基本伪寄存器SB的偏移量. 使用简单名称profileloop从Go源代码为包runtime调用此函数.

全局数据符号由一系列初始化DATA指令和GLOBL指令定义. 每个DATA指令都会初始化相应内存的一部分. 未显式初始化的内存将清零. DATA指令的一般形式是

DATA	symbol+offset(SB)/width, value

它以给定的偏移量和宽度使用给定的值初始化符号存储器. 给定符号的DATA指令必须以增加的偏移量编写.

GLOBL指令将符号声明为全局符号. 参数是可选标志,数据的大小声明为全局,除非DATA指令已将其初始化,否则其初始值将为全零. GLOBL指令必须遵循任何相应的DATA指令.

例如,

DATA divtab<>+0x00(SB)/4, $0xf4f8fcff
DATA divtab<>+0x04(SB)/4, $0xe6eaedf0
...
DATA divtab<>+0x3c(SB)/4, $0x81828384
GLOBL divtab<>(SB), RODATA, $64

GLOBL runtime·tlsoffset(SB), NOPTR, $4

声明并初始化divtab<> (一个只读的64字节表,包含4个字节的整数值),并声明runtime·tlsoffset (一个4字节,不包含指针的隐式清零变量).

指令可能有一个或两个参数. 如果有两个,则第一个是标志的位掩码,可以将其写为数字表达式,或相加或相加,也可以设置符号以方便人类吸收. 在标准的#include文件textflag.h定义的值是:

Runtime Coordination

为了使垃圾回收正确运行,运行时必须知道指针在所有全局数据和大多数堆栈帧中的位置. Go编译器在编译Go源文件时会发出此信息,但是汇编程序必须明确定义它.

标记有NOPTR标志的数据符号(请参见上文)被视为不包含指向运行时分配的数据的指针. 具有RODATA标志的数据符号分配在只读存储器中,因此被视为隐式标记为NOPTR . 总大小小于指针的数据符号也被视为隐式标记为NOPTR . 不能在程序集源文件中定义包含指针的符号. 此类符号必须在Go源文件中定义. 即使没有DATAGLOBL指令,程序集源仍然可以按名称引用符号. 一个好的通用经验法则是在Go中而不是在汇编中定义所有非RODATA符号.

每个函数还需要注释,以在其参数,结果和本地堆栈框架中提供活动指针的位置. 对于没有指针结果且没有本地堆栈框架或没有函数调用的汇编函数,唯一的要求是在同一包中的Go源文件中为该函数定义Go原型. 汇编函数的名称不得包含程序包名称组件(例如,程序包syscall函数Syscall应该使用名称·Syscall代替其TEXT指令中的等效名称syscall·Syscall ). 对于更复杂的情况,需要显式注释. 这些注释使用标准#include文件funcdata.h定义的伪指令.

如果函数没有参数且没有结果,则可以省略指针信息. -0 on the TEXT instruction. 这由TEXT指令上的$ -0变量大小注释表示. 否则,Go原型必须为Go源文件中的函数提供指针信息,即使不是直接从Go调用的汇编函数也是如此. (原型也将让go vet检查参数引用.)在函数的开始,假设参数进行初始化,但结果被假定初始化. 如果结果将在调用指令期间保留活动指针,则该函数应首先将结果清零,然后执行伪指令GO_RESULTS_INITIALIZED . 该指令记录结果现在已初始化,并且在堆栈移动和垃圾回收期间应对其进行扫描. 通常更容易安排汇编函数不返回指针或不包含调用指令. 标准库中没有任何汇编函数使用GO_RESULTS_INITIALIZED .

如果函数没有本地堆栈帧,则可以省略指针信息. on the TEXT instruction. 这由TEXT指令上的$0- 本地帧大小注释指示. 如果该函数不包含任何调用指令,则指针信息也可以省略. 否则,本地堆栈帧不得包含指针,并且程序集必须通过执行伪指令NO_LOCAL_POINTERS来确认这一事实. 由于通过移动堆栈来实现堆栈大小调整,因此在任何函数调用期间堆栈指针都可能发生变化:即使堆栈数据指针也不得保留在局部变量中.

大会职能应始终给予转到原型,既提供了参数和结果的指针信息,并让go vet检查正在使用访问的偏移他们是正确的.

Architecture-specific details

列出每台机器的所有说明和其他详细信息是不切实际的. 要查看为给定机器(例如ARM)定义的指令,请查看该架构的obj支持库的源代码,该库位于目录src/cmd/internal/obj/arm . 在那个目录中是一个文件a.out.go ; 它包含一长串以A开头的常数,例如:

const (
	AAND = obj.ABaseARM + obj.A_ARCHSPECIFIC + iota
	AEOR
	ASUB
	ARSB
	AADD
	...

这是该体系结构的汇编程序和链接程序已知的指令及其拼写列表. 在此列表中,每条指令均以首字母A开头,因此AAND表示按位与指令AND (无前导A ),并在汇编源代码中写为AND . 枚举大部分按字母顺序排列. (在cmd/internal/obj包中定义的与体系结构无关的AXXX表示无效的指令). A名称的顺序与机器指令的实际编码无关. cmd/internal/obj软件包负责该细节.

cmd/internal/obj/x86/a.out.go中列出了386和AMD64体系结构的cmd/internal/obj/x86/a.out.go .

The architectures share syntax for common addressing modes such as (R1) (register indirect), 4(R1) (register indirect with offset), and $foo(SB) (absolute address). The assembler also supports some (not necessarily all) addressing modes specific to each architecture. The sections below list these.

在上一部分示例中显而易见的一个细节是指令中的数据从左向右流动: MOVQ $0, CX清除CX . 该规则甚至适用于常规符号使用相反方向的体系结构.

Here follow some descriptions of key Go-specific details for the supported architectures.

32-bit Intel 386

指向g结构的运行时指针通过MMU中否则未使用(就Go而言)的寄存器的值维护. 如果源在runtime包中,并且为汇编器定义了OS依赖的宏get_tls并且该宏包含特殊的标头go_tls.h

#include "go_tls.h"

Within the runtime, the get_tls macro loads its argument register with a pointer to the g pointer, and the g struct contains the m pointer. There's another special header containing the offsets for each element of g, called go_asm.h. The sequence to load g and m using CX looks like this:

#include "go_tls.h"
#include "go_asm.h"
...
get_tls(CX)
MOVL	g(CX), AX     // Move g into AX.
MOVL	g_m(AX), BX   // Move g.m into BX.

注意:上面的代码仅在runtime程序包中有效,而go_tls.h也适用于armamd64和amd64p32, go_asm.h适用于所有体系结构.

寻址方式:

当使用编译器和汇编的-dynlink-shared固定存储位置的模式,任何加载或存储诸如一个全局变量必须假定重写CX . 因此,为了在这些模式下安全使用,汇编源通常应避免使用CX,除非在内存引用之间.

64-bit Intel 386 (a.k.a. amd64)

两种架构在汇编程序级别上的行为基本相同. 在64位版本上访问mg指针的汇编代码与32位386上的汇编代码相同,不同的是它使用MOVQ而不是MOVL

get_tls(CX)
MOVQ	g(CX), AX     // Move g into AX.
MOVQ	g_m(AX), BX   // Move g.m into BX.

ARM

寄存器R10R11由编译器和链接器保留.

R10指向g (goroutine)结构. 在汇编器源代码中,此指针必须称为g ; 无法识别名称R10 .

为了使人们和编译器更容易编写程序集,ARM链接器允许使用单个硬件指令无法表达的常规寻址形式和伪操作,例如DIVMOD . 它将这些形式实现为多条指令,通常使用R11寄存器来保存临时值. 手写汇编可以使用R11 ,但这样做必须确保链接程序也没有使用它来实现功能中的任何其他指令.

在定义TEXT ,指定帧大小$-4告诉链接器这是一个叶子函数,不需要在输入时保存LR .

名称SP始终引用前面所述的虚拟堆栈指针. 对于硬件寄存器,请使用R13 .

条件代码语法是在指令后附加句点和一个或两个字母的代码,如MOVW.EQ . 可以附加多个代码: MOVM.IA.W . 代码修饰符的顺序无关紧要.

寻址方式:

ARM64

ARM64端口处于实验状态.

R18是在Apple平台上保留的"平台寄存器". 为了防止意外使用,该寄存器名为R18_PLATFORM . R27R28由编译器和链接器保留. R29是帧指针. R30是链接寄存器.

句点之后,指令修饰符会附加到指令中. 唯一的修饰符是P (后增量)和W (前增量): MOVW.PMOVW.W

寻址方式:

Reference: Go ARM64 Assembly Instructions Reference Manual

64-bit PowerPC, a.k.a. ppc64

64位PowerPC端口处于实验状态.

寻址方式:

IBM z/Architecture, a.k.a. s390x

寄存器R10R11被保留. 汇编某些指令时,汇编器使用它们来保存临时值.

R13指向g (goroutine)结构. 该寄存器必须称为g ; 无法识别名称R13 .

R15指向堆栈帧,通常应仅使用虚拟寄存器SPFP进行访问.

多个加载和存储指令对一系列寄存器进行操作. 寄存器的范围由开始寄存器和结束寄存器指定. 例如, LMG (R9), R5, R7将分别以0(R9)8(R9)16(R9)的64位值加载R5R6R7 .

诸如MVCXC类的存储和存储指令以长度作为第一个参数编写. 例如, XC $8, (R9), (R9)将清除R9指定地址的八个字节.

如果向量指令将长度或索引作为参数,则它将是第一个参数. 例如, VLEIF $1, $16, V2会将值16加载到V2索引一中. 使用向量指令时应小心,以确保它们在运行时可用. 要使用矢量指令,机器必须同时具有矢量功能(功能列表中的位129)和内核支持. 如果没有内核支持,向量指令将无效(它将等效于NOP指令).

寻址方式:

MIPS, MIPS64

通用寄存器的名称为R0R31 ,浮点寄存器的名称为F0F31 .

R30保留指向g . R23用作临时寄存器.

TEXT指令中,MIPS的帧大小$-4或MIPS64的帧大小$-4 $-8指示链接器不要保存LR .

SP是指虚拟堆栈指针. 对于硬件寄存器,请使用R29 .

寻址方式:

通过预定义GOMIPS_hardfloatGOMIPS_softfloat可将GOMIPS环境变量( hardfloatsoftfloat )的值提供给汇编代码.

通过预定义GOMIPS64_hardfloatGOMIPS64_softfloat可将GOMIPS64环境变量( hardfloatsoftfloat )的值提供给汇编代码.

Unsupported opcodes

汇编程序旨在支持编译器,因此并非所有架构都定义了所有硬件指令:如果编译器未生成,则可能不存在. 如果您需要使用缺少的指令,则有两种方法可以进行. 一种是更新汇编程序以支持该指令,这很简单,但是只有在很有可能再次使用该指令时才值得. 相反,对于简单的一次性情况,可以使用BYTEWORD指令将显式数据放入TEXT的指令流中. 这是386运行时定义64位原子加载函数的方式.

// uint64 atomicload64(uint64 volatile* addr);
// so actually
// void atomicload64(uint64 *res, uint64 volatile *addr);
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

by  ICOPY.SITE