前言/废话
记录一下 Steam 在 niri + xwayland-satellite + NVIDIA 环境下从无法启动到恢复正常运行的完整修复过程, 包括 GDK 补丁、NVIDIA 驱动版本对齐、以及 Proton 游戏闪退排查等内容
环境
| 项目 | 值 |
|---|---|
| 操作系统 | Arch Linux (rolling) |
| 合成器 | niri (scrollable-tiling Wayland compositor) |
| X11 兼容层 | xwayland-satellite |
| glibc 版本 | 2.43 |
| 显示服务 | :0 (通过 xwayland-satellite 提供的 X11 DISPLAY) |
Steam 启动崩溃修复
Steam 启动后立即崩溃(SIGSEGV),从 GDB 回溯和 Breakpad minidump 分析发现多个崩溃点
| # | 问题 | 类型 |
|---|---|---|
| 1 | GDK Xinerama 崩溃 | SIGSEGV |
| 2 | GDK XRANDR 崩溃 | SIGSEGV |
| 3 | JIT/CEF NULL 字符串崩溃 | SIGSEGV |
| 4 | nvidia_drm.modeset 未启用 | 配置错误 |
| 5 | 32 位 NVIDIA 驱动版本不匹配 | 配置错误 |
GDK Xinerama 崩溃
症状: Steam 首次启动时立即段错误
根源: xwayland-satellite 不支持 XINERAMA 扩展。GDK 检查 Xinerama 时向 NULL 指针写入标志位
修复: NOP 掉 Steam 捆绑的 GDK 二进制文件中写入 movl $0x1,0x22c(%eax) 的指令
# 备份原始文件
cp /home/lumorian/.local/share/Steam/ubuntu12_32/steam-runtime/usr/lib/i386-linux-gnu/libgdk-x11-2.0.so.0.2400.10 \
/home/lumorian/.local/share/Steam/ubuntu12_32/steam-runtime/usr/lib/i386-linux-gnu/libgdk-x11-2.0.so.0.backup
# 文件偏移 0x627cc: 将 c7 80 2c 02 00 00 01 00 00 00 替换为 10 个 NOP
python3 -c "
data = bytearray(open('libgdk-x11-2.0.so.0.2400.10', 'rb').read())
data[0x627cc:0x627d6] = b'\x90' * 10
open('libgdk-x11-2.0.so.0.2400.10', 'wb').write(data)
"
GDK XRANDR 崩溃
症状: 修复 Xinerama 崩溃后 Steam 仍然崩溃, 错误信息包含 XRRGetOutputInfo Workaround: initialized with override: 0 real: (nil)
根源: xwayland-satellite 的 XRANDR 支持不完整, XRRGetOutputInfo() 返回 NULL
修复: NOP 掉跳转到 XRANDR 路径的条件跳转指令, 同时预加载系统 libXrandr
# 文件偏移 0x62737: 将 0f 85 0b 01 00 00 (jne) 替换为 6 个 NOP
python3 -c "
data = bytearray(open('libgdk-x11-2.0.so.0.2400.10', 'rb').read())
data[0x62737:0x6273d] = b'\x90' * 6
open('libgdk-x11-2.0.so.0.2400.10', 'wb').write(data)
"
启动时预加载系统 libXrandr
export LD_PRELOAD="/usr/lib32/libXrandr.so.2"
JIT/CEF NULL 字符串崩溃
症状: 修复 GDK 崩溃后 Steam 进程存活时间更长但仍然崩溃, 回溯指向 JIT 编译的代码中 strlen(NULL) 调用
根源: glibc 2.43 的 strlen(), strcmp(), strstr(), strchr() 使用 IFUNC 机制选择 SSE2/AVX2 优化版本, 不处理 NULL 输入
修复: 创建 LD_PRELOAD 库拦截不安全的字符串函数
// nullsafe.c — NULL 安全字符串拦截器
#define _GNU_SOURCE
#include <stddef.h>
#include <dlfcn.h>
extern void *dlsym(void *, const char *);
#define RTLD_NEXT ((void *)-1)
__attribute__((used))
size_t strlen(const char *s) {
static size_t (*real_fn)(const char*) = NULL;
if (!real_fn) real_fn = (size_t (*)(const char*))dlsym(RTLD_NEXT, "strlen");
if (!s) return 0;
return real_fn(s);
}
__attribute__((used))
int strcmp(const char *s1, const char *s2) {
static int (*real_fn)(const char*, const char*) = NULL;
if (!real_fn) real_fn = (int (*)(const char*, const char*))dlsym(RTLD_NEXT, "strcmp");
if (!s1 && !s2) return 0;
if (!s1) return -1;
if (!s2) return 1;
return real_fn(s1, s2);
}
__attribute__((used))
char *strstr(const char *haystack, const char *needle) {
static char *(*real_fn)(const char*, const char*) = NULL;
if (!real_fn) real_fn = (char *(*)(const char*, const char*))dlsym(RTLD_NEXT, "strstr");
if (!haystack) return NULL;
if (!needle) return (char*)haystack;
return real_fn(haystack, needle);
}
__attribute__((used))
char *strchr(const char *s, int c) {
static char *(*real_fn)(const char*, int) = NULL;
if (!real_fn) real_fn = (char *(*)(const char*, int))dlsym(RTLD_NEXT, "strchr");
if (!s) return NULL;
return real_fn(s, c);
}
编译 (需要注意: 不能包含 <string.h>, 因为 glibc 2.43 使用 _Generic 宏与函数定义冲突)
gcc -m32 -shared -fPIC -o /tmp/nullsafe.so /tmp/nullsafe.c -ldl -O2 -fno-builtin
# 安装到永久路径
cp /tmp/nullsafe.so /home/lumorian/.local/lib/nullsafe.so
nvidia_drm.modeset 未启用
症状: glxinfo 显示 direct rendering: Yes,但 32 位程序无法创建 GLX 直接上下文
根源: GRUB 内核参数 nvidia-drm-modeset=1 格式错误。正确格式应为 nvidia-drm.modeset=1
# 检查当前状态
cat /sys/module/nvidia_drm/parameters/modeset
# 应为 Y, 若为空则未启用
# 修复 GRUB 参数
sudo sed -i 's/nvidia-drm-modeset=1/nvidia-drm.modeset=1/' /etc/default/grub
sudo grub-mkconfig -o /boot/grub/grub.cfg
# 重启生效
32 位 NVIDIA 驱动版本不匹配
症状: nvidia_drm.modeset=Y 后 32 位 GLX 仍然返回 NULL
根源: 32 位 NVIDIA 用户态库被 paru 升级到 580.159.03, 而内核模块和 64 位库为 580.142, 版本不一致
# 检查版本
pacman -Q nvidia-580xx-utils
pacman -Q lib32-nvidia-580xx-utils
# 降级 32 位库
sudo pacman -U /home/lumorian/.cache/paru/clone/lib32-nvidia-580xx-utils/lib32-nvidia-580xx-utils-580.142-1-x86_64.pkg.tar.zst
# 验证
ls -la /usr/lib32/libGLX_nvidia.so.0
Proton 游戏闪退修复
背景
在修复完 Steam 本身的崩溃和 GLX 渲染问题后, Steam 商店和库功能正常, 但运行特定 Windows 游戏时仍然闪退
症状
游戏”星空列车与白的旅行”(Steam AppID 1567800)启动后窗口一闪而过, 约 3-5 秒后进程全部退出
排查过程
检查游戏类型
游戏文件包含 UnityPlayer.dll, GameAssembly.dll, 确定是 32 位 Unity IL2CPP 游戏, 需通过 Proton 运行
查看游戏日志
从 Proton 兼容层目录找到 Unity Player 日志:
cat "/mnt/data/SteamLibrary/steamapps/compatdata/1567800/pfx/drive_c/users/steamuser/AppData/LocalLow/Syawase Works/星空列车与白的旅行/Player.log"
日志显示游戏启动正常, D3D11 设备创建成功, Steam API 初始化完成, 然后:
AspectRatioController:wndProc(IntPtr, UInt32, IntPtr, IntPtr)
ApplicationWantsToQuit: False
StartCoroutine -> DelayedQuit
ApplicationWantsToQuit: True
游戏使用了 DenchiSoft 的 UnityAspectRatioController 组件, 它通过 WinAPI (SetWindowLong + WindowProc) 挂钩窗口消息来锁定宽高比。该组件在说明中明确写了仅支持 Windows。在 Proton/Wine 下, WinAPI 窗口钩子的行为与 Windows 不同, 导致组件在初始化时收到意外窗口消息后触发退出
交叉测试
试了以下方案均无效:
DISABLE_GAMESCOPE=1(排除 gamescope 干扰)-screen-width 1920 -screen-height 1080 -screen-fullscreen 1(Unity 强制分辨率)-popupwindow(强制窗口模式)
导入 Proton 日志
PROTON_LOG=1 %command% # 生成 ~/steam-1567800.log
日志发现 nullsafe.so 和 libXrandr.so.2 的 LD_PRELOAD 泄漏到了 Proton 进程环境中:
ERROR: ld.so: object '/home/lumorian/.local/lib/nullsafe.so' from LD_PRELOAD cannot be preloaded (wrong ELF class: ELFCLASS32): ignored.
nullsafe.so 是 32 位库, 被 Proton 的 32 位 Wine 内部进程加载后干扰了窗口消息的正常处理, 间接导致 AspectRatioController 异常触发退出
解决方案
在 Steam 启动选项中使用以下命令清除继承的 LD_PRELOAD
LD_PRELOAD= %command%
这个命令仅对该游戏生效, 不会影响 Steam 主进程的稳定性
移植到其他游戏的注意点
如果你的 steam-fixed.sh 中设置了全局 LD_PRELOAD, 任何通过 Proton 运行的 Windows 游戏都可能受到干扰。建议每个游戏单独在启动选项添加:
LD_PRELOAD= %command%
完整启动命令
killall -9 steam 2>/dev/null; sleep 1
export LANG=C STEAM_RUNTIME=1 DISPLAY=:0
export LD_PRELOAD="/home/lumorian/.local/lib/nullsafe.so /usr/lib32/libXrandr.so.2"
/home/lumorian/.local/share/Steam/steam.sh -no-cef-sandbox
或使用包装脚本 (完整内容如下)
#!/bin/bash
# Steam 启动包装脚本 — 修复 niri/xwayland-satellite 下的崩溃问题
# 用法: ./steam-fixed.sh [额外参数]
# 永久安装 nullsafe.so:
# sudo cp /home/lumorian/.local/lib/nullsafe.so /usr/local/lib/nullsafe.so
# 然后改脚本中的 NULLSAFE_PATH
NULLSAFE_PATH="/home/lumorian/.local/lib/nullsafe.so"
# NULLSAFE_PATH="/usr/local/lib/nullsafe.so" # 如果用 sudo 安装后取消注释
# 杀掉残留进程
killall -9 steam 2>/dev/null
# 导出环境变量
export LANG=C
export STEAM_RUNTIME=1
export DISPLAY=:0
export LD_PRELOAD="${NULLSAFE_PATH} /usr/lib32/libXrandr.so.2"
# 启动 Steam
exec /home/lumorian/.local/share/Steam/steam.sh "$@"
使用方式:
~/temp/steam-fixed.sh -no-cef-sandbox
Proton 游戏需要在 Steam 启动选项中添加:
LD_PRELOAD= %command%
验证
| 检查项 | 结果 |
|---|---|
| Steam 主进程存活 | ✓ |
| 无新的崩溃转储 | ✓ |
| CEF Web 辅助进程运行 | ✓ |
| 32-bit GLX direct context | ✓ |
| nvidia_drm.modeset | Y |
| 星空列车与白的旅行 Proton 运行 | ✓ |
预防措施
防止 NVIDIA 版本再次不匹配, 锁定 32 位 NVIDIA 包版本
# 编辑 /etc/pacman.conf, 在 [options] 下添加
IgnorePkg = lib32-nvidia-580xx-utils lib32-opencl-nvidia-580xx
关键文件路径
| 文件 | 用途 |
|---|---|
/home/lumorian/.local/lib/nullsafe.so |
NULL 安全字符串拦截器 |
/home/lumorian/temp/steam-fixed.sh |
Steam 启动包装脚本 |
/home/lumorian/.local/share/Steam/ubuntu12_32/steam-runtime/usr/lib/i386-linux-gnu/libgdk-x11-2.0.so.0.2400.10 |
补丁后的 GDK 库 |
/home/lumorian/.local/share/Steam/ubuntu12_32/steam-runtime/usr/lib/i386-linux-gnu/libgdk-x11-2.0.so.0.backup |
原始 GDK 库备份 |
/etc/default/grub |
GRUB 配置 (含 nvidia-drm.modeset=1) |
/mnt/data/SteamLibrary/steamapps/compatdata/1567800/pfx/drive_c/users/steamuser/AppData/LocalLow/Syawase Works/星空列车与白的旅行/Player.log |
游戏 Unity Player 日志 |