程序员的自我修养-静态链接与动态链接
静态链接与动态链接
程序员的自我修养-静态链接与动态链接
静态编译
空间与地址分配
多个目标文件合并,生成的可执行文件代码段和数据段怎么合并?
$gcc -c a.c b.c $ld a.o b.o -e main -o ab
按序合并
缺点:空间浪费,由于每个段都有地址对齐要求
相似段合并(常用)
两步链接:
- 空间与地址分配:扫描所有输入目标文件,获取各个段的长度等信息,并将输入目标文件的符号表中的所有符号定义和符号引用收集起来,统一放到一个全局符号表;
- 符号解析与重定位:使用上述收集到的信息,读取文件中的段的数据、重定位信息,并且进行符号解析和重定位等。
- 虚拟地址在链接之后被分配
- 在Linux下,ELF可执行文件从默认地址0x08048000开始分配
重定位
a.o文件是怎么访问shared变量和swap变量的?
链接器通过符号表的地址对每个需要重定位的指令进行修正。
重定位表
重定位表:用来调整需要调整的指令,专门保存这些与重定位相关的信息;
指令修正方式
jmp指令、call指令、mov指令等
- 绝对寻址
- 相对寻址
- 等等
COMMON块
弱符号:在C语言中,函数和初始化的全局变量(包括显示初始化为0)是强符号,未初始化的全局变量是弱符号
对于它们,下列三条规则使用:
同名的强符号只能有一个,否则编译器报"重复定义"错误。
允许一个强符号和多个弱符号,但定义会选择强符号的。
当有多个弱符号相同时,链接器选择最先出现那个,也就是与链接顺序有关。
- 如果一个弱符号定义在多个目标文件中,而他们类型又不同,怎么办?
- 两个或者两个以上强类型不一致;(链接器报错)
- 有一个强符号,其他都是弱符号,出现类型不一致;
- 两个或者两个以上弱符号类型不一致;
COMMON块机制:沿用Fortran机制,当不同目标文件需要的COMMON块空间大小不一致时,以最大的那块为准(这里只是占用空间以大的为准)
C++相关问题
- c++的语言特性使得必须由编译器和链接器共同支持才能完成工作,如c++重复代码消除、全局构造和析构等。
- 虚函数、函数重载等背后的数据结构异常复杂,这些数据结构往往在不同的编译器和链接器之间不能通用,使得c++程序的二进制兼容性成为了一个很大的问题。
重复代码消除
- 如当一个模板被实例化成相同的类型后会产生重复的代码
解决办法:将每个模板的实例代码单独存放在一个段里,每个段只包含一个模板实例。如:add —— .temp.add .temp.add
全局构造与析构
- c++全局对象的构造在main函数之前执行,析构函数在main之后执行
静态库链接
程序如何使用操作系统提供的API?
一种语言的开发环境会附带语言库,这些库就是对操作系统API的封装。(如printf在Linux下是“write”系统调用,Windows下是“WriteConsole“系统调用)
动态链接
- 为什么要使用动态链接?
- 节约内存和磁盘空间
- 程序的开发与发布:使用静态链接,如果通过网络来更新程序,一旦程序改动一个小模块,整个程序都需要重新下载
**动态链接的基本思想:**把链接的过程推迟到运行时再进行
动态链接优点与缺点:
- 运行时动态的选择和加载各种程序模块,这个优点后来被用作制作程序的插件。
- 消除了不同平台依赖的差异性:如操作系统A和B对于printf的实现机制不同,如果是静态链接的,程序需要分别链接成能够再A和B上运行的两个版本;如果是动态链接,只需要操作系统提供一个动态链接库包含printf,则程序只需要一个版本,就可以动态的选择printf的实现版本。
- DLL Hell:由于缺少一个有效的共享库版本的管理机制,使得用户出现新程序安装后,其他程序无法正常工作的现象。(这是因为某个新的模块与旧的模块之间接口不兼容,新安装的软件的模块覆盖了旧的模块)
- 动态链接会有性能损失
在静态链接中,整个程序只有一个可执行文件;但在动态链接下,一个程序被分成了可执行文件和所依赖的共享库
链接器如何知道某个函数函数是一个静态符号还是一个动态符号?
动态库保存了完整的符号信息
动态链接程序的虚拟空间分布
多了文件的映射
地址无关代码
重定位:重定位是指在程序执行时将程序中的某些地址改为另一个地址的过程。主要是由于在编译和链接程序时,使用了相对地址而非绝对地址,导致程序在运行时无法直接加载到正确的内存位置。
共享对象在被装载时,如何确定进程在虚拟空间的位置?
静态重定位:在程序运行之前,即在程序装入内存的过程中完成,就已经把程序中需要进行地址改变的部分确定下来,并将这些部分的地址修正为正确的绝对地址。这种方式常常使用在可执行文件的链接过程中。
问题:静态共享库的升级必须保持共享库的全局函数和变量地址不能改变,不然库升级后必须重新链接
动态重定位:在程序运行时,根据实际的内存情况对程序中的地址进行修正,使得程序能够正确地运行。更确切地说,是在CPU每次访问内存单元前才进行地址变换。
延迟绑定
在程序运行过程中,可能很多函数在程序执行完时都不会用到,所以有了延迟绑定的做法,基本思想就是当函数第一次被用到时才进行绑定。
动态链接相关结构
interp段
- 动态链接器的位置由可执行文件决定。可执行文件中有一个‘.interp’段保存了动态链接器的位置。
- 可以使用如下命令查看一个可执行文件的命令:
- 动态链接器的位置由可执行文件决定。可执行文件中有一个‘.interp’段保存了动态链接器的位置。
dynamic段
- 这个段保存了动态链接器所需要的基本信息,比如依赖哪些共享对象、动态链接符号表的位置等
动态符号表
动态符号表的段叫做’dynsym’, 动态符号表表示了符号导入导出关系,不包括模块内部的符号。
动态链接重定位表
- got表:用来存储外部变量
- got.plt表:用来存储外部库函数,一个表项对应一个库函数
- .plt表:每个外部库函数对应plt中的一段代码
- 重定位表
其中,‘rel.dyn’是对数据引用的修正,修正的位置位于’.got’以及数据段;’.rel.plt’是对函数引用的修正,修正的位置位于’.got.plt’。
动态链接的步骤
- 动态链接器自举
- 动态链接器自己也是一个动态对象’ld.so’,保证以下条件:
- 不依赖于其他对象:编写时保证不使用任何系统库、运行库
- 动态链接本身所需要的全局和静态变量的重定位由它自己完成:由启动时一段代码来完成这项艰巨额工作,这种启动代码被称为自举
- 动态链接器的入口地址就是自举代码的入口
- 动态链接器本身是静态链接的
- 装载共享对象
- 完成基本自举以后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中,称为全局符号表
- 当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略
- 重定位和初始化
- 遍历可执行文件和每个共享对象的重定位表,将他们的GOT/PLT中的每个需要重定位的位置进行修正
显式运行时链接
运行时加载:让程序自己在运行时控制加载指定的模块
- 动态库的装载是由一系列动态链接器提供的API完成:
- dlopen:打开动态库
- disym:查找符号
- dlerror:错误处理
- dlclose:关闭动态库
区分:静态链接 动态链接 静态重定位 动态重定位 显式运行时链接
可执行文件的装载与进程
进程虚拟地址空间
- 每个程序被运行起来后,都拥有自己独立的虚拟地址空间,虚拟地址空间大小由cpu位数决定
- C语言指针大小的位数与虚拟空间的位数相同
- 32位平台下的4GB空间,我们的程序能否能够任意使用?
- 不行,进程只能使用操作系统分配给进程的地址,访问未经许可的空间时就属于非法操作。如Linux下”Segmentation fault“。
- Linux操作系统虚拟地址空间分配
- windows下进程虚拟地址空间划分给操作系统的占用2GB(可以修改配置文件将操作系统占用减少到1GB)
PAE
- 32位CPU下,程序使用的空间能否超过4GB? 如果空间指定时虚拟地址空间,不行,因为32位CPU只能使用32位指针;如果空间值得是计算机内存空间,可以 PAE技术(Physical Address Extension):扩展地址线位数,修改页映射方式,使得新的映射方式可以访问到更多的物理内存。在Windows下,这种访问内存的方式叫做AWE,在Linux操作系统中使用mmap系统调用来实现。
装载的方式
覆盖装入
程序员在编写程序时手动将程序分割成若干块,然后编写一个小的辅助代码来管理这些模块合适应该常驻内存以及何时被替换掉,这个小的辅助代码就是覆盖管理器。页映射
可执行文件的装载
进程的建立
- 创建一个独立的虚拟地址空间
- 读取可执行文件头,建立虚拟地址空间与可执行文件的映射关系
- 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行
页错误
- 当cpu打算执行某个地址的指令时,发现为空页面,于是认为是一个页错误
- cpu将控制权交给操作系统,操作系统查询数据结构,找到空页面说在的VMA,计算出偏移量
- 再物理内存中分配一个物理页面
段地址对齐
可执行文件最终是要被操作系统装载运行的,这个装载的过程一般是通过虚拟内存的页映射机制完成的。
- 在映射过程中,页是映射的最小单位
- 对于X86系列处理器来说,默认的页大小是4096字节,即我们需要建立物理内存和进程虚拟地址空间的映射关系,并且这段内存空间的长度必须是4096的整数倍
对齐方式
- 长度不足的段仍然占用一个页
- 相邻页合并(unix常用)
进程栈初始化
- 在进程启动前将信息提前保存到进程虚拟空间的栈中
//环境变量 HOME = /home/user PATH = /usr/bin
$ prog 123
- 进程在启动以后,程序的库部分会把堆栈里的初始化信息中的参数信息传递给main函数,也就是main函数的两个参数
Linux内核装载ELF过程简介
- bash进程会调用fork系统调用创建一个新的进程
- 在新的进程调用execve系统调用执行ELF文件
- 原先的bash进程继续返回等待启动的新进程结束,然后等待用户输入命令