目录
1、动静态库的一些细节
- Linux中,是lib+库名+后缀。
- 库没有main函数,不然会冲突。
- 一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认为动态链接库,只有在该库下找不到动态,才会采用同名静态库。我们也可以使用gcc 的-static 强转设置链接静态库。
- 库 : 应用程序 = 1 : N。
- 可以在VS2022上创建库。
2、ELF文件
2.1 ELF文件的四种类型
- 可重定位文件(Relocatable File)。文件扩展名:.o(Unix/Linux)、.obj(Windows)。包含代码和数据,可与其他目标文件链接生成可执行文件或动态库。
- 可执行文件(Executable File)。即可执行程序。
- 共享目标文件(Shared Object File)。文件扩展名:.so(Linux)、.dll(Windows)。动态链接库,可在运行时被多个程序共享。
- 核心转储文件(Core Dump File)。文件扩展名:通常为core或core.<pid>。进程意外终止时的内存快照,用于调试。
2.2 ELF文件的结构组成
- ELF头(ELF Header)。描述文件的基本信息,如定位文件的其他部分。
- 程序头表(Program Header Table)。描述段(Segment)信息,指导操作系统如何加载程序。
- 节头表(Section Header Table)。描述节(Section)信息,供链接器和调试工具使用。
- 节(Sections)。存储特定类型的数据。常见节:.text(代码节):可执行代码(机器指令),.data(数据节):已初始化的全局/静态变量。
2.3 形成ELF可执行文件
- 将多份 C/C++ 源代码,翻译成为目标 .o 文件
- 将多份 .o 文件section进行合并
注意:
实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并,此处不做过多追究。
2.4 加载ELF可执行文件
- 一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment,提高内存使用效率。
- 合并原则:相同属性,如:可读,可写,可执行,需要加载时申请空间等。
- 显然,合并方式在形成ELF的时候,已经确定了。具体合并原则被记录在了 ELF的程序头表 (Program header table) 中。
2.5 ELF加载与进程虚拟地址空间
问题:
- 一个ELF程序,在没有被加载到内存的时候,有没有地址呢?
- 进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?
答案:
- 有地址。采用“平坦模式”编址(统一的线性地址),起始地址为0(或平台约定的基址),为ELF文件的虚拟地址。OS加载可执行文件时,用 ELF文件的虚拟地址 初始化 进程的虚拟地址空间。
- 从ELF各个 segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start,end] 等范围数据。
所以:虚拟地址机制,不光光OS要支持,链接器也要支持。
链接器:核心的虚拟地址分配者,写入ELF文件供OS加载。
操作系统:按ELF文件要求映射地址空间,并管理运行时调整。
3、静态库
3.1 静态库的链接可执行和加载
- 静态库(.a):本质是一个归档(打包.o)文件。不需要解包,直接使用gcc/g++进行链接。
- 程序在链接时,把静态库中程序需要的.o文件的代码合并(拷贝)到可执行文件中,程序运行的时候将不再需要静态库。
- 静态库与.o文件链接成ELF可执行文件,以ELF可执行文件为载体进行加载。
3.2 静态库的制作(了解)
ar -rc lib库名.a 依赖的.o文件
3.3 静态库的使用(了解)
// 场景1:头文件和库文件安装(拷贝)到系统路径下
gcc -o main main.o -l 库名
// 场景2:头文件和库文件和我们⾃⼰的源文件在同⼀个路径下
gcc -o main main.o -L . -l 库名
// 场景3:头文件和库文件有⾃⼰的独⽴路径
gcc -c main.c -I 头文件路径
gcc -o main main.o -L 库文件路径 -l 库名
// 或者 gcc -o main main.c -I 头文件路径 -L 库文件路径 -l 库名
- -L: 指定库路径。
- -I: 指定头文件搜索路径。
- -l: 指定库名。
4、动态库
4.1 动态库的加载
实际上,程序的入口点是_start函数。这是一个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。
_start函数会调用动态链接器(如ld-linux.so)的代码来解析和加载程序所依赖的 动态库(sharedlibraries)。
4.2 动态库的链接可执行
- 动态库(.so):程序在运行时,才去链接动态库的代码,多个程序共享使用库的代码。
- 实际上,动态链接,是将链接的过程推迟到了程序运行的时候。
如何链接动态库的代码?
程序运行时,加载动态库,有了库的起始虚拟地址+方法偏移量(编译时获取),定位库中的方法。
4.3 全局偏移量表GOT
程序运行时,有了库的起始虚拟地址,我们要对加载到内存中的程序的库函数调用处进行地址修改,在内存中二次完成地址设置 (这个叫做加载地址重定位)。
等等,修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?
所以:动态链接采用的做法是在 .data (可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。因为.data区域是可读写的,所以可以支持动态进行修改。
- 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的虚拟地址空间中,各动态库的虚拟地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。
- 在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的虚拟地址。这样,动态库,被加载到任意虚拟地址都能够正常运行,并且能够被所有进程共享。这种方式实现的动态链接就被叫做 PIC 地址无关代码
PIC = 相对编址 + GOT:
相对编址:动态库的代码段使用相对编址(偏移量)访问内部数据/函数。
GOT:数据段来存储运行时解析的虚拟地址。
注意:
- 不仅仅有可执行程序调用库。
- 库也会调用其他库!!库之间是有依赖的,如何做到库和库之间互相调用也是与地址无关的呢??
- 库中也有.GOT,和可执行一样!这也就是为什么大家为什么都是ELF的格式!
- 由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,操作系统还做了一些其他的优化,比如:延迟绑定,或者也叫PLT(过程连接表 (ProcedureLinkageTable))。与其在程序一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。
4.4 动态库的制作(了解)
gcc -fPIC -c .c文件 // 默认生成同名.o文件
gcc -shared -o lib库名.so 依赖的.o文件
4.5 动态库的使用(了解)
// 场景1:头文件和库文件安装(拷贝)到系统路径下
gcc -o main main.o -l 库名
// 场景2:头文件和库文件和我们⾃⼰的源文件在同⼀个路径下
gcc -o main main.o -L . -l 库名
// 场景3:头文件和库文件有⾃⼰的独⽴路径
gcc -c main.c -I 头文件路径
gcc -o main main.o -L 库文件路径 -l 库名
// 或者 gcc -o main main.c -I 头文件路径 -L 库文件路径 -l 库名
- -L: 指定库路径。
- -I: 指定头文件搜索路径。
- -l: 指定库名。
注意:
这时,gcc知道动态库的路径,但是操作系统不知道。
4.6 如何让OS找到动态库
- 拷贝至系统。将动态库安装(拷贝)到系统动态库默认路径下。
- 建立软连接。在系统动态库默认路径下建立一个软连接指向动态库。
- LD_LIBRARY_PATH。OS运行程序,查找动态库,也会在环境变量(LD_LIBRARY_PATH,一般是空的)下查找动态库。
- /etc/ld.so.conf.d/。在/etc/ld加载.so动态库.conf配置文件.d目录/路径下,创建一个文件,里面写入动态库的路径,再sudo ldconfig(重新加载配置文件)。