Steam 点了没反应:一次两层故障的排查记录
环境:Ubuntu 24.04,Intel Arc A770,Wayland 会话,steam-launcher 由 apt 安装。(显卡和 Wayland 在后文都没有起作用——Steam 走 XWayland 正常出窗口,本次故障与它们无关。i386 那条同理,但要限定版本:在本文这个客户端版本上,没启用 i386 就必然崩在加载 steamui.so 那一步;换版本是否一致,以实际日志为准。列在这里只是因为它们是这台机器的事实。)
全文是 两处直接故障 + 一个上游根因:客户端本体下不完、32 位库缺失;而第二处之所以发生,要追到 PATH 里一个影子 python3 让 steamdeps 静默失败(第七节)。
时效与样本说明:本文写于 2026 年 11 月,客户端 build 1792618285。包名、清单结构、下载主机行为都会随版本变化,文中所有速度数字和日志都是这一台机器、这一条网络上的单次样本。方法可以带走,具体结论别当定论。
读者与前提:这篇假设你能看懂 dpkg、ldd、strace 这类命令在做什么,不逐条解释参数。没有这些背景的话,照抄命令或许能修好,但为什么有效会看不懂。
只想尽快用上、不想折腾的话:先试开游戏加速器或代理,切到全局/TUN 模式再启动 Steam,让它自己把客户端更新下完。值得先试,但不保证——加速器的规则未必覆盖 Steam 的更新域名(第三节有实测方法)。不灵再往下看。
一、症状
图标点下去,没反应。等一会儿再点,还是没反应。没有窗口,没有报错弹窗,什么都没发生。
这种情况最容易犯的错是凭印象猜原因。我当时的第一反应是”残留锁文件”——因为 ~/.steam/ 里躺着一堆看着可疑的东西:
starting
steam.pid (内容 5518)
~/.local/share/Steam/.crash
清掉、重启、没用。
这些文件只能证明”上一次不是正常退出的”,不能证明”现在没在运行”。pgrep -af steam 一看,Steam 活得好好的,正在干另一件事。
二、真相在日志里
~/.local/share/Steam/logs/ 下有十几个文件,最关键的三个:
bootstrap_log.txt—— 启动器自己(客户端还没装完时唯一有内容的地方)console-linux.txt—— 启动脚本 + 客户端updateui_child.txt—— 那个更新小窗口
这些日志会被进度行刷屏,直接看等于什么都没看。先把它滤掉:
grep -v -E "Downloading update|Add pending" ~/.local/share/Steam/logs/bootstrap_log.txt
剩下的才是骨架。先看这组对照:
[19:20:26] [ 2%] Downloading update (13,816 of 502,699 KB)...
...
[19:20:56] [ 0%] Downloading update (3,866 of 502,699 KB)...
计数从 13,816 掉回 3,866,百分比从 2% 归零。这不是下载慢,是下载失败后从头重试。它已经在那个循环里转了很久。
再 grep 错误:
Error: Download of package (tenfoot_images_all) failed after 0 bytes (0 : 200).
Error: Download of package (public_all) failed after 0 bytes (0 : 0).
(0 : 200) 表示 HTTP 状态码拿到了 200,但实际收到 0 字节——连接建立、服务端说”好的”、然后数据流被掐断。(0 : 0) 是连 HTTP 响应都没等到。两种形态同时出现,基本可以确定是链路问题,而不是 Steam 或磁盘的问题。
到这里第一层故障清楚了:客户端本体(约 500MB)从没下完过。所以”打不开”,一直是在看一个更新器空转。
steam-launcher 这个包本身只是个启动器,apt 把它装好并不等于客户端就位——真正的客户端要它自己联网去拉。所以严格讲,”装好了”只完成了第一步。
三、找出哪条线路是活的
启动器每次都会在日志开头把准备用的下载主机打出来,这是最权威的信息来源:
Using the following download hosts for Public, Realm steamglobal
1. https://client-update.steamstatic.com, /, Realm 'steamglobal', weight was 1, source = 'baked in'
source = 'baked in' 说明这个地址是编译进二进制里的默认值,不是从配置里读的。
接下来自己测。不要照抄别人文章里的 IP——那是别人的网络拓扑,跟你没关系。方法是这样:拿清单里的任意一个包,给个时间上限跑一次,看平均速度。
# 文件名从清单里随便取一个(下面这个会随版本失效,换成你自己的)
URLPATH="public_all.zip.vz.54c72b908da906e7af3bee17206b30830d6f627a_25274088"
for h in client-update.steamstatic.com \
client-update.akamai.steamstatic.com \
steamcdn-a.akamaihd.net ; do
printf "%-42s " "$h"
curl -s -o /dev/null --max-time 12 \
-w "%{speed_download} B/s\n" "https://$h/$URLPATH"
done
我这次的实测结果:
| 线路 | 结果 |
|---|---|
client-update.steamstatic.com(默认) |
25MB 只下到 2.4MB 即断,之后彻底超时 |
client-update.akamai.steamstatic.com |
2.2 MB/s,能下完 |
steamcdn-a.akamaihd.net |
196 B/s |
cdn.steamstatic.com |
216 B/s |
同一个 CDN 的不同域名,差了四个数量级。默认那条是死的。
顺带:真要用加速器或代理
上面那套测速和预置包,是”手上没有代理”时的路子。有的话可以直接用,但两个坑要知道:一是只设 http_proxy 环境变量对更新器不一定生效——它有自己的网络栈,得在网络层接管(TUN、透明代理);二是加速器的规则常常只覆盖商店和社区、漏掉 client-update,那就还得回第四节。生效与否看日志计数会不会回退(判据见第二节)。使用哪类工具请自行遵守当地法规与服务条款。
四、把包预置进去(不需要 root,不需要代理)
思路来自一行日志。Steam 启动时会先校验本地安装:
Verifying installation...
Verifying file sizes only
Verification complete
Verifying file sizes only——它只比大小。那就意味着:把文件按正确的名字、正确的大小放进 ~/.local/share/Steam/package/,它就不会再去下载了。它不认来源,只要有一条能跑通的线路就够了。
流程很短:从 bootstrap_log.txt 的 Add pending download: / Downloading manifest: 行拿到”要哪些文件、清单在哪”,解析出文件名、大小和 sha256,再从能跑通的线路下载进 package/ 并逐个校验。但我踩了三个坑。
坑一:.vz 文件的真实大小写在文件名里
清单是这样的结构:
"tenfoot_images_all"
{
"file" "tenfoot_images_all.zip.86419c7a56c12dd107b5e0d46f50c8a9b121f3cc"
"size" "6582204"
"sha2" "990676bc29d01736477c6b571b5048..."
"zipvz" "tenfoot_images_all.zip.vz.193cb8c4eb4446698ea2c0a9e8c4e6b6a623dac7_5572671"
"sha2vz" "e1fee3beffa9d08a415a35ddc7d3141af6c44269..."
}
size / sha2 是解压前 zip 的属性;zipvz / sha2vz 才是实际要下载的那个压缩文件。两者的关系全靠文件名末尾那个数字:_5572671 就是 .vz 的字节数。
我第一版脚本拿 size 去比对 .vz 文件,结果把一批已经下好的好文件全判成”校验失败”删掉了,白下一轮。正确的规则是:有 zipvz 就用后缀数字当大小、用 sha2vz 当哈希;只有少数没有 zipvz 的条目才用 size + sha2。
坑二:清单不止一份
只灌 ubuntu12 那一份,重启后日志里会冒出 bins_steamrt64、runtime_steamrt_steamrt64 这种新名字——steam_client_steamrt64 那份还得单独补。
坑三(顺便省一半流量):两份清单的包内容相同,只是名字不同
比对一下就很明显,同一个 sha2vz、同一个大小,出现在两个名字下:
bins_codecs_ubuntu12.zip.vz.0ceb4808651cbce441edfa95ae49474c40a6ed01_12851097
bins_codecs_steamrt64.zip.vz.0ceb4808651cbce441edfa95ae49474c40a6ed01_12851097
所以第二份不用重下。建一个”哈希 → 已有文件路径”的索引,按哈希把现有文件复制成新名字就行。我第二份 493MB 里,实际只新下了 1 个 89MB 的文件,其余 27 个全是复用。
最后一步必须自己算哈希
“只校验大小”既是这个方法能成立的前提,也是它的风险:如果文件内容坏了,Steam 不会替你发现。所以下载完自己跑 sha256sum 对照清单的 sha2vz。
真塞进了坏文件会怎样?症状是启动后莫名崩溃或界面异常,而 Steam 自己不报错——这时候把 ~/.local/share/Steam/package/ 里对应的文件删掉、让它重新下载,就能回到干净状态。
补齐之后重启,日志终于出现:
Installing update...
Extracting package...
Update complete, launching Steam...
第一层通了。
附:可直接用的骨架
上面的流程归整成两个文件就能跑。别想着用 jq——清单是 Valve 的 KeyValues 格式(VDF),不是 JSON,jq 直接报 parse error: Expected separator between values。
把清单解析成制表符分隔的三列(文件名、大小、sha256):
vdf_packages.py(Python)
#!/usr/bin/env python3
import re, sys
def load(path):
text = open(path, encoding="utf-8", errors="replace").read()
toks = re.findall(r'"[^"]*"|[{}]|[^\s{}"]+', text)
root, stack, key = {}, [{}], None
for t in toks:
if t == "{":
d = {}; stack[-1][key] = d; stack.append(d); key = None
elif t == "}":
if len(stack) > 1: stack.pop()
key = None
elif t.startswith('"'):
if key is None: key = t[1:-1]
else: stack[-1][key] = t[1:-1]; key = None
return stack[0]
def packages(node):
for v in node.values():
if not isinstance(v, dict): continue
if "zipvz" in v: # 压缩包:真实大小在文件名末尾
fn = v["zipvz"]
yield fn, fn.rsplit("_", 1)[1], v["sha2vz"]
elif "file" in v and "size" in v: # 少数未压缩条目
yield v["file"], v["size"], v["sha2"]
else:
yield from packages(v)
for name, size, sha in packages(load(sys.argv[1])):
print(f"{name}\t{size}\t{sha}")
分流 / 复用 / 下载 / 校验,参数换成第三节测出来的那条线路:
steam_prefetch.sh(bash)
#!/bin/bash
# steam_prefetch.sh — 把 Steam 客户端更新包预置进 package 目录
# 用法: ./steam_prefetch.sh https://<能跑通的下载主机>
# 例: ./steam_prefetch.sh https://client-update.akamai.steamstatic.com
set -u
HOST="${1:?用法: $0 https://<下载主机>}"
PKG="$HOME/.local/share/Steam/package"
HERE="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$PKG"
# 1) 取清单。启动器要几个 realm 就列几个——从 bootstrap_log.txt 的
# "Downloading manifest:" 行抄过来
MANIFESTS="steam_client_ubuntu12 steam_client_steamrt64"
LIST=$(mktemp)
QUEUE=$(mktemp)
trap 'rm -f "$LIST" "$QUEUE" "$LIST".*' EXIT
for m in $MANIFESTS; do
curl -sS --fail -m 60 -o "$LIST.$m" "$HOST/$m" || { echo "拿不到清单 $m"; exit 1; }
python3 "$HERE/vdf_packages.py" "$LIST.$m"
done | sort -u > "$LIST"
echo "清单合计 $(wc -l < "$LIST") 个包"
# 2) 分流:已一致→跳过;内容相同只是名字不同→本地复制;其余→排队下载
# 先建一次「哈希→路径」索引,避免循环里反复对整个目录算哈希
declare -A H2F
for f in "$PKG"/*; do
[ -f "$f" ] && H2F["$(sha256sum "$f" | cut -d' ' -f1)"]="$f"
done
while IFS=$'\t' read -r fn size sha; do
out="$PKG/$fn"
if [ -f "$out" ] && [ "$(stat -c%s "$out")" = "$size" ] &&
[ "$(sha256sum "$out" | cut -d' ' -f1)" = "$sha" ]; then
continue
fi
src="${H2F[$sha]:-}"
if [ -n "$src" ] && cp --reflink=auto -f "$src" "$out" 2>/dev/null; then
echo "复用 $(basename "$out")"
continue
fi
printf '%s\t%s\t%s\n' "$fn" "$size" "$sha" >> "$QUEUE"
done < "$LIST"
# 3) 下载 + 校验(文件多时可用 xargs -P 6 并发)
while IFS=$'\t' read -r fn size sha; do
tmp="$PKG/$fn.part"
if curl -sS --fail --retry 5 --retry-all-errors --max-time 1200 -C - -o "$tmp" "$HOST/$fn" &&
[ "$(stat -c%s "$tmp")" = "$size" ] &&
[ "$(sha256sum "$tmp" | cut -d' ' -f1)" = "$sha" ]; then
mv -f "$tmp" "$PKG/$fn"
echo "OK $fn"
else
echo "FAIL $fn"
rm -f "$tmp"
fi
done < "$QUEUE"
这份骨架在这台机器上实跑过一遍,分流结果与上面坑三的结论一致。唯一的例外是 webkit_ubuntu12——Steam 装完后把它删了,而它与 steamrt64 那份差一个字节、哈希不同没法复用,只有它真的重新下载了。
它有几处已知边界,当排障工具够用,当长期工具不够:解析器只覆盖 Steam 清单的常见子集(不处理字符串里的转义引号,重复键以后者为准);下载是串行的,没有多线路自动回退,超时固定 --max-time 1200;复用索引只在开头建一次,本次运行中新下的文件不会进索引。真要用得久,这些都得自己补。
五、第二层:ELFCLASS64
更新装完,启动器正常退出并拉起客户端。然后进程又没了。
Error: You are missing the following 32-bit libraries, and Steam may not run:
libGL.so.1
libdrm.so.2
dlmopen /home/dark/.local/share/Steam/ubuntu12_32/steamui.so failed:
libGL.so.1: 错误的 ELF 类:ELFCLASS64
Fatal error: Failed to load steamui.so
ELFCLASS64 是这条报错里信息量最大的四个字。它表达的不是”找不到库”,而是”找到了,但找到的是 64 位的那份“。32 位的 steamui.so 要加载 libGL.so.1,系统把 64 位版本递给了它,ELF 类型对不上。
往下查一层:
$ file ~/.local/share/Steam/ubuntu12_32/steamui.so
ELF 32-bit LSB shared object, Intel 80386 ...
$ dpkg --print-foreign-architectures
(空)
输出为空——这台机器从没启用过 i386 架构,所以系统里一个 32 位库都没有。而 Steam 的界面层是 32 位的。
这一层和第一层是完全独立的两个故障。 修好下载不代表修好启动,反过来说,如果你的网络没问题、只是打不开,那你只需要处理这一层。
六、补 32 位库
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install steam-libs-i386
steam-libs-i386 是 Ubuntu 为 Steam 准备的元包,会拉一百多个 i386 包,libGL、libdrm、libX11、libasound 都在里面(这台机器上一次装了 160 个)。
包名会随发行版版本变(24.04 就因为 64 位 time_t 迁移,把一大批包改成了 -t64 后缀)。所以抄命令前先确认包存在:
apt-cache policy libgtk2.0-0t64:i386
缺哪些库,最终仍以 ldd 的实际输出为准。
但装完还不能收工。实测此时 steamui.so 仍然缺 6 个库——元包的清单本来就不含它们:
libXtst.so.6 libXrandr.so.2 libXi.so.6
libgtk-x11-2.0.so.0 libpipewire-0.3.so.0 libxcb-res.so.0
sudo apt install libxtst6:i386 libxrandr2:i386 libxi6:i386 \
libgtk2.0-0t64:i386 libpipewire-0.3-0t64:i386 \
libxcb-res0:i386
另外全量扫一遍 32 位组件时还会看到 libICE.so.6、libSM.so.6、libopenal.so.1,一并装上更稳妥。
这里有个我自己踩的小坑:不要用 /usr/lib32 判断装没装上。i386 的库在 /usr/lib/i386-linux-gnu/,我按 /usr/lib32 查,明明装好了却显示”仍缺”,差点又绕一圈。
正确的验证方式不是看包列表,而是直接问 ELF 自己:
cd ~/.local/share/Steam/ubuntu12_32
ldd steamui.so | grep "not found"
输出为空,才是真的齐了。
如果装这些 i386 包时 apt 报”未满足的依赖关系”、或 dpkg 报配置失败,可以先 sudo apt install -f 把依赖状态修一次再重跑。但它不能替代上面那份清单:-f 只修”已装但依赖没满足”的包,而这 6 个库不在任何已装包的依赖里,系统当时并不破损。另外它有可能为了自洽而卸载包,回车前把提示看完。
七、往上游再追一步:这些库为什么没被自动装
启动脚本一开始就想装,只是失败了,并且决定”继续跑”:
bin_steam.sh[4456]: Unable to install Steam dependencies by running
/usr/bin/steamdeps, trying to continue anyway...
那句 trying to continue anyway 就是让所有问题延后爆发的那句话。如果它当场报错退出,用户根本不会走到后面这一堆。
那 steamdeps 为什么失败?它自己给的理由是:
Couldn't import apt, please install python3-apt or update steamdeps for your distribution.
但 python3-apt 明明是装着的。往下查:
$ head -1 /lib/steam/bin_steamdeps.py
#!/usr/bin/env python3
$ which -a python3
/home/dark/.local/bin/python3
/usr/bin/python3
$ /usr/bin/python3 -c "import apt" → 成功
$ ~/.local/bin/python3 -c "import apt" → ModuleNotFoundError
~/.local/bin/python3 在 PATH 里排在系统解释器前面——装过 uv、pyenv、poetry 之类工具的机器上很常见。steamdeps 用的是 env python3,于是拿到了这个影子解释器。而 python3-apt 提供的 apt_pkg 模块只装在 /usr/lib/python3/dist-packages/,只有 /usr/bin/python3 看得见它。
这一步不用只信推断,可以直接做 A/B——同一台机器、同一个脚本、同一个 python3-apt,只换 PATH:
$ python3 /lib/steam/bin_steamdeps.py --help
Couldn't import apt, please install python3-apt or update steamdeps for your distribution.
$ PATH=/usr/bin:/bin python3 /lib/steam/bin_steamdeps.py --help
usage: bin_steamdeps.py [-h] [--dry-run] [--interactive] [--install INSTALL] ...
再用 strace 直接看它 execve 了哪个解释器,连”猜”都省了:
$ strace -f -e trace=execve /lib/steam/bin_steamdeps.py --help 2>&1 | grep execve
execve("/lib/steam/bin_steamdeps.py", ...) = 0
execve("/home/dark/.nvm/.../bin/python3", ...) = -1 ENOENT
execve("/home/dark/.cargo/bin/python3", ...) = -1 ENOENT
execve("/home/dark/.local/bin/python3", ...) = 0 ← 命中影子解释器
这台机器的 PATH 里塞了 nvm、npm、pnpm、cargo 一堆 bin 目录,env python3 一路试了 9 个路径才找到那个影子解释器。开发机上这种 PATH 污染相当普遍,所以这不是什么罕见配置。
于是完整的因果链是这样:
PATH 里的影子 python3
→ steamdeps 无法 import apt,静默失败
→ 32 位依赖从未被安装,i386 从未启用
→ steamui.so 加载失败,客户端崩溃
这条链上任何一环修掉都能通,但两种做法性质不同。手工装库是绕过:库到位了,steamdeps 本身还是坏的,下次它再被调用(比如某个游戏要新的 32 位依赖)会照样静默失败。把 ~/.local/bin 从 PATH 前面挪开,才是修复上游。我这次选了绕过,因为最快;代价是那个坑一直留在那儿。
日志里那句 Error: You are missing the following 32-bit libraries 是 Steam 自己的体检输出,不是失败原因;更有误导性的是 steamdeps 那句——它把”缺依赖”伪装成了”缺 python 模块”。
八、确认它真的开了
进程在跑不代表窗口画出来了。两个不含歧义的检查:
# 窗口是否存在(Steam 走 XWayland,所以 xwininfo 看得到)
xwininfo -root -tree | grep Steam
# 0x2400039 "Sign in to Steam": ("steamwebhelper" "steam") 700x440+963+516
# 界面进程是否起齐(steamwebhelper 会派生若干 zygote)
pgrep -af steamwebhelper | wc -l
窗口标题里有 Sign in to Steam、进程数正常,就可以认为起来了。
九、三个尾巴
临时救急与长期方案。常备代理最省事;package/ 预置法介于两者之间——一次操作、之后免挂代理;/etc/hosts 钉 IP 只适合临时救急。
为什么 hosts 只能救急。往 /etc/hosts 写快节点 IP 确实有效——我带 --resolve 验证过:TLS 校验通过、文件完整下完。但 Akamai 的边缘 IP 会轮换,二十分钟内两次解析 client-update.akamai.steamstatic.com,拿到的是 23.221.24.201 和 23.220.255.48 两个不同地址。钉死的地址迟早失效。
排查时的一个操作陷阱。pkill -f steam 会匹配到你自己的命令行——如果你的命令里含有 “steam” 这个词,它会把自己一起杀掉。我这次连续踩了两次,每次都以 shell 被 SIGTERM/SIGKILL 收场。规避办法是在正则里加个字符类:
pgrep -f "ubuntu12_32/stea[m]" # 匹配目标进程,但不会匹配到你这条命令本身
十、复盘:七条判断
回头看不复杂,但顺序很重要:
- 先确认它到底在不在跑,别急着清锁文件
- 把进度行滤掉,只读日志骨架——刷屏的进度条是噪音
- 区分”慢”和”失败重试”,计数回退就是后者
- 错误信息里的数字和类型名值得逐字读——
(0 : 200)、ELFCLASS64都直接指向了根因 - 验证要靠工具而不是靠猜——
ldd ... | grep "not found"比任何包列表都可信 - 故障往往不止一层,修好第一层别以为完事了
- 追到”为什么它没自动做好”,通常比”手工做好”更有价值——我这次真正的根因藏在一个被
trying to continue anyway吞掉的错误里
一个静默降级的失败(continue anyway)比一个响亮的崩溃难查得多。

