中文版-Go的汇编器快速指南

Go的汇编器快速指南

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

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

关于Go的汇编器,最重要的事情是它不是底层机器的直接表示。一些细节正好映射到机器,但有些则不然。这是因为编译器套件(请参见此描述)在常规管道中不需要汇编程序。相反,编译器对一种半抽象的指令集进行操作,并且指令选择部分在代码生成之后发生。汇编器以半抽象形式工作,因此当您看到诸如MOV之类的指令时,工具链实际上为该操作生成的指令可能根本不是移动指令,可能是清除指令或加载指令。或者它可能与该名称的机器指令完全对应。通常,特定于机器的操作倾向于自己出现,而更通用的概念(如内存移动和子例程调用与返回)则更为抽象。具体细节因架构而异,我们对此不准确表示歉意。情况不明确。

汇编程序是一种解析该半抽象指令集的描述并将其转换为要输入到链接器的指令的方法。如果要查看给定体系结构的汇编指令(例如amd64)的外观,那么标准库源代码中有很多示例,例如运行时和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工具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而不是3&(1 << 2)。同样,常量始终被评估为64位无符号整数。因此,-2不是整数值减去2,而是具有相同位模式的无符号64位整数。区分很少有关系,但要避免在设置右操作数的高位时避免歧义,除法或右移。

符号

某些符号(例如R1或LR)已预定义并引用了寄存器。确切的设置取决于体系结构。

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

FP:帧指针:参数和局部变量。
PC:程序计数器:跳转并跳转。
SB:静态基址指针:全局符号。
SP:堆栈指针:堆栈顶部。
所有用户定义的符号都作为偏移量写入伪寄存器FP(参数和局部变量)和SB(全局变量)。

可以将SB伪寄存器视为内存的起源,因此符号foo(SB)是名称foo,它是内存中的地址。该表格用于命名全局功能和数据。如foo <>(SB)中一样,在名称中添加<>可使该名称仅在当前源文件中可见,例如C文件中的顶级静态声明。在名称中添加偏移量是指与符号地址的偏移量,因此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寄存器。

在SP和PC传统上是物理编号寄存器的别名的机器上,在Go汇编器中,名称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(伪)指令。 (如果不是,则链接器将追加一个跳转至自身的指令; TEXT中不会掉线。)在符号之后,参数是标志(请参见下文)和帧大小,常数(但请参见下文):

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源代码为包运行时调用此函数。

全局数据符号由一系列初始化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 <>(4字节整数值的只读64字节表),并声明runtime_tlsoffset(4字节隐式清零的变量,不包含指针)。

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

  • NOPROF = 1
    (对于TEXT项目。)不要分析标记的功能。此标志已弃用。
  • DUPOK = 2
    在单个二进制文件中具有此符号的多个实例是合法的。链接器将选择要使用的重复项之一。
  • NOSPLIT = 4
    (对于TEXT项。)请勿插入序言以检查是否必须拆分堆栈。例程的框架及其所调用的任何内容都必须适合堆栈段顶部的备用空间。用于保护例程,例如堆栈拆分代码本身。
  • RODATA = 8
    (对于DATA和GLOBL项目。)将此数据放在只读部分中。
  • NOPTR = 16
    (对于DATA和GLOBL项目。)此数据不包含任何指针,因此不需要由垃圾收集器进行扫描。
    包裹= 32
    (对于TEXT项目。)这是包装函数,不应视为禁用恢复。
  • NEEDCTXT = 64
    (对于TEXT项目。)此函数是一个闭包,因此它使用其传入的上下文寄存器。

运行时协调

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

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

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

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

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

应当始终为汇编函数提供Go原型,既可以提供参数和结果的指针信息,也可以让兽医检查用于访问它们的偏移量是否正确。

特定于架构的细节

列出每台机器的所有说明和其他详细信息是不切实际的。要查看为给定机器(例如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体系结构的说明。

这些架构共享常见寻址模式的语法,例如(R1)(间接寄存器),4(R1)(间接偏移量寄存器)和$ foo(SB)(绝对地址)。汇编器还支持特定于每种体系结构的某些(不一定是全部)寻址模式。以下各节列出了这些内容。

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

以下是对受支持的体系结构Go特定关键细节的一些描述。

32位Intel 386

指向g结构的运行时指针是通过MMU中否则未使用(就Go而言)的寄存器的值维护的。如果源位于运行时程序包中,并且为汇编程序定义了一个与操作系统相关的宏get_tls,并且该宏包含特殊的标头go_tls.h:

#include "go_tls.h"

在运行时内,get_tls宏将指向g指针的指针加载到其参数寄存器中,并且g结构包含m指针。还有另一个特殊的标头,包含g的每个元素的偏移量,称为go_asm.h。使用CX加载g和m的顺序如下所示:

#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.

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

寻址方式:

  • (DI)(BX * 2):地址DI加BX * 2的位置。
  • 64(DI)(BX * 2):地址DI加BX * 2加64的位置。这些模式仅接受1、2、4和8作为比例因子。

使用编译器和汇编器的-dynlink或-shared模式时,必须假定固定存储器位置(例如全局变量)的任何加载或存储都将覆盖CX。因此,为了安全地在这些模式下使用,汇编源通常应避免使用CX,除非在内存引用之间。

64位Intel 386(又名amd64)

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

get_tls(CX)
MOVQ g(CX, AX //将g移入AX。
MOVQ g_m(AX),BX //将g.m移至BX。

你可能感兴趣的:(Go语言,Go语言源码阅读)