编译过程、静态库和动态库
C/C++代码到可执行文件
从代码到最终的程序,分为4个步骤,分别是:
-
预编译:编译预处理,比如
#define
和#ifndef
之类的,C++的inline
也是在这期间处理的 - 编译:把代码转换成汇编,包括词法分析、语法分析和语义分析等
-
汇编:把汇编语言转换成机器码,输出的是目标文件。比如
*.o
的文件 - 链接:把目标文件聚合成可执行文件
链接:链接本质上是把多个ELF文件,按照符号符号等拼接成执行文件的过程,这个过程会给出各个符号最终的地址,供程序寻址。
链接和链接器
- 重定位:以单个执行文件为例,程序中某个代码片段有删减的时候,变化的代码片段之后的代码段的地址都发生了变化,重定位就是重新计算变化后的代码的地址。
- 符号:汇编语言中的概念,表示一段代码或一个变量的地址
程序模块化开发时,代码分成多文件多模块,此时每个模块进行对应的开发、测试等。
模块化之后,每个对应的模块都有自己的*.o
目标文件,多模块拼接时,需要解决不同模块之间的符号链接问题。不同模块之间的符号引用,最终都要使程序找到对应的地址。
链接的文件是目标文件,目标文件存储的是机器码
链接符号:
- 本目标文件的全局符号可以被其它目标文件引用
- 目标文件引用符号,在本文件中无定义,去其他文件中找,称为全局符号
目标文件的结构
ELF:Executable Linked Format
ELF文件 | 说明 | 格式 |
---|---|---|
可重定位文件 | 包含代码和数据,链接成可执行文件或者共享目标文件 | .o .obj |
可执行文件 | 可以被操作系统执行 | .exe |
共享目标文件 | 链接器使用共享目标文件,与可重定位文件或者共享目标文件链接,称为新的目标文件 | .so .dill |
核心转储文件 | 进程意外终止时,OS把该进程地址空间的内容和一些其它信息,转储到核心文件中 | Linux下core dump |
ELF文件结构:
几个核心的片段:
- ELF Header:一些核心的信息,比如操作系统的平台,段表:描数文件各个段信息的数据。
- .text:代码片段,指的是机器码
- .data:数据段,初始化的全局变量,一些资源之类的
- .bss:未初始化的片段,这个不占存储空间
data和text片段分离的优势:
- 多进程下,text共享,进程只需要有自己的data即可
- 不同访问情况有不同的权限,text只读,data读写,这样方便处理权限
可执行文件的装载和进程
进程置于内存中,以页的形式管理,如果要找的进程中地址不存在,则缺页中断,进行置换,置换到有关的内存页之后,重新加载对应的数据。
ELF的各个段大小不一样,我们称为section;而进程是分段管理的,我们称为segment。为了更高效的利用内存空间,我们把权限小姑娘同的的各个section的内容,有选择地放在一起加载,如下图:
调用ELF的过程:
bash->fork子进程A->A执行execute对应的ELF->A返回执行结果->bash等用户输入
静态链接
不同模块编译出不同的目标文件,比如Linux下的.a
文件,最终把多个模块的目标文件组合成一个可执行文件。不同模块会加入到可执行文件的不同位置中,这样对应的符号就可以找到对应的地址的了,如下图:。
为了高效利用空间,都会像上图所示,把相同段的内容合并到一起。
优势:
没有外部依赖,运行速度快
缺陷:
- 文件膨胀,如果有模块ProgramA和ProgramB都依赖lib.o,且有程序依赖PA和PB,则lib.o最终会有两份。
- 更新麻烦:
如果一个静态库更新,则所有有关的库都要重新链接
动态链接
- 所有的依赖不加载到可执行文件中,仅仅保留符号链接
- 用到符号链接时,再动态加载链接库,并计算有关地址,延迟绑定。
- 装载时定位,用到对应的组件,再装在动态库,此时定位符号地址;而静态库是初始化全部加载