C++(Qt)软件调试---Linux动态库链接异常排查(38)

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)
  • [@[toc]](#文章目录 C++(Qt)软件调试---Linux动态库链接异常排查(38) @[toc] 1 概述🐜 2 加载动态库方法 3 程序链接动态库常见异常现象和后果 4 查看依赖库的加载情况 5 查看动态库搜索路径 6 修改动态库搜索路径 6.1 编译时设置RUNPATH 6.2 修改可执行程序的RUNPATH)
  • [1 概述🐜](#文章目录 C++(Qt)软件调试---Linux动态库链接异常排查(38) @[toc] 1 概述🐜 2 加载动态库方法 3 程序链接动态库常见异常现象和后果 4 查看依赖库的加载情况 5 查看动态库搜索路径 6 修改动态库搜索路径 6.1 编译时设置RUNPATH 6.2 修改可执行程序的RUNPATH)
  • [2 加载动态库方法](#文章目录 C++(Qt)软件调试---Linux动态库链接异常排查(38) @[toc] 1 概述🐜 2 加载动态库方法 3 程序链接动态库常见异常现象和后果 4 查看依赖库的加载情况 5 查看动态库搜索路径 6 修改动态库搜索路径 6.1 编译时设置RUNPATH 6.2 修改可执行程序的RUNPATH)
  • [3 程序链接动态库常见异常现象和后果](#文章目录 C++(Qt)软件调试---Linux动态库链接异常排查(38) @[toc] 1 概述🐜 2 加载动态库方法 3 程序链接动态库常见异常现象和后果 4 查看依赖库的加载情况 5 查看动态库搜索路径 6 修改动态库搜索路径 6.1 编译时设置RUNPATH 6.2 修改可执行程序的RUNPATH)
  • [4 查看依赖库的加载情况](#文章目录 C++(Qt)软件调试---Linux动态库链接异常排查(38) @[toc] 1 概述🐜 2 加载动态库方法 3 程序链接动态库常见异常现象和后果 4 查看依赖库的加载情况 5 查看动态库搜索路径 6 修改动态库搜索路径 6.1 编译时设置RUNPATH 6.2 修改可执行程序的RUNPATH)
  • [5 查看动态库搜索路径](#文章目录 C++(Qt)软件调试---Linux动态库链接异常排查(38) @[toc] 1 概述🐜 2 加载动态库方法 3 程序链接动态库常见异常现象和后果 4 查看依赖库的加载情况 5 查看动态库搜索路径 6 修改动态库搜索路径 6.1 编译时设置RUNPATH 6.2 修改可执行程序的RUNPATH)
  • [6 修改动态库搜索路径](#文章目录 C++(Qt)软件调试---Linux动态库链接异常排查(38) @[toc] 1 概述🐜 2 加载动态库方法 3 程序链接动态库常见异常现象和后果 4 查看依赖库的加载情况 5 查看动态库搜索路径 6 修改动态库搜索路径 6.1 编译时设置RUNPATH 6.2 修改可执行程序的RUNPATH)
  • [6.1 编译时设置RUNPATH](#文章目录 C++(Qt)软件调试---Linux动态库链接异常排查(38) @[toc] 1 概述🐜 2 加载动态库方法 3 程序链接动态库常见异常现象和后果 4 查看依赖库的加载情况 5 查看动态库搜索路径 6 修改动态库搜索路径 6.1 编译时设置RUNPATH 6.2 修改可执行程序的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++程序隐式链接动态库出现动态库找不到的异常分析、排查方法。

排查思路概述:

  • 出现异常;
  • 检查依赖关系lddlddtree
  • 分析搜索路径
  • 编译时指定搜索路径或者使用patchelf 修改搜索路径。
环境 说明
系统 ubuntu20.04

以下是关于Linux动态链接相关命令和配置的详细说明:

命令/配置 说明
ldd 用于打印程序或共享库所依赖的共享库列表。它通过设置特殊的环境变量并运行程序来确定依赖关系,是检查可执行文件依赖关系的常用工具
lddtree 以树状结构显示程序或共享库的依赖关系,比 ldd 提供更清晰的依赖层次视图,有助于理解复杂的依赖关系链
LD_DEBUG 环境变量,用于启用动态链接器的调试输出。可设置不同选项如 libsbindingssymbols 等来获取详细的动态链接过程信息
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 程序链接动态库常见异常现象和后果

  1. 动态库缺失

    • 动态库不存在或者查找动态库路径不对;
    shell 复制代码
    ./untitled: error while loading shared libraries: libtest2.so.1: cannot open shared object file: No such file or directory
    • 现象:程序无法启动
    • 后果:进程直接终止,返回非零退出码
  2. 符号未定义错误

    • 动态库存在,链接动态库成功,但是动态库中函数符号定义不同
    shell 复制代码
    ./untitled: symbol lookup error: ./untitled: undefined symbol: _ZN5Test23addEii
    • 现象:程序加载时报错,指出缺少特定符号
    • 后果:程序无法正常执行,即使能启动也会在调用相关函数时崩溃
  3. 版本不兼容

    • 编译环境与运行环境不一致
    shell 复制代码
    version `GLIBC_2.XX' not found
    • 现象:动态库存在但版本不符合要求
    • 后果:程序可能在运行过程中出现不可预知的行为或崩溃

4 查看依赖库的加载情况

  • 当Linux下出现动态库缺失时,一般异常信息如下所示;

    shell 复制代码
    error while loading shared libraries: libtest2.so.1: cannot open shared object file: No such file or directory
  • 可使用ldd命令或者lddtree命令进行查看依赖库的加载情况;

ldd命令

ldd 是一个用于显示 ELF 可执行文件或共享库依赖关系的命令行工具。

主要功能

  • 显示程序或库文件所依赖的共享库列表
  • 显示每个依赖库的路径和加载地址
  • 检查动态链接库的可用性

lddtree 命令

lddtreeldd 的增强版工具,通常属于 pax-utils 包,能够以树状结构显示依赖关系。

可以使用sudo apt install pax-utils命令安装lddtree工具;

主要功能

  • 以层次化树形结构展示依赖关系
  • 显示完整的依赖链和依赖传递关系
  • 提供比 ldd 更详细的依赖分析
  • 如下所示,用来演示的示例程序,libtest2调用了libtest1,untitled调用了libtest2,并且所有动态库和可执行程序都放在同一路径下;

  • 直接运行./untitled提示找不到libtest2.so

  • 使用ldd命令查看依赖关系如下所示,明明动态库就在同一路径下,还是显示找不到libtest2.so

  • 使用lddtree命令查看;

5 查看动态库搜索路径

  • 明明动态库存在,但是还是显示找不到,这时就可以使用LD_DEBUG环境变量或者readelfpatchelfobjdump等命令来排查;

  • 默认情况下在 Linux 系统中,可执行程序在运行时需要链接动态库(共享库)。系统按照下面顺序搜索这些动态库:

    • 可执行文件本身的 DT_RPATHDT_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++ 本身不直接处理这个选项;

  • 可以使用冒号分隔多个路径;

    shell 复制代码
    g++ main.cpp -o main -L./ -ltest2 -Wl,-rpath,./:/home/mhf/
  • 可以使用相对于可执行文件位置的路径,其中 $ORIGIN 表示可执行文件所在的目录:

    shell 复制代码
    g++ -Wl,-rpath='$ORIGIN/lib' main.cpp -o myapp
  • 编译后就可以在当前路径找到动态库了;

  • 使用readelf -d main命令查看RUNPATH如下所示;

  • 在qmake中可以通过下面代码设置,用法相同;

    cmake 复制代码
    QMAKE_LFLAGS += -Wl,-rpath,/usr/local/lib
  • 在cmake中可以通过如下所示代码

    c 复制代码
    cmake_minimum_required(VERSION 3.15)
      
    project(main)
    # 查找 test2 库
    find_library(TEST2_LIBRARY
        NAMES test2
        PATHS ./
    )
    
    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} PRIVATE
        LINKER:-rpath,./:home/user/lib
    )

6.2 修改可执行程序的RUNPATH

  • 当可执行程序已经存在,不想重启编译或者没有源码重新编译;

  • 可通过下面patchelf命令修改可执行文件本身的 DT_RPATHDT_RUNPATH 段;

    shell 复制代码
    patchelf --set-rpath '$ORIGIN/lib/' ./untitled
  • 如下所示修改untitled后就可以找到libtest2.so了,但是libtest2.so依赖于libtest1.so,所以还需要修改libtest2.soRUNPATH

  • 这样就完成了依赖的查找,程序就可以正常运行了。



相关推荐
Juan_20122 小时前
P3051题解
c++·数学·算法·题解
深思慎考3 小时前
LinuxC++——etcd分布式键值存储系统API(libetcd-cpp-api3)下载与二次封装
linux·c++·etcd
Jiezcode3 小时前
LeetCode 138.随机链表的复制
数据结构·c++·算法·leetcode·链表
前方一片光明3 小时前
Linux—升级openssh常见的问题与解决方案
linux·运维·服务器
那个什么黑龙江4 小时前
关于C++中的“类中的特殊成员函数”
开发语言·c++
siriuuus4 小时前
Linux rsyslog 日志服务及日志转发实践
linux·rsyslog
dawnsky.liu5 小时前
RHEL - 在离线的 RHEL 10 中部署 Lightspeed 命令行助手
linux·人工智能·ai
promising-w5 小时前
TYPE-C接口,其实有4种
linux·c语言·开发语言
云道轩5 小时前
在rocky linux 9.5上安装yq
linux·kubernetes