Linux内核系列:编译链接

预编译

第一步预编译的过程个相当于如下命令

1
$gcc -E hello.c -o hello.i

预编译主要处理源文件中以#开始的预编译命令

  • 将所有#define删除,并展开所有宏定义、
  • 处理所有条件编译指令,如#if,#ifdef,#elif,#else,#endif
  • 处理#include指令,将被包含的文件插入到该编译指令的位置。注意这可能是个递归的过程,也就是被包含的文件可能还包含其他文件.
  • 删除所有注释
  • 添加行号和文件名标识,如#2”hello.c” 2,以便编译时编译器产生调试用的行号信息和产生编译错误或警告时的行号提示。
  • 保留所有#pragma编译器指令,因为编译器必须使用他们。

编译

过程相当于

1
$gcc -S hello.i -o hello.s
  • 词法分析:有限状态机
  • 语法分析:语法树
  • 语义分析:静态语义、动态语义
  • 中间代码生成:生成三地址码
  • 目标代码生成:生成汇编代码
  • 优化

静态链接

静态链接就是将几个输出目标文件合并成一个输出文件。

  1. 最简单的就是按序叠加:

但是这种做法浪费空间并且有大量内存碎片。
2. 相似段合并:

整个链接过程分两步:

  • 空间和地址分配:扫描所有输出目标文件,获得各个段的长度,属性和位置,将输入目标文件的符号表中所有符号定义和符号引用手机起来,统一放到一个全局符号表,连接器能够获得所有输入目标文件的的段长度,将它们合并。
  • 符号解析和重定位:使用第一步的信息,读取输入文件中段的数据,重定位信息,进行符号解析和重定位。核心是重定位过程。

使用objdump查看链接前后地址的分配的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

a.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000028 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000068 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000068 2**0
ALLOC
3 .comment 00000036 0000000000000000 0000000000000000 00000068 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000009e 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 000000a0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA


b.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000044 2**0
ALLOC
3 .comment 00000036 0000000000000000 0000000000000000 00000044 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000007a 2**0
CONTENTS, READONLY


ab: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000003a 00000000004000e8 00000000004000e8 000000e8 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000058 0000000000400128 0000000000400128 00000128 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000004 0000000000600180 0000000000600180 00000180 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .comment 00000035 0000000000000000 0000000000000000 00000184 2**0
CONTENTS, READONLY

在链接之前,目标文件中所有的段的VMA都是0,因为虚拟空间还没有分配。在链接之后被分配了地址0x0000003a。

下一步就是符号地址的确定。
由于符号在段内的相对位置是固定的,因此函数名和变量的地址也就确定了,这时候我们需要给每个符号加上一个偏移量,使他们能够调整到正确的虚拟地址上。
完成上述空间和地址的分配步骤之后,就进行符号解析和重定位。
那么链接器需要知道哪些指令需要被调整。
事实上在ELF文件中有一个重定位表来专门保存。
使用objdump -r a.o来查看。

1
2
3
4
5
6
7
8
9
10
11

a.o: file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000023 R_X86_64_32 shared
0000000000000030 R_X86_64_PC32 swap-0x0000000000000004

RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text

OFFSET对应指令位置,VALUE对应符号.在连接过程中会扫描符号表,如果符号未定义,会报链接错误。
静态链接的缺点很明显,一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

动态链接

动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。下面介绍一下如何解决这两个问题。

  • 动态链接的过程:
    假设现在有两个程序program1.o和program2.o,这两者共用同一个库lib.o,假设首先运行程序program1,系统首先加载program1.o,当系统发现program1.o中用到了lib.o,即program1.o依赖于lib.o,那么系统接着加载lib.o,如果program1.o和lib.o还依赖于其他目标文件,则依次全部加载到内存中。当program2运行时,同样的加载program2.o,然后发现program2.o依赖于lib.o,但是此时lib.o已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的lib.o映射到program2的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。
  • 优缺点
    动态链接的优点显而易见,就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。但是动态链接也是有缺点的,因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
  • 如何重定位?
    前面我们讲过静态链接时地址的重定位,那我们现在就在想动态链接的地址又是如何重定位的呢?虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。