开发过程中,对于动态库的疑问和思考
前言:先简述一下开发时的情况。由于工程代码编译是在 Linux 上,可执行文件是在板子(ARM)上去运行。但是我们需要用到hiredis这个库,我发现无论是 Linux 上还是嵌入式系统上,都需要用到 libhiredis.so 这个动态库,而工程代码是已经包含了 hiredis 的全部头文件了,就相当于是已经具备了对 hiredis 这个库调用一些函数或结构体时,已经是有声明的了。
所以我就产生一个疑问:在编译可执行文件时,为什么 Linux 上也需要包含这个动态库才可以成功编译?动态库不是在程序运行起来后才进行链接的吗,不是板子上包含了这个动态库就好了吗?
带着这个疑问,我就去了解了一下。
结论:Linux 上和板子上都需要该动态库。首先,板子上需要就已经毋庸置疑了,因为动态库会在程序执行时进行动态加载并链接。但 Linux 上是因为在编译时,加了链接选项 -lhiredis
,有了这个选项,链接器就需要去找到动态库的位置和名称,以便能够正确链接和加载动态库,一旦找到动态库,链接器就会把动态库的符号表信息记录在可执行文件中,然后再解析程序中对动态库的符号引用,以便能够正确地将动态库中的函数调用链接到程序中。如果程序中存在未定义的符号引用,或者说工程代码中没有包含该动态库,链接器就无法解析这些符号,会报链接错误。因此,在编译和链接命令中需要包含对动态库的引用,以告知链接器需要链接哪些函数或符号。
还有再说一下,我上面为什么会提到工程代码中已经包含了 hiredis 的全部头文件呢?这和动态库有什么关系呢?
一开始我是想着,有了头文件,不就已经有了我在代码中需要用到的关于 hiredis 的数据结构和函数定义了吗,在编译成可执行文件时,编译器不就可以找到对应的数据结构和函数定义了吗,就不会导致未定义的情况,就不需要在 Linux 系统下包含 libhiredis.so 了啊。
然而这样想,是因为我对应头文件和符号表的认知不够准确,符号表和头文件是两个不同的概念,它们在程序链接过程(预处理-编译-汇编-链接)中扮演不同的角色。
- 头文件:
头文件是一种包含函数和变量声明、宏定义等信息的文本文件,通常以.h为后缀。头文件包含了对应库或模块中的接口定义,可以用来告知编译器某个函数的原型、结构体的定义等,以便编译器在编译时能够正确地解析和检查代码。使用头文件可以避免重复编写相同的代码,并提供了对外部库或模块的接口描述。
在你的情况下,如果你已经包含了 libhiredis 的全部头文件,那么编译器可以在编译阶段正确解析你对 libhiredis 的函数和结构体的引用,以便进行类型检查和语法分析。 - 符号表:
符号表是在链接阶段生成的一种数据结构,记录了可执行文件或动态库中的符号信息,包括函数名、变量名和地址等。符号表的作用是在链接阶段解析程序中对其他库或模块的符号引用,并将这些引用与实际的符号地址进行关联。
当你在程序中调用 libhiredis 中的函数时,编译器会使用符号表来查找该函数的地址。如果在链接阶段找不到对应的符号地址,编译器会报链接错误。
所以需要同时包含 libhiredis 的全部头文件和正确链接 libhiredis 动态库,以确保编译和链接过程的顺利进行。
在链接阶段,编译器只会在可执行文件中留下对动态库的引用,而不会将动态库的函数实现链接到生成的可执行文件中。
具体来说,编译器会将动态库的符号表信息记录在可执行文件中,但并不会将动态库的代码复制到可执行文件中。因此,当程序运行时,操作系统会根据可执行文件中的动态库引用信息,动态地加载对应的动态库,将动态库的代码映射到程序的虚拟地址空间中,并根据符号表信息解析动态库中的函数调用。
顺便介绍一下动态库:
动态库(也称为共享库)在编译时并不会被链接到可执行文件中,而是在程序在运行时由操作系统动态加载并链接。
当程序启动时,如果它依赖于某个动态库,操作系统会在内存中加载这个动态库,并将其链接到程序中。这种动态加载和链接的方式使得程序的可执行文件更加轻量级,因为它不包含所有依赖的库的代码,而是在需要时才进行加载。
动态库的优点包括:
- 节约内存:多个程序可以共享同一个动态库的代码,减少了重复占用内存的情况。
- 灵活性:可以在不重新编译程序的情况下更新动态库,从而修复bug或者提升性能。
- 减少可执行文件大小:因为动态库是在运行时加载的,所以可执行文件的大小会更小。
然而,动态库也有一些缺点,比如在程序启动时需要进行动态链接,可能会引起一些额外的开销;另外,由于动态库的版本问题,可能会引起一些兼容性方面的困扰。