linux笔记归纳8:库制作与原理
库制作与原理
目录
一、库的概念
库:写好的,现有的,成熟的,可复用的代码
一种二进制形式的代码,可以被操作系统载入内存执行
静态库:.a(Linux) 、.lib(windows)
动态库:.so(Linux)、.dll(windows)
动静态库中不需要包含main函数
动静态库的本质是源文件(.c)对应的目标文件(.o)
// ubuntu 动静态库
// C
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so
$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a
// C++
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l
lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
二、静态库(.a)
2.1.静态库的本质
目标文件的打包,一种归档文件,不需要用户解包,而是用gcc/g++直接进行链接
2.2.静态库的生成
ar(archive)命令:归档
常用选项:-rc(replace and create)

makefile文件


2.3.静态库的使用
常用选项:
-L:指定库路径
-I:指定头文件搜索路径
-l:指定库名
//1:头⽂件和库⽂件安装到系统路径下
$ gcc main.c -lmystdio
//2:头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下
$ gcc main.c -L. -lmymath
//3:头⽂件和库⽂件有⾃⼰的独立路径
$ gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
生成目标文件,静态库删除,程序照样运行
库文件名去掉前缀lib,去掉后缀.so或.a




库是要被安装到系统中的
系统中默认的头文件路径在/usr/include路径下

系统中默认的库文件路径在/lib64路径下

三、动态库(.so)
3.1.动态库的生成

![]()

makefile文件


3.2.动态库的使用
只告诉gcc动态库的路径,但系统无法找到动态库
而静态库在链接时,直接将库的实现内容拷贝到了可执行程序
一旦形成可执行程序,可执行程序不再依赖静态库

方法1:用cp指令将动态库拷贝到操作系统内部
方法2:将动态库的软链接存放在操作系统内部
方法3:使用环境变量$LD_LIBRARY_PATH
OS在运行程序的时候要查找动态库,也会在环境变量LD_LIBRARY_PATH下查找动态库
该环境变量经常是空的,需要用户自己添加的动态库所在的路径

方法4:系统配置文件/etc/ld.so.conf.d/中创建.conf文件,将动态库路径写入文件,输入ldconfig

gcc/g++默认使用动态库,如果必须要用静态链接,需要加-static
一旦带了-static,就必须要有对应的静态库,否则会链接报错
只存在静态库,可执行程序对于该库只能进行静态链接
在Linux系统下,默认情况下安装的大部分库都是静态库
库:应用程序 = 1 :n
vs不仅可以形成可执行程序,也可以形成动静态库
四、目标文件(.o)
.o/.obj:可重定位目标文件

五、ELF文件(二进制)
5.1.ELF格式文件的类型
可重定位文件(.o)、可执行文件(.exe),共享目标文件(.so),内核转储(进程上下文)

5.2.ELF格式文件的组成
ELF头(ELF header):描述文件的主要特性
程序头表(program header table):列举所有的有效段和它们的属性
节头表(section header table):描述节的属性
节(section):ELF文件中的基本组成单位,包含了特定的数据
- .text节:程序代码指令
- .data节:初始化的全局变量,局部静态变量
- .rodata节:只读的数据(C语言的字符串),存储在text段中
- .bss(better save space)节:未初始化的全局变量,未初始化的静态局部变量
- .symtab节:Symbol Table符号表,源码中函数名,变量名和代码的对应关系
- .got.plt节:.got保存全局偏移表,.got节与.plt节一起提供了动态库的访问入口

磁盘上只存储未初始化全局变量的描述,等程序加载到内存后,再在内存中申请空间
并且初始化为0,减少了可执行程序占据的磁盘空间
符号表的形式:char lable[] = "helloworld\0func\0libc\0"
六、ELF从形成到加载
6.1.ELF形成可执行文件
第一步:将多份C/C++源代码,翻译成目标.o文件 + 动静态库
第二步:将多份.o文件的section进行合并

6.2.ELF可执行文件加载
一个ELF有多种不同的section,加载到内存时,会进行合并,形成segment
合并原则:相同属性(可读,可写,可执行,需要加载时申请空间)
合并方式:记录在ELF的程序表头
合并原因:减少页面碎片,提高内存使用率,实现不同权限的访问控制
示例:页面大小为4096字节(4KB),.text为4097字节,.init为512字节
如果不合并要占用3个页面,如果合并只要占用2个页面
- 查看可执行文件的section的内容

- 查看可执行文件合并segment的方式


- 查看ELF头
magic:用来表面文件的类型

链接视图:对应节头表,合并section,形成segment,在链接的时候作用
执行视图:对应程序头表,告诉操作系统如何加载可执行文件,完成进程的内存初始化
以及加载进内存后的哪些分段是可读,可写,只读和可执行的

七、链接与加载
7.1.静态链接
静态链接的本质:把.o文件进行链接

- 查看编译后的.o目标文件
对.o文件进行反汇编

未链接时,调用的库函数与其他目标文件函数的地址都为00 00 00 00

- 查看code.o的符号表
puts方法UDF(未定义)

- 查看hello.o的符号表
puts方法和run方法UDF(未定义)

两个文件合并后,hello文件会找到code文件中的run方法
和库文件合并后,两个文件都会找到库文件中的puts方法
- 查看main.exe的符号表

两个.o文件的run函数与main函数代码被合并到代码段,并且进行统一的编址

- 查看main.exe的反汇编
链接的时候,会修改,o中未定义的函数地址,合并完成后会进行call地址

静态链接就是将所有目标文件和用到的静态库拼装成一个可执行文件
所有的模块组合在一起后,链接器会根据重定位表修正函数变量地址

7.2.ELF加载与进程地址空间
一个可执行程序在未加载到内存前也有地址
磁盘上的可执行程序,它的代码与数据的编址是逻辑地址(起始地址+偏移量)

segments、函数、变量的起始地址设为0就成了线性地址(平坦模式编址)
线性地址加载进内存后,从进程的视角来看就是虚拟地址空间

Linux中main函数的入口程序是start函数


mm_struct和vm_area_struct在进程创建的时候,根据ELF文件中s egments的起始地址和长度
来初始化[start,end]范围数据,然后再用详细的地址,填充页表



7.3.动态链接与动态库加载
7.3.1.动态链接
动态链接的本质:将链接的整个过程推迟到程序加载
- 被进程看到:将动态库映射进程的虚拟地址空间
- 被进程调用:在进程的虚拟地址空间中进行跳转

多进程共享动态库

在C/C++程序中,程序的入口点是start函数,是一个由链接器提供的特殊函数
设置堆栈:为程序创建一个初始的堆栈环境
初始化数据段:将程序的数据段从初始化的数据段复制到相应的内存位置,清零未初始化的数据段
动态链接:调用动态链接器的代码解析程序所依赖的动态库,并且加载这些动态库到内存
(Linux通过环境变量和配置文件来指定动态库的搜索路径)
调用_libc_start_main:设置信号处理函数,初始化线程库
调用main函数:_libc_start_main调用main函数,将执行控制权交给用户代码
处理main函数返回值:_libc_start_main处理main函数返回值,调用_exit函数终止程序
7.3.2.动态库的相对地址
动态库为了随时进行加载,为了支持映射到任意进程的任意位置
对动态库中的方法进行了统一的编址,采用相对编址的方法进行

7.3.3.动态库的函数调用
动态库映射到当前进程的虚拟地址空间中
已知库在虚拟地址空间的起始地址,库中每个方法的偏移量
通过库的起始虚拟地址+方法偏移量,就可以定位库中的方法
从代码区跳转到共享区,调用完毕后再返回到代码区

7.3.4.加载地址重定位
在程序运行之前,先把所有的动态库加载并映射,获取动态库的起始虚拟地址
然后再对加载到内存中的程序的库函数调用地址进行修改,在内存中二次设置
7.3.5.全局偏移量表GOT
加载地址重定位会修改代码区的数据,而代码区的数据不可被修改
动态链接在.data中专门预留了一片用来存放函数的跳转地址的空间
全局偏移量表(global offset table):库函数偏移量@库起始地址

7.3.6.PIC地址无关代码
动态库不需要做任何修改,被加载到任意内存地址都能够正常运行
并且能够被所有进程共享,所以动态链接时,需给编译器指定-fPIC
PIC = 相对编址(GOT表与.text的相对位置) + GOT(库函数偏移量 + 动态库起始地址)
7.3.7.库间依赖
不仅仅由可执行程序调用库,库也会调用其他的库,所以库也有.GOT

7.3.8.延迟绑定
PLT(Procedure Linkage Table):延迟绑定
动态链接在程序加载的时候需要对大量的函数进行重定位
为了降低开销,操作系统提供延迟绑定进行优化
将重定位的过程延迟到函数第一次被调用的时候
因为绝大多数动态库中的函数在运行时不会用到


openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)