前面介绍了单个.o文件的格式以及里面的内容。那么如果我们有多个目标文件,如何将它们链接成一个可执行的文件呢。多个目标文件就涉及到了链接。我们首先介绍静态链接。
有如下2个文件,test.c/test1.c
test.c
#include<stdlib.h>
#include<stdio.h>
extern int shared;
int main(){
int a=100;
swap(&a,&shared);
}
test1.c
#include <stdio.h>
int shared=1;
void swap(int *a,int *b){
int temp;
temp=*a;
*a=*b;
*b=temp;
}
首先我们通过gcc分别得到2个文件的.o文件。 gcc -c test.c test1.c
从代码中可以看到,test1.c总共定义了两个全局符号,一个是变量shared,一个是函数swap。test.c里面引用到了test1.c中的shared和swap。接下里的工作就是要把test.o和test1.o这两个目标文件链接在一起形成一个可执行的文件。
前面介绍过每个目标文件都有代码段,数据段等等。那么多个目标文件链接在一起的话,这些段该如何组合呢。
一个最简单的方法就是叠加,如下图所示,在最终的目标文件中,把所有的目标文件都依次叠加。这样做的确很简单明了。但是会造成一个问题,在有很多输入文件的情况下,输出文件将会有很多零散的段。比如一个规模稍大的工程就有好几百个文件。而且每个文件都有.data,.text.bss段。那么最后输出的文件将会有成百上千个零散的段。这样非常浪费空间,而且每个段都必须要用一定的地址和空间对齐要求。比如X86的硬件,段的装载地址和空间的对齐单位是页,也就是4096字节。如果一个段的长度是1个字节。在内存中就要占用4096字节。多个这样的段将会极大的浪费内存。
那么第二种方法就是相似段合并了。我们把目标文件的.data,.text,.bss段分别整合在一起。如下图所示。
这种链接方法内部分为两个步骤:
第一步: 空间与地址分配
扫描所有的输入目标文件,获得它们的各个段的长度,属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来。统一放到一个全局符号表。这一步中,链接器能够所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
第二步:符号解析与重定位
使用上面收集到的所有信息,读取输入文件中段的数据,重定位消息,并且进行符合解析与重定位,调整代码中的地址等
比如我们将test.o和test1.o链接起来生成一个可执行的文件
gcc test.o test1.o -o ab
首先来看test.o的文件
root@zhf-maple:/home/zhf/c_prj# objdump -h test.o
test.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000051 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000091 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000091 2**0
ALLOC
objdump -h test1.o
test1.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 0000002d 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 00000070 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000074 2**0
ALLOC
objdump -h ab
ab: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .text 00000202 0000000000000560 0000000000000560 00000560 2**4
22 .data 00000014 0000000000201000 0000000000201000 00001000 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .bss 00000004 0000000000201014 0000000000201014 00001014 2**0
ALLOC
24 .comment 00000023 0000000000000000 0000000000000000 00001014 2**0
CONTENTS, READONLY
从上面的结果可以看到,在链接之前,VMA的地址都是0,这是因为虚拟空间还没有分配。等到链接之后,可执行文件ab中的各个段都被分配到了相应的虚拟地址。在ab中.text段被分配到了0000000000000560,大小为0x202字节。.data段从地址0000000000201000开始。大小为14字节。
完成空间和地址的分配步骤以后,链接器就进入了符号解析与重定位的步骤。这也是静态链接的核心。在分析符号解析和重定位之前。我们首先看下test.o里面是怎么使用这两个外部符号的。
objdump -d test.o
test.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
f: 00 00
11: 48 89 45 f8 mov %rax,-0x8(%rbp)
15: 31 c0 xor