谈静态动态库和显式隐式加载
静态库与动态库
从程序编译期的角度来看,我们可以将库分为静态库和动态库。静态库在链接时,编译器会将目标文件和静态文件组织成一个可执行文件。而动态库则是在程序运行时才会完成加载。
静态库的优势在于:
- 独立性强:静态库包含所有的链接代码和数据,使得可执行文件在不依赖外部库的情况下独立运行。
- 性能优越:静态链接在编译时完成,无需运行时加载,因此通常执行速度更快。
- 更容易分发:无需关心目标系统上是否存在相应版本的动态库,因为所有依赖关系在编译时已经解析。
静态库的劣势:
- 占用空间:静态库会使可执行文件变得更大,因为每个可执行文件都包含了库的完整副本。
- 更新困难:如果库有更新,需要重新编译和重新分发整个应用程序。
- 不适用于大型应用:对于大型应用程序,使用静态库可能导致编译时间较长,而且可执行文件过大。
动态库的优势在于:
- 节省空间:多个应用程序可以共享同一个动态库,减少了磁盘空间占用。
- 更新方便:如果库有更新,只需要替换动态库文件,无需重新编译整个应用程序。
- 运行时加载:动态库在运行时加载,可以实现更灵活的更新和部署。
动态的劣势:
- 依赖关系:程序在运行时需要检查系统中是否存在相应版本的动态库,如果不存在或版本不匹配,可能会导致运行错误。
- 性能损失:动态在运行时需要加载,可能引入一些运行时性能开销。
- 版本兼容性:确保动态库的向后兼容性,以避免因为库的升级导致已有应用程序出现问题。
我们需要根据具体的应用场景和需求来确定使用动态库还是静态库,在某些场景下,可以同时使用两者以发挥各自的优势。例如,一个应用程序可以使用动态库以减小可执行文件的大小,同时静态链接一些特定的库以提高性能。
显式与隐式加载
从程序运行时的角度来看,动态库的加载又可以分为隐式加载和显式加载。隐式加载又叫载入时加载,指在程序载入内存时搜索动态库,并将动态库载入内存。显式加载又叫运行时加载,指主程序在运行过程中需要动态库中的函数时再加载。隐式加载需要在编译时将头文件包含进源码中,然后需要在编译时告诉编译器库的搜索路径,这样编译器才能找到库中的符号(例如函数或变量)进行连接。例如,在使用 GCC 编译器时,使用 -L 来指定库的搜索路径。值得注意的是,动态在运行时也需要被找到。这通常是通过设置环境变量(LD_LIBRARY_PATH)或者将库放在系统已知的搜索路径(例如 /usr/lib)来实现的。如果运行时无法找到库,程序将无法启动。
隐式加载
隐式加载是动态链接器(例如 ld.so 或 ld-linux.so)在程序启动时负责搜索并加载共享库。这是一种自动的过程,程序本身无需显式指定加载共享库的代码,而是由动态链接器根据默认规则和环境变量自动完成。隐式加载中如果库文件位于非默认路径,可以通过以下方式指定搜索路径:
- 设置 LD_LIBRARY_PATH 环境变量。
- 编辑运行时动态链接器配置文件,如 /etc/ld.so.conf 或 /etc/ld.so.conf.d 目录下的配置文件。
- 在程序中使用 -rpath 选项指定运行时库的搜索路径。
- 在程序中使用 $ORIGIN 指定运行时库相对于可执行文件的路径。
值得注意的是如果我们是采用修改运行时动态链接器的配置文件,修改完成后我们需要通过 sudo ldconfig
命令来更新动态链接器的缓存。
隐式加载的优势在于:
- 简单易用:只需要在编译时链接到库,然后就可以像调用任何其它函数一样调用库的函数。
- 类型检查:由于库的头文件通常被包含在源代码中,因此可以在编译时进行类型检查,帮助防止类型错误。
- 性能:由于函数地址在程序启动时已经确定,所有调用库函数的开销可能会略小一些。
隐式加载的劣势:
- 灵活性:如果库的版本修改,你可能需要重新编译你的程序,即使新版本的库是向后兼容的。
- 部署:你的程序需要在运行时能够找到库,这可能需要设置 LD_LIBRARY_PATH 或者其他相关环境变量,或者库需要被安装在一个系统已知的位置。
显式加载
显式加载是程序本身通过调用动态链接器提供的函数(例如 dlopen、dlsysm)来主动加载共享库。这种方式允许程序在运行时动态选择加载哪些共享库,以及何时加载。程序可以根据需要进行加载和卸载,提供了更大的灵活性。Linux 下的显式加载方式可以参考Linux 动态库显式加载方法。
显式加载的优势在于:
- 灵活性:程序可以在运行时选择加载哪个版本的库,或者决定是否加载库。这使得实现插件或者扩展系统更为方便。
- 错误处理:如果库无法加载,你的程序可以决定如何处理,而不是直接失败。
显式加载的劣势:
- 复杂性:需要手动加载库,查找函数,错误处理,以及可能的卸载库。
- 缺少类型检查:因为库的函数是在运行时通过指针调用的,编译器无法在编译时检查类型错误。
- 性能:虽然差异通常可以忽略不计,但是函数调用的开销可能会略大,因为需要通过函数指针调用函数。
开发调试技巧
使用 LD_LIBRARY_PATH 设置库路径
使用 LD_LIBRARY_PATH 环境变量设置动态库的搜索路径。
export LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH
使用 ldd 命令查看动态库依赖
ldd 命令可以查看一个可执行文件或共享库依赖的动态库列表。
ldd your_program
查看动态库的符号信息
使用 nm 命令可以列出共享库中的符号信息,包含函数和变量。
nm -D your_library.so
使用 strace 跟踪系统调用
使用 strace 命令可以跟踪程序执行期间的系统调用,包括动态库加载。
strace ./your_program