云计算百科
云计算领域专业知识百科平台

PHP 扩展加载失败

php -m 报 Unable to load dynamic library。答案其实就写在这条报错里,只是写在最里面那层括号里。

这个故障容易让人绕远。报错文本长、带路径、带括号嵌套,第一眼看不出该查什么。于是转头去翻面板、重装扩展、比对 php.ini,折腾一圈问题还在。

这台是 PHP 8.4.25 容器,镜像 1panel-php-fpm:8.4.25。基础镜像 Debian 13(trixie)。方法不依赖 Docker,任何 Linux 上的 PHP 都适用。

export PHP_CTN="php84" # 换成你的 PHP 容器名

先把那条报错读明白

完整的一整行长这样(2026-09-16 实测):

PHP Warning: PHP Startup: Unable to load dynamic library 'zip' (tried: …/zip.so (libzip.so.5: cannot open shared object file: No such file or directory)) in Unknown on line 0

这条报错是嵌套的,从外往里读三层。

第 1 层是 Unable to load dynamic library 'zip'。说的是谁失败了:名叫 zip 的扩展。

第 2 层是 tried: …/zip.so,说的是它去找了哪个文件。注意这个 .so 文件是存在的,否则不会说 tried。

第 3 层在最里面那层括号里。括号里写的是 (libzip.so.5: cannot open shared object file: No such file or directory)。问题就在这一层:zip.so 自己要用的 libzip.so.5 找不到。

前两层只回答了谁失败了、它叫什么名字。第三层才是答案。

这条报错给出的核心认知是:扩展的 .so 文件存在,不代表它能被加载。

.so 是动态库,它自己也要依赖别的动态库。PHP 加载 zip.so 的流程是找到文件、交给动态链接器。动态链接器再去解析 zip.so 的依赖,依赖里有一个找不到,整个加载就失败。

所以扩展文件在不在和扩展能不能用,是两个独立的问题。面板只回答第一个,ldd 才回答第二个。

用 ldd 把依赖关系摊开

ldd 打印一个程序或者动态库需要哪些共享库,以及每个依赖最终解析到了哪个文件。

# 先问 PHP 自己要扩展目录在哪(别硬编码路径,见下方说明)
docker exec -i "$PHP_CTN" php -r 'echo ini_get("extension_dir"), "\\n";'

# 然后对具体扩展跑 ldd,只挑出找不到的
docker exec -i "$PHP_CTN" bash -c '
EXT_DIR="$(php -r "echo ini_get(\\"extension_dir\\");")"
ldd "$EXT_DIR/zip.so" | grep "not found"'

别硬编码扩展目录。它的名字里带着 PHP 的 API 版本号,换个小版本就可能变。比如 /usr/local/lib/php/extensions/no-debug-non-zts-20240924/。用 php -r 'echo ini_get("extension_dir");' 问 PHP 自己,永远是对的。

正常的 ldd 输出长这样,这是 glibc 的 man page 里给的标准示例:

linux-vdso.so.1 (0x00007ffcc3563000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f87e5459000)
libc.so.6 => /lib64/libc.so.6 (0x00007f87e4e92000)

格式是 名字 => 解析到的路径 (加载地址)。左边的名字是需要哪个库,这是写在文件里的 soname。=> 右边是实际找到了哪个文件。末尾那个 (0x…) 是加载地址,排查缺库时不用管。

缺库的时候 => 右边变成 not found:

libzip.so.5 => not found

两个特殊项第一次看会疑惑,都不用管。linux-vdso.so.1 没有 =>,它是内核提供的虚拟 DSO。它不对应磁盘上的文件,所以没有路径可解析,这不叫缺库。ld-linux-x86-64.so.2 是动态链接器自己,出现在依赖列表里是正常的。

我这次实测到的结果

四个扩展同时加载失败,一条命令列出全部缺失库(2026-09-16 实测):

— gd —
libpng16.so.16 => not found
libavif.so.16 => not found
libwebp.so.7 => not found
libjpeg.so.62 => not found
libXpm.so.4 => not found
libfreetype.so.6 => not found
— intl —
libicuio.so.76 => not found
libicui18n.so.76 => not found
libicuuc.so.76 => not found
— zip —
libzip.so.5 => not found
— memcached —
libmemcached.so.11 => not found

四个扩展,共缺 11 个共享库。到这里,问题已经从「扩展加载失败」变成了一个具体的、可执行的清单。

定位到库名只完成了一半,你还需要知道这个库属于哪个包。

最快的办法是按命名规律推。Debian 的包名和库名有大致对应关系。libzip.so.5 通常对应 libzip5。libpng16.so.16 对应 libpng16-16,libicuuc.so.76 对应 libicu76。规律是「lib + 名字 + .so. + 主版本号」对应「lib + 名字 + 主版本号」。能用,但不能写死,下面那个坑就是反例。

最准的是直接查:

# Debian/Ubuntu:查某个文件属于哪个包(需要 apt-file)
apt-file search libzip.so.5

# 已装的包反查它提供了哪些文件
dpkg -S libzip.so.5

apt-file search 是最可靠的答案。代价是先装 apt-file 并跑一次 apt-file update,会拉一份索引,有点重。

最省事的是试装循环,把候选包名逐个试,谁成功用谁,容忍不确定:

for p in libzip5 libzip4; do
apt-get install -y –no-install-recommends "$p" 2>/dev/null && { echo "libzip 来自 $p"; break; }
done

Debian 正在做 64 位 time_t 过渡(t64),一批库改了包名。

libpng16.so.16 对应的包,从 libpng16-16 变成了 libpng16-16t64。后果很直接。要是你的补库脚本写死了 libpng16-16,在 t64 之后的系统上它会直接报错。报的是 E: Unable to locate package libpng16-16,而库名看起来一点没错。

所以候选名要兼容新旧:

for p in libpng16-16t64 libpng16-16; do
apt-get install -y –no-install-recommends "$p" 2>/dev/null && break
done

这条给了一个更一般的教训:排查脚本里凡是按名字推断出来的东西,都要留一条退路。库名是事实,ldd 读出来的;包名是推断,按规律猜的。两者的可靠度完全不同。

排查时通常不止一个扩展出问题,我这次是四个。与其一个个来,不如一次扫完:

# 遍历扩展目录,列出所有依赖缺失的扩展
docker exec -i "$PHP_CTN" bash -c '
EXT_DIR="$(php -r "echo ini_get(\\"extension_dir\\");")"
for so in "$EXT_DIR"/*.so; do
[ -e "$so" ] || continue # 通配符没匹配到时的保护
miss=$(ldd "$so" 2>/dev/null | grep "not found")
if [ -n "$miss" ]; then
echo "— $(basename "$so") —"
echo "$miss"
fi
done'

三个细节。

[ -e "$so" ] || continue。目录里没有 .so 时,$so 会是字面量 *.so,这行把它挡掉。

2>/dev/null。目录里可能有非动态库文件,ldd 会对它们报错到 stderr。这里是故意静音,这类报错不是问题。

ldd 只回答缺不缺,不回答该不该装。缺库的扩展不代表你都需要,补之前先确认哪些扩展是项目真在用的。

ldd 的两条边界

边界一:它默认只查找不找得到,不查符号。

ldd 能告诉你 libzip.so.5 找不到。但如果库找到了,里面的符号版本却对不上,ldd 会显示一切正常,程序照样起不来。比如系统升过库、扩展还是按老版本编译的。

glibc 的 man page 对 -d 和 -r 有明确说明。这两个选项会执行重定位并报告缺失的对象或函数:

# 更严格的检查:连符号一起查
ldd -r "$EXT_DIR/zip.so" | grep -E "not found|undefined symbol"

排查时建议直接带上 -r。反正都要跑,一次把两类问题都覆盖掉。

边界二:ldd 会执行目标程序。

这一点很多人不知道,man page 专门用一整段警告了它。原文:

In the usual case, ldd invokes the standard dynamic linker
with the LD_TRACE_LOADED_OBJECTS environment variable
set to 1, which causes the linker to display
the library dependencies.
Be aware, however, that in some circumstances,
some versions of ldd may attempt to obtain
the dependency information by directly
executing the program.
Thus, you should never employ ldd
on an untrusted executable, since this may result
in the execution of arbitrary code.

对应的安全替代方案,man page 也直接给了:

# 静态读取 ELF 头里的 NEEDED 段,完全不执行文件
objdump -p /path/to/program | grep NEEDED

但别以为 objdump 能整体替代 ldd

这里有个容易被忽略的差别。objdump -p | grep NEEDED 只列直接依赖,ldd 会展开整棵依赖树。

对排查缺库这件事来说差别很关键。我自己的文件、要查某个库为什么找不到,用 ldd,因为需要完整依赖树,还要看实际解析路径。来源不明、不放心的二进制,用 objdump -p | grep NEEDED 或 readelf -d,只读头部,不执行。

一个实测发现:不同实现报错文本不一样

我在本机 Cygwin 的 ldd 上实测了两种异常输入:

$ ldd /tmp/plain.txt
ldd: /tmp/plain.txt: Exec format error

$ ldd /tmp/nonexistent.so
ldd: /tmp/nonexistent.so: No such file or directory

而 glibc 的 ldd 碰到静态链接的程序,报的是 not a dynamic executable。

同一件事,不同实现、不同文本。Cygwin 报的是 Exec format error,glibc 报的是 not a dynamic executable。为什么会有这个差别,我没往下挖。这条我到现在也没查明白。

所以看这类输出别死记某一句报错,要看它在说什么。not found 和 No such file or directory 说的是缺库或缺文件。Exec format error 和 not a dynamic executable 说的是给它的不是动态库。可能是脚本、文本、或者静态二进制。

完整排查路径

#mermaid-svg-S7GYUPIywMIDFC99{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-S7GYUPIywMIDFC99 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-S7GYUPIywMIDFC99 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-S7GYUPIywMIDFC99 .error-icon{fill:#552222;}#mermaid-svg-S7GYUPIywMIDFC99 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-S7GYUPIywMIDFC99 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-S7GYUPIywMIDFC99 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S7GYUPIywMIDFC99 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S7GYUPIywMIDFC99 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-S7GYUPIywMIDFC99 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S7GYUPIywMIDFC99 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S7GYUPIywMIDFC99 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-S7GYUPIywMIDFC99 .marker.cross{stroke:#333333;}#mermaid-svg-S7GYUPIywMIDFC99 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-S7GYUPIywMIDFC99 p{margin:0;}#mermaid-svg-S7GYUPIywMIDFC99 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-S7GYUPIywMIDFC99 .cluster-label text{fill:#333;}#mermaid-svg-S7GYUPIywMIDFC99 .cluster-label span{color:#333;}#mermaid-svg-S7GYUPIywMIDFC99 .cluster-label span p{background-color:transparent;}#mermaid-svg-S7GYUPIywMIDFC99 .label text,#mermaid-svg-S7GYUPIywMIDFC99 span{fill:#333;color:#333;}#mermaid-svg-S7GYUPIywMIDFC99 .node rect,#mermaid-svg-S7GYUPIywMIDFC99 .node circle,#mermaid-svg-S7GYUPIywMIDFC99 .node ellipse,#mermaid-svg-S7GYUPIywMIDFC99 .node polygon,#mermaid-svg-S7GYUPIywMIDFC99 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-S7GYUPIywMIDFC99 .rough-node .label text,#mermaid-svg-S7GYUPIywMIDFC99 .node .label text,#mermaid-svg-S7GYUPIywMIDFC99 .image-shape .label,#mermaid-svg-S7GYUPIywMIDFC99 .icon-shape .label{text-anchor:middle;}#mermaid-svg-S7GYUPIywMIDFC99 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-S7GYUPIywMIDFC99 .rough-node .label,#mermaid-svg-S7GYUPIywMIDFC99 .node .label,#mermaid-svg-S7GYUPIywMIDFC99 .image-shape .label,#mermaid-svg-S7GYUPIywMIDFC99 .icon-shape .label{text-align:center;}#mermaid-svg-S7GYUPIywMIDFC99 .node.clickable{cursor:pointer;}#mermaid-svg-S7GYUPIywMIDFC99 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-S7GYUPIywMIDFC99 .arrowheadPath{fill:#333333;}#mermaid-svg-S7GYUPIywMIDFC99 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-S7GYUPIywMIDFC99 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-S7GYUPIywMIDFC99 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S7GYUPIywMIDFC99 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-S7GYUPIywMIDFC99 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S7GYUPIywMIDFC99 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-S7GYUPIywMIDFC99 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-S7GYUPIywMIDFC99 .cluster text{fill:#333;}#mermaid-svg-S7GYUPIywMIDFC99 .cluster span{color:#333;}#mermaid-svg-S7GYUPIywMIDFC99 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-S7GYUPIywMIDFC99 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-S7GYUPIywMIDFC99 rect.text{fill:none;stroke-width:0;}#mermaid-svg-S7GYUPIywMIDFC99 .icon-shape,#mermaid-svg-S7GYUPIywMIDFC99 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S7GYUPIywMIDFC99 .icon-shape p,#mermaid-svg-S7GYUPIywMIDFC99 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-S7GYUPIywMIDFC99 .icon-shape .label rect,#mermaid-svg-S7GYUPIywMIDFC99 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S7GYUPIywMIDFC99 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-S7GYUPIywMIDFC99 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-S7GYUPIywMIDFC99 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

有

没有

php -m 报 Unable to load dynamic library

读报错最内层括号拿到缺失的库名

对扩展的 .so 跑 lddgrep not found

有 not found 吗?

库名反推包名命名规律 + apt-file + 试装循环

库都在带上 -r 再查一遍符号

装包 → 重启容器 → 复验 php -m

拿这个坑走一遍,就是这次的实际路径。读报错,ldd 出 11 个缺失库,逐库反推包名,装包,复验 php -m 零告警。

赞(0)
未经允许不得转载:网硕互联帮助中心 » PHP 扩展加载失败
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!