LOADING

加载过慢请开启缓存 浏览器默认开启

Steam 在 niri (xwayland-satellite) 下的崩溃修复指南

前言/废话

记录一下 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.solibXrandr.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 日志

参考