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 目录。

编译器搜索库的路径,大致如下:

  1. 编译时指定的 -L 路径(优先级最高)
  2. 环境变量 LIBRARY_PATH(编译时链接器用)
  3. 系统默认路径:/lib、/usr/lib、/usr/local/lib
  4. 交叉编译器的 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。运行时,动态链接器直接查缓存,速度飞快。

你安装了一个新库,想让系统认识它,步骤是:

  1. 把 .so 文件放到 /usr/lib 或 /usr/local/lib
  2. 运行 sudo ldconfig
  3. 搞定

交叉编译时,你可能会在目标板的文件系统里运行 ldconfig。但要注意:交叉编译环境里的 ldconfig 是针对本机的,不能用来更新目标板的缓存。你得在目标板上跑它自己的 ldconfig。

4.4 一张图理清关系

说了这么多,我画张图帮你梳理一下整个流程:

交叉编译中库的搜索与加载流程 编译阶段(静态/动态链接) 1. 编译器搜索 -L 指定路径 2. 搜索 LIBRARY_PATH 环境变量 3. 搜索系统默认路径 4. 搜索交叉编译器 sysroot 可执行文件 (含动态库引用) 运行阶段(动态加载) 1. 检查 LD_LIBRARY_PATH 2. 检查 ld.so.cache 缓存 3. 检查 /lib, /usr/lib 默认路径 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 怎么用——这些搞清楚了,交叉编译环境里的库依赖问题,你基本就能自己搞定了。

可可精品资料库