☰
AnyPS5 异常处理实录:Guest 信号与 Windows SEH 的完整对接实现
2026/10/8 9:41:53 网站建设 项目流程

AnyPS5 异常处理实录:Guest 信号与 Windows SEH 的完整对接实现

【免费下载链接】AnyPS5Tool for automatic PS5 executables porting to Linux and Windows项目地址: https://gitcode.com/GitHub_Trending/an/AnyPS5

AnyPS5 是一款将 PS5 可执行文件自动移植到 Linux 和 Windows 的工具。移植后程序"跑得起来"只是第一步,更关键的是:当客端程序(游戏)出错时,宿主机的异常机制必须接得住、报得清、还能继续跑。这篇文章带你完整看懂 AnyPS5 中 Guest 信号、Windows SEH 与 C++ 异常运行时三套机制是如何协同工作的。

为什么移植后的程序会"死法不同"?

PS5 游戏在 PlayStation 系统上通过POSIX 信号(SIGSEGV、SIGINT 等)感知和响应错误;而 Windows 宿主机上,硬件故障走的是SEH(结构化异常处理)通道,Linux 宿主机则沿用标准信号。同一个"访问非法内存"的指令,在三种系统里表现为三种不同的事件。

AnyPS5 的解法可以概括为三层:

层级职责核心文件
Guest 信号层把客端信号号翻译成宿主机信号并调度回调Signals.cpp
Windows SEH 层拦截致命故障,输出诊断报告或就地修复CrashReport.cpp
C++ 异常运行时完整实现 Itanium ABI 的 try/catch 展开Runtime.cpp

Guest 信号:如何把 POSIX 信号桥接到宿主机

客端游戏调用signal(15, handler)注册的信号号是 PS5 的编号。AnyPS5 在 Signals.cpp 中维护了一张映射表,把客端信号号翻译成宿主机原生信号:

  • 2 → SIGINT(中断)
  • 4 → SIGILL(非法指令)
  • 6 → SIGABRT(中止)
  • 8 → SIGFPE(浮点错误)
  • 11 → SIGSEGV(段错误)
  • 15 → SIGTERM(终止)

客端调用signal_nid_postfix时,系统会把宿主机信号绑定到一个Dispatch分发器;当真实信号到达时,分发器反向查表得到客端信号号,检查是否被sigprocmask屏蔽,然后调用客端注册的处理函数。

这里有一个很巧妙的细节:在 Windows 上,CRT 每次投递信号后会重置处理器,因此分发器会在每次触发时重新注册自己(源码注释:"Preserve the guest's persistent registration across CRT delivery"),保证客端注册的信号处理"常驻有效",而不是只活一次。

对应的验证测试见 GuestSignals.cpp——它完整覆盖了注册、raise、忽略、屏蔽掩码查询与恢复等所有路径,包括错误信号号返回errno=22的标准行为。

Windows SEH 对接:让致命故障变成一份"事故报告"

Windows 上最有价值的对接在 CrashReport.cpp。程序启动时通过AddVectoredExceptionHandler注册一个向量化异常处理器,所有硬件故障都会先流经这里,处理流程是一条"拦截流水线":

  1. 客端内存写入修复:如果是写访问违例且命中客端内存区域,直接放行继续执行;
  2. 写监视点(Watchpoint):开发调试用,把某个 8 字节地址的页设为只读,任何写入都会触发单步陷阱,报告"是谁、从哪条指令、写了什么值"——专治"这个数据为什么被改"类灵异问题;
  3. SSE4a 指令仿真:PS5 的 Zen 2 芯片有 AMD 专属的 EXTRQ/INSERTQ 指令,游戏真的会用它们。在 Intel 宿主上这些指令会抛出"非法指令"异常,处理器就地在故障线程的 CONTEXT 上仿真执行,游戏完全无感知;
  4. 致命故障报告:若以上都不匹配且属于致命错误,则打印一份完整事故报告后终止。

这份报告的信息密度非常高:

  • 全部 15 个通用寄存器的值,以及每个寄存器所指向的 8 个字(方便识别坏指针来自哪个结构体);
  • 帧指针链回溯出的调用栈,每一帧都解析成"模块名+偏移";
  • 栈上可执行地址的返回地址候选——即使栈帧已被破坏,也能给出"最近刚返回过哪些地址";
  • 进程内所有其他线程的现场快照(逐一挂起、抓取 CONTEXT、恢复)。

此外还提供了几个开关型环境变量(详见下文速查表),比如挂死看门狗:程序卡死指定秒数后主动 dump 全部线程,再触发 abort。

💡 报告刻意绕过 CRT 的 stderr 流锁,直接WriteFile到句柄——因为故障线程自己可能正持有这个锁(比如在 printf 内部崩的),否则报告代码会再次死锁。

C++ 异常运行时:让移植游戏的 try/catch 照常工作

大量 PS5 游戏使用 C++ 异常。AnyPS5 在 core/libs/prx/libc/src/exception/ 下完整实现了一套Itanium ABI 异常运行时:

  • __cxa_throw与展开:抛异常后沿栈逐帧搜索,由 personality 例程(Personality.cpp 中的__gxx_personality_v0)解析 LSDA 展开表,匹配到 catch 后跳转落地;
  • 类型匹配:不只是简单相等判断,而是遍历单继承/多继承基类链和指针类型链(含 const/volatile 修饰),处理菱形继承偏移调整——catch (const Base&)能否接住throw Derived在这里被精确裁决;
  • exception_ptr跨线程传递:ExceptionPointer.cpp 通过引用计数实现异常对象在多个线程间 rethrow,析构恰好一次;
  • terminate 兜底:未捕获异常会走 Runtime.cpp 中的DefaultTerminate,打印"terminate called after throwing an instance of '类型名' + what()",类型名还会做 C++ demangle,最后 abort。

跨 DLL 抛异常(客端代码在移植后的 EXE/DLL 中,宿主侧捕获)同样是受支持的——这正是移植场景下的常态。

验证一切正常:两套针对性异常测试

  • WindowsExceptions.cpp:在 Windows 目标上跑通typed catch、基类 catch、catch-all、rethrow、析构顺序、跨 DLL 异常六项断言,外加 RTTI 类型表与exception_ptr四线程 rethrow 测试;
  • GuestSignals.cpp:覆盖信号注册/忽略/屏蔽的完整状态机;
  • 运行器 run_exception_tests.py 负责编排这些用例的执行。

实用环境变量速查表 🛠️

环境变量作用
APS5_WATCH_WRITE=<ELF地址>监视客端数据写入,报告每次写操作来源
APS5_WATCH_ADDR=<绝对地址>监视后映射区域的地址(后台线程等待页提交后布防)
APS5_NO_SSE4A_EMULATION=1关闭 SSE4a 仿真,让故障按致命错误报告
APS5_TRACE_SSE4A=1逐条打印被仿真的 extrq/insertq 指令
APS5_HANG_DUMP_SECS=<秒>挂死看门狗:超时后 dump 全线程并 abort
APS5_TRACE_EXIT进程退出时打印静态析构栈

总结

AnyPS5 的异常处理体现了移植工具的核心功力:不是让 PS5 的异常体系在宿主机上原样存在,而是为每一层故障都找到宿主的等价物,并保留客端视角的语义。Guest 信号层维持了 POSIX 语义的一致性,SEH 层把 Windows 的硬件故障变成了可读的诊断(甚至就地修复 SSE4a 这类平台差异),C++ 运行时层则保证了游戏逻辑层的 try/catch 行为与原版一致。对想理解"跨平台异常桥接"如何落地的读者,这三个目录(libkernel/System/src/Signals.cpp、libc/src/specifics/windows/CrashReport.cpp、libc/src/exception/)是绝佳的阅读起点。

【免费下载链接】AnyPS5Tool for automatic PS5 executables porting to Linux and Windows项目地址: https://gitcode.com/GitHub_Trending/an/AnyPS5

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询