[What]链接、装载与库 --> 静态链接

了解静态链接的详细过程。

空间与地址分配

按序叠加

简单粗暴的将各个目标文件的各个段按照顺序依次放入最终链接的文件。

此方法最大的缺点是会产生很多的零散段,各个文件的段有一定的地址和空间对齐要求,这会导致文件空间过大。会造成内存空间大量的内部碎片。

相似段合并

将文件中相似的段组合在一起排列。这种链接方法更为合理,一般都采用两步链接的方法(Two-pass Linking).

  • 第一步空间与地址分配:扫描所有的输入目标文件,获得它们各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。

合并所有输入目标文件,计算出输出文件中各个合并后的长度与位置,并建立映射关系。

  • 第二步符号解析与重定位: 使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。

示例代码:

//file: a.c
extern int shared;
extern void swap(int *a, int *b);

int __stack_chk_fail;

int main(void)
{

int a = 100;
swap(&a, &shared);

return 0;
}
//file: b.c
int shared = 1;

void swap(int *a, int *b)
{

*a ^= *b ^= *a ^= *b;
}
  • 编译后链接:
gcc -c a.c b.c
ld a.o b.o -e main -o ab
  • 比对输出:
cec@virtual:~/learn/c/static_link$ objdump -h a.o

a.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000004a  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  0000008a  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  0000008a  2**0
                  ALLOC
  3 .comment      00000036  0000000000000000  0000000000000000  0000008a  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000c0  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
cec@virtual:~/learn/c/static_link$ objdump -h b.o

b.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000004b  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  0000008c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000090  2**0
                  ALLOC
  3 .comment      00000036  0000000000000000  0000000000000000  00000090  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000c6  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000c8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
cec@virtual:~/learn/c/static_link$ objdump -h ab

ab:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000095  00000000004000e8  00000000004000e8  000000e8  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .eh_frame     00000058  0000000000400180  0000000000400180  00000180  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .data         00000004  00000000006001d8  00000000006001d8  000001d8  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .bss          00000004  00000000006001dc  00000000006001dc  000001dc  2**2
                  ALLOC
  4 .comment      00000035  0000000000000000  0000000000000000  000001dc  2**0
                  CONTENTS, READONLY
  • 链接完成后, __stack_chk_fail 被确定为在bss段
  • 虚拟地址和装载地址在链接后被确定了
  • main 函数位于代码段起始,仅跟着后面的是函数 swap

符号解析与重定位

重定位

在完成空间和地址的分配步骤后,链接器就进入了符号解析与重定位的步骤。

为了能够让链接器知道哪些地址需要重定位,在目标文件中就会有一个重定位表。 对于每个要被重定位的ELF段都有一个对应的重定位表,而一个重定位表往往就是一个ELF文件中的一个段, 所以重定位表也叫作重定位段。

比如代码段 ".text" 如有被重定位的地方,那么会有一个相对应的叫 ".rel.txt" 的段保存了代码段的重定位表。

使用命令 objdump -r hello.o 来查看重定位表。

每一个要被重定位的地方叫一个 重定位入口(Relocation Entry).

cec@virtual:~/learn/c/static_link$ objdump -r a.o

a.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000023 R_X86_64_32       shared
000000000000002b R_X86_64_PC32     swap-0x0000000000000004
0000000000000044 R_X86_64_PC32     __stack_chk_fail-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text
  • 比如上面表示 shared 需要重定位到代码段的 0x23 偏移处

对应重定位表的数据结构为:

typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
} Elf64_Rel;

符号解析

重定位过程中也伴随着符号的解析过程,每个目标文件都可能定义一些符号,也可能引用到定义在其他目标文件的符号。重定位的过程中,每个重定义的入口都是对一个符号的引用, 当链接器需要对某个符号的引用进行重定位时,它就需要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。

当链接器没有找到需要被重定位符号的对应绝对符号时,就会报错 undefined reference to `***`.

使用命令 readelf -s hello.o 来查看符号表

指令修正方式

被重定位的地址修正具有绝对地址修正和相对地址修正:

  • 绝对地址修正后的地址为该符号的实际地址
  • 相对地址修正为符号距离被修正位置的地址差

COMMON 块

当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号(未初始化的全局变量就是典型的弱符号),那么该弱符号最终所占用空间的大小在此是未知的, 因为有可能其他编译单元中该符号所占用的空间比本编译单元该符号所占的空间要大。所以编译器此时无法为该弱符号在BSS段分配空间,因为所需要空间的大小未知。但是链接器 在链接过程中可以确定弱符号的大小,因为当链接器读取所有输入目标文件后,任何一个弱符号的最终大小都可以确定了,所以它可以在最终输出文件的BSS段为其分配空间。

所以总体来看,未初始化全局变量最终还是被放在BSS段的。

GCC 的 -fno-common 允许我们把所有未初始化的全局变量不以 COMMON 块的形式处理,或者使用 __attribute__ 扩展

int global __attribute__((nocommon));

节省输出文件的大小

GCC编译器中提供了编译选项 -ffunction-sections-fdata-sections ,作用是将每个函数或变量分别保存到独立的段中, 这样链接器在链接时只将最终代码用到的函数和数据链接进输出文件中去,减小输出文件的大小。但由于编译的分段操作和链接的查询操作, 使得最终生成输出文件的时间会比普通方式增加不少。

静态库链接

静态库可以简单地看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。

在linux中通过使用 ar 程序将目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索,就形成了 libc.a 这种静态库。

  • 使用 ar -t libc.a 来查看 libc.a 库中包含了哪些目标文件。
  • 使用 objdumpreadelf 加上 grep 就能够找到调用的函数属于库中的哪个目标文件。

链接过程控制

对于一些特殊需求的情况下(比如嵌入式),需要指定链接地址以控制代码的运行过程。

链接控制脚本

链接器有如下三种方法来控制链接过程:

  • 使用命令行来给链接器指定参数。(比如使用 ld -o)
  • 将链接指令存放在目标文件里面,编译器经常会通过这种方法向链接器传递指令
  • 使用链接控制脚本(最为灵活而强大)。

当不指定链接脚本时,ld 使用默认脚本,使用命令 ld -verbose 打印出默认的链接脚本。

ld 链接脚本语法简介

链接脚本由一系列语句组成,语句分为两种,一种是 命令语句 ,另外一种是 赋值语句 。 链接脚本语法与 C 语言有如下相似之处:

  • 语句之间使用分号 ";" 作为分割符。
    • 命令语句可以使用换行来结束该语句
    • 赋值语句必须以 ";" 结束。
  • 脚本语言可以使用C语言类似的表达式和运算操作符。比如 "+,-,*,/,+=,-= , *= ,&,|,>>,<<"
  • 注释和字符引用。 使用 /**/ 作为注释。脚本文件中使用到的文件名、格式名、段名等凡是包含 ";"或其他的分隔符的,都要使用双引号将该名字全称引用起来,如果文件名包含引号则无法处理。
常用的命令语句 说明
ENTRY(symbol) 指定 symbol 的值为入口地址(代码运行的第一条指令的地址)。
STARTUP(filename) 将文件 filename 作为链接过程中的第一个输入文件
SEARCH_DIR(path) 将路径 path 加入到链接器的库查找目录
INPUT(file,file,…) 将指定文件作为链接过程中的输入文件
INCLUDE filename 将指定文件包含进本链接脚本,类似于 #include
PROVIDE(symbol) 在链接脚本中定义某个符号。该符号可以在程序中被引用

入口地址设置的优先级依次如下:

  1. ld命令行的 -e 选项
  2. 链接脚本的 ENTRY(symbol) 命令
  3. 如果定义了 _start 符号,使用 _start 符号值
  4. 如果存在 .text 段,使用 .text 段的第一字节的地址
  5. 使用0

SECTIONS 命令

SECTIONS
{
  ...
  /*
  符合 contents 中的规则的输入文件段将合并到输出文件段 secname 中

 注意:secname 后面要跟空格。 

  当 secname 为 "/DISCARD/" 时,代表其contents中符合条件的段都将被丢弃
  */
  secname : {contents}
  ...
}

contents 中可以包含若干个条件,每个条件之间以空格隔开,如果输入段符合这些条件中的任意一个即表示这个输入段符合 contents 规则。

条件写法为: filename(sections)

  • file1.o(.data) 表示file1.o文件中名为 .data 的段符合条件
  • file1.o(.data .rodata) 或 file1.o(.data, .rodata) 表示file1.o文件中的 .data或.rodata段符合条件
  • file1.o 如果直接指定文件名而省略后面的小括号和段名,则代表所有段都符合条件
  • *(.data) 所有文件中的 .data 段符合条件
  • [a-z]*(.text*[A-Z]) 所有输入文件中以小写字母a到z开头的文件中的所有段名以.text开头,并且以A到Z结尾的段,符合条件。

BFD库

BFD库将标准的目标文件进行了一层抽象,让编译器操作这些抽象过的目标文件。

这样就简化了编译器的复杂度,最终的不同平台下的目标文件则是由BFD去负责管理。

Last Updated 2018-11-26 Mon 07:31.
Render by hexo-renderer-org with Emacs 26.1 (Org mode 9.1.14)