4、依赖库基础:静态库与动态库的区别、库的搜索路径、ldconfig与LD_LIBRARY_PATH
做嵌入式交叉编译,绕不开一个话题——库。
说白了,你的程序不是孤岛。它要调用别人写好的功能,比如打印、数学运算、网络通信。这些功能被打包成库,供你链接。但库有两种形态:静态库和动态库。选错了,够你折腾半天。
4.1 静态库 vs 动态库:到底差在哪?
我刚开始做嵌入式时,觉得库嘛,能链接上就行。直到有一次,我编译了一个HTTP服务器,扔到开发板上,发现可执行文件足足大了三倍。板子Flash只有8MB,直接塞不下了。这才逼我去搞清楚静态库和动态库的区别。
静态库(.a 文件):编译时,库的代码直接复制进你的可执行文件里。好处是——程序独立运行,不需要额外带库文件。坏处是——每个程序都拷贝一份,体积膨胀。你想想看,如果系统里10个程序都用libc,那libc的代码就被复制了10份。
动态库(.so 文件):编译时,只记录一个引用。程序运行时,才去加载库。好处是——多个程序共享一份库代码,节省空间。坏处是——运行环境必须要有这个库,否则程序启动就报错。
我个人的习惯是:
- 工具类、调试类程序,用静态库。省心,不怕环境缺失。
- 正式产品、多进程系统,用动态库。节省Flash和RAM。
4.2 库的搜索路径:编译器去哪找库?
你写代码时用了 #include <stdio.h>,编译器知道去哪找头文件。但你链接时用了 -lm,编译器知道去哪找 libm.so 吗?
嗯,这里有个搜索顺序的问题。我当年踩过一个坑:交叉编译一个程序,明明把库放到了 /usr/lib,但链接器就是报 undefined reference。后来发现,交叉编译器的搜索路径根本不是我本机的 /usr/lib,而是它自己的 sysroot 目录。
编译器搜索库的路径,大致如下:
- 编译时指定的
-L路径(优先级最高) - 环境变量
LIBRARY_PATH(编译时链接器用) - 系统默认路径:
/lib、/usr/lib、/usr/local/lib - 交叉编译器的 sysroot 路径(交叉编译时特有)
举个例子,你交叉编译时经常这么写:
arm-linux-gnueabihf-gcc main.c -o app \
-L/home/arm/lib \
-lmyutils
-L 告诉编译器去 /home/arm/lib 找库。-lmyutils 告诉它链接 libmyutils.a 或 libmyutils.so。
$HOME/arm-sysroot。然后用 --sysroot 参数指定。这样路径清晰,不会跟本机库搞混。
4.3 运行时库搜索:ldconfig 与 LD_LIBRARY_PATH
编译通过了,链接成功了,可执行文件也生成好了。你高高兴兴把它拷贝到开发板上,一运行——
./app: error while loading shared libraries: libmyutils.so: cannot open shared object file: No such file or directory
崩溃不?我经历过,不止一次。
编译时能找到库,不代表运行时也能找到。这是两个完全不同的阶段。运行时,系统需要知道去哪加载动态库。这时候,两个东西就派上用场了:
4.3.1 LD_LIBRARY_PATH:临时救急
这是一个环境变量。你告诉系统:「喂,除了默认路径,再去这些地方找找库。」
export LD_LIBRARY_PATH=/home/arm/lib:$LD_LIBRARY_PATH
./app
我建议只在调试时用这个。为什么?因为它影响所有程序。你设了 LD_LIBRARY_PATH,系统里所有动态链接的程序都会去那个路径找库。万一路径下有同名但版本不同的库,整个系统都可能出问题。
/etc/profile 里全局设置了 LD_LIBRARY_PATH。结果系统里好几个工具链的库版本冲突,ls、cp这些基础命令都报段错误。最后只能进单用户模式修复。从那以后,我再也不在生产环境里全局设这个变量了。
4.3.2 ldconfig:系统级管理
ldconfig 是系统用来管理动态库缓存的工具。它扫描 /etc/ld.so.conf 里配置的目录,把找到的库信息写入 /etc/ld.so.cache。运行时,动态链接器直接查缓存,速度飞快。
你安装了一个新库,想让系统认识它,步骤是:
- 把
.so文件放到/usr/lib或/usr/local/lib - 运行
sudo ldconfig - 搞定
交叉编译时,你可能会在目标板的文件系统里运行 ldconfig。但要注意:交叉编译环境里的 ldconfig 是针对本机的,不能用来更新目标板的缓存。你得在目标板上跑它自己的 ldconfig。
4.4 一张图理清关系
说了这么多,我画张图帮你梳理一下整个流程:
4.5 实战建议:交叉编译时怎么配库路径
说了这么多理论,来点实际的。我总结了一套交叉编译时配置库路径的流程:
| 场景 | 做法 | 说明 |
|---|---|---|
| 编译时找库 | -L/path/to/libs |
在 Makefile 或 CMakeLists.txt 中指定 |
| 运行时找库(调试) | export LD_LIBRARY_PATH=... |
临时设置,别写进系统配置文件 |
| 运行时找库(正式) | 把库放到目标板 /usr/lib,运行 ldconfig |
系统级管理,稳定可靠 |
| 编译时指定运行时路径 | -Wl,-rpath,/path/to/libs |
把搜索路径硬编码进可执行文件 |
我个人最推荐最后一种——-Wl,-rpath。它把运行时库路径直接写进可执行文件里。这样你不需要设置任何环境变量,也不用担心 ldconfig 没更新。程序自己知道去哪找库。
arm-linux-gnueabihf-gcc main.c -o app \
-L/home/arm/lib \
-lmyutils \
-Wl,-rpath,/opt/arm/lib
这样编译出来的 app,运行时就会去 /opt/arm/lib 找 libmyutils.so。干净利落。
-Wl,-rpath-link 和 -Wl,-rpath 搞混过。记住:-rpath-link 只在链接时用,-rpath 会写入可执行文件供运行时用。两个不一样,别搞反了。
好了,关于库的基础知识就聊到这。静态库和动态库怎么选,搜索路径怎么配,ldconfig 和 LD_LIBRARY_PATH 怎么用——这些搞清楚了,交叉编译环境里的库依赖问题,你基本就能自己搞定了。