Linux库制作与使用(一):静态库与动态库入门
目录一、什么是库1. 简单认识库2. 演示代码二、目标文件三、静态库1. 静态库概念2. 静态库生成3. 静态库使用四、动态库1. 动态库概念2. 动态库生成3. 动态库使用五、库搜索路径1. 编译 / 运行时路径2. 如何像系统库一样使用3. 实际演示七、外部库1. apt 安装原理2. ncurses 库使用示例总结一、什么是库在之前的文章中我们详细讲解了文件系统及内核相关数据结构。本文将视角转向应用层开发中不可或缺的核心内容库Library1. 简单认识库设想每次开发新项目时都需要从零开始编写 屏幕字符输出、平方根计算 或 网络协议栈处理 这些基础功能开发效率将会变得极其低下库的出现主要解决了三个痛点代码复用将通用的功能封装起来一次编写到处运行模块化开发大型项目可以拆分成多个模块由不同团队维护互不干扰分发便捷你可以只提供库文件和头文件给别人使用而不需要暴露底层源代码在 Linux 环境下库的本质是一组预先编译好的目标文件的集合。它就像一个工具箱提供了可以直接调用的函数和变量根据链接时机的不同库分为两类静态库Static Library链接时将库代码直接拷贝到可执行程序中动态库Shared Library链接时仅建立引用关系程序运行时才去加载库2. 演示代码为了方便演示我们准备一套简单的数学工具代码。它包含两个文件一个头文件 my_math.h 定义接口一个源文件 my_math.c 实现逻辑头文件my_math.h#ifndef __MY__MATH__ #define __MY__MATH__ // 简单的加法 int add(int a, int b); // 简单的减法 int sub(int a, int b); #endif实现文件my_math.c#include my_math.h int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; }测试程序main.c#include stdio.h #include my_math.h int main() { int x 10, y 5; printf(add: %d\n, add(x, y)); printf(sub: %d\n, sub(x, y)); return 0; }二、目标文件在谈论库之前我们必须先理解它的前身——目标文件执行gcc -c my_math.c时编译器并不会生成一个可以直接运行的程序而是生成了一个后缀为 .o 的文件即目标文件定义与特点中间产物它是源代码经过预处理、编译、汇编后的二进制文件不可直接执行虽然它包含了机器码但由于文件内部的函数地址尚未重定位操作系统无法直接运行它文件格式在 Linux 下目标文件和库文件通常遵循ELF格式底层逻辑构建程序如同建造房屋源代码是设计图纸目标文件.o如同预制好的砖块而库文件则是分类打包的建筑材料包。就像不能直接住进砖堆里一样必须通过链接过程按照图纸将这些材料组装起来才能建成最终可执行程序这座房子三、静态库静态库在 Linux 中通常以 .a 作为后缀其本质就是将多个 .o 文件打包压制成一个单一文件1. 静态库概念静态库在程序编译链接阶段会被完整地复制到可执行程序中优点程序运行速度快无需动态寻址发布程序时不需要携带库文件因为库已经被加载进程序里了缺点浪费空间每个程序都包含一份库的副本维护困难库代码更新时所有依赖程序都需要重新编译2. 静态库生成我们将使用前文准备的 my_math.c 来生成名为 libmymath.a 的静态库第一步将源文件编译为目标文件我们需要 .o 格式的机器码但不需要它变成可执行程序第二步使用 ar 工具打包ar 是 Linux 下专门用于创建、修改和提取归档文件的工具命名规范Linux 下的库文件必须以 lib 开头以 .a 结尾。中间的 mymath 才是它的真实逻辑名称ar命令常用选项说明- r (replace)若库中已存在同名文件则替换它若不存在则添加- c (create)创建一个库3. 静态库使用有了 libmymath.a 和 my_math.h我们就可以编译 main.c 了。为了模拟真实开发环境我们假设头文件在 include 目录库文件在 lib 目录使用示例:关键选项这是 Linux C/C 开发中最基础也最核心的三个选项选项全称含义与作用-IInclude指定头文件搜索路径。告诉编译器去哪里找 #include ... 里的文件-LLibrary Path指定库文件搜索路径。告诉链接器去哪个文件夹里找库文件-lLink Library指定要链接的库名。注意这里要去掉前缀 lib 和后缀 .a。例如 libmymath.a 只写 mymath链接器在工作时会根据 -lmymath 拼凑出 libmymath.a 这个文件名然后在 -L 指定的路径中挨个寻找。如果找到了就会把 libmymath.a 中被 main.c 用到的那部分机器码链接到最终的 my_app 二进制文件中验证静态库我们可以使用 ldd 命令来查看生成的可执行程序依赖哪些库我们发现结果中找不到 libmymath。这证明了 mymath 的代码已成功整合到 main 的内部它不再依赖外部的库文件即可独立运行四、动态库相较于静态库在编译时将代码整合至可执行文件中的方式动态库共享库采用运行时加载与内存共享的机制是现代操作系统中核心的库实现方式在 Linux 系统下动态库文件一般以 .so 作为后缀名1. 动态库概念动态库的核心特征在于代码不在编译时拷贝而是在运行时加载编译阶段链接器仅在可执行文件中记录 我需要这个库 的符号信息索引而不复制实际代码运行阶段当程序启动时操作系统的动态链接器会将库文件加载到内存中多进程共享如果 100 个程序都使用了同一个 libc.so内存中只会有一份该库的代码副本所有程序通过虚拟地址空间映射到同一块物理内存2. 动态库生成制作动态库比静态库多了一个关键步骤生成 位置无关代码第一步生成目标文件-fPIC告诉编译器产生的机器码不要使用绝对地址而要使用相对地址。因为动态库在运行时会被加载到进程空间的任何位置如果写死了地址换个地方就跑不通了第二步生成动态库为什么制作动态库不使用 ar?静态库的生成本质上是利用 ar 工具对目标文件.o进行的物理归档仅涉及文件的封装与索引维护不触发链接动作而动态库作为遵循 ELF 格式的可加载二进制对象其生成过程必须由链接器参与以完成符号表的合并、依赖关系的导出以及位置无关代码PIC的逻辑编排等。因此动态库的构建属于编译链接范畴而非简单的文件归档3. 动态库使用动态库的使用分为链接和加载两个完全独立的阶段阶段一链接编译时这一步和静态库完全一样。编译器检查 libmymath.so 是否存在以及里面的函数签名是否对得上。如果通过它会在生成的 main 里打上一个标签我依赖 libmymath.so阶段二加载运行时当我们尝试执行 ./main 时会看到这样的报错因为此时是操作系统在找库而不是编译器。操作系统默认只会在 /lib64 或 /usr/lib64 等标准目录下寻找。由于我们的库在当前目录系统看不见为了加深理解我们对比一下两者在程序生命周期中的表现静态库动态库集成时机编译链接时完全嵌入程序运行时按需加载程序体积较大包含所有库代码较小仅包含引用描述内存占用多个程序运行会有重复副本物理内存中仅存一份全系统共享更新维护必须重新编译整个程序只需替换 .so 文件程序重启即生效运行依赖独立运行不依赖库文件运行时必须能找到对应的库文件五、库搜索路径在 Linux 开发中程序对库文件的定位分为编译链接时与运行时两个独立阶段。理解这两个阶段的搜索路径及其优先级是解决找不到库文件问题的核心1. 编译 / 运行时路径编译时路径在编译阶段链接器通过开发者指定的选项来查找静态库或动态库的符号信息-L选项显式指定链接器搜索库文件的路径-l选项指定具体的库名称去掉 lib 前缀与后缀标准路径如果未指定 -L链接器默认搜索 /lib、/usr/lib 以及 /usr/local/lib 等系统目录运行时路径对于动态链接的可执行程序当其启动时系统的动态链接器负责将程序依赖的 .so 文件加载进内存。由于动态链接器不直接读取编译时的 -L 参数它依赖以下机制按优先级进行查找(1) 环境变量 LD_LIBRARY_PATH定义这是一个进程级的环境变量用于临时指定动态库的搜索目录。应用场景常用于开发调试或在不具备系统权限的情况下指向非标准目录中的库文件生效方式export LD_LIBRARY_PATH/path/to/your/lib:$LD_LIBRARY_PATH特点优先级高仅对当前终端及其子进程生效属于临时配置(2) 配置文件 /etc/ld.so.conf定义这是一个系统级的配置文件用于永久保存全局动态库的搜索路径结构通常该文件会包含 /etc/ld.so.conf.d/ 目录下的所有 .conf 文件建议为每个第三方库在该目录下创建独立的配置文件应用场景安装长期运行的服务或系统级组件时使用(3) 缓存工具 ldconfig作用动态链接器为了提高查找效率并不会在程序启动时实时遍历所有路径而是读取一个预先生成的二进制缓存文件 /etc/ld.so.cache机制ldconfig 程序负责读取 /etc/ld.so.conf 中的路径扫描这些目录下的动态库并更新 /etc/ld.so.cache必要性当你在系统目录如 /usr/lib或 /etc/ld.so.conf 指定的目录中新增 .so 文件后必须以 root 权限执行 ldconfig否则系统无法感知库的更新(4) 默认系统目录如果以上路径均未命中动态链接器将最后检索标准系统路径主要是 /lib 和 /usr/lib。优先级查找机制性质1编译时指定的 RPATH嵌入在 ELF 文件内部的路径最高优先级2LD_LIBRARY_PATH环境变量用户临时干预3/etc/ld.so.conf由 ldconfig 根据ld.so.conf生成的系统级缓存4/lib 和 /usr/lib操作系统默认的标准库存放目录2. 如何像系统库一样使用要让系统像识别标准库一样识别我们的 libmymath.so需要完成头文件与库文件的双向集成(1) 头文件的集成内核与编译器默认的头文件搜索路径是 /usr/include。内核与编译器默认的头文件搜索路径是 /usr/include操作将 my_math.h 拷贝至 /usr/include 目录下在代码中引用时可以将 #include my_math.h 改为 #include my_math.h。编译器在预处理阶段会自动在此标准路径下命中该文件从而无需在编译中指定头文件搜索路径(2) 库文件的集成系统的默认库文件搜索路径通常包括 /lib64 或 /usr/lib6464位系统操作将 libmymath.so 拷贝至 /usr/lib64或 /usr/lib解决报错由于动态链接器ld.so默认会检索该目录将库移动并执行 ldconfig 刷新缓存。至此可以直接解决运行程序时出现的 cannot open shared object file 错误ldconfig 的必要性仅仅将 .so 文件拷贝到系统目录是不够的。如前所述动态链接器为了性能会读取缓存文件 /etc/ld.so.cache。所以在拷贝完库文件后必须以 root 权限执行 ldconfig该命令会重新扫描 /usr/lib64 等标准目录将新发现的 libmymath.so 加载到二进制缓存映射表中。此时程序启动时便能通过缓存瞬间定位到该动态库的物理地址3. 实际演示在完成上述集成步骤后我们的开发工作流将极大简化简化后的编译指令# 此时不再需要 -I 指定头文件路径也不需要 -L 指定库路径 gcc main.c -o main -lmymath运行测试结果分析编译时链接器在 /usr/lib64 找到了 libmymath.so校验符号成功运行时动态链接器通过 /etc/ld.so.cache 找到了库的物理路径成功将其加载至进程地址空间程序正常执行七、外部库1. apt 安装原理在 Linux 中使用 sudo apt install 安装一个开发库时系统并非简单地下载文件而是执行了一系列符合文件系统层次标准的自动化部署动作以安装 libncurses5-dev 为例其幕后流程如下二进制分发从镜像源下载预编译好的 .so 动态库文件、.a 静态库文件以及 .h 头文件。路径归档将头文件解压至 /usr/include/ 目录将库文件解压至 /usr/lib/ 目录符号链接创建自动创建版本号之间的软链接例如让 libncurses.so 指向真实的实体文件 libncurses.so.5.9确保编译器通过 -lncurses 就能找到最新版本缓存更新自动触发 ldconfig 程序将新安装的 .so 路径写入 /etc/ld.so.cache 缓存确保护程序在运行时能瞬间完成动态装载在 Linux 环境下调用外部库通常需遵循以下三个步骤包含在源码中使用 #include xxx.h。由于 apt 已将头文件放入标准目录无需再手动指定 头文件搜索路径查找在编译时使用-l选项。链接器会自动在标准库目录下搜索名为 libxxx.so 或 libxxx.a 的文件链接确定链接方式。默认情况下如果同时存在静态库和动态库编译器优先选择动态链接2. ncurses 库使用示例ncurses 是一个提供独立于终端的屏幕绘制和键盘处理能力的函数库(1) 环境准备sudo apt install libncurses5-dev(2) 示例程序demo.c该程序演示了如何初始化图形模式、在特定坐标打印文字并捕获键盘输入#include ncurses.h // 必须包含头文件 int main() { // 初始化窗口进入 ncurses 模式 initscr(); // 禁用行缓冲输入字符立即传递给程序 cbreak(); // 屏幕不回显输入的字符 noecho(); // 在屏幕中心附近移动光标并打印信息 mvprintw(12, 30, Welcome to VFS Library World!); mvprintw(14, 30, Press any key to exit...); // 刷新逻辑屏幕以显示内容 refresh(); // 等待用户输入 getch(); // 退出 ncurses 模式恢复标准终端 endwin(); return 0; }(3) 编译指令由于 ncurses 不是标准库必须在指令末尾显式链接gcc demo.c -o demo -lncurses执行效果总结综上所述从目标文件到静态库与动态库的制作与使用我们梳理了代码复用在工程中的基本实现路径。无论是 .a 还是 .so本质上都是对目标文件的组织与链接方式的不同选择前者在编译阶段完成整合后者则将绑定延迟到运行时从而带来更高的灵活性与此同时通过库的搜索路径与系统库的使用方式我们也进一步理解了程序从编写代码到运行执行之间的完整链路但更深一层的问题是这些目标文件与库文件内部究竟是如何组织的程序在加载与运行时又是如何被解析与链接的在下一篇中我们将以 ELF 格式为切入点深入探讨程序的底层结构与运行机制