
目录一、什么是库二、库的本质三、静态库1. 打包制作静态库2. 如何使用静态库方法1使用gcc指令选项方法2拷贝头文件和库文件到系统目录下方法3使用软链接四、动态库1. 打包制作动态库2. 如何使用动态库找不到动态库解决加载找不到库的方法(4种)3. 理解动态库的加载宏观理解程序地址空间2关于地址的认识程序加载到内存之前逻辑地址程序加载到内存之后虚拟地址动态库的地址理解 --- 为什么要使用gcc的 -FPIC 选项五、补充内容1. 动态链接和静态链接2. gcc 链接第三方库一、什么是库当我们写好了一些包含了很多方法函数的代码没有main函数之后想要将我们写的这些代码给别人用我们有两种方法一种就是将源代码直接给别人。一种则是让我们写的源代码的方法实现想办法打包成库然后再配合头文件把库拿给别人用。这个库中就包含了我们写好的函数方法的定义而头文件中则包含了这些函数的声明是库中方法的使用说明书。所以库就是我们写好的函数方法而我们拿给别人用的库有两种类型静态库和动态库。在Linux下静态库的名字叫做libxxxx.a.zzz其中 lib 和.a.zzz只是前缀和后缀静态库真正的名字是这里的 xxxx 动态库的名字叫做libyyyy.so.zzz其中lib是前缀.so.zzz是后缀动态库的名字是这里的 yyyy 。例如 libc.so.6 就是C标准库是一个动态库版本号是6。区别静态库.a程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。此时即使删掉静态库程序也能运行。动态库.so程序在运行的时候才去链接动态库的代码多个程序共享使用库的代码。如果删掉动态库程序就不能运行了。二、库的本质注以下我们称有main函数的源文件为 main.c 。我们知道一堆.c源文件和.h头文件要形成可执行程序就必须经历4个步骤预处理-编译-汇编-链接 。这些代码经过前三个步骤之后每一个.c文件就会形成一个的.o 文件直到第四部链接的时候才会将所有.o文件进行链接从而形成可执行文件。这些.o文件可以分成两种包含 main 函数的 .o 文件这是程序的入口决定了程序的执行起点。不包含 main 函数的 .o 文件这些文件包含了几乎都是函数的定义。这些函数会通过main被调用。所谓的库本质上就是将这些 “ 常用的且不包含 main 函数的 .o 文件 ” 按照特定的格式打包起来的一个文件。此后我们想链接形成可执行程序时只需要让main.o与这个库链接就行了。举个例子如果有 test1.ctest2.ctest3.c 这几个源文件且它们的函数会被不同项目即不同的 main 函数重复使用。为避免每次编译都重复处理这些代码我们可以将它们打包成库先将这几个 .c 文件分别编译为 test1.o、test2.o、test3.o再通过特定工具将这些 .o 文件打包为一个文件——这就是“库”的形成过程。此时若某个项目的 main.c 需要调用这些库中的函数只需将 main.c 编译为 main.o再与打包好的库链接就能生成可执行文件。无论静态库如 libxxx.a还是动态库如 libxxx.so本质都是 .o 文件的集合内部封装了各类函数或变量等。调用这些功能函数或变量时只需在链接阶段指定对应的库即可。三、静态库1. 打包制作静态库我们已经知道静态库其实就是我们要用的几个 .c 源代码编译写成的 .o 文件打包起来的一个文件那么我们要怎么做一个静态库呢我们现在有以下头文件和源文件要让这两个源文件中的方法打包成库我们可以通过以下步骤实现首先将 .c 文件编译成机器码形式的 .o 文件。注意这里只编译不链接需要使用gcc的 -c 选项即以下指令gcc -c my_add.c -o my_add.o gcc -c my_sub.c -o my_sub.o然后使用归档工具 ar将这两个 .o 文件打包成一个 .a 文件。通常库的命名规范是lib 库名 .a。假设我们将这个库命名为 mymath则文件名为 libmymath.a。需要使用以下指令和选项ar -rc libmymath.a my_add.o my_sub.o其中选项解释如下-r替换或添加文件到归档中。若库中已存在同名 .o 文件则覆盖若不存在则新增。-c创建归档文件。若指定的 .a 文件不存在则自动创建若已存在则不报错配合 -r 使用更安全最后我们可以得到一个库 libmymath.a 。之后要让我们的库能够被别人使用我们需要给别人两个文件一个文件中放了的是我们与这个库相关的头文件另一个文件中放的是所有的库文件。所以接下来我会将以上过程中的头文件和库文件分开。假如要要放在mylib目录下则将头文件放在include下目录库文件放在 lib 目录下。得到如下结果此后如果别人要用my_add.c和my_sub.c中的方法则只需要将这里的所有头文件给他头文件是函数使用的说明书以及库目录下的所有库这里是把 libmath.a给他。2. 如何使用静态库在以上mylib的基础上建立一个main.c 在其中我们要调用这个库中的方法该怎么做方法1使用gcc指令选项其中这个main的代码如下#include stdio.h #include my_add.h #include my_sub.h int main() { int a 10; int b 20; printf(%d %d %d\n, a, b, add(a, b)); printf(%d - %d %d\n, a, b, sub(a, b)); return 0; }接下来我们需要使用 gcc 进行将代码编译形成可执行程序但是如果当前我们直接使用gcc 不带特定选项则就无法编译因为gcc找不到我们的代码在那里如图所以我们还需要使用到以下三个选项-I大写的 i 表示在指定头文件的搜索路径-L大写的 l表示在指定库文件的搜索路径-l小写的 L指定要链接的库名称。这里后跟库的名称即去掉前缀 lib和后缀 .a 和 .so 之后的部分剩余的部分就是库名一般是 -l 后面紧跟库文件名。示例说明指定头文件搜索路径是为了找到源代码中包含的头文件 my_add.h 和 my_sub.h 在哪里。指定库文件搜索路径是为了找到我们使用到的库在哪里。因为头文件只有函数声明没有实现具体的代码逻辑都在库里。如果我们不将其头文件和库文件拷贝到系统目录下那么这三个选项缺一不可并且它们后面库加空格也可以不加空格。如果要链接多个库则直接在后面一直添加-l库名即可。方法2拷贝头文件和库文件到系统目录下这种方法的核心就是通过系统的默认路径来找头文件和库。而拷贝具体过程如下把我们要使用的头文件拷贝到系统目录 /usr/include/ 下库文件拷贝到 /lib64/ 下。以上操作使用cp指令拷贝即可。然后我们就可以使用gcc进行编译了但是要注意此时还是需要指明是链接系统路径库文件目录下的那一个库不然也编不过。如下图所示这时可能会问为什么使用 C 语言自己的库如 stdio.h、stdlib.h 等标准库不需要手动用 -l 指定这是因为 gcc 编译器本来就是设计出来编译C语言的在设计时它就将C 标准库libc设定为了默认自动链接的对象。 这意味着只要编译 C 程序链接器就会无条件地把 libc 加入链接而我们写的自定义库如 libmymath或特定的扩展库如数学库 libm并非所有程序都需要所以编译器不会自动加载它们因此必须通过 -l 参数让我们显式指定。拓展其实我们拷贝头文件和库文件到系统路径下这两个操作就是安装库底层的核心操作。但是我们不推荐将我们自己的写的代码做成库拷贝到系统目录下因为这样会污染系统环境甚至导致系统工具崩溃或产生难以排查的冲突。方法3使用软链接这种方法的核心也是通过系统的默认路径来找头文件和库这种方法的操作如下对我们使用的头文件的目录的绝对路径建立软链接到系统路径 /usr/include/ 下当前库文件所在的绝对路径建立软链接到系统链接 /lib64/ 下。注意这里一定要使用绝对路径。如下图所示注意如果按照上面方法则我们代码中的头文件也需要修改如图所示因为软链接文件中存的是路径所以在包头文件时就应该认识到我们是通过软链接来找到我们的头文件的所以代码中具体头文件之前要写上 “ 软链接名/ ” 不然也编不过。四、动态库1. 打包制作动态库动态库也是库它本质也是几个.o 文件打包形成的一个文件。但是打包形成动态库和打包静态库的操作也有区别动态库的打包过程如下我们仍然以以下代码为例第一步将源代码编译形成 .o 文件。与静态库不同动态库的 .o 文件的形成需要使用 gcc 的 -FPIC 选项这样选项的意思是与位置无关码关于它的具体理解在本章后面的动态库加载小节解释这里暂且认为动态库形成 .o 必须带该选项即可。如图所示第二步将形成的 .o 文件打包形成动态库。与静态库不同形成动态库不需要 ar 指令只需要使用 gcc 编译器即可但是需要用到 gcc 的 -shared 选项。如图所示第三步将库文件和头文件组织起来。和静态库一样要让库能被别人用我们需要给他两个文件一个放所有头文件一个放所有库文件。所以这里我就将头文件放在 mylib 目录下的 include 目录下即库文件放在 mylib 目录下的 lib 目录下如图2. 如何使用动态库找不到动态库我们仍然以以下 main.c 的文件代码来测试#include stdio.h #include my_add.h #include my_sub.h int main() { int a 10; int b 20; printf(%d %d %d\n, a, b, add(a, b)); printf(%d - %d %d\n, a, b, sub(a, b)); return 0; }具体的文件结构如下图所示然后我们使用 gcc 的-I-L-l这三个选项来编译写成可执行程序。得到以下结构但是我们可以看到通过链接动态库形成的可执行程序并不可以直接运行它会提示找到不到文件。其原因如下通过 gcc 的-I-L-l这三个选项是告诉了编译器我们使用的头文件和库文件在哪里了而一旦可执行程序形成了就和编译器没有关系了。当要将程序运行时负责加载程序的是操作系统的加载器。此时程序会去系统的标准路径如 /lib, /usr/lib或者环境变量指定的路径下寻找那个 .so 文件。 因为我们的 libmymath.so 只是放在当前项目目录下的 mylib/lib 里并不在系统的标准搜索路径中所以加载器找不到它就会报错cannot open shared object file。所以我们还需要告诉操作系统的加载器我们的动态库在哪里。我们可以通过ldd指令来查看程序所依赖的动态链接库如图同时我们也可以发现上面的程序除了链接我们指定的库也要链接C标准库而C标准库就是在系统的标准路径下/lib64/的所以它才找得到。那么我们要如何解决找不到动态库的问题呢这里方法有4种如下......。解决加载找不到库的方法(4种)方法1将库文件拷贝到系统的库路径 /lib64/ 或 /usr/li64/ 下因为加载器在程序启动时会优先搜索系统内置的“标准库目录”如 /lib64、/usr/lib64。所以将我们的 .so 文件动态库直接复制进去系统自然能找到。使用cp指令即可# sudo cp 需要的库文件 系统路径 sudo cp ./mylib/lib/libmymath.so /lib64/如图方法2在系统默认库路径/lib64/ 或 /usr/li64/ 下建立软链接与方法1类似但不移动原文件而是创建一个指向原文件的软链接。系统访问该软链接时会跳转到真实的库文件所在路径。sudo ln -s 当前路径文件名 /lib64/libmymath.so如图方法3将自己库所在的路径添加到系统的环境变量 LD_LIBRARY_PATH中LD_LIBRARY_PATH 是 Linux 系统中用于指定动态库.so 文件搜索路径的环境变量它在程序运行时由加载器读取用于在标准系统路径如 /lib, /usr/lib之外额外查找所需的动态库。使用export来添加路径export LD_LIBRARY_PATH$LD_LIBRARY_PATH:当前路径/mylib/lib/这种方法只是临时的重启我们的shell之后添加的信息就没有了。如图注意一般云服务器或 Linux 系统默认没有设置这个环境变量或者它是空的。如果你发现当前系统中存在该变量通常是因为之前的用户手动配置过或者某些特定软件在安装时自动添加了路径。方法4在 /etc/ld.so.conf.d/ 建立配置文件然后使用 ldconfig 更新系统在启动时会读取 /etc/ld.so.conf 中列出的所有路径并将这些路径下的库信息缓存到 /etc/ld.so.cache 文件中。ldconfig 命令用于更新这个缓存。所以我们可以通过以下两个步骤来实现在 /etc/ld.so.conf下添加一个.conf文件在·其中保存我们库的路径然后使用 ldconfig 更新一下配置文件即可。如图所示最后虽然我们介绍了4种方法但是实际上我们使用的库都是别人的成熟的库一般都是直接采用安装到系统的方式相当于方法1。3. 理解动态库的加载宏观理解共享库的概念我们知道动态库在进程运行时是要被加载到内存中的而静态库则是在程序在编译链接阶段库代码会被完整拷贝到可执行程序中。往往大多数程序会依赖相同的常见动态库例如所有使用C语言标准库的程序都需要libc.so。此时这些动态库被称为共享库。 共享的核心价值就是“一份代码多进程复用”即 当多个程序同时运行并依赖同一个共享库时内存中只加载一份库代码来共用而非每个程序都加载一份因为没必要从而大幅节省内存资源。那么动态库加载之后会被所有进程共享是怎么做到的动态库加载的宏观理解在一个进行运行时会有它自己的task_struct 进程地址空间和页表还有内存中加载的整个进程的代码和数据然后通过页表将物理内存中的代码和数据映射到进程地址空间。那么如果进程使用了动态库因为程序编译时就已经告诉这个程序需要的动态库在哪里动态库也是一个文件所以当这个进程启动时加载器就会将动态库这个文件加载到内存然后通过该进程的页表将动态库映射到进程地址空间的共享区中。当进程执行到调用动态库函数的指令时CPU 会根据页表中的记录跳转去执行共享区中动态库对应的代码执行完毕后再返回主程序继续运行。这就完成了一次对动态库的访问。动态库的共享因为操作系统在运行时一定会存在多个动态库所以动态库都要被操作系统管理先描述再组织起来因此对于所以动态库的加载情况操作系统一定非常清楚。所以当第二个进程启动并需要同一个动态库时操作系统会进行检查如果发现该库的代码已经在物理内存中存在就不会再次加载副本而是直接修改新进程的页表将其指向同一块物理内存区域。如图所示注意库中也有一些全局变量比如错误码 errno虽然多个进程是共有同一个库的但是如果多个进程要修改库中的变量则就会发生写时拷贝。通过以上理解我们并不能够知道动态库加载的具体过程仍有一些问题比如CUP中读到的地址是什么以及CUP怎么知道要访问的下一个地址是什么呢所以接下来我们还需要理解一下关于地址的问题如下所示。程序地址空间2关于地址的认识程序加载到内存之前逻辑地址在程序加载到内存之前也就是编译形成可执行程序之后此时的程序内部的每一条指令都是有地址的但这时的地址并不是物理地址即物理内存上的地址而是逻辑地址指的是段地址偏移量的方法的地址在如今也可以叫虚拟地址。因为在编译的时候程序还不知道自己将来会被加载到物理内存的哪个位置可能是在地址 0x1000也可能是在地址 0x100000。如果写死了物理地址程序就可能与内存其他进程发生冲突无法灵活运行了。 所以编译器编译时会假设程序是从 0 地址开始存放的或者从某个固定的基址如 Linux 下的 0x08048000所以编译好后再磁盘中的可执行程序的机器码中像其中的跳转指令如 call jmp等等的地址全部都是基于这个假设基址的相对偏移量也就是逻辑地址。当编译器把源代码编译成目标文件链接器再把它链接成可执行文件时它并不知道这个程序将来会被放在内存的哪个角落照顾操作系统。因此编译器也会按照功能把代码和数据切分成不同的“段”如下.code 段 存放 CPU 执行的指令比如图片里的 main, fun。.data 段 存放已经初始化了的全局变量和静态变量。.bss 段 存放未初始化的全局变量只记录大小不占磁盘空间。......而对于每一个段中的各个地址通常是相对于该段起始位置的偏移量。所以我们可以得出一个结论编译链接后生成的可执行文件其内部包含的地址本质上都是基于“假设基址”的逻辑地址在磁盘文件中 为相对于文件头或段头的偏移量此时还没有真正的“虚拟地址”概念因为程序还没被操作系统接管在加载到内存后 操作系统会把这些地址映射到进程的虚拟地址空间中。此时才正式被称为虚拟地址程序加载到内存之后虚拟地址当程序加载如内存之后因为程序的每一条指令都会在物理内存中占一个地址此时表示物理内存中的地址叫做物理地址。所以当程序加载到内存一个有两个地址一个是程序内部的逻辑地址记载到内存之后就没有逻辑地址这个概念了在内存中称为虚拟地址一个是内存中的物理地址。当程序被加载到内存后操作系统会为它分配task_struct虚拟地址空间页表当要执行第一条指令时CPU的程序计数器PC或指令指针IP寄存器会被设置为该程序的入口地址这个入口地址在程序编译好后就有了在程序内部所以这个地址在磁盘中是一个逻辑地址在CPU中就称为虚拟地址。当CPU尝试根据这个虚拟地址去获取指令时会通过取查询页表。由于程序刚刚加载其代码段所在的页面可能尚未映射到物理内存或者根本没有加载到内存这次查询会失败从而触发一个缺页中断。让操作系统从磁盘中找到程序对应的代码和数据并将它们加载到空闲的物理内存页框中加载完成后操作系统会更新该进程的页表建立起程序的虚拟地址与新分配的物理内存地址之间的映射关系这样CUP就找到第一个指令开始执行了。那么当执行到call,jmp这样的跳转地址的指令时call 后跟着的地址指令间跳转也是虚拟地址。当程序加载到内存之前它就已经是虚拟地址了所以CUP从读取程序当中的地址到分析处理后二次访问它的整个过程中的读到指令的地址全部都是虚拟地址只是读到虚拟地址之后要找到物理地址需要页表映射但是指令间的定位看到的都是虚拟地址。动态库的地址理解 --- 为什么要使用gcc的-FPIC 选项通过上面的理解我们知道了CPU要指令一个程序的代码指令都是通过编译好之后程序的虚拟地址内存中叫虚拟磁盘上叫逻辑执行访问与执行跳转的。那么当执行到一个程序中的访问动态库的代码时按照之前的逻辑编译采用的绝对编址的方法比如在程序的代码为printf(xxx)则编译之后这条代码就会变成call 0x11223344类似这样的指令所以此时在CPU看了现在要跳转到进程地址空间的虚拟地址必须为0x11223344这个地方去执行程序代码。如果是这样会有什么问题这就意味着printf 函数必须永远固定在虚拟内存的 0x11223344 这个位置。共享库如 libc.so很大而且可能被很多个程序同时使用。如果把它固定加载到某个位置比如所有程序都规定放在 0x90000一旦那个位置被别的程序占用了怎么办或者不同程序对库的加载地址要求冲突了怎么办所以操作系统不需要把库固定在某个死板的地址。它可以根据当前内存的空闲情况灵活地把 libc.so 映射到进程虚拟地址空间的“共享区”或者其他空闲区域。即实现库可以在虚拟地址中任意位置加载因此在编译动态库函数代码的时候就不要采用绝对编址而让这里编译的地址只表示每个函数在库中的偏移量即可也就是说让之前的绝对编址call 0x11223344这样的指令改成基于库起始位置的相对跳转。这样一来无论操作系统把这个库加载到内存的哪个角落只要知道库的起始地址因为操作系统要对库做管理那它就一定知道库的起始地址在哪里加上这里固定的偏移量就能瞬间算出函数的真实运行地址找到库函数代码并执行了。以上这就是位置无关代码技术直接使用偏移量对库中的函数进行编址。所以为什么我们在制作动态库的时候形成 .o 文件时要加上-fPIC-fPIC 就是告诉编译器生成这种位置无关的代码即产生位置无关码。补充问题为什么静态库不谈加载不谈与位置无关在程序编译链接阶段链接器会把静态库.a 文件中所有被引用的目标文件.o直接合并到最终的可执行文件中。程序运行时这些代码已经是可执行文件的一部分不需要像动态库那样在启动时由操作系统“加载”到内存并且静态库中的代码在链接时就已经被分配了固定的虚拟地址相对于可执行文件的起始地址。程序运行时这些地址是确定的不需要通过“基址 偏移”来动态计算。五、补充内容1. 动态链接和静态链接链接方法可以分为动态和静态链接静态链接库的代码被复制到了最终的可执行文件内部。动态链接库的代码独立存在于外部的 .so (Linux) 或 .dll (Windows) 文件中。你的我们的可执行程序里只留了一个“地址”或“名字”运行时操作系统才去外面找对应文件来用。如果使用到的库的类型不同gcc的链接方法也有不同一般有一些三种情况如果只有静态库.a / .lib 只能进行静态链接。如果只有动态库.so / .dll 只能进行动态链接。当同时有静态库和动态库时 默认优先选择动态链接。如果要使用静态链接则需要在使用 gcc 的 -static 选项。2. gcc 链接第三方库使用gcc 链接第三方库时首先需要三个选项这三个选项都支持重复出现。-I(头文件路径)指定 #include 搜索路径。如果多个库的头文件散落在不同目录必须为每个目录单独写一个-I参数-L(库路径)告诉链接器去哪里找 .so 或 .a 文件。如果库文件分布在不同的路径下则需要写写多个 -L 分别指定。-l(库名)需要列出所有需要的库。指定具体要链接的库名去掉 lib 前缀和后缀。如果要链接多个库列出所有需要的库时必须严格遵守依赖顺序原则使用者在前被使用者在后。示例如果库 A 用到了库 B 的函数那么-lA必须写在-lB的前面。如果写反了链接器可能会因为扫描顺序问题报错 undefined reference。感谢各位观看希望能多多支持