手把手带你了解链接全过程(一)
文章目录
目标文件
可重定位目标文件格式
首先要知道
- 目标文件就是纯粹的字节块的集合
- 这些块中有: 包含程序代码,程序数据,链接器与加载器的数据结构
.text
已编译程序代码.rodata
只读数据.data
已经初始化的全局和静态变量.bss
未初始化全局和静态变量.symtab
符号表,每一个可重定位目标文件中都有一个symtab
.rel.text
一个.text
节中位置列表.rel.data
被模块引用或者定义的全局变量的重定位信息debug
调试符号表 编译时候加入-g
生成.line
远吗行号与.text
中机器指令的映射,编译-g
得到.strtab
字符串表
目标文件分类
我们知道,通过as(汇编器),可以将汇编文件.s
翻译成目标文件
目标文件有以下几种
- 可重定位目标文件,包含二进制代码,可以与其他可重定位目标文件合并起来生成可执行目标文件
- 可执行目标文件,包含二进制代码个数据,可以直接将复制到内存并执行
- 共享目标文件,一种特殊的可重定位目标文件,可以在加载或者运行的时候被动加载进内存并链接
编译器和汇编器会生成可重定位目标文件(包括共享目标文件).链接器生成可执行目标文件.
一个目标模块是一个字节序列,一个目标文件就是一个以文件形式存放在磁盘的目标模块
链接器的作用
- 符号解析 , 目标文件定义和引用符号(函数,一个全局变量,一个静态变量).符号解析的是为了将每一个符号的引用和定义关联起来
- 重定位, 编译器和汇编器生成地址从0开始的代码和数据节,链接器通过将每个符号的定义与一个内存位置关联起来,从而定位出这个节,这时候就可以修改所有对这些符号的引用,使得都指向这个内存位置
就是先关联,再绑定内存,之后修改指向
符号和符号表
我们知道,每一个可重定位目标模块都有一个symtab
,里面有符号定义与引用的信息.
符号分为三种
- 由模块m定义并可以被其他模块引用的全局符号
非静态的C函数(默认是extern
的)以及全局变量
- 外部符号, 由其他模块定义被模块m引用的全局符号
其他模块中定义的非静态的C函数(默认是extern
的)以及全局变量
- 局部符号 只能被模块m定义和引用的局部符号
带有static
属性的C函数和全局变量.这些在模块m的任意位置可见,但是不能被其他模块引用相当于C++中的私有private
任何带有static
属性声明的函数以及全局变量都是私有的;任何不带有static属性声明的函数以及全局变量都是公共的可以被其他模块访问
.symtab
中不包含的符号由栈管理(局部static变量除外)
符号解析
符号解析的目的是,将每一个引用与它的输入的可重定位目标文件中的符号表symtab
中的定义关联起来
对于局部符号,编译器会确保它们有唯一的名字
对于全局符号,当遇到外部符号的时候,会假设该符号在其他的模块中有定义,会生成一个链接器符号条目表,并把它交给链接器处理.链接器会在所有的输入模块的全局符号中查找定义,如果没有查找到就会出错.一般是undefine reference to
所以声明可以声明多次,之后选择其中一个,因为声明是弱符号。
int a
是定义
extern int a
是声明,不分配空间,可以写多次
静态库
出现静态库的原因主要在于不方便将每一个实现都放在一个可重定位目标文件里面,如果这样的话,引用一个实现就需要在内存中加载整个.o
文件,且有一点改动都需要完成的重新编译.
所有有了静态库的概念,静态库其实就是一组可重定位目标文件,在执行文件时,只会赋值静态库中被应用程序引用的目标模块
链接器如何引用静态库
**过程如下:
所以才会有这个链接顺序问题
gcc ./libvector.a main.c
这时候如果mian.c
中用到了存档文件中的函数或者全局变量就会报错
因为,当libvector.a
取匹配的U中未引用符号的时候,会匹配不到,其中的可重定位目标文件
就不会加入E
最后导致加入main.o
文件时U
非空,链接出错
到这里符号解析就全部完成了
重定位
当符号解析完成以后.每一个符号引用都会对应一个符号定义.这时候链接器就知道输入的目标模块中的代码节.text
和.data
的大小(所以数据函数的大小在编译的时候就知道啦),就可以开始重定位
重定位有两个步骤一共
- 重定位节和符号定义
将输入的目标文件的所有的相同类型的节合并成一个聚合节成为可执行文件的一个节,比如所有的.data
聚合成可执行目标文件的.data
节.
之后链接器将运行时内存地址赋予新的聚合节,这样所有的符号定义都有唯一的运行时候的内存地址了
- 重定位节中的符号引用
修改代码节和数据节中的符号引用,使它们都指向正确的运行时候的地址
也就是运行时候的地址是唯一的,是在重定位的时候确定的
加载可执行目标文件
./prog
终端执行该命令的时候,因为prog
不是shell
内置的命令.所以.shell
会认为prog
是一个可执行的目标文件(目标文件类型的第二种).会通过execve
函数来调用加载器,将可执行目标文件的代码和数据从磁盘复制的内存,并跳转到入口点来执行函数.这时候就会运行一个内存映像,也就是虚拟内存
动态链接库
首先得了解共享库的概念
共享库是一个目标模块(目标文件类型的第三种)在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程就叫做动态链接
共享库的作用
- 相比于静态库更新需要重新编译部署,动态库更新只需要更换动态库的内容就可以
- C的标准
I/O函数
几乎都会用到,如果每一个进程都将这些函数的代码加载进内存,那么几百个进程就会造成内存资源的浪费,动态库只会加载一份,所有的程序都共同享有.共同享有的是代码和数据
在Linux
下动态库.so
结尾.
不同于静态链接,
静态链接是在链接器的作用下会把所有重定位目标文件的代码和数据聚合到可执行目标文件,最终形成一个完全链接的可执行文件
动态链接的过程中,首先会静态执行一部分链接,在时候的程序的加载和运行的时候在动态完成链接的过程.这时候libvector.so
的代码和内容并没有真正的复制到可执行文件prog里面,而是复制了一些重定位和符号表信息
当我们真正的记载运行可执行目标文件prog
的时候,这时候prog
中有一个.interp
节含有动态链接器(ld-linux.so
)的路径名.本身也是一个共享目标,程序会加载这个共享目标完成以下任务
-
重定位
libc.so
的文本个数据到某一个内存段 -
重定位
libvector.so
的文本个数据到某一个内存段 -
重定位
prog
中所有由libc.so
和libvector.so
定义的符号的引用
最后动态链接器将程序归还应用程序.这个时候,共享库的位置在内存中就固定了,并且在运行的过程中都不会改变
应用程序中加载和链接共享库
上面说的都是,在程序被加载后但是在执行之前动态加载和链接动态库的情景.
应用程序可以在运行的时候手动要求动态链接器加载和链接某个共享库,不需要在白编译的时候将库的链接到应用中
主要有两个作用
- 分发软件 其实就是版本更新
- 构建高性能Web服务器
工作方式类似于
涉及函数
dlopen
dlsym
dlclose
dlerror