裸机开发一个 main.c 编译到底,所有代码揉成一个二进制。上了 Linux,ls /lib 一看,满屏的 .so 和 .a。为什么要把代码拆成库?静态库和动态库到底差在哪?交叉编译时 -L、-I、--sysroot 怎么配?这篇文章用 WSL 环境从零走一遍,附上:从零构建一个动态库 demo,每条命令都经过 WSL 实测验证。
一、先看现象:/lib 下为什么有 .so 和 .a
随便进一个 Linux 系统的 /lib 目录,你会看到两种截然不同的文件:
$ ls /lib/x86_64-linux-gnu/libc.a libc.so.6 libm.so.6libpthread.a libpthread.so.0 libdl.so.2
.a 是静态库(archive),本质是 .o 目标文件的打包集合。.so 是动态库(shared object),程序运行时才加载。
用 file 命令看得更清楚:
$ file /usr/lib/x86_64-linux-gnu/libc.alibc.a: current ar archive # 一堆 .o 的 tar 包$ file /lib/x86_64-linux-gnu/libc.so.6libc.so.6: ELF 64-bit LSB shared object # 可执行格式的共享库
本节要点:.a 是编译时链接的静态库,.so 是运行时加载的动态库。file 命令能直接区分两者。
二、为什么需要动态库
MCU 裸机开发很少拆库——Flash 就那么大,代码全塞一起编译就行。但嵌入式 Linux 完全不同。
2.1 代码复用:别把 libc 塞进每个程序
如果不拆库,每个程序都要把自己用到的 printf、malloc 代码全部编译进去。一个系统跑几十个进程,磁盘和内存里全是重复代码。
$ ldd /bin/ls libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6# libc.so.6 只有一个副本,所有程序共享
2.2 升级不重编译
OpenSSL 出了安全补丁,只更新 libssl.so 一个文件,所有依赖它的程序自动生效。用静态库的话,每个程序都得重新编译、重新部署。
2.3 内存共享:动态库只加载一次
内核的 mmap 机制让同一个 .so 在物理内存中只存一份,多个进程的虚拟地址映射到同一块物理页。这对嵌入式设备的内存控制很关键——物理内存通常只有几百 MB,能省一点是一点。
本节要点:动态库解决三个核心问题——代码复用不重复打包、升级不重编译、内存共享只加载一次。
三、动静态库优缺点对比
本节要点:静态库适合极简固件和关键模块,动态库适合通用系统和需要热更新的场景。嵌入式里两者往往混用——核心用静态、上层用动态。
四、交叉编译中怎么指定库
嵌入式开发不会在目标板(ARM)上编译,而是在 x86 主机上用交叉编译链编译。这就需要告诉编译器:库在哪、头文件在哪、目标系统根目录在哪。
4.1 核心参数速查
arm-linux-gnueabihf-gcc \ --sysroot=/path/to/target/rootfs \ # 目标系统根目录 -I/path/to/include \ # 头文件搜索路径 -L/path/to/lib \ # 库文件搜索路径 -lmylib \ # 链接 libmylib.so 或 libmylib.a -Wl,-rpath,/usr/lib \ # 运行时库搜索路径(写入 ELF) main.c -o app
| | |
|---|
--sysroot | | --sysroot=/home/hello/rootfs |
-I | | -I/home/hello/rootfs/usr/include |
-L | | -L/home/hello/rootfs/usr/lib |
-l | | -lmylib |
-Wl,-rpath | 运行时库搜索路径(写入 ELF 的 RUNPATH) | -Wl,-rpath,/usr/lib |
-static | | |
4.2 环境变量
编译时和运行时,还有几个环境变量影响库的查找:
# 编译时:pkg-config 查找 .pc 文件$ export PKG_CONFIG_PATH=/home/hello/rootfs/usr/lib/pkgconfig# 编译时:C 编译器找头文件$ export C_INCLUDE_PATH=/home/hello/rootfs/usr/include# 运行时:动态链接器找 .so(优先级低于 rpath)$ export LD_LIBRARY_PATH=/usr/local/lib:/usr/lib
4.3 运行时查找顺序
动态链接器(ld-linux.so)找库的优先级,从高到低:
本节要点:交叉编译核心三要素——--sysroot 定根、-I/-L 定搜索路径、-Wl,-rpath 定运行时路径。环境变量 LD_LIBRARY_PATH 是运行时兜底。
五、WSL 实操:从零构建一个动态库
附上:从零构建一个动态库 demo。下面在 WSL2(Ubuntu)环境里走一遍完整流程:写一个数学工具库,编译成静态库和动态库,再用程序调用。每条命令都已在 WSL 中实测通过。
5.1 目录结构
$ mkdir -p ~/libdemo && cd ~/libdemo$ mkdir src include lib build最终结构:libdemo/├── include/mymath.h # 头文件├── src/mymath.c # 库源码├── src/main.c # 测试程序├── lib/ # 生成库放这里└── build/ # 编译中间文件
5.2 写库代码
// include/mymath.h#ifndef MYMATH_H#define MYMATH_Hint add(int a, int b);int sub(int a, int b);int mul(int a, int b);double div_op(int a, int b); // 返回 -1 表示除零#endif
// src/mymath.c#include "mymath.h"int add(int a, int b) { return a + b; }int sub(int a, int b) { return a - b; }int mul(int a, int b) { return a * b; }double div_op(int a, int b) {if (b == 0) return -1.0;return (double)a / (double)b;}
// src/main.c#include <stdio.h>#include "mymath.h"int main(void) {printf("add(3, 4) = %d\n", add(3, 4));printf("sub(10, 3) = %d\n", sub(10, 3));printf("mul(6, 7) = %d\n", mul(6, 7));printf("div(15, 4) = %.2f\n", div_op(15, 4));return 0;}
5.3 编译静态库
# 第一步:编译成目标文件$ gcc -c -Iinclude src/mymath.c -o build/mymath.o# 第二步:打包成静态库$ ar rcs lib/libmymath.a build/mymath.o# 验证:查看静态库包含哪些 .o$ ar t lib/libmymath.amymath.o# 第三步:静态链接生成可执行文件(-static 强制全静态)$ gcc -Iinclude src/main.c -Llib -lmymath -static -o build/test_static# 运行$ ./build/test_staticadd(3, 4) = 7sub(10, 3) = 7mul(6, 7) = 42div(15, 4) = 3.75
5.4 编译动态库
# 第一步:编译成位置无关代码(-fPIC 是动态库的关键)$ gcc -c -fPIC -Iinclude src/mymath.c -o build/mymath_pic.o# 第二步:生成动态库$ gcc -shared -o lib/libmymath.so build/mymath_pic.o# 验证:查看动态库的符号表$ nm -D lib/libmymath.so | grep " T "00000000000010f9 T add000000000000113e T div_op0000000000001127 T mul0000000000001111 T sub# 第三步:链接动态库生成可执行文件$ gcc -Iinclude src/main.c -Llib -lmymath -Wl,-rpath,./lib -o build/test_dynamic# 检查依赖$ ldd build/test_dynamic libmymath.so => ./lib/libmymath.so # 找到了我们自己的库# 运行$ ./build/test_dynamicadd(3, 4) = 7sub(10, 3) = 7mul(6, 7) = 42div(15, 4) = 3.75
5.5 对比两个可执行文件
$ ls -lh build/test_*-rwxr-xr-x 1 root root 16K build/test_dynamic # 动态链接-rwxr-xr-x 1 root root 768K build/test_static # 静态链接(全静态)$ file build/test_staticbuild/test_static: ELF 64-bit LSB executable, statically linked$ file build/test_dynamicbuild/test_dynamic: ELF 64-bit LSB shared object, dynamically linked$ ldd build/test_static not a dynamic executable # 完全不依赖 .so$ ldd build/test_dynamic libmymath.so => ./lib/libmymath.so libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
静态链接把整个 libc 都嵌进去了,768K vs 16K,差了近 50 倍。换成实际项目里链接几十个库,差距会更大。
本节要点:静态库用 ar rcs 打包、动态库用 gcc -shared -fPIC。静态链接加 -static 得到完全独立的大体积二进制,动态链接用 -Wl,-rpath 把库路径写进 ELF,运行时不用设 LD_LIBRARY_PATH。
六、常见问题排查
| | | |
|---|
| error while loading shared libraries | | 加 -Wl,-rpath 或设 LD_LIBRARY_PATH |
| version GLIBCXX_3.4.26 not found | | |
| relocation R_X86_64_32 against ... | | |
| cannot find -lmylib | --sysroot | 先 ls 确认目标 rootfs 下库文件确实存在 |
| undefined reference | | -la -lb |
本节要点:动态库最常踩的两个坑——找不到 .so 和版本不匹配。前者靠 rpath 解决,后者要保证编译和运行环境一致。
七、总结
嵌入式 Linux 的库体系,本质是把"一个 main.c 编译到底"的裸机思维拆成**"核心 + 共享组件"**的模块化思维。
- 静态库:编译时嵌入,
ar rcs 打包,-static 链接。适合极简固件和关键模块,无外部依赖但体积大。 - 动态库:运行时加载,
gcc -shared -fPIC 生成,-Wl,-rpath 指定路径。适合共享内存、热更新、插件化场景,体积小但依赖目标环境。
交叉编译时,--sysroot 定目标根、-I/-L 定搜索路径、-Wl,-rpath 定运行时路径,配合 LD_LIBRARY_PATH 和 PKG_CONFIG_PATH,就能覆盖绝大多数库管理需求。
下次在嵌入式 Linux 上 ls /lib 看到满屏 .so 时,不妨把它当成系统在说:"代码拆成积木,想换哪块换哪块,别把所有东西焊死在一块板子上。"