在此博客文章中,咱们将介绍Go链接器,Go对象文件和重定位。 咱们为什么要关怀这些事件? 好吧,如果您想学习任何大型项目的外部常识,则须要做的第一件事就是将其拆分为组件或模块。 其次,您须要理解这些模块互相提供的接口。 在Go中,这些高级模块是编译器,链接器和运行时。 编译器提供并链接器应用的接口是一个指标文件,明天咱们将在这里进行钻研。
生成Go对象文件
让咱们做一个理论的试验-编写一个超级简略的程序,对其进行编译,而后查看将生成哪个指标文件。在咱们的例子中,程序如下
package mainfunc main() { print(1)}
当初咱们须要对其进行编译
go tool compile -N -l -S ./main.go > main.s
该命令产生test.6
指标文件。为了钻研其内部结构,咱们将应用goobj库。它在Go源代码外部应用,次要用于实现一组单元测试,以验证在不同状况下是否正确生成了指标文件。对于此博文,咱们编写了一个非常简单的程序,该程序将从googj库生成的输入打印到控制台。您能够在此GitHub存储库中查看该程序的源代码。
首先,您须要下载并装置该程序。
go get github.com/s-matyukevich/goobj_explorer
而后,执行以下命令。
goobj_explorer -o test.6
当初,您应该可能goob.Package
在控制台中看到该构造。
考察指标文件
指标文件中最乏味的局部是Syms
数组。这实际上是一个符号表。您在程序中定义的所有内容(函数,全局变量,类型,常量等)均写入此表。让咱们看一下与该main
函数绝对应的条目。(请留神,咱们当初曾经从输入中剪切了Reloc
和Func
字段。稍后咱们将探讨它们。)
&goobj.Sym{ SymID: goobj.SymID{Name:"main.main", Version:0}, Kind: 1, DupOK: false, Size: 48, Type: goobj.SymID{}, Data: goobj.Data{Offset:137, Size:44}, Reloc: ..., Func: ...,}
goobj.Sum
构造中字段的名称十分不言自明。
The names of the fields in the goobj.Sum
structure are pretty self-explanatory.
Field | Description |
---|---|
SumID | 由符号名称和版本组成的惟一符号ID。版本有助于辨别名称雷同的符号。 |
Kind | 批示符号属于哪种类型(稍后会有更多详细信息)。 |
DupOK | DupOK |
Size | 符号数据的大小。 |
Type | 对示意一个符号类型(如果有)的另一个符号的援用。 |
Data | 蕴含二进制数据。对于不同品种的符号,此字段具备不同的含意,例如,性能的汇编代码,字符串符号的原始字符串内容等。 |
Reloc | 重定位列表(稍后将提供更多详细信息)。 |
Func | 蕴含性能符号的非凡性能元数据(请参见上面的更多详细信息)。 |
当初,让咱们看一下不同品种的符号。所有可能的符号类型都在goobj
包中定义为常量(您能够在GitHub存储库中找到它们)。上面,咱们复制了这些常量的第一局部。
const ( _ SymKind = iota // readonly, executable STEXT SELFRXSECT // readonly, non-executable STYPE SSTRING SGOSTRING SGOFUNC SRODATA SFUNCTAB STYPELINK SSYMTAB // TODO: move to unmapped section SPCLNTAB SELFROSECT ...
如咱们所见,该main.main
符号属于与STEXT
常量绝对应的品种1 。STEXT
是蕴含可执行代码的符号。当初,让咱们看一下Reloc
数组。它由以下构造组成。
type Reloc struct { Offset int Size int Sym SymID Add int Type int}
每次重定位都意味着[Offset, Offset+Size]
应将位于该距离的字节替换为指定的地址。该地址是通过将Sym
符号的地位与Add
字节数相加得出的。
理解relocations
go tool compile -N -l -S ./main.go > main.s
让咱们浏览一下汇编器并尝试找到次要性能。
"".main t=1 size=48 value=0 args=0x0 locals=0x8 0x0000 00000 (test.go:3) TEXT "".main+0(SB),$8-0 0x0000 00000 (test.go:3) MOVQ (TLS),CX 0x0009 00009 (test.go:3) CMPQ SP,16(CX) 0x000d 00013 (test.go:3) JHI ,22 0x000f 00015 (test.go:3) CALL ,runtime.morestack_noctxt(SB) 0x0014 00020 (test.go:3) JMP ,0 0x0016 00022 (test.go:3) SUBQ $8,SP 0x001a 00026 (test.go:3) FUNCDATA $0,gclocals·3280bececceccd33cb74587feedb1f9f+0(SB) 0x001a 00026 (test.go:3) FUNCDATA $1,gclocals·3280bececceccd33cb74587feedb1f9f+0(SB) 0x001a 00026 (test.go:4) MOVQ $1,(SP) 0x0022 00034 (test.go:4) PCDATA $0,$0 0x0022 00034 (test.go:4) CALL ,runtime.printint(SB) 0x0027 00039 (test.go:5) ADDQ $8,SP 0x002b 00043 (test.go:5) RET ,
在当前的博客文章中,咱们将认真钻研此代码,并尝试理解Go运行时的工作形式。目前,咱们对以下行感兴趣。
0x0022 00034(test.go:4)CALL,runtime.printint(SB)
该命令在性能数据中的偏移量为0x0022(十六进制)或00034(十进制)。该行实际上负责调用该runtime.printint
函数。问题是编译器在编译过程中不晓得runtime.printint
函数的确切地址。此函数位于编译器不晓得的其余指标文件中。在这种状况下,它将应用重定位。以下是与此办法调用绝对应的确切重定位(咱们从goobj_explorer
实用程序的第一个输入中复制了它)。
{ Offset: 35, Size: 4, Sym: goobj.SymID{Name:"runtime.printint", Version:0}, Add: 0, Type: 3,},
此重定位通知链接器,从35个字节的偏移量开始,它须要用runtime.printint
符号起始点的地址替换4个字节的数据。然而,与主函数数据的偏移量为35个字节实际上是咱们先前所见的调用指令的参数。(该指令从一个34字节的偏移量开始。一个字节对应于调用指令代码,而四个字节则指向该指令的地址。)
链接器如何操作
当初咱们理解了这一点,咱们能够弄清楚链接器是如何工作的。以下架构十分简化,但反映了次要思维。
- 链接器从主程序包援用的所有程序包中收集所有符号,并将它们加载到一个大字节数组(或二进制映像)中。
- 对于每个符号,链接器都会在此图像中计算一个地址。
- 而后,它利用为每个符号定义的重定位。当初很容易,因为链接器晓得从那些重定位援用的所有其余符号的确切地址。
- 链接器为Linux上的可执行和可链接(ELF)格局或Windows上的可移植可执行(PE)格局筹备所有必须的标头。而后,它生成一个带有后果的可执行文件。
理解TLS
仔细的读者会留神到goobj_explorer utility
main办法的输入中产生了奇怪的重定位。它不对应于任何办法调用,甚至指向空符号。
{ Offset: 5, Size: 4, Sym: goobj.SymID{}, Add: 0, Type: 9,}
那么,这次搬迁有什么用呢?咱们能够看到它的偏移量为5个字节,其大小为4个字节。在此偏移量处,有一个命令。
0x0000 00000(test.go:3)MOVQ(TLS),CX
它从偏移量0开始并占用9个字节(因为下一个命令从偏移量9个字节开始)。咱们能够猜想,此重定位将奇怪的(TLS)
语句替换为某个地址,然而TLS是什么,它应用什么地址?
TLS是“线程本地存储”的缩写。这项技术已在许多编程语言中应用。简而言之,它使咱们可能领有一个变量,该变量在由不同线程应用时指向不同的内存地位。
在Go中,TLS用于存储指向G构造的指针,该G构造蕴含特定Go例程的外部详细信息(无关更多详细信息,请参见前面的博客文章)。因而,有一个变量(当从不同的Go例程拜访时)始终指向具备此Go例程的外部详细信息的构造。链接器晓得此变量的地位,而该变量正是上一条命令中移至CX寄存器的内容。TLS能够针对不同的体系结构以不同的形式实现。对于AMD64,TLS是通过FS
寄存器实现的,因而咱们之前的命令被转换为MOVQ FS
和CX
。
为了完结对重定位的探讨,咱们将向您展现蕴含所有不同类型的重定位的枚举类型(enum
)。
// Reloc.typeenum{ R_ADDR = 1, R_SIZE, R_CALL, // relocation for direct PC-relative call R_CALLARM, // relocation for ARM direct call R_CALLIND, // marker for indirect call (no actual relocating necessary) R_CONST, R_PCREL, R_TLS, R_TLS_LE, // TLS local exec offset from TLS segment register R_TLS_IE, // TLS initial exec offset from TLS base pointer R_GOTOFF, R_PLT0, R_PLT1, R_PLT2, R_USEFIELD,};
从该枚举中能够看出,重定位类型3为R_CALL
,重定位类型9为R_TLS
。这些enum
名称完满地解释了咱们后面探讨的行为。
在下一篇文章中,咱们将持续探讨指标文件。咱们还将为您提供更多必要的信息,以使您继续前进并理解Go运行时的工作形式。如果您有任何疑难,请随时在评论中发问。