【深入理解计算机系统03】程序的链接
第三篇:程序的链接
> 链接器的由来
高级语言出现之后,需要多人开发不同模块。
链接步骤:
1) 确定符号引用关系
确定符号的定义
2) 合并相关.o文件
同一地址空间,安排虚拟地址空间
3) 确定每个符号的地址
4) 在指令中填入新的地址
使用链接的好处:
1. 模块化
2. 提高编译效率和节省内存空间(共享库的复用)
只需要重新编译被修改的源程序文件
> 目标文件格式
分类:
1. 可重定位目标文件:.O文件,地址和数据从0开始
2. 可执行目标文件:可以直接被复制到内存中执行。这里发生了地址映射,程序可以被访问到了。
3. 共享的目标文件 Linux中的.so,可动态装入内存。windows 下共享库文件 .dll
重点:区分可重定位目标文件和可执行目标文件,ELF
以 Linux 为例
//每个模块有代码和数据(初始化/未初始化全局变量,静态变量、局部变量)
gcc -o2 -g
链接过程本质:合并相同的节
将不同可重定位目标文件中的
.text
.data
.bss
分类合并,得到可执行文件。
可执行文件存储映像
合并到虚拟地址空间(并非内存)
目标文件的格式:
关键词:
目标代码
目标文件
.bss 未初始化的全局变量和局部静态变量
.data 已经初始化的全局变量和局部静态变量
节是ELF文件中具有相同特征的最小可处理单位。
从两种角度来看:
1. 链接视图——可重定位目标文件
ELF
.bss节不占据磁盘空间,不须记录初始值,只声明占据的内存空间
ELF 头:
分为64位和32位版本
头信息:最先的四个字节是魔数
读取方式 readelf -h main.o
ELF 节头表:
记录每一个节的起始位置,大小,对齐方式进行记录。
32位的表项每个大小为40B
有四个节会分配存储空间:
.text
.data
.bss
.rodata (read only data)
对齐方式:不同的对齐方式对于内存的分配的地址安排会有不同结果。
2016-09-14
2. ELF执行视图——可执行文件格式
重点:ELF头程序头表:表述节和段之间的关系
可执行文件的存储器映射:
> 第三节 符号表和符号解析
通过一个实验来了解符号解析:
生成一个可执行文件时,要进行符号解析,因而需要有符号表来确定包含在程序模块中的被定义的所有符号。
>>符号表:
符号有三种类型:
1. 全局符号:被其他模块引用的非静态全局变量名。
2. 外部符号:在其他模块中定义的外部函数名和外部变量名。
3. 本地符号:static函数和全局变量名。
>>符号解析:
1. 全局符号的强弱型
已初始化的全局变量名是强符号,未初始化的全局变量名是弱符号。
规则:
1. 强符号重复定义链接会起冲突。
2. 强定义遇到弱定义,服从强符号定义
3. 若有多个若符号定义,则任选其一。
实验一:
//main.c
#include <stdio.h>
int x=10;
void p1(void);
int main()
{
p1();
printf("x=%d \n",x);
return 0;
}
//test.c
int x;
void p1()
{
x=200;
}
> gcc -o test main.o test.o
> ./test
x=200
解说:p1模块中的 x 地址等于 main 模块中的 x 的地址,即二者视为同一个变量。
实验二:
//main.c
#include <stdio.h>
int x=10;
void p1(void);
int main()
{
char a = 'a';
p1();
printf("x=%d \n",x);
printf("x size is %d\n", sizeof(x));
printf("\'x\' ASCII = %d \n", 'x');
printf("size of char is %d\n",sizeof(a));
return 0;
}
//test.c
char x;
void p1()
{
x = 'x';
}
$ gcc -o test main.o test.o
$ ./test
x=120
x size is 4
'x' ASCII = 120
size of char is 1
解说:强弱立见。
另外:
printf("%d",sizeof('a'));
在这里输出结果是4,因为 'a',会先转化为对应 ascii 96,作为一个 int,大小就变成4了。可以先声明一个变量char,sizeof(char) 还是 1;
>> 与静态库的链接
生成的可执行文件能直接加载到存储器执行,不需要在加载或者运行时再动态链接其他模块。
> 重定位
重定位:在符号解析的基础上将所有关联的目标模块合并,并确定运行时每个定义符号在虚拟地址空间中的地址,在定义符号的引用处重定位引用的地址。
节和定义符号的重定位,还有对于引用的符号,要去溯源。
重定位信息:见 .rel.text节和 .rel.data节
重定位过程:略
> 可执行文件加载
调出加载器,根据可执行目标文件中的程序头表信息,将可执行目标文件中的相关节的内容与虚拟地址空间中的只读代码段和可读写数据段通过页表建立映射,然后启动可执行目标文件中的第一条指令执行。
连续的只读代码段和可读写数据段,是的加载器对连续区域分页和初始化变得简单。
加载时,只读代码段和可读写数据段对应页表初始化为未缓存页(涉及到操作系统和组成原理知识),指向磁盘上目标文件中某个地方。所以,程序加载过程中,并没有真正从磁盘上加载代码和数据到主存,只是创建了相应的页表项。在执行代码的过程中发生缺页,才会从磁盘中加载代码和数据。
关键技术:虚拟存储管理
> 动态链接
共享库,『共享』『动态』
加载器:一个操作系统
动态链接器
》总结:
两种链接方式:
静态链接
动态链接
链接涉及到的三种目标文件格式:
1. 可重定位目标文件
2. 可执行目标文件
3. 共享目标文件
两种ELF目标文件格式:
1. 链接视图:可重定位目标文件
2. 执行视图:可执行目标文件格式
链接过程中的工作:
1. 符号解析:将符号引用同定义关联起来。
2. 重定位:找到在虚拟地址空间中的位置,确定每个符号的最终存储地址。
这里主要用到的工具:
OBJDUMP
Readelf –elf 显示重定位
再看一遍链接本质: