1.什么是库
库是预先编写完成、经过验证的成熟代码集合,核心价值是代码复用。现实中几乎所有程序都依赖大量基础底层库,无需从零开发,因此库的存在对软件开发至关重要。
从本质上讲,库是可执行代码的二进制形式,能被操作系统加载到内存中执行。Linux 系统中,库主要分为两类:
1.1静态库(Static Library)
后缀:.a(Linux)、.lib(Windows)
核心特点:编译时会被完整复制到可执行文件中,生成的程序不依赖外部库文件,可独立运行;
但是会导致可执行文件体积大,库更新后需重新编译程序。
通俗理解:把库的代码 "抄" 进自己的程序里,程序和库融为一体。
1.2 动态库(Shared Library / 共享库)
后缀:.so(Linux,全称Shared Object)、.dll(Windows)
核心特点:编译时仅记录库的引用路径,不复制代码;程序运行时才加载到内存,多个程序可共
享同一份动态库,节省内存和磁盘空间;库更新后无需重新编译程序,只需替换库文件即可。
通俗理解:程序只记 "库的地址",运行时临时调用,多个程序共用同一个库文件。
2.库的存储
库的物理存储:库文件本质是磁盘上的二进制文件,和普通文件一样存在磁盘分区里(有自己的 inode、数据块);
2.1 动态库
动态库(.so):运行时 "挂载" 到虚拟地址空间
①编译阶段:编译器只记录 "需要调用某个动态库" 的引用信息,不复制库代码,可执行文件体积很小;
②运行阶段: 进程启动时**,操作系统的动态链接器(ld-linux.so) 会找到对应的.so文件;
把动态库加载到进程虚拟地址空间的 共享库区域** (通常是 0x7ffff7a00000 附近,64 位系统);
多个进程可共享同一份动态库的物理内存(但每个进程的虚拟地址空间中,库的虚拟地址可能不同,内核会做地址映射);

总结:动态库代码是运行时单独加载到进程虚拟地址空间的共享区域,多个进程复用物理内存,节省资源。
2.2 进程间如何共享动态库
动态库在内存中只需要加载一份,未来还有进程需要直接将内存中的动态库地址和页表进行映射即可;所以动态库才叫做共享库;而静态库会将加载到内存中的库内容拷贝一份给虚拟空间地址,所以才会浪费空间;
2.3静态库
静态库(.a):编译时 "融入",运行时直接在虚拟地址空间
①编译阶段:静态库的二进制代码会被完整复制到可执行文件中(可执行文件本身也是磁盘文件);
②运行阶段 :可执行文件被加载到进程虚拟地址空间时,静态库的代码也跟着进入,和程序自身代码一起放在 代码段(Text Segment)
总结:静态库代码最终在进程虚拟地址空间,但不是 "单独加载",而是和程序融为一体。
2.4 为什么使用动态库更多

这里的 libc.so 是 C 语言的运行时库,里面提供了常用的标准输入输出、文件操作、字符串处理等功能。那为什么编译器默认不使用静态链接呢?静态链接会将编译产生的所有目标文件,连同用到的各种库,合并形成一个独立的可执行文件,它不需要额外的依赖就可以运行。照理来说应该更加方便才对,是吧?静态链接最大的问题在于生成的文件体积大,并且会耗费大量内存资源。随着软件复杂度的提升,操作系统也会变得越来越臃肿,不同的软件可能都包含了相同的功能和代码,显然会浪费大量的硬盘空间。这个时候,动态链接的优势就体现出来了。我们可以将需要共享的代码单独提取出来,保存成一个独立的动态链接库,等到程序运行的时候再将它们加载到内存。这样不但可以节省硬盘空间,还能节省内存 ------ 因为同一个模块在内存中只需要保留一份副本,可供不同的进程共享。
动态链接到底是如何工作的?
首先要明确一个结论:动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们运行一个程序,操作系统会首先将程序的代码和数据,连同它用到的一系列动态库加载到内存中。其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。当动态库被加载到内存以后,一旦它的内存地址被确定,我们就可以修正动态库中那些函数的跳转地址了
3.手动制作静态库
3.1安装到系统里面
在做库的时候,.h文件要暴露,.c文件要隐藏起来;
①将所有的.c编译成我们的.o;

② 打包我们的.o文件

rc选项的意思是,如果不存在就create ,如果存在就replace;
库的形式是libmystdio.a ,库的格式的开始必须是lib,结尾必须是.a
最后是我们要创建的.o文件名;
③如何将我的库安装到系统

现在在lib64目录下就有我们的库了;

未来在我们的系统里使用我们自己写的库就不再使用"*.h"了,可以直接使用#include <my_stdio.h>
#include <my_string.h>,这是因为如果你要包含的头文件和你是在同一个路径下,使用的是" ",不在同一个路径下使用< >;但是我们的库在shell看来还是第三方库,所以它还是不认识的,所以我们得用下面的命令了;
bash
gcc main.c -lmystio
注意:库的格式是libmystdio.a,但是库的名字是去掉lib和.a之后的内容,而我们现在写的-l是link连接的缩写,可以引入指定名称的第三方库
3.2 和源文件一起
除了系统路径,也要在自己的路径下能找到自己的库,写库里面一定不能有main函数,只要提供头、源就可以;并且我们需要将实现库的头文件放在include目录下,把库放在lib目录下;

但是 -L 选项只能用于在当前路径下找;
对应makefile是这样写的,但是我们还需要把库发布出来,所以tmakefile文件需要像下面一样写,其中stdc是我们在当前目录下的一个新的目录,所以我们想要clean的时候应该clean我们的stdc,因为它里面包括了我们所有的.a文件和.h文件
3.3 使用带路径的库


别人怎么使用



4.手动制作动态库
4.1如何形成动态库
gcc -shared 不要形成可执行程序,帮我形成.so库;




所以要告诉到底使用什么库

4.2 如何给系统指定的路径,查找我的动态库
①拷贝到系统的默认路径下,比如/lib64 (比较常见)
② 在系统路径下建立软连接
③linux中,OS查找动态库,找到环境变量将我们的库作为环境变量加载进去 (比较常见)
④向配置文件中写
这个方法适用于写的库非常重要的情况下,也就是你想让的你的库 永远不失效,并且在哪里都能跑的情况下使用;
首先找到我们的配置文件目录

在此目录下增加我们名为gy.cond的配置文件

最后将我们的库的路径写到我们刚才的创建的配置文件gy.cond中
最后在ldcongfig也就是加载一下我们的配置文件;
5.动静态库的补充知识


6.main函数执行前的底层初始化过程
在 C/C++ 程序中,当程序开始执行时,它并不会直接跳转到 main 函数。实际上,程序的入口点是_start,这是一个由 C 运行时库(通常是 glibc)或链接器(如 ld)提供的特殊函数。在_start 函数中,会执行一系列初始化操作,这些操作包括:
①设置堆栈:为程序创建一个初始的堆栈环境。
②初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到对应的内存位置,并将未初始化的数据段清零。
③动态链接:这是关键的一步,_start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确映射到动态库中的实际地址。
动态链接器:
◦ 动态链接器(如 ld-linux.so)负责在程序运行时加载动态库。
◦ 当程序启动时,动态链接器会解析程序中的动态库依赖,并将这些库加载到内存中。
环境变量和配置文件:
Linux 系统通过环境变量(如 LD_LIBRARY_PATH)和配置文件(如 /etc/ld.so.conf 及其子配置文件)来指定动态库的搜索路径。这些路径会在动态链接器加载动态库时被搜索。
缓存文件:
◦ 为了提高动态库的加载效率,Linux 系统会维护一个名为 /etc/ld.so.cache 的缓存文件。
◦ 该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件。调用__libc_start_main:一旦动态链接完成,_start 函数会调用__libc_start_main(这是 glibc 提供的一个函数)。__libc_start_main 函数负责执行一些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使用了线程)等。
调用 main 函数
最后,__libc_start_main 函数会调用程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。
处理 main 函数的返回值:当 main 函数返回时,__libc_start_main 会负责处理这个返回值,并最终调用_exit 函数来终止程序。
上述过程描述了 C/C++ 程序在 main 函数之前执行的一系列操作,但这些操作对于大多数程序员来说是透明的。程序员通常只需要关注 main 函数中的代码,而不需要关心底层的初始化过程。然而,了解这些底层细节有助于更好地理解程序的执行流程和调试问题;
7.程序如何进行库函数调用
7.1动态库中的相对地址
动态库为了能够被随时加载,且支持映射到任意进程的任意内存位置,会对库内的方法进行统一编址,采用相对编址的方案编制(其实可执行程序也是如此,都要遵守平坦模式,只不过可执行程序是直接加载的)。我们的程序,怎么和库具体映射起来的
📌 注意:
动态库也是一个文件,要访问必须先被加载,要加载则必须先被打开;让进程找到动态库的本质:本质也是文件操作,但访问库函数是通过虚拟地址跳转实现的,因此需要将动态库映射到进程的地址空间中。
7.2 我们的程序,怎么进行库函数调用
📌 注意:
库已被映射到当前进程的地址空间中;库的虚拟起始地址已确定;库中每个方法的偏移量地址也已知;因此:访问库中任意方法,只需通过 "库的起始虚拟地址 + 方法偏移量" 即可定位到目标方法;此外:整个调用过程是从进程的代码区跳转到共享库区域,调用完毕后返回代码区,全程都在进程的地址空间内完成。
7.3 全局偏移量表 GOT(global offset table)
📌 注意:
程序运行前,会先加载并映射所有依赖的动态库,所有库的起始虚拟地址需提前确定;随后对内存中程序的库函数调用地址进行修改,在内存中二次完成地址设置(这个过程称为加载地址重定位);
疑问:修改的是代码区?但代码区在进程中是只读的,能否修改?
因此,动态链接的解决方案是:在.data段(可执行程序或动态库自身的.data段)中专门预留一片区域,用于存放函数的跳转地址,这片区域就是全局偏移量表(GOT)。表中每一项对应本运行模块要引用的一个全局变量或函数的地址。
由于.data区域是可读写的,因此支持动态修改。

补充说明:
由于代码段只读,无法直接修改,而 GOT 表的存在让代码段可被所有进程共享。但不同进程的地址空间中,各动态库的绝对地址、相对位置不同,因此每个进程的每个动态库都有独立的 GOT 表,进程间无法共享 GOT 表;在单个动态库(.so)中,GOT 表与.text段的相对位置是固定的,因此可以利用 CPU 的相对寻址找到 GOT 表;调用函数时会先查询 GOT 表,再根据表中的地址跳转,这些地址会在动态库加载时被修改为实际的内存地址;
这种实现动态链接的方式被称为 PIC(地址无关代码)。换句话说,动态库无需任何修改,被加载到任意内存地址都能正常运行,且可被所有进程共享 ------ 这也是编译动态库时需要指定-fPIC参数的原因(PIC = 相对编址 + GOT)。


