Linux常用命令--ldd

摘要

  • 本文介绍 ldd 命令的使用方法,以及动态链接库依赖的排查思路。

  • 本文基于CentOS8(x86_64)

ldd 是什么

  • ldd = list dynamic dependencies,用来打印可执行文件或共享库(.so)的动态链接依赖。

  • 一个动态链接的程序在运行时并不会把所有代码都打进二进制里,而是依赖系统里的 .so。程序启动时,动态链接器会按一定规则把这些库加载进进程地址空间。

  • ldd 做的事情就是:模拟这次加载过程,把“需要哪些库、最终解析到哪个路径、加载到什么地址”打印出来

  • 常见用途:

    • 程序报 error while loading shared libraries 时,找出缺的是哪个 .so
    • 确认二进制实际链接到了哪一份库(避免“系统里有库,但程序没用上”)
    • 查看当前系统的 glibc 版本
    • 对比源码编译出来的程序,依赖是否齐全
  • ldd 属于 glibc,一般系统自带。如果没有,可以安装:

1
yum install glibc-common -y

静态链接的程序用不了 ldd

  • 如果二进制是静态链接的,ldd 会提示 not a dynamic executable
  • 可以先用 file 确认文件类型:
1
2
3
4
5
$ file /bin/ls
/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.2.0, BuildID[sha1]=17d3dacdf54722071bc2cb0242e1526620156890, stripped

$ file /sbin/sln
/sbin/sln: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=8a0a7373b8d3624a71d7a140c26b80b8e64fddac, not stripped
  • dynamically linked 才能用 lddstatically linked 说明依赖已经打进二进制,运行时不再找 .so

ldd 如何工作

  • ldd 本身通常是一个 shell 脚本,真正干活的是动态链接器,例如 x86_64 上的 /lib64/ld-linux-x86-64.so.2

  • 它会设置环境变量 LD_TRACE_LOADED_OBJECTS=1,让动态链接器只做依赖解析并打印结果,而不是正常启动程序。

  • 下面三条命令效果基本等价:

1
2
3
4
5
ldd /bin/ls
# 或者
LD_TRACE_LOADED_OBJECTS=1 /lib64/ld-linux-x86-64.so.2 /bin/ls
# 也可以直接让动态链接器列出依赖
/lib64/ld-linux-x86-64.so.2 --list /bin/ls
  • 这也是为什么 ldd 能打印出间接依赖:它走的是真实加载路径,不只看二进制自身声明的 NEEDED

不要对不可信的二进制执行 ldd

  • 某些版本的 ldd 会通过直接执行目标程序来获取依赖信息。如果这个程序是恶意的,它可能在被 ldd 分析时就开始运行。
  • 对来源不明的文件,优先用只读解析 ELF 的方式:
1
2
readelf -d /path/to/bin | grep NEEDED
objdump -p /path/to/bin | grep NEEDED
  • 这两种方式只读取 ELF 头,不会执行程序。代价是只能看到直接依赖,看不到间接依赖。
1
2
3
4
5
6
7
8
9
10
11
$ readelf -d /bin/ls | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libselinux.so.1]
0x0000000000000001 (NEEDED) Shared library: [libcap.so.2]
0x0000000000000001 (NEEDED) Shared library: [libacl.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

$ objdump -p /bin/ls | grep NEEDED
NEEDED libselinux.so.1
NEEDED libcap.so.2
NEEDED libacl.so.1
NEEDED libc.so.6

基本用法

  • 语法

1
ldd [option]... file...
  • 最常见的用法就是直接跟一个可执行文件或 .so

1
2
3
4
5
6
7
8
9
10
11
$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffc881b9000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f3002608000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f30023ff000)
libacl.so.1 => /lib64/libacl.so.1 (0x00007f30021f6000)
libc.so.6 => /lib64/libc.so.6 (0x00007f3001e49000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f3001be7000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f30019e3000)
/lib64/ld-linux-x86-64.so.2 (0x00007f300282f000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007f30017de000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f30015c0000)
  • 也可以一次看多个文件:

1
ldd /bin/ls /usr/bin/cp
  • 对共享库本身也可以用 ldd,这在排查 .so 继续依赖哪些库时很有用:

1
ldd /lib64/libssl.so.1.1

输出结果说明

  • ldd 每一行通常是:库名 => 实际路径 (加载地址)

字段 含义
左边的库名 程序声明需要的 soname,例如 libc.so.6
=> 后面的路径 动态链接器最终解析到的真实文件
括号里的地址 本次探测时库被映射到的虚拟地址,每次运行可能不同
not found 声明了依赖,但按搜索规则找不到对应文件
没有 => 的行 一般是虚拟库或动态链接器本身
  • 几类特殊输出需要单独记住:

1
2
3
4
5
6
7
8
9
10
11
12
# 1. linux-vdso.so.1
# 这是内核提供的 Virtual Dynamic Shared Object,并不对应磁盘上的真实文件。
# 程序通过它快速调用一些系统调用(如 gettimeofday),所以 ldd 会列出来,但找不到对应 .so 文件是正常的。

# 2. /lib64/ld-linux-x86-64.so.2
# 这是动态链接器自己。它负责加载其他所有 .so,因此也会出现在列表里。

# 3. libxxx.so.1 => not found
# 这是最需要处理的情况:运行时一定找不到这个库。

# 4. not a dynamic executable
# 目标不是 ELF 动态链接文件,可能是脚本、静态链接程序,或架构不匹配(例如在 x86_64 上看 arm 二进制)。

soname 和文件名不是一回事

  • 程序依赖的是 soname,例如 libssl.so.1.1,磁盘上真正的文件可能是 libssl.so.1.1.1k,再通过符号链接指向它。
  • ldd 打印的左边是 soname,右边是解析后的路径。排查时两边都要看,不要只按文件名去搜。

常用参数

参数 作用 示例
--version 打印 ldd / glibc 版本 ldd --version
-v / --verbose 打印全部信息,包括符号版本 ldd -v /bin/ls
-u / --unused 打印未使用的直接依赖 ldd -u /usr/local/nginx/sbin/nginx
-d / --data-relocs 执行数据重定位并报告缺失的对象 ldd -d ./myapp
-r / --function-relocs 执行函数重定位,报告缺失的函数和对象 ldd -r ./myapp
--help 帮助信息 ldd --help

查看 glibc 版本

  • ldd --version 是查看系统 glibc 版本最方便的方式之一:

1
2
3
4
5
6
$ ldd --version
ldd (GNU libc) 2.28
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
  • 下载 MySQL、Oracle JDK、部分商业软件的 generic Linux 包时,包名里常带 glibc2.12glibc2.17glibc2.28。这表示编译时的最低 glibc 要求,不是必须刚好等于这个版本。

  • 例如当前系统是 glibc 2.28,选择 <= glibc2.28 的包即可。关于这一点,在 MySql--从Mysql5.7升级到Mysql8 里升级 MySQL 时也用过同样的判断方法。

  • 也可以从 libc.so.6 直接看:

1
/lib64/libc.so.6

-v:看符号版本

  • 有些库不仅要求“有这个 .so”,还要求里面带特定的符号版本,例如 GLIBC_2.28

  • ldd -v 会把每个库用到的 Version 需求打出来,适合排查 “库文件在,但版本不够” 的问题:

1
2
3
4
5
6
7
8
9
10
$ ldd -v /bin/ls | head -n 30
linux-vdso.so.1 (0x00007ffc9d1f3000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f8c3a1b0000)
...

Version information:
/bin/ls:
libselinux.so.1 (GLIBC_2.2.5) => /lib64/libselinux.so.1
libc.so.6 (GLIBC_2.28) => /lib64/libc.so.6
libc.so.6 (GLIBC_2.2.5) => /lib64/libc.so.6
  • 如果程序是在更高版本 glibc 的机器上编译的,拷到低版本系统后,即使 ldd 能找到 libc.so.6,运行时仍可能报:

1
./myapp: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by ./myapp)
  • 这类问题不能靠再装一份同名 .so 糊弄过去,本质是 glibc 向前兼容、不向后兼容。正确做法是在目标系统上重新编译,或换用针对更低 glibc 编译的二进制。

-u:找出没用到的直接依赖

  • ldd -u 会列出“链接进去了,但当前探测时看起来没用到”的直接依赖。

  • 适合检查自己编译的程序有没有多余的 -lxxx

1
2
3
$ ldd -u ./myapp
Unused direct dependencies:
/lib64/libm.so.6
  • 注意:这只是 ldd 的探测结果,动态加载(dlopen)的库它看不到,所以不要看见 Unused 就立刻删链接参数。

-r:检查缺失的符号

  • 有时 .so 文件在,但里面缺函数,程序启动或运行到某个调用时才会失败。

  • ldd -r 会做函数重定位,把缺失符号提前报出来:

1
2
3
4
5
$ ldd -r ./myapp
linux-vdso.so.1 (0x00007ffd1c5c0000)
libfoo.so.1 => /usr/local/lib64/libfoo.so.1 (0x00007f3a2c001000)
libc.so.6 => /lib64/libc.so.6 (0x00007f3a2bc26000)
undefined symbol: foo_bar (/usr/local/lib64/libfoo.so.1)
  • 这比等到运行时报 undefined symbol 更早,适合检查自己编译出来的 .so 是否完整。

排查 “not found”

  • 这是 ldd 最常见的实战场景。程序启动失败时,错误通常是:

1
./myapp: error while loading shared libraries: libxxx.so.1: cannot open shared object file: No such file or directory
  • 第一步先用 ldd 把缺失项全部列出来,不要修一个再撞下一个:

1
2
3
4
5
$ ldd ./myapp
linux-vdso.so.1 (0x00007ffc12345000)
libxxx.so.1 => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f11ab000000)
/lib64/ld-linux-x86-64.so.2 (0x00007f11ab3d8000)
  • 然后确认这个库到底在不在磁盘上:

1
2
3
4
5
# 在常见库目录里找
find /lib64 /usr/lib64 /usr/local/lib64 -name 'libxxx.so*'

# 看动态链接器缓存里有没有
ldconfig -p | grep libxxx
  • 如果缓存里没有、磁盘上也没有,就按发行版安装对应软件包:

1
2
3
4
5
6
# CentOS / RHEL 系列:先查哪个包提供这个文件
yum provides '*/libxxx.so.1'
yum install xxx -y

# 安装后刷新缓存
ldconfig
  • 如果文件在,但 ldd 仍是 not found,说明搜索路径没覆盖到这个目录。常见原因有:

    • 库装在 /usr/local/lib64,但系统默认不搜这里
    • 程序是自己编译的,编译时没写 rpath
    • 只放了 libxxx.so,缺少带版本号的 soname 链接,例如 libxxx.so.1

记一次缺失 .so 导致程序起不来

  • 在 Amazon Linux 2023 上启动 RocketMQ Proxy 时,报 UnsatisfiedLinkError,底层原因是找不到 libcrypt.so.1
  • 系统里只有 libcrypt.so.2,而 Netty tcnative 编译时锁的是 libcrypt.so.1
  • 处理办法不是改程序,而是补兼容包:
1
2
3
ldconfig -p | grep libcrypt
sudo yum install libxcrypt-compat
ldconfig -p | grep libcrypt
  • 这类问题的本质是:soname 对不上ldd / ldconfig -p 看到的是 libcrypt.so.2,程序要的是 libcrypt.so.1,名字差一个版本号就等于找不到。更完整的过程见 RocketMQ 的安装及使用

动态库是怎么被找到的

  • ldd 打印的路径,遵循动态链接器的搜索顺序。理解这个顺序,才能解释“明明有库,为什么 ldd 找不到 / 为什么找到的不是我想要的那一份”。

搜索顺序

  1. 二进制里的 DT_RPATH(仅当没有 DT_RUNPATH 时生效)

  2. 环境变量 LD_LIBRARY_PATH

  3. 二进制里的 DT_RUNPATH

  4. /etc/ld.so.cache(由 ldconfig 生成)

  5. 默认路径 /lib64/usr/lib64(32 位则是 /lib/usr/lib

  • 查看程序自己带的 RPATH / RUNPATH

1
2
$ readelf -d ./myapp | grep -E 'RPATH|RUNPATH'
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]
  • $ORIGIN 表示二进制所在目录。很多自包含软件(Nginx、MySQL、JDK)都会用它,保证把程序拷到别的目录后仍能找到旁边的 .so

LD_LIBRARY_PATH

  • 临时验证“把某个目录加进搜索路径后能不能解析”,用 LD_LIBRARY_PATH 最方便:

1
2
3
# 只对这一次 ldd / 运行生效
LD_LIBRARY_PATH=/usr/local/openssl/lib64 ldd ./myapp
LD_LIBRARY_PATH=/usr/local/openssl/lib64 ./myapp
  • 需要长期生效时,可以写进用户的 ~/.bashrc,或系统的 /etc/profile.d/

  • 生产环境更推荐 RPATH/RUNPATHldconfig,少用全局 LD_LIBRARY_PATH。它会影响该环境里所有程序的库搜索,容易把系统自带程序链接到错误版本的库。

ldconfig

  • ldconfig 扫描配置里的目录,生成 /etc/ld.so.cache。动态链接器平时主要查这份缓存,而不是每次都遍历磁盘。

  • 从源码安装共享库后,如果没有跑 ldconfigldd 经常仍然是 not found。源码安装 curl 时也需要这一步,见 Linux常用命令--curl与wget

1
2
3
4
5
6
7
8
9
10
# 查看缓存中已注册的库
ldconfig -p
ldconfig -p | grep ssl

# 安装或拷贝 .so 到标准目录后,刷新缓存
ldconfig

# 需要加入非标准目录时,新增一个配置文件
echo '/usr/local/openssl/lib64' > /etc/ld.so.conf.d/openssl.conf
ldconfig
  • /etc/ld.so.conf 以及 /etc/ld.so.conf.d/*.conf 里写的是目录,不是单个 .so 文件。

  • 注意:ldconfig 对 64 位库通常认 /lib64usr/lib64。把 .so 丢到 /usr/local/lib 却在 64 位程序上跑,有时仍然解析不到。

同一份库被解析到了意外的路径

  • 系统里同时存在 /lib64/libssl.so.1.1/usr/local/openssl/lib64/libssl.so.1.1 时,ldd 显示哪一份,取决于搜索顺序,而不是“哪份更新”。
  • 排查时不要只看库在不在,还要看右边的绝对路径是不是预期路径:
1
ldd ./myapp | grep ssl
  • 如果解析错了,优先检查 LD_LIBRARY_PATHRUNPATHldconfig -p 的顺序,而不是再装一份库。

和 readelf、objdump、nm 的区别

  • 这几个命令都能看“程序和库的关系”,但层次不同,不要混用。

命令 看什么 会不会执行程序 能不能看到间接依赖
ldd 运行时真实解析结果 某些实现可能会
readelf -d ELF 动态段,直接 NEEDED 不会 不能
objdump -p 类似,打印 ELF 头/动态段 不会 不能
nm -D 动态符号表 不会 不能
1
2
3
4
5
6
7
8
# 只看直接依赖,安全,适合检查不可信文件
$ readelf -d /bin/ls | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libselinux.so.1]
0x0000000000000001 (NEEDED) Shared library: [libcap.so.2]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

# 看动态符号(程序从外部库引用了哪些函数)
$ nm -D /bin/ls | grep ' U ' | head
  • 经验上:

    • 程序起不来、报缺库:先 ldd
    • 确认二进制声明了什么:用 readelf -d
    • 确认用的是哪一份库:看 ldd 右边的绝对路径
    • 确认缺的是函数而不是文件:用 ldd -rnm

架构不匹配

  • ldd 提示 not a dynamic executable 时,不一定是静态链接,也可能是架构不对

1
2
3
4
5
6
# 在 x86_64 主机上看一个 aarch64 的二进制
$ file ./myapp
./myapp: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1

$ ldd ./myapp
not a dynamic executable
  • 这时不是缺库,而是当前系统的动态链接器根本认不了这个 ELF。容器、交叉编译、从别的机器拷二进制时经常遇到。

  • 先看 fileuname -m 是否一致:

1
2
3
uname -m
file ./myapp
readelf -h ./myapp | grep Machine

常见实战组合

  • 下面几条命令基本能覆盖日常 80% 的动态库问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 程序类型
file ./myapp

# 2. 运行时依赖是否都能解析
ldd ./myapp

# 3. 缺库时,确认缓存和软件包
ldconfig -p | grep libxxx
yum provides '*/libxxx.so.1'

# 4. 临时指定库目录验证
LD_LIBRARY_PATH=/opt/myapp/lib ldd ./myapp

# 5. 确认解析到的是不是预期那一份
ldd ./myapp | grep ssl

# 6. 看直接依赖和 RUNPATH
readelf -d ./myapp | grep -E 'NEEDED|RUNPATH|RPATH'

# 7. glibc 够不够
ldd --version
ldd -v ./myapp | grep GLIBC

ldd 看到的是“加载时”依赖

  • dlopen("libxxx.so") 这种运行期再打开的库,ldd 不会列出来。
  • Java / Python 调用 JNI、C 扩展时,经常属于这种情况。此时要结合报错信息、strace 或进程已经加载的内存映射来查:
1
2
3
# 看进程已经加载了哪些 .so
cat /proc/<PID>/maps | grep '\.so'
lsof -p <PID> | grep '\.so'