GCC的工作,到生成汇编代码为止。剩下的工作,交给了Binutils来完成:assembler和static linker。最近详细地研究了一下linker的工作过程。
linker主要完成的是静态链接,目标文件合并的工作。例如,把多个.o文件合并成一个可执行文件。

两步链接

两步链接指的是:

  1. 空间与地址的分配
    链接器会首先扫描所有的输入文件,获得各个段的长度、属性和位置,将段合并;并将输入目标文件中的符号表合并为全局符号表。
  2. 符号解析与重定位
    使用上一步中收集到的信息,进行符号解析和重定位,调整代码中的地址等。这一步也是链接过程的核心,特别是重定位过程。
    链接器首先获取各个段的虚拟地址;在确定段的虚拟地址之后,也就能确定各个符号的虚拟地址了。

重定位与符号解析

在完成空间和地址的分配之后,链接器开始进行符号解析和重定位的过程。在链接之前,各个段中的符号地址,都是以0为基地址的,对于未知的地址,也通通用0进行替代。编译器在编译时,对于不知道的符号地址,全部用一个假值替代,把真正的工作留给链接器去做。
而链接器在分配了虚拟地址之后,就可以修正每一个需要重定位的入口。这个工作是借助于重定位表来实现的。重定位表包括:重定位入口(也就是需要重定位的地方),偏移表示入口在被重定位的段中的位置。
在x86_64下,重定位表的结构也很简单(定义在elf.h当中):

typedef struct{
    Elf64_Addr r_offset;
    Elf64_Xword r_info;
}Elf64_Rel;

typedef struct{
    Elf64_Addr r_offset;
    Elf64_Xword r_info;
    ELF64_Sxword r_addend;
}Elf64_Rela

这里,r_offset指定应用重定位操作的位置;r_info则指定必须对其进行重定位的符号表索引以及要应用的重定位类型。其低位表示重定位入口的类型;高位表示重定位入口的符号,在符号表中的下标。(不同处理器的格式不一样)
符号解析则是为符号的重定位提供帮助,根据多个目标文件中的符号表,生成全局符号表,找到相应的符号并进行重定位。对于未定义的符号,链接器都应该能在全局符号表中找到,否则就报出符号未定义的错误。
PS:x86_64只使用Elf64_Rela。

指令修正

在x86_64中,call、jmp、mov、lea等指令的寻址方式千差万别。对于重定位来说,修正指令的寻址方式定义在binutils/elfcpp/x86_64.h当中。这其中主要包括:R_X86_64_64R_X86_64_PC32两种。这是因为X86_64上,相对寻址依然只支持32位(实际上这也很科学;因为一个可执行文件通常不会有4G那么大)。
两种寻址方式的修正方法分别为:符号地址 + 保存在被修正位置的值符号地址 + 保存在被修正位置的值 - 被修正的位置相对于段开始的偏移量

源码分析

第一步:初始化,parsing command line & script file

linker的入口,在ldmain.c当中(通常在链接的时候,通过编译器内部直接进行调用)。首先,linker会调用bfd库,识别二进制文件的格式,生成各个段的描述符,并且转换为canonical form。(例如linker中的符号表识别工作,就是首先由BFD来进行分析和转化,然后linker直接在canonical form上进行操作,再由BFD来进行输出)因此在ldmain.c的main中,首先进行的也是bfd的初始化bfd_init。随后linker进行了一系列的设置,包括路径,回调函数、初始化,加载插件、读取命令、linker script等。

第二步:文件和符号的加载

随后,lang_process中,linker会对每个输入文件进行处理。对于每个输入文件,linker都会分配一个bfd,对输入文件进行扫描,识别出其中的符号。首先open_input_bfds为所有文件建立了bfd,随后载入文件中的所有符号。每个符号对应一个bfd_link_hash_entry,它们保存在bfd_link_hash_table当中。bfd_link_add_symbols将符号添加到hash_table当中。

第三步:输入文件的分析和合并

在链接的第一部分完成后,第二部分开始前,链接器首先调用了ldctor_build_sets函数,它主要为C++中的constructor/dectructor提供支持。随后链接器lang_do_memory_region计算出内存区域(它们保存在lang_memory_region_list当中)。再通过lang_common处理全局符号,将它们添加到对应的section,移除没有被使用的sections等。随后链接器建立输入section和输出section之间的映射关系,并且将文件的section合并,以及设置段的属性等。

第四步:重定位

第四步是符号的重定位工作。这里lang_size_section首先获取所有section的大小,然后lang_set_startof会修正section的大小和位置。在确定了sections的信息之后,就可以对符号进行重定位了,这便是lang_do_assignmentsldexp_finalize_syms的工作。它们会按照前面提到的方法,对符号进行重定位。最后链接器还会检查符号和section的正确性。

第五步:交给bfd,输出文件

在完成重定位之后,如果没有出现异常,linker就把工作交给bfd了。ldwrite负责把链接好的文件输出。完成一些清理工作后,整个链接过程就结束了。