Skip to content

feat(libkernel): start runtime-loaded modules with their arguments on Linux - #2323

Closed
hugodearaujo wants to merge 3 commits into
boykopovar:mainfrom
hugodearaujo:feat/linux-deferred-module-start
Closed

hugodearaujo wants to merge 3 commits into
boykopovar:mainfrom
hugodearaujo:feat/linux-deferred-module-start

Conversation

@hugodearaujo

Copy link
Copy Markdown
Contributor

What

Depends on #846. This branch contains its two commits. Only the last commit, feat(libkernel): start runtime-loaded modules with their arguments on Linux, belongs to this PR.

#846 starts --module-dir modules from sceKernelLoadStartModule with the call's args/argp on Windows. On Linux, the host loader runs their DT_INIT at dlopen with zeroed arguments, so #846 throws there when start arguments are given. This PR does on Linux what #846 does on Windows, with the same table:

  • Relinker, Linux writer. A module from --module-dir gets no DT_INIT, DT_INIT_ARRAY, DT_FINI or DT_FINI_ARRAY. Like the Windows .ginit section, it exports __aps5_guest_initialize: the initializer's address, the number of INIT_ARRAY slots, and each slot's address. The symbol is added to the module's dynamic symbol table and hash. Modules from sce_module/prx are written as before.
  • libkernel. After dlopen, OpenModule looks up __aps5_guest_initialize. It takes the module's load bias from dl_iterate_phdr and calls the initializer once with (args, argp, nullptr). It stores the return value in *res, then runs the INIT_ARRAY slots. The table walk and the "only once per module" set are now shared with the Windows path, which keeps its behavior. The NotImplemented for start arguments on Linux is removed.
  • TechnicalDebt. The feat(relinker): add --module-dir for modules loaded at run time #846 line now says both platforms start these modules once, from the first call that loads them, and never run their finalizers. Windows already skipped the finalizers.

Evidence: Hollow Knight: Silksong (PPSA12544) on Linux

Unity loads its plugins from /app0/Media/Plugins with sceKernelLoadStartModule, and PSN.prx is started with start arguments. All runs below are relinked with --module-dir Media/Plugins, on main d70b89989 + #1836 + #1431 (the title stops before the plugins without them) + #846:

  • Without this PR: terminate ... what(): sceKernelLoadStartModule with start arguments not implemented.
  • With this PR: PSN.prx starts. The next stop is in the title's bundled libSceNpCppWebApi: CppWebApi::Common::initialize() failed. ret = 0x80553504, then an uncaught Unity.PSN.PS5.PSNException with that code. Common::initialize maps flexible memory and passes it to the bundled libc's sceLibcMspaceCreate, which refuses the address on Linux. That is the address-window problem of fix(guest-arena): place guest address space in the PS5 window on Linux #861, not this change.
  • With this PR and fix(guest-arena): place guest address space in the PS5 window on Linux #861: the title reaches its title menu (START GAME / OPTIONS / EXTRAS) and keeps running at about 60 fps with no error until I stopped it (100 s).

Tested

  • Linux (Arch, GCC 16.2, Release, RX 9070 XT): ctest 464/464 on this branch.
    • guest_module_dir gains a Linux case. A --module-dir module with DT_INIT, DT_FINI and one INIT_ARRAY slot is written without those tags. It exports __aps5_guest_initialize as a 12-byte object holding (0x1000, 1, 0x2380). The same module under sce_module keeps its tags and has no such symbol. The test fails on feat(relinker): add --module-dir for modules loaded at run time #846 without this commit.
    • guest_dynamic_loader gains a Linux case with a host fixture module that exports the table. GuestLoadStartModule_nid_no_patch with 16 bytes of arguments must call its start function once with those args/argp, store its return value (7) in *res, and run the INIT_ARRAY slot once. Loading the module a second time must not run either again. The test aborts on feat(relinker): add --module-dir for modules loaded at run time #846 without this commit, on its NotImplemented.
  • Windows (Server 2025 VM, WinLibs GCC 15.2, CI flags, no GPU, so Vulkan tests are skipped): 458/458.
  • Silksong as above.

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: feat(relinker): add --module-dir for modules loaded at run time #846
  • AI-assisted: yes

@merwie: you mentioned you can't build the Linux path. If you'd rather carry this in #846, take the commit and I'll close this one.

🤖 Generated with Claude Code

merwie and others added 3 commits October 8, 2026 17:43
Unity titles load native plugins from Media/Plugins with
sceKernelLoadStartModule; they are neither in a module directory nor
needed by the executable, so they stayed unrelinked and the load failed
(PPSA12544: DllNotFoundException for 'PSN', /app0/Media/Plugins/PSN.prx).

--module-dir <dir> adds a directory relative to the input whose ELF
modules are relinked like the bundled ones, with their outputs at the
same relative path. The directory must exist inside the input directory
and differ from the others. These modules are not initialized at
startup: the writer exports their DT_INIT entry as
__aps5_guest_initialize, and sceKernelLoadStartModule calls it with its
args and argp and returns the result in *res, as the plugins check that
argument (PSN refuses to start without it). On Linux they are left out
of DT_NEEDED.

AI-assisted: yes

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

On Linux the host loader runs a module's initializers at dlopen with the
host process arguments, so the args and argp passed to
sceKernelLoadStartModule never reach a module that checks them (Unity's
PSN plugin refuses to start without its {16, 0x200, ...} block), and
*res stays 0. Passing start arguments now throws there until deferred
initialization exists on Linux; Windows is unchanged. TechnicalDebt
records the gap.

AI-assisted: yes

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#1102 core/libs/prx/libkernel/Module/src/Module.cpp L157-158, L162-163
core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp L57, L76-77
#1249 core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp L57-58
#1597 core/relinker/cli/src/CliArgs.cpp L59, L68
core/relinker/main.cpp L94
core/relinker/relinker/include/relinker/guest/GuestImage.hpp L58
core/relinker/relinker/src/guest/GuestModuleBuilder.cpp L34, L273
#2117 core/relinker/cli/src/CliArgs.cpp L66-70
#2119 core/relinker/cli/src/CliArgs.cpp L59, L68
core/relinker/main.cpp L94
core/relinker/relinker/include/relinker/guest/GuestImage.hpp L58
core/relinker/relinker/src/guest/GuestModuleBuilder.cpp L34
#2212 dlopen_nid_postfix core/relinker/relinker/tests/test_guest_module_dir.py

@Kikoarl

Kikoarl commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Tested on Linux (CachyOS x86_64, Intel RPL-S iGPU / Mesa 26.3.0-devel / Vulkan 1.4.363, GCC 16.2.1) at b076f0f2:

  • Build: Built cleanly in RAM (/tmp/anyps5_build) with zero warnings/errors.
  • Conventions: python3 tools/check_conventions.py --base origin/main --head HEAD -> PASS (0 errors).
  • Verification: Verified deferred module startup with CLI arguments on Linux platform in DynamicLoader.cpp and GuestDynamicLoader.cpp.

LGTM!

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Closing as a duplicate of #1102, which passes args/argp to runtime-loaded modules on Linux on current main without needing #846. If PPSA12544 still stops at module start once #1102 and #846 are both in, please reopen with that log line.

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