Liunx——动静态库

1. 库

库是指一组预编译的、可复用的代码集合,这些代码实现了一些特定的功能或算法,供其他程序调用使用,而无需开发者每次都从头编写这些代码。库一般可以分为静态库和动态库。

静态库在Linux系统上以.a为后缀,在Windows上以.lib为后缀。静态库会在编译时将库代码直接嵌入到生成的可执行文件中,因此生成的可执行文件体积较大,但是在运行时可以无需依赖外部库文件。

动态库在Linux系统上以.so为后缀,在Windows上以.dll为后缀。动态库则无需嵌入文件,而只在程序运行时被加载到内存,多个程序可以共享这一份动态库代码。

1.1 静态库

1.1.1 创建静态库

我们通过编写一个makefile文件来完成编译和打包静态库的工作。

bash 复制代码
libmstdio.a : mstdio.o mstring.o
	@ar -rc $@ $^
	@echo "build $^ to $@ done successfully"
%.o:%.c
	@gcc -c $<
	@echo "compiling $< to $@ done successfully"
.PHONY:clean
clean:
	@rm -rf *.a *.o pack*
	@echo "clean done"
.PHONY:output
output:
	@mkdir -p pack/include
	@mkdir -p pack/lib 
	@cp -f *.h pack/include
	@cp -f *.a pack/lib 
	@tar -czf pack.tgz pack 
	@echo "output pack successfully"
bash 复制代码
ar -rc libmystdio.a my_stdio.o my_string.o

ar是一个用于创建、修改和提取归档文件(通常是静态库)的工具,我们使用ar指令来打包目标文件(.o)来创建静态库。

-r选项:将文件插入归档文件中。如果归档中已经存在同名的文件,则替换之。可以确保当.o文件更新时,生成的静态库文件对应位置会被新的内容覆盖。

-c选项:创建归档文件时不显示警告信息(即使归档文件之前不存在,也不会提示)。

-s选项:创建或更新归档的索引(符号表)。对于静态库来说,这个操作会使得链接器更快地查找符号。通常在插入、删除文件后,需要调用此选项来更新索引。

-t选项:针对生成的.a静态库,列出其中的文件。

-v选项:在-t选项的基础上给出静态库中文件的详细信息。

cpp 复制代码
%.o: %.c
	@gcc -c $<
	@echo "compiling $< to $@ done successfully"

%.o:%.c是一个规则头部,其中%是通配符,表示对于任意的.c文件,都可以生成对应的.o文件。即如果需要生成某个.o文件,可以用对应的.c文件作为依赖。

接下来就是gcc的编译指令,-c选项表示使用GCC编译器将C源文件编译成目标文件。

其中$<表示规则头部的第一个依赖文件,在此处就是%.c文件。

bash 复制代码
.PHONY:clean
clean:
	@rm -rf *.a *.o pack*
	@echo "clean done"
.PHONY:output
output:
	@mkdir -p pack/include
	@mkdir -p pack/lib 
	@cp -f *.h pack/include
	@cp -f *.a pack/lib 
	@tar -czf pack.tgz pack 
	@echo "output pack successfully"

.PHONY用于声明为目标(phony target),表示强制执行下面的命令。以上两步完成了清除与输出的任务。

可以看到我们的目录中生成了一个libmstdio.a的静态库文件。

1.1.2 使用静态库

我们需要正常编译main.c肯定需要我们另外的两个c文件和h文件,而现在.c文件被整合进了静态库.a文件当中,所以使用静态库的前提就是找到静态库和头文件的位置。

给出静态库和头文件全部路径

bash 复制代码
gcc main.c -I/path/to/headers -L/path/to/libs -lmystdio

-I选项后跟指定头文件所在目录。

-L选项后跟指定库文件所在目录。

-l选项后跟静态库名称。需要注意的是搜索静态库是在-L选项指定的目录中搜索,而且静态库的命名规范是 lib库名称.a(so) 。真正的库名是去掉前后缀的结果,如c标准库libc.so.6的真正名称是c。

给出头文件路径

gcc会默认在当前路径和系统默认路径/usr/include中寻找头文件。

不给出静态库路径

gcc会默认在/lib64路径下查找静态库。

小结一下,当编译时gcc会从当前路径、系统默认路径/usr/include、用户-I给出的路径下寻找头文件;会从用户-L给出的路径、系统默认路径/lib64下寻找静态库。

所以使用静态库时需要指明头文件和静态库文件的位置,当然为了可以方便使用,也可以将我们自己的文件放入系统对应路径中。一般来说对于除了c标准库和c++标准库以外的第三方静态库,都需要通过-l选项来指出使用的库名

1.2 动态库

1.2.1 创建动态库

bash 复制代码
libmstdio.so : mstdio.o mstring.o
	@gcc -o $@ $^ -shared
	@echo "build $^ to $@ done successfully"
%.o:%.c
	@gcc -fPIC -c $<
	@echo "compiling $< to $@ done successfully"
.PHONY:clean
clean:
	@rm -rf *.so *.o pack*
	@echo "clean done"
.PHONY:output
output:
	@mkdir -p pack/include
	@mkdir -p pack/lib 
	@cp -f *.h pack/include
	@cp -f *.so pack/lib 
	@tar -czf pack.tgz pack 
	@echo "output pack successfully"
bash 复制代码
gcc -o libmstdio.so mstdio.o mstring.o -shared

gcc的-shared选项可以链接多个.o目标文件,生成一个共享库(.so)。

bash 复制代码
gcc -fPIC -c <source_file>.c

-c表示只编译不链接。

-fPIC指生成位置无关代码(Position Independent Code),这是一种特殊的代码生成方式,它的核心特点是代码可以在内存中的任意位置运行,而不需要修改代码本身。

动态库在运行时被加载到内存中,并且可能被多个进程共同使用。实际上动态库只被实际加载到了物理内存一次,但是由于各个进程都通过页表映射到了这个动态库,所以在每个进程的地址空间视角下,同一份动态库有着不同的逻辑地址。所以动态库必须采取和位置无关的代码,即使用偏移量的方法来寻址访问。

1.2.2 使用动态库

对于动态库的使用和静态库基本相似,也需要通过gcc的选项指出头文件和动态库所在目录。

给出静态库和头文件全部路径

bash 复制代码
gcc main.c -I/path/to/headers -L/path/to/libs -lmystdio

-I选项后跟指定头文件所在目录。

-L选项后跟指定库文件所在目录。

-l选项后跟库名称。

给出头文件路径

gcc会默认在当前路径和系统默认路径/usr/include中寻找头文件。

不给出静态库路径

gcc会默认在/lib64路径下查找动态库。

和静态库相同,当编译时gcc会从当前路径、系统默认路径/usr/include、用户-I给出的路径下寻找头文件;会从用户-L给出的路径、系统默认路径/lib64下寻找动态库。

在编译成功生成可执行文件后,如果动态库文件没有位于系统目录下,那么执行可执行程序有可能会出现新的问题。

通过ldd分析可执行程序的依赖库,可以发现mstdio动态库未找到。这是因为动态库是程序执行时才被载入内存的,当使用gcc进行编译时给出了动态库所在路径因此编译成功。但是当执行时,shell并不知道需要的动态库在哪里,因此出现了错误。

解决这个问题也不难,只需要告诉shell动态库的位置即可,操作系统默认搜索路径包括lib、lib64等系统标准库路径,然后再去一个名叫LD_LIBRARY_PATH的环境变量的指定路径。

​ 所以可行的解决方案包括:将动态库拷贝到系统默认路径下、在系统默认路径下建立软连接、修改环境变量LD_LIBRARY_PATH增加动态库所在路径、新增配置文件。其中新增配置文件一般在/etc/ld.so.conf.d/目录下新建一个自己的xxx.conf,然后在其中设置新的动态库链接路径,最后ldconfig使配置重新加载生效即可。

不难发现静态库和动态库的链接命令都是一样的,当同时存在静态库和动态库时,gcc和g++会优先选择动态库,而如果想要强制静态链接则需要使用-static选项

2. ELF文件

ELF(Executable and Linkable Format)是一种文件类型,主要在Linux环境下使用,是一种用于可执行文件、目标文件、共享库和核心转储的标准文件格式。

在Linux中使用ELF文件格式的文件包括:

可执行目标文件(Executable):编译链接后生成的可执行文件,是可以直接运行的程序。

可重定位目标文件(Relocatable):编译生成的中间文件(.o 文件),需要进一步链接。

共享库(Shared Object):动态链接库(.so 文件),在程序运行时被加载。

核心转储文件(Core Dump):在程序崩溃时生成的内存转储文件,用于调试。

2.1 ELF文件结构

2.1.1 ELF文件基本框架

图片来源: doc.embedfire.com/linux/imx6/base/zh/latest/linux_driver/module.html

2.1.1.1 ELF 头(ELF Header)

ELF头用于描述文件的基本信息。

其中包括:

魔数(Magic Number):标识文件为 ELF 格式。

文件类型:可执行文件、目标文件、共享库等。

目标架构:如 x86、ARM。

入口点地址:程序的起始地址。

程序头表和节头表的偏移量和大小。

2.1.1.2 程序头表(Program Header Table)

程序头表描述段的信息,用于加载和执行程序。

其中包括:

每个段在文件中的位置和大小。

段在内存中的加载地址。

段的权限(读、写、执行)。

2.1.1.3 节头表(Section Header Table)

节头表描述节的信息,用于链接和调试。

其中包括:

每个节在文件中的位置和大小。

节的类型(代码、数据、符号表等)。

节的属性(可读、可写、可执行)。

2.1.1.4 节(Sections)和段(Segments)

是 ELF 文件中的最小组织单位,用于存储特定类型的数据。保存了代码、数据、符号表、字符串表、重定位信息等,是链接和调试的基本单位。每个节的信息在节头表中给出。

常见的节:

.text:存储程序的指令。

.data:存储已初始化的全局变量。

.bss:存储未初始化的全局变量。

.rodata:存储只读数据(如字符串常量)。

.symtab:存储符号表(函数和变量的名称和地址)。

.strtab:存储字符串表(符号名称和节名称)。

.rel.*:存储重定位信息(用于链接目标文件)。

是 ELF 文件在内存中的加载单位,用于描述如何将文件内容加载到内存中。一个段可以包含一个或多个节,是加载和执行的基本单位。每个段的信息在程序头表中给出。

常见段:

LOAD:需要加载到内存的段(如代码段和数据段)。

DYNAMIC:动态链接信息。

INTERP:指定动态链接器的路径。

编译器在生成目标文件时,会将代码、数据等内容组织成不同的节,链接器在生成可执行文件或动态库时,也会使用节中的信息进行符号解析和重定位。但是到了操作系统在加载 ELF 文件时,会根据段的信息将文件内容映射到内存中。所以一个段可以包含多个节,这些节在内存中通常是连续的。

当多个.o目标文件被链接时,他们的所有相同属性的section会进行合并,生成最后的可执行文件。

2.1.2 ELF文件示例

2.1.2.1 可重定位目标文件(Relocatable Object File)

文件类型:ET_REL(可重定位文件)。

用途:编译生成的中间文件(.o 文件),需要进一步链接。

特点:

包含代码、数据和符号表,但未分配最终的内存地址。

包含重定位信息(.rel.text 和 .rel.data),用于链接时修正地址。

不包含程序头表(Program Header Table),因为不需要加载到内存。

2.1.2.2 可执行目标文件(Executable Object File)

文件类型:ET_EXEC(可执行文件)。

用途:可以直接运行的程序。

特点:

包含代码、数据和符号表,且已分配最终的内存地址。

包含程序头表(Program Header Table),用于加载到内存。

不包含重定位信息,因为地址已经确定。

入口点地址(Entry Point)指向程序的起始地址。

2.1.2.3 动态库(Shared Object File)

文件类型:ET_DYN(共享对象文件)。

用途:动态链接库(.so 文件),可以在运行时加载。

特点:

包含代码、数据和符号表,但地址是位置无关的(PIC)。

包含程序头表(Program Header Table),用于加载到内存。

包含动态链接信息(如 .dynamic 节和 .got 节)。

可以被多个程序共享,节省内存空间。

2.2 ELF文件加载

2.2.1 虚拟空间的内存结构

我们在进程部分曾经介绍过,对于每一个进程而言,他们有自己独立的task_struct结构体来管理自己进程的信息。在task_struct结构体中有一个字段叫做mm_struct,它是进程的内存描述符,用于描述一个进程的整个虚拟内存空间。

稍微回顾一下,在mm_struct中有这样一些字段:

mmap(vm_area_struct):指向进程的虚拟内存区域链表。

pgd:指向进程的页全局目录(Page Global Directory,PGD),用于页表管理。

start_code、end_code:代码段的起始和结束地址。

start_data、end_data:数据段的起始和结束地址。

start_brk、brk:堆的起始和当前结束地址。

start_stack:栈的起始地址。

其中vm_area_struct是虚拟内存区域描述符,用于描述进程虚拟地址空间中的一个连续区域。其字段包括:

vm_start:区域的起始地址。

vm_end:区域的结束地址。

vm_flags:区域的权限标志(如可读、可写、可执行)。

vm_file:如果区域映射了文件,指向对应的文件对象。

vm_next:指向下一个 vm_area_struct,形成链表。

这时会有一个疑问,似乎mm_struct和vm_area_struct的字段重复了。这是因为mm_struct目的是提供进程内存管理的全局视图,适合快速访问和整体管理;而vm_area_struct提供进程内存管理的局部视图,适合精细化的内存管理,其中包含着区域的权限、文件等信息。

以一个例子具体化理解:

假设一个进程的虚拟内存布局如下:

代码段:0x08048000 - 0x08049000

数据段:0x08049000 - 0x0804a000

堆:0x0804a000 - 0x0804b000

栈:0xbf800000 - 0xbf801000

mm_struct:

start_code = 0x08048000

end_code = 0x08049000

vm_area_struct 链表:

节点 1:代码段(0x08048000 - 0x08049000)。

节点 2:数据段(0x08049000 - 0x0804a000)。

节点 3:堆(0x0804a000 - 0x0804b000)。

节点 4:栈(0xbf800000 - 0xbf801000)。

2.2.2 可执行程序加载

对于一个ELF可执行程序而言,它的文件中自带地址,我们可以通过objdump -S 指令来查看其内容,会发现其中已经存在了了各个指令的地址信息。

针对这个地址,我们称之为逻辑地址 ,是程序视角中的地址,通常是相对于某个段基址的偏移量。与之相似的是虚拟地址,是进程视角中的地址,也就是进程虚拟地址空间中的地址。另外还有物理地址,是实际内存硬件中的地址,虚拟地址通过页表可以映射到物理地址。

当把程序置于平坦模式下时,由于内存地址空间是连续的、线性的,没有分段机制,所以这个时候程序整体不做分割加载到内存中,逻辑地址也就和虚拟地址在数值上一样了。

2.2.2.1 加载执行过程举例

假设我们有一个简单的 ELF 可执行文件a.out,其内存布局如下:

代码段:0x08048000 - 0x08049000

数据段:0x08049000 - 0x0804a000

堆:0x0804a000 - 0x0804b000

栈:0xbf800000 - 0xbf801000

①首先操作系统会读取这个ELF文件的ELF头,明确这个文件的类型是可执行文件,读取程序头表的位置和大小,并且找到文件入口地址(即0x08048000)。

②接着操作系统解析程序头表,获取各个段的信息,如:

代码段:文件偏移 0x000000,大小 0x1000,虚拟地址 0x08048000,权限 R-X。

数据段:文件偏移 0x001000,大小 0x1000,虚拟地址 0x08049000,权限 RW-。

③然后操作系统为进程分配虚拟地址空间:

代码段:0x08048000 - 0x08049000。

数据段:0x08049000 - 0x0804a000。

④加载段到内存并设置页表映射

代码段:将文件偏移 0x000000 处的 0x1000 字节加载到虚拟地址 0x08048000。

数据段:将文件偏移 0x001000 处的 0x1000 字节加载到虚拟地址 0x08049000。

假设物理内存的可用区域从 0x10000000 开始。

代码段:虚拟地址 0x08048000 映射到物理地址 0x10000000。

数据段:虚拟地址 0x08049000 映射到物理地址 0x10001000。

⑤开始执行后,CPU会完成对虚拟地址到物理地址的计算

当cpu试图访问某一内存地址的内容时,就需要用已知的虚拟地址映射出真正的物理地址,这其中就会用到页表。

⑥取得物理地址后,cpu就可以到指定的地址处读取数据了。

2.2.3 动态库加载

静态链接的情况下,因为静态库本身作为了可执行程序代码的一部分,所以与正常的可执行文件加载没有什么不同。

对于动态链接而言,则并不是这样,因为动态库时独立于可执行程序之外的。

①当程序启动时,操作系统会加载程序的 ELF 文件,并解析其依赖的动态库。在解析到程序需要的动态库之后,使用动态链接器(如 ld-linux.so)来加载和链接动态库。此时动态库被加载到了物理内存之中。为了使得程序可以调用动态库,于是将动态库的地址映射到程序虚拟地址空间的共享区位置。

经过这一步操作,动态库只被在进程中加载了一份,而多个程序都可以通过映射的方法找到这一份动态库。

②接下来动态链接器会解析动态库中的符号(如函数和变量)。然后动态链接器将符号的地址更新到 GOT 中;将 PLT 中的条目指向 GOT 中的地址。

GOT(Global Offset Table,全局偏移表)

GOT 通常位于动态库的数据段(.data 或 .got 节),每个动态库有自己的 GOT。这个表中存储了动态库中的全局变量和函数的实际地址。因为动态库被加载的位置未知,所以GOT 中的地址是运行时确定的。在动态库加载时,动态链接器会更新 GOT 中的地址。

PLT(Procedure Linkage Table,过程链接表)

PLT 通常位于动态库的代码段(.plt 节),用于实现延迟绑定(Lazy Binding)。PLT 中的条目指向 GOT 中的地址,在首次调用函数时,PLT 会跳转到动态链接器,解析函数的实际地址并更新 GOT。

简而言之GOT给出了自己动态库中各符号的地址信息,在动态库加载时相当于地址信息被确定,所以会更新表。而PLT则是如果在程序一开始就对所以函数进行地址解析,那么会导致启动时间很长。所以延迟绑定只有在函数第一次被调用时才解析其地址。

相关推荐
jiarg29 分钟前
linux 内网下载 yum 依赖问题
linux·运维·服务器
yi个名字1 小时前
Linux第一课
linux·运维·服务器
Kurbaneli1 小时前
深入理解 C 语言函数的定义
linux·c语言·ubuntu
菜鸟xy..1 小时前
linux 基本命令教程,巡查脚本,kali镜像
linux·运维·服务器·kali镜像·巡查脚本·nmtui
暴躁的小胡!!!1 小时前
Linux权限维持之协议后门(七)
linux·运维·服务器·网络·安全
dxaiofcu2 小时前
双网卡电脑,IP地址漂移
linux·服务器·网络
ChinaRainbowSea2 小时前
Linux: Centos7 Cannot find a valid baseurl for repo: base/7/x86_64 解决方案
java·linux·运维·服务器·docker·架构
vortex52 小时前
在Kali中使用虚拟环境安装python工具的最佳实践:以 pwncat 为例
linux·python·网络安全·渗透测试·pip·kali
LKAI.3 小时前
MongoDB用户管理和复制组
linux·数据库·mongodb
linux修理工3 小时前
moodle 开源的在线学习管理系统(LMS)部署
linux