3、ldd命令详解:ldd工作原理、ldd输出解读、常见问题排查、实战:分析一个二进制文件的依赖

说实话,ldd 这个命令,我用了十几年了。它看起来简单,但背后藏着的门道不少。很多刚入行的朋友觉得 ldd 就是「列一下动态库」,其实远不止这么回事。

今天我就把 ldd 的里里外外掰开揉碎了讲清楚。你想想看,做嵌入式 Rootfs 构建,最头疼的是什么?就是依赖缺失。而 ldd 恰恰是解决这个问题的第一把钥匙。

3.1 ldd 的工作原理

ldd 本身不是一个独立的工具。它其实是个 shell 脚本,或者在某些系统上是个二进制程序。它的核心机制是:调用动态链接器(ld-linux.so)来加载目标程序,然后打印出加载过程中的依赖信息。

具体来说,ldd 做了三件事:

  • 解析 ELF 文件的 .dynamic 段,找到 NEEDED 条目
  • 按照动态链接器的搜索路径,去查找每个依赖库
  • 递归地解析每个库的依赖,直到全部解析完毕

我画了一张流程图,帮你理解这个过程:

ldd 工作原理流程图 输入:目标 ELF 文件 解析 .dynamic 段 提取 NEEDED 条目 循环 按搜索路径查找库 递归解析子依赖 输出依赖列表

嗯,这里要注意:ldd 实际上会运行目标程序(通过设置特殊环境变量)。所以对于交叉编译的程序,千万别在开发板上直接跑 ldd——它可能会尝试执行 ARM 指令,然后崩溃。

⚠️ 重要警告: 永远不要在目标设备上对交叉编译的程序执行 ldd!我见过有人把 ARM 版的 busybox 拿到 x86 主机上跑 ldd,结果 shell 直接挂了。正确的做法是用交叉工具链里的 readelf 或者 objdump。

3.2 ldd 输出解读

我们来看一个实际的例子。假设我有一个叫 my_app 的二进制文件:

$ ldd my_app
    linux-vdso.so.1 (0x00007ffd3a5c0000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f8a1c400000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f8a1c1e0000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a1be00000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f8a1c800000)
    libexample.so.1 => not found

输出分三列,我解释一下:

列 含义 说明
第一列 库的 SONAME 比如 libm.so.6,这是库的接口名称
第二列 实际路径 => 后面的路径是链接器找到的实际文件
第三列 加载地址 括号里的十六进制数,是库被加载到的虚拟地址

看到最后一行了吗?libexample.so.1 => not found。这就是典型的依赖缺失。我在项目中遇到过好几次,明明把库文件放进去了,ldd 还是说找不到。后来发现是 SONAME 不匹配。

💡 小技巧: 用 readelf -d your_binary | grep NEEDED 可以只看依赖列表,不执行程序。这在交叉编译环境下特别有用。

3.3 常见问题排查

我总结了几种最常见的 ldd 问题,以及对应的排查思路:

问题一:not found

ldd 报告某个库找不到。原因可能有:

  • 库文件确实不存在
  • 库文件存在,但路径不在搜索范围内
  • 库的 SONAME 和文件名不匹配

排查方法:先用 find / -name "libxxx.so*" 看看文件在不在。如果在,检查 ldconfig -p | grep libxxx 看缓存里有没有注册。

问题二:版本不匹配

比如程序需要 GLIBC_2.28,但系统只有 2.27。ldd 不会直接报错,但运行时会崩溃。我建议用 readelf -V your_binary 查看版本需求。

问题三:链接器路径错误

有时候 ldd 输出里会出现 /lib/ld-linux.so.2 这样的路径,但实际系统里是 /lib64/ld-linux-x86-64.so.2。这通常是因为程序是用 32 位工具链编译的,但跑在 64 位系统上。

🔑 核心原则: 做 Rootfs 时,我习惯先列一个「依赖清单」,然后用 ldd 逐个验证。确保每个库的版本、路径、SONAME 都正确。这一步省不了,偷懒的话后面调试会花十倍时间。

3.4 实战:分析一个二进制文件的依赖

好,我们来动手做一遍。假设我们要分析一个叫 sensor_daemon 的二进制文件,它是 ARM 架构的,运行在嵌入式设备上。

第一步:在主机上分析

因为这是 ARM 程序,我们不能直接跑 ldd。用交叉工具链里的 readelf:

$ arm-linux-gnueabihf-readelf -d sensor_daemon

Dynamic section at offset 0x2a3c contains 28 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libpthread.so.0]
 0x00000001 (NEEDED)                     Shared library: [libm.so.6]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x00000001 (NEEDED)                     Shared library: [libgpiod.so.2]
 0x0000000f (RPATH)                      Library rpath: [/usr/local/lib]

看到了吗?它依赖 libgpiod.so.2。这个库是操作 GPIO 的,在嵌入式系统里很常见。

第二步:检查库文件是否存在

假设我们的 Rootfs 目录在 /home/project/rootfs:

$ find /home/project/rootfs -name "libgpiod*"
/home/project/rootfs/usr/lib/libgpiod.so.2.0.0
/home/project/rootfs/usr/lib/libgpiod.so.2

嗯,文件在。但注意,这里只有 libgpiod.so.2 和 libgpiod.so.2.0.0。如果程序需要的是 libgpiod.so.3,那就对不上了。

第三步:验证 SONAME

$ arm-linux-gnueabihf-readelf -d /home/project/rootfs/usr/lib/libgpiod.so.2

0x0000000e (SONAME)                     Library soname: [libgpiod.so.2]

SONAME 是 libgpiod.so.2,和程序需要的匹配。没问题。

第四步:检查搜索路径

程序里有个 RPATH 是 /usr/local/lib。但我们的库在 /usr/lib 下。这就有问题了。要么改 RPATH,要么把库放到正确位置,要么设置 LD_LIBRARY_PATH。

✅ 最终方案: 我选择把库链接到 /usr/local/lib 下,因为改程序 RPATH 需要重新编译,而嵌入式设备上改环境变量不太可靠。

做完这四步,依赖关系就理清了。整个过程其实就一句话:用 readelf 替代 ldd,逐层检查 SONAME 和路径。

我个人习惯把这个流程写成脚本,每次构建 Rootfs 后自动跑一遍。省心,也避免遗漏。

可可精品资料库