golang 深入剖析,初始化,编译器,目标文件,链接器

一、go程序初始化顺序

  • 在 main 包中的 go 文件默认总是会被执行
  • 同包下的不同 go 文件,按照文件名“从小到大”排序顺序执行
  • 其他的包只有被 main 包 import 才会执行,按照 import 的先后顺序执行
  • 被递归 import 的包的初始化顺序与 import 顺序相反,例如:导入顺序 main –> A –> B –> C,则初始化顺序为 C –> B –> A –> main
  • 一个包被其它多个包 import,但只能被初始化一次
  • main 包总是被最后一个初始化,因为它总是依赖别的包
  • 避免出现循环 import,例如:A –> B –> C –> A

二、go编译器内部机制

1. 初始化一些通用数据结构。

2. 遍历提供的所有 Go 源代码文件,并针对每个文件调用 yyparse 方法。该方法会完成真正的语法分析。Go 编译器使用 Bison 作为程序分析生成器。语法描述存储在文件 go.y 中(后面我会提供详细的说明)。最终,这一步会生成一个完整的分析树,其中每个结点表示编译后程序的一个元素。

结点是一个结构体(你可以在这里找到其定义)。这个结构体包含了大量的属性,这是因为它需要各种不同类型的结点类型,而不同类别的结点又有着不同的属性。

3. 递规地遍历生成的树,并做出一定修改,例如为那些应当隐式定义的节点指定类型信息、重写在运行时包中传递给函数的某些语言元素——如类型转换,以及其它一些工作。

例如在一个程序中将一个对象转换成一个接口类型,这里面会有一个嵌套循环。我们遍历所有接口的方法。对于接口中的每一个方法,我们都会尽力在类型中找到一个对应的方法(这些方法存储于 mhdr 集合中)。检查两个方法是否相同的方法是相当明了的。

4. 语法解析树处理完成后,再执行真正的编译,将结点翻译成汇编代码。

5. 在磁盘上创建目标文件,并将翻译生成的汇编代码以及一些额外的数据结构,如符号表等,写入目标文件中。

目标代码的形式可以是绝对指令代码或可重定位的指令代码或汇编指令代码。如目标代码是绝对指令代码,则这种目标代码可立即执行。如果目标代码是汇编指令代码,则需汇编器汇编之后才行运行。必须指出,现在多数实用编译程序所产生的目标代码都是一种可重定位的指令代码。golang编译后是可重定位的指令代码。这种目标代码在运行前必须借助于一个连接装配程序把各个目标模块(包括系统提供的库函数)连接在一起,确定程序变量(或常数)在主存中的位置,装入内存中指定的起始地址,使之成为一个可以运行的绝对指令代码程序。

三、go目标文件

1、符号表

go程序编译后生成目标文件,目标文件中有一个Syms数组,这个数组是一个符号表,程序中定义的所有东西,包括函数,全局变量,类型,常量等等,都写在这个表里。

&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:  ...,
}
Field Description/描述
SymID 唯一的符号 ID。这个 ID 值包含了符号的名称与版本号。版本信息可以帮助区分同名称的符号。
Kind 标识符号的所属的类型(稍后会有更加详细的介绍)
DupOK 标识是否允许符号冗余(同名符号)。
Size 符号数据的大小。
Type 引用另外一个表示符号类型的符号(如果存在)。
Data 包含二进制数据。不同类型的符号该域的含义不同。例如,对于函数该域表示汇编代码,对于字符串符号该域表示原始字符串,等等。
Reloc 重定位列表(稍后会有详细介绍)。
Func

包含函数符号的元数据(稍会有详细介绍)。

2、Reloc-go重定位

go目标文件中符号表中有Reloc数组,结构如下:

type Reloc struct {
    Offset int
    Size   int
    Sym    SymID
    Add    int
    Type int
}

3、函数元数据的结构体:

Func: &goobj.Func{
    Args:    0,
    Frame:   8,
    Leaf:    false,
    NoSplit: false,
    Var:     {
    },
    PCSP:   goobj.Data{Offset:255, Size:7},
    PCFile: goobj.Data{Offset:263, Size:3},
    PCLine: goobj.Data{Offset:267, Size:7},
    PCData: {
        {Offset:276, Size:5},
    },
    FuncData: {
        {
            Sym:    goobj.SymID{Name:"gclocals·3280bececceccd33cb74587feedb1f9f", Version:0},
         Offset: 0,
     },
     {
         Sym:    goobj.SymID{Name:"gclocals·3280bececceccd33cb74587feedb1f9f", Version:0},
               Offset: 0,
           },
       },
       File: {"/home/adminone/temp/test.go"},
   },

如上的结构体是由编译器在目标文件中创建的函数元数据,在go运行时会用到这个数据结构。在运行时包内,这个结构体被映射为如下的结构体:

type _func struct {
	entry   uintptr // start pc
	nameoff int32   // function name
 
	args  int32 // in/out args size
	frame int32 // legacy frame size; use pcsp if possible
 
	pcsp      int32
	pcfile    int32
	pcln      int32
	npcdata   int32
	nfuncdata int32
}

其中 pcsp、pcfile 与 pln。在程序计数器(program counter)被转换成栈指针、文件名、以及行号时会分别用到这三个域。

4、垃圾收集(GC)是如何使用函数元数据

Go语言使用标记-清除垃圾收集器,这种垃圾收集器分为两个阶段工作。第一阶段为标记阶段,GC遍历所有仍在使用的对象,并将其标记为可达。第二阶段为清除阶段,所有没有被标记的对象在该阶段被删除。

垃圾收集器从几个位置搜索可达的对象,包括全局变量,寄存器,栈帧以及可达对象的指针。它到底是如何区分栈中的变量是一个指针还是非指针类型呢?这就需要FuncData来发挥作用了。编译器都会为其创建两个位图向量。其中一个表示函数的参数的范围。另一个则表示栈帧中存储局部变量的区域。这两个位图变量可以告诉垃圾收集器栈帧中哪些位置上是指针,这些信息就可以帮助垃圾收集器完成垃圾收集工作了。

五、go链接器

1、链接器收集main包引用的所有其它包中的符号信息,并将它们装载到一个大的字节数组中

2、对于每个符号,链接器计算它在(数组)镜像中的地址。

3、然后他为每个符号应用重定位,

4、链接器准备所有ELF格式(linux系统中)文件或者PE格式文件(windows系统中)所需的文件头。然后它再生成一个可执行的文件。

六、启动过程

当生成运行时包时,只有与当前系统和架构相关的文件会被选用。而其余的则会被略过

 

 

你可能感兴趣的:(golang 深入剖析,初始化,编译器,目标文件,链接器)