关于golang:Golang内部构件第3部分链接器目标文件和重定位

37次阅读

共计 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 函数绝对应的条目。(请留神,咱们当初曾经从输入中剪切了 RelocFunc字段。稍后咱们将探讨它们。)

&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 FSCX

为了完结对重定位的探讨,咱们将向您展现蕴含所有不同类型的重定位的枚举类型(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 运行时的工作形式。如果您有任何疑难,请随时在评论中发问。

正文完
 0