Skip to content

feat(libc): unwind guest frames and keep the host's exception runtime in lifted builds - #10

Open
oberonlift wants to merge 3 commits into
lifted-build-switchfrom
lifted-libc-exceptions
Open

oberonlift wants to merge 3 commits into
lifted-build-switchfrom
lifted-libc-exceptions

Conversation

@oberonlift

@oberonlift oberonlift commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Open on AnyPS5 as boykopovar#15800; this copy follows it.

ps5lift is a static recompiler for PS5 executables: it lifts a game's x86-64 code to LLVM IR and compiles it ahead of time to aarch64, which runs on Linux against AnyPS5's own libraries with no interpreter or JIT. It runs AnyPS5's PS5-style test programs and a homebrew media player to its home screen; commercial games are not playable yet. Branch ps5lift-preview merges all of its PRs (how to try it); native Apple Silicon support is coming along fast. It is a separate approach from the ARM64 compiler in boykopovar#5021; boykopovar#1835 has an introduction.

This is 4 of 8 PRs that let AnyPS5's libraries run a lifted program; the lifter itself follows them.

x86-64: unchanged: the code compiles to the same objects. The new host_exception_pointer test builds and runs in every build; in CI's lifted build it runs from the last PR of the series on, since libc first compiles for a lifted guest there.

What

  • The DWARF unwinder starts from the guest frame that called the import, finds frames through the unwind tables of the guest images, resolves the personality routine from its import name and installs a landing pad by handing its registers back to the guest CPU, through new GuestCpu functions.
  • In a lifted build ElfTypes.hpp redirects dl_iterate_phdr to the runtime's GuestCpuIterateImages, which lists the guest images. libc and libkernel include ElfTypes.hpp instead of <link.h> where they walk images (the unwinder, the application heap's process parameters, direct memory, module info and the AGC driver's guest memory), so each of them sees the guest images.
  • Lifted builds don't export the host ABI's exception names: Exports.cpp's __cxa_* and _Unwind_* aliases and Personality.cpp's global __gxx_personality_v0. A lifted executable loads libc before libstdc++ and libgcc_s, so the host's own throw bound to libc's versions and aborted.
  • A lifted build keeps the host's C++ runtime, but with libstdc++ libc still defined std::current_exception, std::rethrow_exception and exception_ptr's members over its own runtime. Host code that caught an exception then got a null exception_ptr: the AGC driver reported a failed compute dispatch as "null asynchronous failure" and terminated. Lifted builds now give the guest its own exception pointer type, as libc++ builds do, and leave the host's exception pointers to the host runtime; HostExceptionPointer tests it. The guest's _Result_base destructor export releases the guest pointer in place.
  • The libc debt notes record that a lifted build calls only AnyPS5's personality routines and does not unwind through native frames.

boykopovar#5021 also changes some of the same files: GuestMemory.cpp, GuestAllocations.cpp, DirectMemory.cpp, ModuleInfo.cpp.

Tested

Compiled for x86-64 Linux before and after this PR (GCC 13, same flags): all 217 objects it reaches are byte-identical; the new host_exception_pointer test builds after it only.

On a DGX Spark (aarch64 Linux, NVIDIA GB10, LLVM 18), with all eight PRs and ps5lift on top: every library builds for a lifted program, 2,288 of AnyPS5's own tests pass in that build on lavapipe, and ps5lift's 69 tests pass, among them its libc, exceptions, kernel, fiber and module-loading fixtures and upstream's libc test programs.

The conventions check passes on this branch.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: feat(build): add ANYPS5_GUEST_LIFTED to build the libraries for ps5lift #9
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

🤖 Generated with Claude Code

@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch 3 times, most recently from d91f0c5 to 7ba4609 Compare November 2, 2026 17:02
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 7ba4609 to f98143b Compare November 2, 2026 18:30
@oberonlift
oberonlift force-pushed the lifted-build-switch branch 2 times, most recently from 7127627 to dcc7de1 Compare November 2, 2026 22:07
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch 2 times, most recently from 60cb2c7 to 6a4e3b7 Compare November 3, 2026 16:34
@oberonlift

Copy link
Copy Markdown
Owner Author

Review findings (unverified)

Code review of 6a4e3b71fd. None of these have been built, run or checked by a second reviewer yet. Most severe first.

  1. core/libs/prx/libc/src/exception/Unwind.cpp:760: host exceptions can be unwound through guest frames. In lifted builds _Unwind_Resume_nid_postfix no longer hands foreign (host-runtime) exceptions to the host's _Unwind_Resume, yet Exports.cpp:31 still exports libc's version under the global name _Unwind_Resume on Linux. A host landing-pad cleanup that binds to the alias then starts unwinding at the last guest import frame through GuestCpuUnwindStart. Host destructors are skipped, and a guest landing pad may be installed for a host exception, or std::abort runs.
  2. core/libs/prx/libc/src/exception/ExceptionPointer.cpp:4: the null current_exception can still happen. Lifted builds stop defining std::current_exception, std::rethrow_exception and the exception_ptr members, but Exports.cpp still exports __cxa_throw, __cxa_begin_catch, __cxa_end_catch, __cxa_rethrow, _Unwind_RaiseException and others globally. Where host code binds to libc's __cxa_begin_catch (the global scope lists libc.prx before libstdc++), the caught exception lands in LibcException::globals, and libstdc++'s current_exception returns null. Gating every host-ABI alias in Exports.cpp behind !ANYPS5_GUEST_LIFTED would fix both.
  3. core/libs/prx/libc/include/specifics/linux/ElfTypes.hpp:9: #define dl_iterate_phdr GuestCpuIterateImages_nid_no_patch only reaches files that include this header. libSceAgcDriver/Execution/src/GuestMemory.cpp includes <link.h> directly, so mainImageExtent still caches the host executable as the main image, while libc's guest-image registry sees guest images.
  4. core/libs/prx/libc/src/exception/Unwind.cpp:735: any host frame between the throw and the handler ends the search. The lifted unwind finds frames only through guest unwind tables. When a guest callback called from a host import (a qsort comparator, a call_once body, a kernel callback) throws, phase 1 reaches the host trampoline and returns _URC_END_OF_STACK, so the program terminates. If a handler is found below a host frame, GuestCpuUnwindResume jumps past it, and its destructors and lock releases never run.
  5. core/libs/prx/libc/include/GuestCpu.hpp:19: uint64_t* vs uintptr_t*. GuestCpuUnwindStart_nid_no_patch and GuestCpuUnwindResume_nid_no_patch take std::uint64_t*, but every caller passes _Unwind_Context::registers (std::uintptr_t[17]). These differ on Apple arm64, and the lifted branches sit inside #if defined(__linux__) || defined(_WIN32) || defined(__APPLE__), so a lifted macOS build does not compile.
  6. core/libs/prx/libc/src/exception/Unwind.cpp:151: an unresolved personality fails silently. When GuestCpuImportName returns null or an unknown name, ResolveImport returns the guest address unchanged, and CallPersonality returns _URC_FATAL_PHASE1/2_ERROR without naming the personality. Failing with NotImplemented_nid_no_patch and the import or address would say why.
  7. core/libs/prx/libc/src/exception/Unwind.cpp:153: ResolveImport repeats MatchCompactPersonality. It matches __gxx_personality_v0 and __gcc_personality_v0 by name, as exceptions/CompactUnwind.hpp already does, then turns the match into an address only to compare it again. Extending MatchCompactPersonality with the NIDs XwLA5cTHjt4 and FW2QeoYK7-c and dispatching on the kind, as the __APPLE__ branch does, would remove the second table.
  8. core/libs/prx/libc/src/exception/Unwind.cpp:827: the start-of-unwind split appears four times. RaiseException, Resume, ForcedUnwind and Backtrace each repeat #ifdef ANYPS5_GUEST_LIFTED GuestCpuUnwindStart #else LibcCaptureRegisters + Step #endif. A bool StartContext(_Unwind_Context&) helper would leave each caller with only its failure action.
  9. core/libs/prx/libc/include/specifics/linux/ElfTypes.hpp:8: GuestCpuIterateImages_nid_no_patch is declared here. The other runtime entry points are declared in GuestCpu.hpp's extern "C" block. The header also gains an unrelated <cstddef>.
  10. core/libs/prx/libc/src/exception/Unwind.cpp:7: GuestCpu.hpp is included unconditionally. Only lifted code uses it. In other builds it pulls <x86intrin.h> into Exports.cpp, and would break a non-x86 build that is not lifted. Put it under #ifdef ANYPS5_GUEST_LIFTED.

@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 6a4e3b7 to f0a368b Compare November 4, 2026 15:40
@oberonlift oberonlift changed the title feat(libc): unwind guest frames and keep host exception pointers in lifted builds feat(libc): unwind guest frames and keep the host's exception runtime in lifted builds Nov 4, 2026
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from f0a368b to 23da0d9 Compare November 4, 2026 16:20
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 23da0d9 to ba34115 Compare November 4, 2026 20:18
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from ba34115 to 56040c8 Compare November 5, 2026 01:21
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 56040c8 to 09961d7 Compare November 5, 2026 15:34
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch 2 times, most recently from 1da9fef to 2fa99de Compare November 6, 2026 00:18
@oberonlift
oberonlift force-pushed the lifted-build-switch branch 2 times, most recently from 1988618 to c7d8a9b Compare November 6, 2026 15:35
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 2fa99de to 98e0f9a Compare November 6, 2026 15:35
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 98e0f9a to 44e9092 Compare November 6, 2026 17:39
oberonlift and others added 3 commits November 6, 2026 20:42
In a lifted build the DWARF unwinder starts from the guest frame that
called the import, finds frames through the unwind tables of the guest
images, which dl_iterate_phdr lists through the program's
GuestCpuIterateImages, resolves the personality routine from its import
name and installs a landing pad by handing its registers back to the
guest CPU, through new GuestCpu functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In an ANYPS5_GUEST_LIFTED build the guest images are not host shared
objects, so glibc's dl_iterate_phdr cannot see them: the application heap
never found the executable's process parameters. Every caller now
includes ElfTypes.hpp instead of <link.h>, which in a lifted build lists
the program's guest images through GuestCpuIterateImages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lifted build keeps the host's C++ runtime, but with libstdc++ the libc
still defined std::current_exception, std::rethrow_exception and
exception_ptr's members over its own runtime. Host code that caught an
exception then got a null exception_ptr from the runtime that had not
thrown it: the AGC driver reported a failed compute dispatch as "null
asynchronous failure" and terminated. Lifted builds now give the guest
its own exception pointer type, as libc++ builds do, and leave the
host's exception pointers to the host runtime. The guest's
_Result_base destructor export releases the guest pointer in place.

Lifted builds also stop exporting the host ABI's names: Exports.cpp's
__cxa_* and _Unwind_* aliases and Personality.cpp's global
__gxx_personality_v0. A lifted executable loads libc before libstdc++
and libgcc_s, so the host's own throws bound to libc's versions and
aborted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 44e9092 to 41c7aed Compare November 6, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant