共计 4254 个字符,预计需要花费 11 分钟才能阅读完成。
在此博客文章中,咱们将介绍 Go 链接器,Go 对象文件和重定位。咱们为什么要关怀这些事件?好吧,如果您想学习任何大型项目的外部常识,则须要做的第一件事就是将其拆分为组件或模块。其次,您须要理解这些模块互相提供的接口。在 Go 中,这些高级模块是编译器,链接器和运行时。编译器提供并链接器应用的接口是一个指标文件,明天咱们将在这里进行钻研。
生成 Go 对象文件
让咱们做一个理论的试验 - 编写一个超级简略的程序,对其进行编译,而后查看将生成哪个指标文件。在咱们的例子中,程序如下
package main
func 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.type
enum
{
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 运行时的工作形式。如果您有任何疑难,请随时在评论中发问。