Linux : 动静态库制作、ELF格式
Linux:基础IO && 文件系统
- 一、动态库和静态库
- (一)静态库
- 1、静态库制作
- 2、静态库链接
- 汇编阶段
- 链接阶段
- (二)动态库
- 1、动态库制作
- 2、动态库链接
- (三)链接原理
- 1、静态链接
- 二、ELF格式
- 可执行文件(Executable)
- 可重定位文件(Relocatable)
- 共享目标文件(Shared Object)
- 核心转储文件(Core Dump)
- (一)ELF组成
- 重要的节
- (二)ELF形成可执行
- (三)ELF常用指令
- (四)ELF转化成可执行程序
- 1、逻辑地址,物理地址,虚拟地址
- 2、磁盘加载程序
- 3、动态库如何与可执行程序关联
一、动态库和静态库
库是现有的成熟的可重复使用的代码,本质上是一种可执行的二进制形式(即
.o
,可被操作系统载入内存运行。
(一)静态库
.a
[Linux],.lib
[windows]
静态库实质是对.o
文件进行打包,生成.a
的归档文件,使用时直接用gcc, g++
链接即可。
1、静态库制作
ar
将文件进行归档,rc
即replace and create
。
2、静态库链接
汇编阶段
我们在程序中包含的头文件如果不在同一个路径下,系统是找不到的,因此需要使用-I
来指定路径
链接阶段
默认情况下,g++
不会到当前路径去找库,需要添加选项-L
,同时如果是自己写的库,也要注明编译器才会去查找。
或者说我们也可以直接将两个阶段合并成一个。
- L 指定库的路径
- -I指定头文件的路径
- -l指定库名
(二)动态库
so
[Linux]dll
[windows]
1、动态库制作
- 首先在编译时加入无关码选项。
- 生成的动态库也要添加选项,和静态库添加
ar
有所差异。
2、动态库链接
我们直接再像之前静态库一样不能正常运行,此时我们有四种方法可以解决这个问题
- 1、将动态库拷贝到
/lib64/
下。 - 2、制作软链接,指向
lib64/
。 - 3、通过系统的环境变量来指明路径,通常情况下这个环境变量为空,我们需要手动创建。
- 4、在
/etc/ld.so.conf.d/
路径下创建任何一个以.conf
为后缀的文件,文件中的内容存放动态库的路径。
(三)链接原理
可执行文件仅包含一个函数入口地址表(如PLT表),而非外部函数的完整机器码。外部函数的实际代码存储在动态库(如.so或.dll文件)中。操作系统在程序启动时或函数首次调用时,将动态库中的函数机器码从磁盘加载到内存,并更新可执行文件中的地址表以指向内存中的函数实现。
1、静态链接
静态链接是程序构建过程中的一个重要环节,它通过将目标文件和库文件合并成一个独立的可执行文件,下面以两份代码来模拟这个过程,以code.c
代表静态库。
code.c
:
#include<stdio.h>void run()
{printf("runing...\n");
}
hello.c
:
#include<stdio.h>void run();int main()
{printf("hello, world!\n");run();return 0;
}
而我们链接将这两个.o
文件合并成一个新的.o
文件后,符号表就会互相填充,不再是UND
未定义。
二、ELF格式
ELF是Linux系统中常见的文件格式,用于可执行文件、目标文件、共享库等。以下是ELF的四种主要类型:
可执行文件(Executable)
直接运行的程序,包含二进制代码和数据,由操作系统加载到内存执行。例如编译后的a.out
或/bin/ls
。
可重定位文件(Relocatable)
编译生成的中间目标文件(.o
文件),需通过链接器处理生成可执行文件或共享库。包含代码、数据及未解析的符号引用。
共享目标文件(Shared Object)
动态链接库(.so
文件),可在运行时被多个程序共享加载。例如libc.so
,提供函数供程序动态调用。
核心转储文件(Core Dump)
程序崩溃时生成的内存转储文件(如core
),用于调试分析。记录程序异常终止时的内存状态。
(一)ELF组成
- ELF header: 描述文件的特征,定位文件其它部分。
- Program header table(程序表头): 表里记录了每个段的开始位置和位移,长度。
- Section header table(节头表): 包含对节的描述。
- Section(节): ELF文件的基本组成单位,包含特定类型数据。如代码节存储可执行代码,数据节存储全局变量和静态变量。
程序头表和节头表作用:
链接视图(Linking view)-对应节头表Section header table
文件结构的粒度更细,将文件按功能模块的差异进行划分, 为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(seqment)、可读写的段、只读段等。合并了后,空间利用率就高了。
执行视图(execution view)-对应程序头表program header table
告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有program header table
.
重要的节
- Text 段:存储程序的代码,只读且共享。
当你编写一个简单的函数
int add(int a, int b) { return a + b; },
编译后,这个函数的指令会被存储在 Text 节中。
- Data 段:存储初始化的全局变量和静态变量,可读写。
int global_var = 10; // 这个变量会存储在 Data 段中
static int static_var = 20; // 同样存储在 Data 段
- BSS 段:存储未初始化的全局变量和静态变量,运行时自动初始化为零,节省空间。
int uninitialized_global; // 未初始化的全局变量,存储在 BSS 段
static int uninitialized_static; // 未初始化的静态变量,也存储在 BSS 段
- .got / .got.plt
全局偏移表(Global Offset Table)。.got存储全局变量引用,.got.plt存储动态链接函数地址。
(二)ELF形成可执行
一个ELF会有多种不同的
Section
,在加载到内存的时候,也会进行Section
合并,形segment
合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起,这个合并工作也已经在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table)中
- 1、将多份
c++
代码编译形成.o
文件 + 动静态库(ELF) - 2、将多份
.o
文件section
进行合并
1、Section合并的主要=是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
2、操作系统在加载程序时,会将具有相同属性的section
合并成一个大的segment
,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
(三)ELF常用指令
使用size
查看ELF
格式文件的组成。
使用readelf -S
可以用来查看文件的节头表,当前一共 30 个结点,里面标识了起始地址和偏移量。
使用readelf -l
可以用来查看文件的程序头表,将 30 个节打包成指定数量的段segment
使用readelf -h
可以用来查看文件的程序头表
其中Magic
用来判断该ELF
是一个可执行程序,Entry point address
是指程序的入口地址。
使用readelf -s
查看ELF
文件的符号表
(四)ELF转化成可执行程序
1、逻辑地址,物理地址,虚拟地址
程序在开始时,我们都选择从 0 开始,这种编制方式也叫平坦模式。其实函数在编译好之后使用的地址已经是虚拟地址了,对应虚拟地址空间。
在存储时,每一个虚拟地址都对应了磁盘中的一个物理地址。
操作系统读取磁盘时,将磁盘上的物理地址和虚拟地址同时读入内存。
2、磁盘加载程序
cpu
将可执行程序第一条语句的地址到寄存器EIP
中,开始执行程序。
3、动态库如何与可执行程序关联
动态链接实际上将链接的过程推迟到了程序加载的时候。
在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。实际上,程序的入口点是_start这是一个由C运行时库(通常是glibc)或链接器(如Id)提供的特殊函数。
在start函数中,会执行一系列初始化操作,这些操作包括:
- 1.设置堆栈:为程序创建一个初始的堆栈环境。
- 2.初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
- 3.动态链接:这是关键的一步,_start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。
- 4.调用 -1ibc_start_main:一旦动态链接完成,_start 函数会调用1ibc_start_main(这是glibc提供的一个函数)。1ibc_start_main 函数负责执行一些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使用了线程)等。
- 5.调用 main 函数:最后,libc_start_main 函数会调用程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。
- 6.处理 main 函数的返回值:当 main 函数返回时,1ibc_start_main 会负责处理这个返回值,并最终调用_exit 函数来终止程序。