C++(Qt)软件调试---Linux动态库链接异常排查(38)
C++(Qt)软件调试—Linux动态库链接异常排查(38)
文章目录
- C++(Qt)软件调试---Linux动态库链接异常排查(38)
- @[toc]
- 1 概述🐜
- 2 加载动态库方法
- 3 程序链接动态库常见异常现象和后果
- 4 查看依赖库的加载情况
- 5 查看动态库搜索路径
- 6 修改动态库搜索路径
- 6.1 编译时设置RUNPATH
- 6.2 修改可执行程序的RUNPATH
文章目录
- C++(Qt)软件调试---Linux动态库链接异常排查(38)
- @[toc]
- 1 概述🐜
- 2 加载动态库方法
- 3 程序链接动态库常见异常现象和后果
- 4 查看依赖库的加载情况
- 5 查看动态库搜索路径
- 6 修改动态库搜索路径
- 6.1 编译时设置RUNPATH
- 6.2 修改可执行程序的RUNPATH
更多精彩内容 |
---|
👉内容导航 👈 |
👉C++软件调试 👈 |
1 概述🐜
本文主要讲解Linux系统中C++程序隐式链接动态库出现动态库找不到的异常分析、排查方法。
排查思路概述:
- 出现异常;
- 检查依赖关系
ldd
、lddtree
- 分析搜索路径
- 编译时指定搜索路径或者使用
patchelf
修改搜索路径。
环境 | 说明 |
---|---|
系统 | ubuntu20.04 |
以下是关于Linux动态链接相关命令和配置的详细说明:
命令/配置 | 说明 |
---|---|
ldd | 用于打印程序或共享库所依赖的共享库列表。它通过设置特殊的环境变量并运行程序来确定依赖关系,是检查可执行文件依赖关系的常用工具 |
lddtree | 以树状结构显示程序或共享库的依赖关系,比 ldd 提供更清晰的依赖层次视图,有助于理解复杂的依赖关系链 |
LD_DEBUG | 环境变量,用于启用动态链接器的调试输出。可设置不同选项如 libs 、bindings 、symbols 等来获取详细的动态链接过程信息 |
LD_LIBRARY_PATH | 环境变量,指定动态链接器在搜索共享库时的额外搜索路径。优先级高于系统默认路径,常用于临时覆盖库搜索路径 |
LD_PRELOAD | 环境变量,允许预加载指定的共享库,优先于程序正常加载的库。常用于库函数拦截、性能分析或功能增强 |
/etc/ld.so.conf | 系统级配置文件,定义了系统范围内共享库的搜索路径。通常包含多个include指令指向 /etc/ld.so.conf.d/ 目录下的配置文件 |
ldconfig | 系统命令,用于更新共享库缓存。它会扫描 /etc/ld.so.conf 中定义的目录,创建或更新 /etc/ld.so.cache 文件以加速库搜索 |
readelf | ELF文件分析工具,用于显示ELF格式文件的详细信息,包括节头、程序头、符号表、重定位信息等,是分析可执行文件结构的重要工具 |
objdump | 通用目标文件分析工具,可用于反汇编、显示目标文件信息、重定位信息等。支持多种目标文件格式,是二进制分析的核心工具 |
patchelf | 专门用于修改ELF可执行文件和共享库的工具。可以更改解释器路径、修改RPATH/RUNPATH、添加或删除所需的库等 |
2 加载动态库方法
C++加载动态库(DLL)主要有两种方式:
隐式链接
- 在编译时通过
-l
参数链接动态库 - 程序启动时自动加载所需的共享库
- 符号解析在编译时完成
- 使用简单,直接调用库中的函数和变量
- 优点:
- 使用简单直观
- 性能较好,无需手动管理库的加载
- 编译时就能发现符号错误
- 缺点:
- 程序依赖的库必须在运行时存在
- 程序启动时必须加载所有依赖库
- 灵活性较差
显式链接
- 通过
dlopen
系列函数在程序运行时手动加载动态库 - 优点:
- 灵活性高,可以按需加载库
- 支持插件化架构
- 可以在运行时决定加载哪个库
- 库不存在不会影响程序启动
- 缺点:
- 使用复杂,需要手动管理库的加载和卸载
- 容易出现内存泄漏或悬空指针
- 运行时错误处理复杂
- 符号解析延迟到运行时
3 程序链接动态库常见异常现象和后果
-
动态库缺失
- 动态库不存在或者查找动态库路径不对;
./untitled: error while loading shared libraries: libtest2.so.1: cannot open shared object file: No such file or directory
- 现象:程序无法启动
- 后果:进程直接终止,返回非零退出码
-
符号未定义错误
- 动态库存在,链接动态库成功,但是动态库中函数符号定义不同
./untitled: symbol lookup error: ./untitled: undefined symbol: _ZN5Test23addEii
- 现象:程序加载时报错,指出缺少特定符号
- 后果:程序无法正常执行,即使能启动也会在调用相关函数时崩溃
-
版本不兼容
- 编译环境与运行环境不一致
version `GLIBC_2.XX' not found
- 现象:动态库存在但版本不符合要求
- 后果:程序可能在运行过程中出现不可预知的行为或崩溃
4 查看依赖库的加载情况
-
当Linux下出现动态库缺失时,一般异常信息如下所示;
error while loading shared libraries: libtest2.so.1: cannot open shared object file: No such file or directory
-
可使用
ldd
命令或者lddtree
命令进行查看依赖库的加载情况;
ldd命令
ldd
是一个用于显示 ELF 可执行文件或共享库依赖关系的命令行工具。主要功能
- 显示程序或库文件所依赖的共享库列表
- 显示每个依赖库的路径和加载地址
- 检查动态链接库的可用性
lddtree 命令
lddtree
是ldd
的增强版工具,通常属于pax-utils
包,能够以树状结构显示依赖关系。可以使用
sudo apt install pax-utils
命令安装lddtree
工具;主要功能
- 以层次化树形结构展示依赖关系
- 显示完整的依赖链和依赖传递关系
- 提供比
ldd
更详细的依赖分析
-
如下所示,用来演示的示例程序,libtest2调用了libtest1,untitled调用了libtest2,并且所有动态库和可执行程序都放在同一路径下;
-
直接运行
./untitled
提示找不到libtest2.so
; -
使用
ldd
命令查看依赖关系如下所示,明明动态库就在同一路径下,还是显示找不到libtest2.so
; -
使用
lddtree
命令查看;
5 查看动态库搜索路径
-
明明动态库存在,但是还是显示找不到,这时就可以使用
LD_DEBUG
环境变量或者readelf
、patchelf
、objdump
等命令来排查; -
默认情况下在 Linux 系统中,可执行程序在运行时需要链接动态库(共享库)。系统按照下面顺序搜索这些动态库:
- 可执行文件本身的
DT_RPATH
或DT_RUNPATH
段; LD_LIBRARY_PATH
环境变量指定的目录;LD_PRELOAD
环境变量;/etc/ld.so.cache
缓存文件,这是由ldconfig
命令根据/etc/ld.so.conf
及其包含的配置文件生成的缓存;
- 可执行文件本身的
-
如下所示使用
LD_DEBUG=libs ./untitled
命令显示库搜索过程,并没有包含./
搜索路径,所以动态库放在当前路径也找不到; -
使用
readelf -d executable_name
命令看完整的动态段信息如下所示,RUNPATH
段内容为/opt/Qt5.14.2/5.14.2/gcc_64/lib
;- 程序运行就会优先从
/opt/Qt5.14.2/5.14.2/gcc_64/lib
路径去找动态库;
-
或者使用
objdump -p executable_name
也可以查看; -
使用
patchelf --print-rpath executable_name
命令也可以查看; -
使用
echo $LD_LIBRARY_PATH
命令可以查看LD_LIBRARY_PATH
环境变量当前设置; -
使用
echo $LD_PRELOAD
命令可以查看LD_PRELOAD环境变量当前设置;
主要区别对比
特性 LD_LIBRARY_PATH LD_PRELOAD 作用机制 指定搜索目录 指定具体要加载的库文件 搜索方式 添加到库搜索路径中 直接加载指定的库文件 覆盖能力 不能覆盖同名库的特定版本 可以覆盖库中的具体函数 使用场景 解决库文件位置问题 实现库函数拦截和替换 语法格式 目录路径列表 具体库文件路径列表
-
使用
cat /etc/ld.so.conf
命令和cat /etc/ld.so.conf.d/*.conf
命令可以查看/etc/ld.so.conf
配置文件指定系统范围内的动态链接库搜索路径; -
使用
ldconfig -p
命令可以查看/etc/ld.so.cache
缓存内容;
6 修改动态库搜索路径
DT_RPATH
- 传统的行为方式
- 会传递给依赖的共享库
- 在搜索 LD_LIBRARY_PATH 之前搜索
DT_RUNPATH
- 新的行为方式
- 不会传递给依赖的共享库
- 在搜索 LD_LIBRARY_PATH 之后搜索
- 默认情况下,现代链接器通常生成 DT_RUNPATH。
- 查看了所有的搜索路径都没有当前路径,那么将怎么将动态库放在当前路径,然后发布出去给用户呢;
- 方法1:通过安装脚本自动设置环境变量
LD_LIBRARY_PATH
; - 方法2:修改程序本身的
DT_RUNPATH
段;
6.1 编译时设置RUNPATH
-
编译时使用g++的
-rpath
选项指定搜索路径; -
通常与
-L
和-l
选项一起使用 -
-rpath
是一个链接器选项,用于在可执行文件或共享库中嵌入运行时库搜索路径; -
不过需要注意的是,
-rpath
通常是通过-Wl
选项传递给链接器的,因为 g++ 本身不直接处理这个选项; -
可以使用冒号分隔多个路径;
g++ main.cpp -o main -L./ -ltest2 -Wl,-rpath,./:/home/mhf/
-
可以使用相对于可执行文件位置的路径,其中
$ORIGIN
表示可执行文件所在的目录:g++ -Wl,-rpath='$ORIGIN/lib' main.cpp -o myapp
-
编译后就可以在当前路径找到动态库了;
-
使用
readelf -d main
命令查看RUNPATH
如下所示;
-
在qmake中可以通过下面代码设置,用法相同;
QMAKE_LFLAGS += -Wl,-rpath,/usr/local/lib
-
在cmake中可以通过如下所示代码
cmake_minimum_required(VERSION 3.15)project(main) # 查找 test2 库 find_library(TEST2_LIBRARYNAMES test2PATHS ./ )add_executable(${PROJECT_NAME} main.cpp) # 链接找到的库 if(TEST2_LIBRARY)target_link_libraries(${PROJECT_NAME} PRIVATE ${TEST2_LIBRARY}) else()message(FATAL_ERROR "test2 library not found") endif() # 设置查找路径 target_link_options(${PROJECT_NAME} PRIVATELINKER:-rpath,./:home/user/lib )
6.2 修改可执行程序的RUNPATH
-
当可执行程序已经存在,不想重启编译或者没有源码重新编译;
-
可通过下面
patchelf
命令修改可执行文件本身的DT_RPATH
或DT_RUNPATH
段;patchelf --set-rpath '$ORIGIN/lib/' ./untitled
-
如下所示修改
untitled
后就可以找到libtest2.so
了,但是libtest2.so
依赖于libtest1.so
,所以还需要修改libtest2.so
的RUNPATH
; -
这样就完成了依赖的查找,程序就可以正常运行了。