Skip to content

fix(libkernel): report guest unwind tables in sceKernelGetModuleInfoForUnwind - #904

Merged
adriadam10 merged 3 commits into
boykopovar:mainfrom
merwie:fix/kernel-module-info-for-unwind
Oct 8, 2026
Merged

adriadam10 merged 3 commits into
boykopovar:mainfrom
merwie:fix/kernel-module-info-for-unwind

Conversation

@merwie

@merwie merwie commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What

On Windows, sceKernelGetModuleInfoForUnwind reported no unwind tables for any address (eh_frame_hdr_addr, eh_frame_addr and eh_frame_size all 0). A title that ships its own libc.prx unwinds C++ exceptions with that libc's libunwind, which asks this call where a frame's FDEs are. With the empty answer it finds nothing, and the first C++ exception thrown in guest code ends in terminate.

PPSA12544 (Hollow Knight: Silksong, Unity/IL2CPP) died that way right after its intro cinematic:

Terminating due to uncaught exception of type Il2CppExceptionWrapper

The exception is a System.Reflection.AmbiguousMatchException that Unity's invoke wrapper in the executable normally catches and logs; the search phase just never reached it.

For an address in a relinked guest image, the call now returns:

  • eh_frame_hdr_addr: the PT_GNU_EH_FRAME segment the relinker names in .ehmeta (the same section the emulator's own libc unwinder reads);
  • eh_frame_addr: decoded from the header's eh_frame_ptr;
  • eh_frame_size: found by walking the records to the zero terminator, not counting the terminator;
  • seg0: the whole image.

Once an image has .ehmeta, every other outcome throws instead of giving the empty answer:

  • an eh_frame_hdr version other than 1;
  • an eh_frame_ptr encoding other than absptr or pcrel (datarel, funcrel, aligned and indirect throw, as do the omit encoding and unknown value formats);
  • a record with a 64-bit length (0xffffffff);
  • an eh_frame_ptr outside the image, or no terminator inside it.

A MEM_IMAGE without DOS/NT signatures also throws. Host images, which have no .ehmeta, keep the empty answer. Linux is unchanged.

eh_frame_size and the terminator: I followed LLVM libunwind, which Sony's libunwind derives from. CFI_Parser::findFDE scans from the start of .eh_frame until either the given length runs out or it reads a zero length. So a size with or without the terminator finds the same FDEs. For a PT_GNU_EH_FRAME found through dl_iterate_phdr, LLVM passes SIZE_MAX and relies on the terminator alone. What the console's kernel reports is not public, so TechnicalDebt records the choice.

The guest_unwind_module_info test (Windows) uses a small fixture DLL whose .ehmeta points at an eh_frame_hdr with one CIE. It checks:

  • every field for an address inside the fixture;
  • a throw for version 2, for the eh_frame_ptr encodings 0x3b (datarel), 0x9b (indirect), 0x0f (unknown format) and 0xff (omit), and for a 64-bit length record;
  • the empty answer for an address in kernel32.

The fixture is a separate DLL because the emulator's unwinder reads the same .ehmeta: with a broken table in the test executable itself, the thrown exception could not be caught there. The test fails without the change.

Tested

  • Windows 11, PPSA12544: the title no longer terminates after the cinematic and reaches gameplay (bring-up: Hollow Knight: Silksong (PPSA12544) #726). The exception is caught.
  • The non-GPU tests pass (239).

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 in TechnicalDebt
  • 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: none
  • AI-assisted: yes

🤖 Generated with Claude Code

@mugurc mugurc mentioned this pull request Oct 6, 2026
9 tasks done
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed labels Oct 6, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks, the Silksong trace makes the cause clear, and the happy path looks right to me: the header comes from .ehmeta, eh_frame_ptr is decoded, the walk goes to the terminator, and seg0 is the whole image.

One point on exactness. Once an image has .ehmeta, every other outcome should throw rather than fall back to the empty answer, since an empty answer just moves the failure to a terminate later on:

  • FillGuestUnwindInfo returns silently when the header version isn't 1, when ReadEncoded doesn't know the eh_frame_ptr encoding (it returns 0), and when the DOS/NT signatures don't match for a MEM_IMAGE.
  • ReadEncoded applies only the pcrel bit (0x10). Any other application bits (datarel, funcrel, aligned, indirect) are ignored, so the value it returns would be wrong without any error. It should throw for anything other than absptr or pcrel.
  • The record walk stops at a 64-bit length (0xffffffff) and reports the size up to that point, which is a short eh_frame_size. Throwing there would be better too.

Images without .ehmeta (host DLLs) keeping the empty answer is fine.

Also, after the last batch this branch conflicts with main in core/libs/prx/libkernel/CMakeLists.txt, so please rebase onto main when you push the change.

@ps54all ps54all mentioned this pull request Oct 7, 2026
9 tasks done

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with Dean's points above (throw instead of the empty answer once .ehmeta exists, only absptr/pcrel in ReadEncoded, throw on the 64-bit length, rebase on main for libkernel/CMakeLists.txt). Two additions:

  • core/libs/tests/GuestUnwindModuleInfo.cpp: once those paths throw, each needs a case (version != 1, unsupported eh_frame_ptr encoding, 64-bit length record); today only the happy path and the host image are covered.
  • Module.cpp:58: eh_frame_size stops before the zero terminator (16 in the test). Please say which reference the libunwind consumer follows for this (with or without the terminator), in the PR description.

@ps54all

ps54all commented Oct 7, 2026

Copy link
Copy Markdown

Test report from a second machine, in case it helps review.

Setup: Windows 11, Intel i9-10850K, NVIDIA RTX 3080, WinLibs GCC 15.2.0. PPSA12544 v1.0.30021, relinked with --windows --to-intel --module-dir Media/Plugins.

Stack: 0d3c1eec plus #759, #760, #846, #842, #844, #839, #904 (f5cebd30), #788 (8710433f), #903, #518, #301, #1101, #864, #1120, and the POSIX rename from #1192 (this title imports it).

Result: the title boots past the Team Cherry logos to the main menu (Start Game / Options / Extras) and holds 60 FPS (vsync) there for several minutes, from a warm or a cold shader cache (cold: one dip while pipelines compile around 30 s). APS5_PROFILE_DRAW=1 at the menu shows about 1,790 draws/s, about 90-100 us of CPU per draw, and the GPU worker about 23% busy. I have not gone past the menu yet, so I can't confirm the post-cinematic path this PR fixes on my side; I'll report back when I do.

Small visual artifact at the menu: a white block covers the first letter of "Options". I have not looked into which draw produces it.

AI-assisted (Claude Code) testing and write-up.

@merwie

merwie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks both. Addressed in 732a8b5 and 6eed05c:

  • Throw instead of the empty answer once .ehmeta exists: version other than 1, an eh_frame_ptr outside the image, no terminator inside the image, and a MEM_IMAGE without DOS/NT signatures. Images without .ehmeta keep the empty answer.
  • ReadEncoded: only absptr and pcrel are accepted. Any other application bits, the indirect bit, the omit encoding and unknown value formats throw (NotImplemented_nid_no_patch).
  • 64-bit length: throws (NotImplemented_nid_no_patch).
  • Tests: version 2, the encodings 0x3b, 0x9b, 0x0f and 0xff, and a 64-bit length record each expect a throw, followed by a check that the restored table works again. The fake .ehmeta moved into a small fixture DLL. The emulator's unwinder reads the same section, so with a broken table in the test executable the exception could not be caught there. The fixture defines the section with a top-level asm declaration, since the new conventions check rejects __attribute__. Without the change the test fails.
  • eh_frame_size: it stops before the terminator. The reference is in the description: LLVM libunwind's CFI_Parser::findFDE stops at either the length or a zero length, whichever comes first, so both values find the same FDEs, and LLVM's own ELF path passes SIZE_MAX and relies on the terminator. The console's value is unknown, so TechnicalDebt has a line for it.
  • main: merged, which resolves the libkernel/CMakeLists.txt conflict. I haven't rebased yet; I'll follow up on that separately.

Non-GPU tests pass (239). tools/check_conventions.py reports one doc-link error for the new TechnicalDebt line when run on Windows only. os.path.normpath turns the link into a backslash path there, and the target exists, so it does not happen on Linux CI.

merwie and others added 3 commits October 8, 2026 00:52
…orUnwind

On Windows the call returned no eh_frame_hdr for every address, so a
title's own libc (its libunwind) found no frame information and
terminated on the first C++ exception thrown in guest code. PPSA12544
died that way after its intro cinematic: IL2CPP raised an
AmbiguousMatchException that Unity catches and logs, and the runtime
called terminate instead. For an address in a relinked guest image the
call now returns the PT_GNU_EH_FRAME segment named by .ehmeta, the
eh_frame it points to and its size, and the image as seg0; host images
keep the empty answer.

AI-assisted: yes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…InfoForUnwind

Once an image has .ehmeta, an eh_frame_hdr version other than 1, an
eh_frame_ptr encoding other than absptr or pcrel, a 64-bit record length,
an eh_frame_ptr outside the image or a missing terminator now throw
instead of returning the empty answer. The test queries a fixture DLL
whose .ehmeta it can break, since the unwinder reads the same section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tensions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@merwie
merwie force-pushed the fix/kernel-module-info-for-unwind branch from 6eed05c to 7f4f887 Compare October 7, 2026 22:58
@merwie
merwie requested a review from adriadam10 October 7, 2026 22:59
@merwie

merwie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (df16c4c) as requested; the merge commits are gone. The branch is three commits: the original fix, the throw paths with their tests, and the fixture without compiler extensions. Same changes as before the rebase. Non-GPU tests pass (239).

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 8, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the diff against Dean's and my earlier requests: throws once .ehmeta exists, only absptr/pcrel in ReadEncoded, 64-bit length throws, fixture tests for each, rebased, terminator choice recorded in TechnicalDebt. CI is green on this head and the Windows job runs guest_unwind_module_info (Passed). Windows-only code, Linux unchanged; merging.

@adriadam10
adriadam10 merged commit c6bb7c1 into boykopovar:main Oct 8, 2026
4 checks passed
@ps54all

ps54all commented Oct 10, 2026

Copy link
Copy Markdown

Follow-up as promised: PPSA12544 now gets past the intro cinematic into gameplay with this change on main.

Setup: Windows 11, Intel i9-10850K, NVIDIA RTX 3080, DualSense over SDL. main 6cc33583 plus a local integration branch (#846 for the runtime-loaded plugins, and a few open AGC PRs). Relinked with --windows --to-intel --module-dir Media/Plugins.

Result: the game was played from the menu through the cinematic into gameplay. The window-title counter read 59.92 FPS during gameplay (5847 frames at 135 s, including boot and loading), and it played smoothly. The log has no exceptions and no skipped draws. The gameplay was played by hand and only the counter was measured, with no profiling.

Written with AI assistance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants