当前位置:首页>Linux>别再被 Linux 依赖绕晕:从 .a、.so 到 ELF/readelf 和 deb 包,一次讲透

别再被 Linux 依赖绕晕:从 .a、.so 到 ELF/readelf 和 deb 包,一次讲透

  • 2026-10-11 05:47:45
别再被 Linux 依赖绕晕:从 .a、.so 到 ELF/readelf 和 deb 包,一次讲透

很多 Linux/C/C++ 工程里,真正让人困惑的往往不是代码本身,而是这些报错和现象:

undefined reference to xxx
error while loading shared libraries: libxxx.so: cannot open shared object file
ldd 能看到,readelf 又不一样
deb 包明明装了,程序为什么还是跑不起来
.a 和 .so 到底有什么区别
RUNPATH、RPATH、NEEDED 到底是谁用的
⧉

这些问题看起来分散,其实都围绕同一条主线:

源码如何变成可执行文件?可执行文件如何找到共享库?Debian/Ubuntu 又如何把这些库变成包依赖?

本文就把这条链路系统化讲清楚。

先纠正一个小点:你说的 LEF、readlef,在这个上下文里大概率是 ELF 和 readelf。

ELF 是 Linux/Unix 世界常见的二进制文件格式,全称是 Executable and Linkable Format。readelf 是查看 ELF 文件内部信息的工具,其中 readelf -d / readelf --dynamic 用来显示 ELF 文件的 dynamic section,也就是动态链接器运行时会读取的一组元数据。

先给结论

如果只记一件事,请记住这条分层:

编译期找头文件,链接期解符号,运行期找共享库,打包期把共享库名映射成 Debian 包名。

很多所谓“依赖问题”,本质上是把这些层次混在了一起。

报错或现象
更可能属于哪一层
#include <xxx.h>
 找不到
源码 / 编译期依赖
undefined reference to xxx
链接期依赖
cannot open shared object file
ELF 运行期依赖或库搜索路径
readelf
 看到 NEEDED,但包名不是这个
ELF 层与 Debian 包层映射
ldd
 和 readelf 看起来不一样
静态 ELF 元数据 vs 当前系统实际解析结果

从源码到运行

一个典型 C/C++ 程序,大致会经历这些阶段:

这里有几个关键角色:

文件或概念
含义
.o
编译后的目标文件
.a
静态库,本质是一组 .o 的归档
.so
共享库 / 动态库
ELF
Linux 下常见的可执行文件、共享库、目标文件格式
.deb
Debian/Ubuntu 的软件包格式

很多依赖问题,其实是把这些层次混在了一起。

比如:

  • 编译时报错:可能是头文件或链接库没找到。
  • 链接时报错:可能是符号没解析。
  • 运行时报错:可能是 .so 没找到。
  • apt
     安装时报错:可能是 deb 包依赖不满足。

它们都叫“依赖问题”,但不是同一层的依赖。

.o、.a、.so 分别是什么?

.o:目标文件,还不能直接运行

.o 是源码经过编译后得到的目标文件。

它里面有机器码、符号表、重定位信息等,但通常还不是完整程序。

比如:

gcc -c foo.c -o foo.o
⧉

得到的 foo.o 里可能定义了一个函数:

intfoo() {
return1;
}
⧉

也可能引用了别处的函数:

externintbar();
⧉

如果 bar() 还没找到定义,.o 也可以存在。真正要解决这些符号关系,是链接阶段的事情。

.a:静态库,本质是 archive

.a 文件不是“运行时要加载的库”。它更像一个装着很多 .o 文件的包。

例如:

gcc -c foo.c -o foo.o
gcc -c bar.c -o bar.o
ar rcs libmylib.a foo.o bar.o
⧉

这里 libmylib.a 里面装的是 foo.o、bar.o。

链接时:

gcc main.o -L. -lmylib -o app
⧉

链接器会从 libmylib.a 里挑出真正需要的 .o,复制进最终可执行文件。

重点是:

.a 是链接期使用的;.a 通常不会在程序运行时被加载。

这也是为什么有时候你把某个库静态链接进去了,运行机器上不需要安装对应的 .a 文件。

.so:共享库,运行时由动态链接器加载

.so 是 shared object,也就是共享库 / 动态库。

和 .a 不同,链接 .so 时,最终可执行文件通常不会把 .so 的代码完整复制进去,而是在自己的 ELF 元数据里记录:

我运行时需要 libfoo.so.1
⧉

这个记录就是 dynamic section 里的 DT_NEEDED,readelf -d 会显示成:

0x0000000000000001 (NEEDED) Shared library: [libfoo.so.1]
⧉

程序启动时,ld.so / ld-linux.so 负责找到并加载这些共享库。Linux 的动态链接器会寻找程序需要的 shared objects,准备程序运行,然后把控制权交给程序。

.a 和 .so 的链接逻辑完全不同

这是第一组容易混淆的地方。

链接 .a:按需抽取

假设有:

gcc main.o -lA -lB -o app
⧉

如果 main.o 用到了 libA.a 里的函数,链接器就会从 libA.a 中抽出对应 .o。

但是静态库有一个经典坑:顺序很重要。

比如:

gcc main.o -lfoo -lbar
⧉

和:

gcc main.o -lbar -lfoo
⧉

可能结果不一样。

原因是传统链接器处理 archive 时,通常按命令行顺序扫描。GNU ld 文档说明,archive 一般只会按出现顺序搜索一次;如果库之间存在循环引用,可以用 --start-group ... --end-group 让链接器重复搜索,但这会带来明显性能成本。

例如:

gcc main.o \
  -Wl,--start-group -lfoo -lbar -Wl,--end-group \
  -o app
⧉

但这通常是解决循环依赖的兜底手段,不应该滥用。

链接 .so:记录运行时依赖

如果链接的是动态库:

gcc main.o -L. -lfoo -o app
⧉

最终 app 里通常会记录:

NEEDED libfoo.so.1
⧉

注意,这里记录的往往不是你链接时看到的 libfoo.so,而是共享库自己的 SONAME。

典型共享库命名结构是:

libfoo.so          # linker name,链接时用
libfoo.so.1        # SONAME,运行时 ABI 名
libfoo.so.1.2.3    # real file,真实文件
⧉

常见软链接关系:

libfoo.so      -> libfoo.so.1.2.3
libfoo.so.1    -> libfoo.so.1.2.3
libfoo.so.1.2.3
⧉

这就是为什么你链接时写:

-lfoo
⧉

但 readelf -d 里看到的是:

NEEDED libfoo.so.1
⧉

依赖到底分为哪几种?

“依赖”这个词太大了。实际排查时,最好分成六层。

层次
解决的问题
常见线索
源码 / 编译期依赖
头文件、宏、声明在哪里
#include <openssl/ssl.h>
、Build-Depends
链接期依赖
符号实现在哪里
undefined reference
、-lssl -lcrypto
ELF 运行期硬依赖
程序启动必须加载哪些 .so
`readelf -dW ./app
库搜索路径依赖
这些 .so 到底在哪里
RPATH
、RUNPATH、LD_LIBRARY_PATH、ld.so.cache
可选运行时依赖
运行中才加载的插件或后端
dlopen()
、GPU、Qt/GTK/GStreamer 插件
包管理器依赖
哪些 Debian 包必须安装
Depends
、dpkg-shlibdeps、${shlibs:Depends}

第一层:源码 / 编译期依赖

比如代码里有:

#include<openssl/ssl.h>
⧉

这要求系统里有 OpenSSL 的头文件。

在 Debian/Ubuntu 上,这通常来自:

libssl-dev
⧉

这类依赖用于编译。

对应到 Debian 源码包,通常写在:

Build-Depends: libssl-dev
⧉

第二层:链接期依赖

编译能过,不代表链接能过。

比如你用了 OpenSSL 的函数,但链接时没加:

-lssl -lcrypto
⧉

就可能遇到:

undefined reference to SSL_new
⧉

链接期依赖解决的是:

  • 符号在哪里?
  • 函数实现在哪里?

这时链接器要么从 .a 里抽代码,要么在 .so 上建立动态依赖关系。

第三层:ELF 运行期硬依赖

这是 readelf -d 里最常见的:

NEEDED
⧉

例如:

readelf -dW ./app | grep NEEDED
⧉

输出:

0x0000000000000001 (NEEDED) Shared library: [libssl.so.3]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
⧉

这表示:

程序启动时,动态链接器必须找到这些库。找不到,程序就起不来。

这类依赖是 ELF 层面的运行时依赖。

第四层:库搜索路径依赖

NEEDED 只告诉系统:

我要 libfoo.so.1
⧉

但它不一定告诉系统:

libfoo.so.1 在哪里
⧉

库搜索路径由动态链接器决定。

常见来源包括:

  • DT_RPATH
  • LD_LIBRARY_PATH
  • DT_RUNPATH
  • /etc/ld.so.cache
  • /lib
    、/usr/lib、/lib64、/usr/lib64 等默认路径

Linux 动态链接器在依赖名不带 / 时,会按规则搜索路径:如果存在 DT_RPATH 且没有 DT_RUNPATH,会先用 DT_RPATH;然后考虑 LD_LIBRARY_PATH;再考虑 DT_RUNPATH;接着查 /etc/ld.so.cache;最后查默认库路径。

这里最容易踩的坑是:

DT_RUNPATH 只用于直接 DT_NEEDED 依赖,不会自动传递给子依赖。

例如:

app
 └── libA.so
      └── libB.so
⧉

如果只有 app 有:

RUNPATH=$ORIGIN/lib
⧉

它可能帮 app 找到 libA.so,但不一定帮 libA.so 找到 libB.so。

更稳的方式是:

  • libA.so
     自己也带正确 RUNPATH。
  • 或者把 libB.so 安装到系统库搜索路径。

第五层:可选运行时依赖

有些依赖不会出现在 NEEDED 里。

例如程序运行时才调用:

dlopen("libcuda.so.1", RTLD_NOW);
⧉

这类依赖常见于:

  • 插件系统
  • 图像编解码器
  • 数据库驱动
  • GPU / CUDA / OpenCL 后端
  • Qt / GTK / GStreamer 插件
  • PAM / NSS 模块

这意味着:

readelf -d app 只能看到 ELF 显式声明的直接动态依赖,不能保证看到所有运行时可能用到的依赖。

第六层:包管理器依赖

ELF 层看到的是:

libssl.so.3
libc.so.6
libstdc++.so.6
⧉

Debian 包层看到的是:

libssl3
libc6
libstdc++6
⧉

这两套名字不是同一套体系。

  • ELF 记录的是共享库名。
  • deb 包记录的是 Debian 包名。

中间的映射主要由 Debian 打包工具完成,例如 dpkg-shlibdeps。它会分析可执行文件或共享库依赖哪些 shared libraries,然后把结果写入 debian/substvars 中的依赖变量;它生成依赖信息时会使用 symbols 或 shlibs 文件。

readelf -d

常用命令:

readelf -dW ./app
⧉

可能看到:

Dynamic section at offset 0x2dd0 contains 27 entries:
  Tag                Type                 Name/Value
  0x0000000000000001 (NEEDED)             Shared library: [libfoo.so.1]
  0x000000000000001d (RUNPATH)            Library runpath: [$ORIGIN/../lib]
  0x000000000000000e (SONAME)             Library soname: [libbar.so.2]
  0x000000000000000c (INIT)               0x1000
  0x0000000000000019 (INIT_ARRAY)         0x3d80
  0x0000000000000017 (JMPREL)             0x680
  0x0000000000000007 (RELA)               0x560
⧉

几个最重要的字段如下。

NEEDED

表示当前 ELF 文件直接依赖的共享库。

例如:

NEEDED libfoo.so.1
⧉

意思是:

运行时必须加载 libfoo.so.1。

你说的 NEED 多半是 NEEDED 的简写或看漏了。readelf 常规输出里一般显示的是 (NEEDED)。

SONAME

SONAME 是共享库对外声明的 ABI 名称。

比如一个真实文件叫:

libfoo.so.1.2.3
⧉

它的 SONAME 可能是:

libfoo.so.1
⧉

其他程序链接它时,最终 NEEDED 里通常写入的就是这个 SONAME。

这也是 Linux 共享库版本管理的关键:

libfoo.so.1     表示 ABI 主版本
libfoo.so.1.2.3 表示具体实现版本
⧉

只要 ABI 兼容,libfoo.so.1.2.4 可以替换 libfoo.so.1.2.3。

RPATH

RPATH 是老式运行时库搜索路径。

如果 ELF 里有 RPATH,动态链接器可以用它来搜索共享库。

但现代构建里更常见的是 RUNPATH。如果一个对象里同时存在 DT_RPATH 和 DT_RUNPATH,ELF gABI 说明动态链接器只处理 DT_RUNPATH。

RUNPATH

RUNPATH 是新式运行时库搜索路径。

常见写法:

gcc main.o -Wl,-rpath,'$ORIGIN/../lib' -o app
⧉

这里 $ORIGIN 表示当前程序或共享对象所在目录。ld.so 文档说明,动态链接器会在多个位置展开 $ORIGIN,包括 DT_NEEDED、DT_RPATH、DT_RUNPATH、LD_LIBRARY_PATH 以及 dlopen() 参数等场景。

例如:

app 位于 /opt/myapp/bin/app
RUNPATH=$ORIGIN/../lib
⧉

则运行时会搜索:

/opt/myapp/lib
⧉

这非常适合做自包含应用。

其他字段怎么理解?

字段
简要含义
INIT
 / INIT_ARRAY
初始化函数相关信息,例如 C++ 全局对象构造、constructor 函数等
RELA
 / REL
重定位表,动态链接器加载共享库后用于修正引用
JMPREL
 / PLTGOT
和函数调用的动态绑定有关,涉及 PLT、GOT、lazy binding 等
VERNEED
 / VERSYM / VERDEF
符号版本信息,例如 GLIBC_2.34、GLIBCXX_3.4.29、OPENSSL_3.0.0

比如你在新系统上编译,生成的程序依赖 GLIBC_2.34,拿到只提供 GLIBC_2.28 的系统上,就可能无法运行。

动态链接器到底做了什么?

一个动态链接的 ELF 可执行文件里,通常有一个 PT_INTERP,用来告诉内核:

请用哪个动态链接器来启动我。

可以这样查看:

readelf -lW ./app | grep 'Requesting program interpreter'
⧉

常见输出:

[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
⧉

ELF gABI 中也说明,参与动态链接的可执行文件会带有 PT_INTERP,系统在 exec 过程中通过它找到程序解释器,也就是动态链接器。动态链接器负责装入共享对象、执行重定位,最后把控制权交给程序。

整个过程可以简化成:

所以,真正让动态程序跑起来的,不只是 app 自己,还有动态链接器和一整棵共享库依赖树。

deb 包的依赖结构是什么样的?

现在从 ELF 层跳到 Debian/Ubuntu 的包管理层。

.deb 文件本身是什么?

.deb 本质上也是一个归档文件。

Debian 的 deb(5) 手册说明,.deb 是 Debian 二进制包格式;其文件是一个 ar archive,并且内部成员必须按固定顺序出现。

一个典型 .deb 里有:

debian-binary
control.tar.*
data.tar.*
⧉

含义如下:

成员
含义
debian-binary
记录 deb 格式版本,例如 2.0
control.tar.*
包元数据,例如 control 文件、maintainer scripts、shlibs、symbols、conffiles、md5sums 等
data.tar.*
真正要安装到系统里的文件,例如 /usr/bin/xxx、/usr/lib/xxx.so、/etc/xxx.conf

可以用这些命令查看:

ar t package.deb
⧉

或者:

dpkg-deb -I package.deb   # 查看控制信息
dpkg-deb -c package.deb   # 查看安装文件列表
⧉

二进制包的 control 文件里会有类似内容:

Package: foo
Version: 1.0-1
Architecture: amd64
Depends: libc6 (>= 2.34), libssl3 (>= 3.0.0)
Recommends: foo-data
Suggests: foo-doc
Conflicts: old-foo
Breaks: foo-plugin (<< 1.0)
Replaces: old-foo
Provides: virtual-foo
Description: ...
⧉

Debian Policy 规定,包之间的关系通过控制字段声明,例如 Depends、Pre-Depends、Recommends、Suggests、Enhances、Breaks、Conflicts 等。

可以这样理解:

字段
含义
Depends
必须依赖。缺了通常不能正常工作
Pre-Depends
更强的 Depends,甚至在解包或配置前就要求满足;基础系统包里更常见,普通包不要随便用
Recommends
强推荐。APT 默认通常会安装
Suggests
弱推荐。通常是增强功能、文档、插件等
Conflicts
不能和某些包同时安装
Breaks
会破坏某些版本的包,需要升级或移除对方
Replaces
允许替换另一个包中的文件,常和 Conflicts / Breaks 配合
Provides
提供虚拟包能力,例如多个 MTA 都可以提供 mail-transport-agent

依赖表达式也有语法规则。

比如:

Depends: libc6 (>= 2.34), libssl3 (>= 3.0.0)
⧉

逗号表示 AND:

A, B
⧉

意思是 A 和 B 都要满足。

竖线表示 OR:

default-mta | mail-transport-agent
⧉

意思是满足其中之一即可。

deb-control(5) 手册也说明,Depends、Pre-Depends、Recommends、Suggests 的语法是一组可替代包;逗号分组表示 AND,竖线表示 OR,并且竖线优先级更高。

ELF 依赖如何变成 deb 包依赖?

这是 Debian 打包里非常关键的一步。

假设你编译出来一个程序:

debian/foo/usr/bin/foo
⧉

它的 ELF 依赖是:

readelf -dW debian/foo/usr/bin/foo | grep NEEDED
⧉

输出:

NEEDED libssl.so.3
NEEDED libc.so.6
⧉

但最终 deb 包里不能写:

Depends: libssl.so.3, libc.so.6
⧉

因为 Debian 包管理器识别的是包名,而不是 .so 名。

它应该写成类似:

Depends: libssl3, libc6
⧉

这个转换主要由 dpkg-shlibdeps 完成。

常见 debian/control 写法是:

Package: foo
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
⧉

构建时,dpkg-shlibdeps 会扫描 ELF 文件,找出链接了哪些共享库,再通过 symbols 或 shlibs 文件推导出应该依赖哪些 Debian 包。dpkg-shlibdeps 文档说明,它会计算可执行文件的共享库依赖,并写入 debian/substvars;它会查找库对应的 symbols 或 shlibs 信息。

Debian Policy 也说明,如果共享库包提供了 symbols 文件,dpkg-shlibdeps 通常会优先使用它,而不是 shlibs 文件;symbols 能提供更细粒度的符号级最小版本依赖。

简单说:

ELF:
  NEEDED libssl.so.3

 dpkg-shlibdeps:
  找到哪个包提供 libssl.so.3
  查 symbols / shlibs
  推导版本约束

deb:
  Depends: libssl3 (>= x.y.z)
⧉

这就是 ELF 层和 deb 包层之间的桥。

readelf 和 ldd 有什么区别?

很多人排查依赖时会直接用:

ldd ./app
⧉

它确实方便,但要知道它和 readelf 看的不是同一层。

工具
看什么
优点
局限
readelf -dW ./app
静态查看 ELF 文件里的 dynamic section
不运行程序;适合查看 ELF 自身记录了什么;适合分析不可信文件的直接依赖
主要看到直接 NEEDED;看不到当前系统实际解析路径;看不到 dlopen 插件依赖
ldd ./app
当前系统环境下实际解析出来的共享库路径
直观;能看到完整依赖树;能看到实际解析路径
受当前环境和 LD_LIBRARY_PATH 影响;不适合随便分析不可信二进制

readelf -dW ./app 能看到:

NEEDED
RUNPATH
RPATH
SONAME
INIT_ARRAY
RELA
VERNEED
⧉

ldd ./app 可能显示:

libssl.so.3 => /usr/lib/x86_64-linux-gnu/libssl.so.3
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6
⧉

ldd 手册明确提醒,不要对不可信的可执行文件使用 ldd,因为某些情况下可能导致任意代码执行;更安全的替代方式是用:

objdump -p /path/to/program | grep NEEDED
⧉

但这种方式只显示直接依赖,而 ldd 会显示完整依赖树。

所以经验规则是:

看 ELF 自己写了什么:
  readelf -dW ./app

看当前系统实际解析到哪里:
  ldd ./app

分析不可信文件:
  优先 readelf / objdump,不要直接 ldd
⧉

最容易踩的 6 个坑

坑 1:把 .so 名当成 deb 包名

ELF 里看到:

libssl.so.3
⧉

Debian 包里通常是:

libssl3
⧉

不是同一个名字。

坑 2:.a 的链接顺序不对

gcc main.o -lfoo -lbar
⧉

和:

gcc main.o -lbar -lfoo
⧉

可能一个成功,一个失败。

静态库按需抽取,顺序很重要。

坑 3:以为 RUNPATH 会传递

app -> libA.so -> libB.so
⧉

app 的 RUNPATH 不一定能帮 libA.so 找 libB.so。

如果 libA.so 依赖 libB.so,更可靠的做法是让 libA.so 自己也有正确的 RUNPATH。

坑 4:只看 readelf,漏掉 dlopen()

插件、GPU 后端、数据库驱动等依赖,可能运行时才加载。

这些不一定出现在:

readelf -d
⧉

里。

坑 5:以为 strip 会影响动态依赖

strip 可以删调试信息、普通符号表等,但动态链接所需的 .dynamic、.dynsym、NEEDED 等通常不能删。

否则程序就没法被动态链接器正确加载。

坑 6:忽略 --as-needed

现代发行版或构建系统常使用:

-Wl,--as-needed
⧉

GNU ld 文档说明,--as-needed 会影响 ELF 的 DT_NEEDED 输出:只有当某个动态库在当时确实满足非弱未定义符号引用时,链接器才会为它生成 DT_NEEDED。

所以有时候你命令行里写了:

-lfoo
⧉

但最终:

readelf -d app | grep foo
⧉

什么都没有。

这不一定是错,而可能是 --as-needed 认为它没被真正用到。

一张图总结

ELF 的 NEEDED、RUNPATH、SONAME 是二进制运行时链接信息;Debian 的 Depends、Recommends、Provides 是包管理层信息。二者有关联,但不是同一层东西。

当你理解了这条链路,再看:

undefined reference
cannot open shared object file
NEEDED
RUNPATH
libxxx.so.N
Depends: libxxxN
⧉

这些信息就不再是零散术语,而是一套完整系统。

延伸阅读与官方资料

下面这些资料适合进一步查细节,建议优先看官方文档和 man page。

  • GNU Binutils readelf 文档
    :适合查 readelf -d、readelf -h、readelf -l、readelf -V 等选项含义。
  • Linux ld.so(8) 手册
    :重点看动态链接器的作用、库搜索顺序、LD_LIBRARY_PATH、RPATH、RUNPATH、$ORIGIN。
  • ELF gABI Dynamic Linking
    :适合深入理解 PT_INTERP、.dynamic、动态链接器如何参与进程启动。
  • GNU ld Options
    :重点看 --start-group、--end-group、--as-needed、-Bstatic、-Bdynamic、-rpath 等链接器选项。
  • Debian deb(5) 手册
    :适合理解 .deb 文件内部结构:debian-binary、control.tar.*、data.tar.*。
  • Debian Policy:包关系字段
    :适合查 Depends、Pre-Depends、Recommends、Suggests、Breaks、Conflicts 等字段的正式语义。
  • deb-control(5)
    :适合查 Debian 控制文件语法,尤其是逗号表示 AND、竖线表示 OR、版本约束和虚拟包写法。
  • dpkg-shlibdeps(1)
    :适合理解 Debian 打包时如何从 ELF 共享库依赖生成 ${shlibs:Depends}。
  • Debian Policy:Shared libraries
    :适合深入理解 shlibs、symbols、SONAME 和 Debian 共享库依赖生成机制。
  • ldd(1) 手册
    :适合查 ldd 的行为和安全注意事项,尤其是不要对不可信二进制直接运行 ldd。

最新文章

随机文章