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 | $ file /bin/ls |
dynamically linked才能用ldd;statically linked说明依赖已经打进二进制,运行时不再找.so。
ldd 如何工作
-
ldd本身通常是一个 shell 脚本,真正干活的是动态链接器,例如 x86_64 上的/lib64/ld-linux-x86-64.so.2。 -
它会设置环境变量
LD_TRACE_LOADED_OBJECTS=1,让动态链接器只做依赖解析并打印结果,而不是正常启动程序。 -
下面三条命令效果基本等价:
1 | ldd /bin/ls |
-
这也是为什么
ldd能打印出间接依赖:它走的是真实加载路径,不只看二进制自身声明的NEEDED。
不要对不可信的二进制执行 ldd
- 某些版本的
ldd会通过直接执行目标程序来获取依赖信息。如果这个程序是恶意的,它可能在被ldd分析时就开始运行。 - 对来源不明的文件,优先用只读解析 ELF 的方式:
1 | readelf -d /path/to/bin | grep NEEDED |
- 这两种方式只读取 ELF 头,不会执行程序。代价是只能看到直接依赖,看不到间接依赖。
1 | $ readelf -d /bin/ls | grep NEEDED |
基本用法
-
语法
1 | ldd [option]... file... |
-
最常见的用法就是直接跟一个可执行文件或
.so:
1 | $ ldd /bin/ls |
-
也可以一次看多个文件:
1 | ldd /bin/ls /usr/bin/cp |
-
对共享库本身也可以用
ldd,这在排查.so继续依赖哪些库时很有用:
1 | ldd /lib64/libssl.so.1.1 |
输出结果说明
-
ldd每一行通常是:库名 => 实际路径 (加载地址)
| 字段 | 含义 |
|---|---|
| 左边的库名 | 程序声明需要的 soname,例如 libc.so.6 |
=> 后面的路径 |
动态链接器最终解析到的真实文件 |
| 括号里的地址 | 本次探测时库被映射到的虚拟地址,每次运行可能不同 |
not found |
声明了依赖,但按搜索规则找不到对应文件 |
没有 => 的行 |
一般是虚拟库或动态链接器本身 |
-
几类特殊输出需要单独记住:
1 | # 1. linux-vdso.so.1 |
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 | $ ldd --version |
-
下载 MySQL、Oracle JDK、部分商业软件的 generic Linux 包时,包名里常带
glibc2.12、glibc2.17、glibc2.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 | $ ldd -v /bin/ls | head -n 30 |
-
如果程序是在更高版本 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 | $ ldd -u ./myapp |
-
注意:这只是
ldd的探测结果,动态加载(dlopen)的库它看不到,所以不要看见Unused就立刻删链接参数。
-r:检查缺失的符号
-
有时
.so文件在,但里面缺函数,程序启动或运行到某个调用时才会失败。 -
ldd -r会做函数重定位,把缺失符号提前报出来:
1 | $ ldd -r ./myapp |
-
这比等到运行时报
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 | $ ldd ./myapp |
-
然后确认这个库到底在不在磁盘上:
1 | # 在常见库目录里找 |
-
如果缓存里没有、磁盘上也没有,就按发行版安装对应软件包:
1 | # CentOS / RHEL 系列:先查哪个包提供这个文件 |
-
如果文件在,但
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 | ldconfig -p | grep libcrypt |
- 这类问题的本质是:soname 对不上。
ldd/ldconfig -p看到的是libcrypt.so.2,程序要的是libcrypt.so.1,名字差一个版本号就等于找不到。更完整的过程见 RocketMQ 的安装及使用。
动态库是怎么被找到的
-
ldd打印的路径,遵循动态链接器的搜索顺序。理解这个顺序,才能解释“明明有库,为什么 ldd 找不到 / 为什么找到的不是我想要的那一份”。
搜索顺序
-
二进制里的
DT_RPATH(仅当没有DT_RUNPATH时生效) -
环境变量
LD_LIBRARY_PATH -
二进制里的
DT_RUNPATH -
/etc/ld.so.cache(由ldconfig生成) -
默认路径
/lib64、/usr/lib64(32 位则是/lib、/usr/lib)
-
查看程序自己带的
RPATH/RUNPATH:
1 | $ readelf -d ./myapp | grep -E 'RPATH|RUNPATH' |
-
$ORIGIN表示二进制所在目录。很多自包含软件(Nginx、MySQL、JDK)都会用它,保证把程序拷到别的目录后仍能找到旁边的.so。
LD_LIBRARY_PATH
-
临时验证“把某个目录加进搜索路径后能不能解析”,用
LD_LIBRARY_PATH最方便:
1 | # 只对这一次 ldd / 运行生效 |
-
需要长期生效时,可以写进用户的
~/.bashrc,或系统的/etc/profile.d/。 -
生产环境更推荐
RPATH/RUNPATH或ldconfig,少用全局LD_LIBRARY_PATH。它会影响该环境里所有程序的库搜索,容易把系统自带程序链接到错误版本的库。
ldconfig
-
ldconfig扫描配置里的目录,生成/etc/ld.so.cache。动态链接器平时主要查这份缓存,而不是每次都遍历磁盘。 -
从源码安装共享库后,如果没有跑
ldconfig,ldd经常仍然是not found。源码安装 curl 时也需要这一步,见 Linux常用命令--curl与wget。
1 | # 查看缓存中已注册的库 |
-
/etc/ld.so.conf以及/etc/ld.so.conf.d/*.conf里写的是目录,不是单个.so文件。 -
注意:
ldconfig对 64 位库通常认/lib64、usr/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_PATH、RUNPATH和ldconfig -p的顺序,而不是再装一份库。
和 readelf、objdump、nm 的区别
-
这几个命令都能看“程序和库的关系”,但层次不同,不要混用。
| 命令 | 看什么 | 会不会执行程序 | 能不能看到间接依赖 |
|---|---|---|---|
ldd |
运行时真实解析结果 | 某些实现可能会 | 能 |
readelf -d |
ELF 动态段,直接 NEEDED |
不会 | 不能 |
objdump -p |
类似,打印 ELF 头/动态段 | 不会 | 不能 |
nm -D |
动态符号表 | 不会 | 不能 |
1 | # 只看直接依赖,安全,适合检查不可信文件 |
-
经验上:
- 程序起不来、报缺库:先
ldd - 确认二进制声明了什么:用
readelf -d - 确认用的是哪一份库:看
ldd右边的绝对路径 - 确认缺的是函数而不是文件:用
ldd -r或nm
- 程序起不来、报缺库:先
架构不匹配
-
ldd提示not a dynamic executable时,不一定是静态链接,也可能是架构不对。
1 | # 在 x86_64 主机上看一个 aarch64 的二进制 |
-
这时不是缺库,而是当前系统的动态链接器根本认不了这个 ELF。容器、交叉编译、从别的机器拷二进制时经常遇到。
-
先看
file和uname -m是否一致:
1 | uname -m |
常见实战组合
-
下面几条命令基本能覆盖日常 80% 的动态库问题:
1 | # 1. 程序类型 |
ldd 看到的是“加载时”依赖
dlopen("libxxx.so")这种运行期再打开的库,ldd不会列出来。- Java / Python 调用 JNI、C 扩展时,经常属于这种情况。此时要结合报错信息、
strace或进程已经加载的内存映射来查:
1 | # 看进程已经加载了哪些 .so |
lsof的详细用法见 Linux常用命令--lsof。