Skip to content

fix(relinker): keep PT_GNU_EH_FRAME in the relinked executable - #1586

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
awake-devel:fix/relinker-keep-eh-frame-hdr
Oct 9, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
awake-devel:fix/relinker-keep-eh-frame-hdr

Conversation

@awake-devel

Copy link
Copy Markdown
Contributor

Investigated and written by Claude Code (AI), and posted by me after review.

What

The relinker dropped the PT_GNU_EH_FRAME program header from the Linux executable it writes. SegmentFilter skips every header in PT_LOOS..PT_HIOS (0x61000000..0x6fffffff) to remove the SCE-specific ones, and PT_GNU_EH_FRAME (0x6474e550) is in that range. The .eh_frame_hdr data itself stayed in the image, but nothing pointed to it any more.

Everything that finds unwind tables for the main executable looks for that header:

  • the project's libc unwinder (FindFrame in core/libs/prx/libc/src/exception/Unwind.cpp, through dl_iterate_phdr);
  • sceKernelGetModuleInfoFromAddr on Linux (ModuleInfo.cpp), which fills eh_frame_hdr_addr, eh_frame_addr and eh_frame_size from it.

Without it, a C++ exception thrown in the main executable cannot be caught: the unwinder finds no frame description and the process ends in std::terminate. Guest modules were not affected: GuestModuleWriter::WriteLinux already keeps PT_GNU_EH_FRAME.

The fix keeps PT_GNU_EH_FRAME in SegmentFilter::ShouldSkip, the same way PT_OS_PROCPARAM is kept. The header is copied unchanged and points to data already inside a kept PT_LOAD.

The relinker writes the new program headers into the input's own table, so keeping this header needs one more slot. ProgramHeaderLayoutBuilder::WriteLayout keeps it only when the whole output table, with the five headers it adds (PT_PHDR, two PT_LOAD, PT_DYNAMIC, PT_INTERP), still fits in the input's table. Otherwise it drops PT_GNU_EH_FRAME as before and prints a WARNING: line on stderr. An input that relinks today still relinks, with the same headers. (The existing "Not enough program header slots" check counts four added headers, kSyntheticProgramHeaderCount, while five are written; this PR leaves that check as it is.)

Windows is not affected: SegmentFilter is only used by LinuxElfPatcher. The PE path (WindowsPePatcher, WindowsGuestModuleWriter) reads PT_GNU_EH_FRAME straight from the input headers to build its unwind data.

This is one of the fixes needed for Hollow Knight Silksong on Linux; discussion #1519 has the full stack. Silksong ships its own guest libc, whose unwinder asks sceKernelGetModuleInfoForUnwind. That function also needs a Linux fix, which comes in a separate PR. The two are independent: this one alone fixes titles that use the project's libc, and the other one needs this header to find the executable's tables.

New test linux_eh_frame_header (core/relinker/relinker/tests/test_linux_eh_frame_header.py, same style as linux_load_alignment): it relinks a small ELF with a PT_GNU_EH_FRAME header for Linux and checks that the output has the same header, that the bytes it points to are unchanged, and that the SCE-specific headers are still removed. Two more cases give the input one or two slots fewer: they still relink as before, the output has no PT_GNU_EH_FRAME, and the warning is printed.

Tested

Arch Linux, i7-8750H, GTX 1050 Ti Max-Q (NVIDIA 580.178.04). Base: main at cba8d86, Release build with gcc, compared with an unmodified build of the same commit.

  • ctest -j4: 453 of 453 pass, 0 ***Failed, 0 ***Skipped (unmodified build: 452 of 452, 0 failed, 0 skipped). The only difference is the new test.
  • Validation layer (VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ctest -j2 -V): the same single VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 message (counted 2) as the unmodified build, no new VUIDs; 453 of 453 pass.
  • Mutation checks, each with the test kept and only the fix changed: without the SegmentFilter line, the test fails (no PT_GNU_EH_FRAME in the output); without the fallback, the 5-slot input fails to relink with "Not enough program header slots"; with the slot check written as <=, the 6-slot input writes 7 headers. With the fix restored, it passes.
  • Relinked both titles I have, Horizon Zero Dawn Remastered and Hollow Knight Silksong (eboot.bin, 14 program headers each, PT_GNU_EH_FRAME present), with the unmodified relinker and with this branch: both succeed both times. The output keeps PT_GNU_EH_FRAME with this branch (14 headers instead of 13), and every guest module is byte-identical (1 for Horizon, 7 for Silksong).
  • python3 tools/check_conventions.py --base cba8d86d: passes.
  • Hollow Knight Silksong (PPSA12544), together with the sceKernelGetModuleInfoForUnwind fix: before, the game ended right after the intro cinematic with Terminating due to uncaught exception of type Il2CppExceptionWrapper (an IL2CPP System.Reflection.AmbiguousMatchException that the game normally catches). With both fixes (on a local branch that also merges other open PRs), the cinematic starts at about t+112 s and finishes, and the game goes into gameplay, saves, and keeps running until the run was stopped at t+500 s. Stopping the run ends in a separate shutdown error that is already known.

Not built or run on Windows or macOS; the change does not touch the PE output.

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

https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8

The segment filter dropped every header in the PT_LOOS..PT_HIOS range,
which includes PT_GNU_EH_FRAME. The unwinders find the executable's
unwind tables through that header, so no C++ exception thrown in the
main executable could be caught on Linux.

The header is kept only when the output program header table still fits
in the input's table; otherwise it is dropped as before, with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8
@hikazey

hikazey commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Windows run, since the description says it wasn't built there: Windows 11 Pro (26200), MinGW-w64 GCC 15.2.0 (WinLibs r7, the CI build), Release, configured like CI, Ryzen 9 5900X. This commit merged onto main 4b005b22, which merges cleanly.

build linux_eh_frame_header
this PR 5/5 passed
SegmentFilter.cpp from main, test kept 5/5 failed
full suite with this PR 458/458 passed

To check "the change does not touch the PE output" on a real input, I relinked DOOM Shareware PPSA99666 v1.1.1 (eboot.bin + bundled sce_module/libc.prx, unwrapped from their SELFs) with this branch's relinker and with main's (the three changed files reverted, rebuilt):

output executable guest libc.prx
--windows byte-identical (5854720 bytes) byte-identical
Linux differs: 14 program headers with PT_GNU_EH_FRAME vs 13 without, same size byte-identical

No slot warning for this input. In code, SegmentFilter and ProgramHeaderLayoutBuilder are only wired into LinuxElfPatcher (main.cpp), which matches.

AI-assisted: yes (Claude Code).

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.

3 participants