我们在开发无系统的单片机程序时,编译器会将我们程序依赖的库(例如 Math 数学库)链接到我们的程序中,最终得到的固件就仅有 1 个 .hex 文件,这就是静态链接。
无系统的单片机只能运行一个程序所以将程序依赖的库直接链接到程序中,这没有问题。但是在 Linux 系统中可以运行多个应用程序,如果直接将依赖的库链接到程序中这会导致每个应用程序都各自包含一个 Math 库。
但实际上每个程序的 Math 库是相同的,所以最好的办法是把 Math 库放到 Linux 根文件系统中,让需要的程序去根文件系统加载 Math,这样每个程序共用一个 Math 库,可以避免重复 Math 库浪费空间,这就是动态链接。
在嵌入式 linux 中运行过应用程序的小伙伴因该多多少少都遇到过在 Linux 终端启动运用程序时终端输出 “-sh ./xxx: not found” 的问题,这是应用程序依赖的动态库缺失导致的,验证该问题的办法是使用静态编译去编译应用程序(静态编译会将应用程序依赖的库和应用程序本身打包在一起,所以静态编译的应用程序体积很大,所以一般不使用)。
Linux 系统缺失依赖库时可以使用静态编译或在 Linux 根文件系统中添加程序依赖的库文件,静态编译只需要给 gcc 添加 -static
编译选项即可,本文主要以 C 库为例说说缺失的动态库怎么导入到 Linux 以及该导入什么样的动态库。
在 Linux 系统中应用程序的共享库,依赖的动态链接库被存放在 rootfs 中的 /lib
目录下。同样嵌入式 Linux 系统也是如此,所以应用程序如果依赖了动态链接库,那么需要将相关的动态链接库导入到该目录下,如果 /lib
目录不存在则需要我们手动新建该目录(/lib
是必须目录,一般都存在),导入方式很简单就是从别的地方复制粘贴到这里。
程序依赖库该放哪里知道了,那么库文件该从哪里来呢?你知道交叉编译工具链由几部分组成的吗?由交叉编译器(gcc,g++,ld),ARM 的 C 库(Glibc)和二进制工具(即 Binutils)这三部分组成。所以库文件从交叉编译器的 C 库中获取,找到交叉编译工具链所在的目录,看看你搭建交叉编译环境的时候将交叉编译器放到哪个目录下了。
比如我的工具链位于 /usr/local/arm 目录下,在 arm 目录下的 arm-linux-gnueabi/libc/lib 目录下可以看到很多 .so
后缀的库文件。
zhbi98@ubuntu:/usr/local/arm/arm-linux-gnueabi/libc/lib$ ls
debug libgfortran.a libnss_nisplus.so.2
ld-2.25.so libgfortran.so libnss_nis.so.2
ld-linux.so.3 libgfortran.so.4 libpcprofile.so
ldscripts libgfortran.so.4.0.0 libpthread-2.25.so
libanl-2.25.so libgfortran.spec libpthread.so.0
libanl.so.1 libgomp.a libresolv-2.25.so
libasan.so libgomp.so.1.0.0 librt.so.1
libasan.so.4 libgomp.spec libsanitizer.spec
libatomic.so.1 libitm.so.1.0.0 libssp.so
libatomic.so.1.2.0 libitm.spec libssp.so.0
libBrokenLocale-2.25.so libm-2.25.so libssp.so.0.0.0
libc-2.25.so libm.so.6 libstdc++fs.a
...
...
...
如果你的嵌入式 Linux 系统 rootfs 空间够大,那可以把工具链目录下的所有 .so
文件导入到嵌入式 Linux。因为程序使用交叉工具链编译所有依赖库均来自工具链目录,肯定不会存在库缺失。但是不建议这么做,因为有很多我们可能用不到的库文件,造成空间浪费以及嵌入式 Linux rootfs 不整洁,所以需要裁剪,特别在嵌入式 Linux rootfs 空间不足的情况下更是如此。
应用程序需要工具链 C 库目录下所有的文件吗?不需要,来分析一下 ARM 交叉编译工具链中 C 库目录内容的组成,具体分为五大类:
(1) 目标文件(Object):以 .o
为后缀,例如 libasan_preinit.o
,用于 GCC 链接可执行文件。
(2) 静态库(Static Library):以 .a
为后缀,例如 libstdc++.a
,包含了编译后的目标代码,可以在链接时与应用程序一起打包成最终的可执行文件。
(3) 共享库(Shared Library):以 .so
为后缀,例如 libc.so.6
,也称为动态链接库。共享库是一种编译后的目标代码,可以在运行时动态加载到内存中,多个应用程序可以共享同一个共享库,节省系统资源和内存空间。
(4) 动态链接库加载器:例如 ld-linux.so.3
,依赖动态链接的应用程序本身不会将 C 库链接到应用程序中,因此应用程序运行到需要 C 库的部分代码时 Linux 必须要定位然后加载应用程序依赖的所有动态库文件,而这项工作就是由动态链接库加载器来负责完成的。
(5) 其他文件:例如 libgomp.spec
。
所以对于动态链接应用程序重点关注的文件类型只有 (3) 和 (4) 两类,所以只要把这两类文件导入嵌入式 Linux 即可,这样就裁剪掉了部分无用的文件。
实际上一小节 (3) 和 (4) 两类文件还可以再裁剪,嵌入式 Linux 需要导入哪些库完全取决于应用程序使用了哪些库函数,实际只要把使用的库,以及相应动态链接库加载器导入嵌入式 Linux 即可。怎么知道应用程序依赖了哪些库文件,以及相应的链接库加载器呢,继续往下分析。
给嵌入式 Linux 系统导入程序的依赖库之前需要知道程序依赖的 C 库有哪些,对于完全个人编写的程序我们自己很清楚使用了哪些 C 库,但如果程序引用了开源项目该怎么确定开源项目本身依赖哪些 C 库,去开源项目的源码找?这是一种办法但过于耗时,实际上 Linux 系统提供了相应的工具,这些工具可以快速不遗漏的给我们列出整个应用程序依赖库的名称,在 Linux 系统中动态链接库后缀名是 .so
结尾的。
(1) readelf 工具,ARM 交叉编译工具链的 Binutils 工具集都会提供这个工具,使用方法为 arm-linux-gnueabi-readelf -a [文件名] 例如 arm-linux-gnueabi-readelf -a mian 可以看到 main 依赖了 libm.so.6 和 libc.so.6 这两个动态链接库。
$ arm-linux-gnueabi-readelf -a main
...
...
0x00000001 (NEEDED) Shared library: [libm.so.6]
0x00000001 (NEEDED) Shared library: [libc.so.6]
...
...
如果希望 readelf 仅输出依赖库部分的信息,那么命令可以加上 | grep "Shared"
管道符来过滤出 “Shared” 相关信息,如下。
$ arm-linux-gnueabi-readelf -a main | grep "Shared"
0x00000001 (NEEDED) Shared library: [libm.so.6]
0x00000001 (NEEDED) Shared library: [libc.so.6]
(2) objdump 工具,ARM 交叉工具链的 Binutils 工具集中都会提供这个工具,使用方法为 arm-linux-gnueabi-objdump -x [文件名],例如 arm-linux-gnueabi-objdump -x main 可以看到 main 依赖了 libm.so.6 和 libc.so.6 这两个动态链接库。
$ arm-linux-gnueabi-objdump -x main
...
...
Dynamic Section:
NEEDED libm.so.6
NEEDED libc.so.6
...
...
如果希望 objdump 仅输出依赖库部分的信息,那么命令可以加上 | grep NEEDED
管道符来过滤出 NEEDED 相关信息,如下。
$ arm-linux-gnueabi-objdump -x main | grep NEEDED
NEEDED libm.so.6
NEEDED libc.so.6
(3) ldd 命令,Linux 提供了 ldd 命令,在 Linux 终端中使用 ldd [文件名] 即可查看指定 elf 文件所需要的动态链接库,以及动态链接库加载器,例如 ldd mian 可以看到 main 依赖了libm.so.6 和 libc.so.6,以及动态链接库加载器 ld-linux.so.3。
$ ldd mian
checking sub-depends for 'not found'
libm.so.6 => not found (0x00000000)
libc.so.6 => not found (0x00000000)
/lib/ld-linux.so.3 => /lib/ld-linux.so.3 (0x00000000)
通过上述的三种方法就可以知道应用程序具体的依赖库了,所以这些列出的依赖库需要导入嵌入式 Linux。
在 Linux 系统中,一个动态链接库包含三部分:实际共享链接库,主修订版本的符号链接,与版本无关的符号链接。
(1) 其中实际共享链接库,是真正能够链接到应用程序的动态链接库,并且通常会使用版本号来命名标识其不同的版本,命名格式为 lib[库名称]-[库版本].so
,例如 libm-2.25.so。
(2) 其中主修订版本的符号链接,并不是真正的共享链接库,而是 (1)
实际共享链接库的软链接,并且通常会使用一位主版本号来命名标识其不同的版本,命名格式 为 lib[库名称].so.[主修订版本号]
,例如 libm.so.6。
(3) 其中与版本无关的符号链接,并不是真正的共享链接库,而是 (2)
主修订版本的软链接,为了兼容性不区分版本(作为编译程序时的通用库名称),所以命名不带版本号,命名格式为 lib[库名称].so
,例如 libm.so。
使用 ls
命令并附加 -l
选项来以长格式显示文件详细信息包括符号链接,例如查看动态库 libm.so.6 的符号链接,可以看到 libm.so.6 -> libm-2.25.so(即 libm.so.6 是 libm-2.25.so 的软链接)。
$ ls -l libm.so.6
lrwxrwxrwx 1 root root 12 Apr 15 00:13 libm.so.6 -> libm-2.25.so
所以还需要把实际共享链接库导入嵌入式 Linux。
通过以上分析,我们只需要把组成动态链接库包含的元素(1 个主修订版本的符号链接和 1 个实际的共享链接库),以及组成动态链接库加载器包含的元素(1 个主修订版本的符号链接和 1 个实际链接库加载器)导入嵌入式 Linux 即可。
例如要导入一个数学库,就要把组成数学库的元素(libm.so.6 和 libm-2.25.so)以及组成动态链接库加载器包含的元素(ld-linux.so.3 和 ld-2.25.so)导入嵌入式 Linux。
注意:所有动态链接库是共用一个动态链接库加载器的,所以无论导入什么库,库加载器都是必须导入的。
前面说了符号链接也是动态链接库的组成部分,下面以 libm 数学库为例说明 Linux 利用符号链接实现动态链接库兼容的原理。
例如 libm.so.6 是一个符号链接,它指向实际共享链接库 libm-2.25.so,这是因为在 Linux 系统中,动态链接库通常会使用版本号来标识其不同的版本,libm-2.25.so 表示 libm 库的 2.25 版本。
而 libm.so.6 是一个兼容性符号链接它会始终指向系统中安装的最新版本的 libm 库(例如始终指向 libm-2.25.so),以便应用程序可以在不同的 libm 版本之间进行切换和兼容,这种方式可以做到应用程序在更新库版本时不需要重新编译,同时可以确保应用程序在不同的系统上都能正常运行。
现在再使用一个实例来说明应用程序是如何定位并链接到动态链接库的,下面便以编译和运行应用程序 main 为例。
例如当我们使用 gcc main.c -o main -lm 编译程序 main 时,gcc 会根据 -lm 选项,给 m 加 lib 并以 .so 为后缀而得到与版本无关的符号链接 libm.so。从而沿着与版本无关的符号链接(libm.so -> libm.so.6)找到 libm.so.6 并记录在 main 的 ELF 头中以表示 main 需要使用 libm.so.6 这个库文件所代表的数学库中的库函数。
而当 main 被 Linux 执行的时候,动态链接库加载器会从 main 的 ELF 头中找到 libm.so.6 这个记录,然后沿着主修订版本的符号链接 (libm.so.6 -> libm-2.3.6.so) 找到实际的共享链接库 libm-2.3.6.so,从而将其与 main 作动态链接。
可见,与版本无关的符号链接是供编译器使用的,主修订版本的符号链接是供动态链接库加载器使用的,而实际的共享链接库则是供应用程序使用的。
详细查看:
https://blog.csdn.net/linux_0416/article/details/79640199
https://blog.csdn.net/wsp_1138886114/article/details/128110849
ld-linux.so.3 动态库链接库加载器
https://blog.csdn.net/elfprincexu/article/details/51701242