Repository navigation
fix(relinker): prefer 0x400000 for Windows executables - #9332
Conversation
|
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.
Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.
|
|
Windows run of 14ffbf9 merged onto VLC (PPSA85300, 01.000.007), converted by
ccleste (PPSA59322) and DriftCheck (PPSA98766) on the new default: 2 of 2 rendering each (116.5 and 117.1 fps; 54.3 and 58.5 fps).
Not run: Ruffle (PPSA68091) and XPSemu (PPSA97358), which I don't have. |
|
Thanks, that's the title run this PR was missing. Good to see the default and |
|
Windows and Linux run of 14ffbf9 merged onto
Not run: the converted titles themselves (my build is relinker-only); ycbuzi's VLC runs above cover that. AI-assisted: yes (Claude Code); results above were run on this machine. |
14ffbf9 to
92fbd0a
Compare
|
Rebased onto current |
|
The rebased head on Windows: 92fbd0a merged into AI-assisted: yes (Claude Code) |
|
@BKlaasWerkman thanks for the Windows 11 run on the rebased head; the description's Tested section now lists it beside the hosted Server 2025 one, with the two tests whose expectations changed named. |
|
@rooklz thanks, the change looks right and the homebrew results from @wachi35676 are useful. Since this moves every Windows-converted title to the new base, could you (or anyone on Windows) run one converted retail title on this head and confirm it reaches its first rendered frame at 0x3f0000? Any title is fine; a log line or screenshot showing the image base is enough. Once that's in, this can go ahead. |
|
@oneandonlydean will do. I have a Windows 11 / RTX machine coming online shortly and will convert one retail title on this head there and post the log line with the mapped image base and the first presented frame. If anyone on Windows with a retail title gets there first, please go ahead; a line showing |
roboterspinne
left a comment
There was a problem hiding this comment.
Reviewed head 92fbd0a merged onto upstream/main aeeaefa (2026-10-24); the merge is clean (only the two docs files auto-merge).
Environment: Windows 11 Home 10.0.26200, WinLibs GCC 15.2.0 UCRT, CMake 4.3.1, Ninja 1.13.2, Python 3.13.9, -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON, Release.
Results
- Baseline
mainaeeaefa: 238 tests, 0 failed,long_pathsskipped. PR merged: 238 tests, 0 failed,long_pathsskipped;windows_fixed_base2.7 s,windows_tls_protected_entries0.8 s,windows_address_space0.5 s. - Red/green: with the four C++ changes reverted and the PR's tests kept,
windows_fixed_basefails at the header assertion (line 71,0x140000000 != 0x3f0000) andwindows_tls_protected_entriesfails ataccess-start(0x400210); restored, both pass. - Native header dump of the converted
test_optional_pltfixture:maindefault ImageBase0x140000000; PR default ImageBase0x3f0000, Characteristics0x22, DllCharacteristics0x100,.relocdirectory kept (size0xc), SizeOfImage unchanged. The imports-offset fixture from the test exits0xC0000005withmain's default and 42 with the PR's; started suspended, the PR output reportsPEB.ImageBaseAddress=0x3f0000.--windows-fixed-base 0x400000output is identical onmainand the PR (ImageBase0x3f0000, Characteristics0x23, no.reloc). - Byte comparison
mainvs PR:app.exediffers only in the ImageBase field (plus the one absolute GOT slot the fixture bakes in); a bundled module converted alongside it is byte-identical with ImageBase0x140000000, and the executable with that module exits 42 on both. check_conventions.pypasses;pr_overlap.pyagainst currentmainlists only the "could not compare" docs entries.
Should fix
- Rebase note (file is on
main, not in this diff):core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp:77, added by 2657a81 after this branch's merge-base, callsWindowsPeWriter().Write(..., false, ImageBase, false). With the new signature those tokens meanimageBase=0x140000000, fixedBase=false, dynamicBase=true, so the probe losesIMAGE_FILE_RELOCS_STRIPPED(Characteristics0x22instead of0x23). It compiles cleanly (-Wall -Wextra -Wconversion, no diagnostic), and thewindows_import_identitytarget is only built withoutANYPS5_RELINKER_ONLY(CMakeLists.txt:163), so the relinker matrix cannot notice. The test still passes (it only runs the probe, which has no relocation directory), but please change the call toWrite(..., false, ImageBase, true, false)on rebase to keep its fixed-base intent. docs/dev/debt/relinker.md:11and the body: the relocation example cannot occur. From0x3f0000an image needs SizeOfImage ≥0x7fbf0000to reach0x7ffe0000, but the loader refuses any converted image with SizeOfImage above0x77000000: measured here withtest_optional_plt.fixtureandp_memszpatched, SizeOfImage0x77000000loads at0x3f0000and exits 42,0x77001000fails with CreateProcess error 193 (NtCreateSection→STATUS_INVALID_IMAGE_FORMAT), the same atmain's0x140000000and with--windows-fixed-base. So the default executable ends below0x773f0000and never reaches the shared user data page. Also, I found no run in this PR, #4237 or #3043 that observed a relocation of the executable at either base (every reportedPEB.ImageBaseAddresshere was the preferred one, and the only occupancy test uses a fixed base and expectsSTATUS_CONFLICTING_ADDRESSES), while the entry drops its "Verified only on Windows 10" sentence. Suggested wording is in the inline comment; "as it did when0x140000000was taken" in the body would need a pointer to the run that showed it.
Nits
- Body: #8973 was merged on 2026-10-22 (44661db) and is an ancestor of this branch, so "the two don't depend on each other" reads as if it were still open. #8729 was closed unmerged on 2026-10-24, so the
pr_overlap.pynote no longer applies. - Body, Tested: ycbuzi's and wachi35676's runs were on the pre-rebase head 14ffbf9 (same diff), only BKlaasWerkman's on 92fbd0a.
test_windows_fixed_base.py:84/88: the imports-offset fixture (cmp qword ptr [rax+0x400000], 0) only checks that the read at0x400300succeeds and is non-zero; it would also pass if any committed non-zero memory sat there. On the PR build the slot holds the resolved0x400240; comparing against it, or jumping through it asabsolute_jump()does, would make a wrong value fail too (same for the fixed-base variant on line 89).- The bundled module's ImageBase
0x140000000is asserted only for the--windows-fixed-base 0x400000conversion (line 128);test_windows_address_space.pychecks the default module'sDYNAMIC_BASEbut not its base. A defaultconvert(..., modules=True)with the same assertion would cover the first case where executable and module bases differ. Pre-existing gap. - Under Exploit Protection's "Force randomization for images (Mandatory ASLR)", off by default, Windows "will forcibly rebase DLLs and EXEs that haven't opted in to ASLR" (learn.microsoft.com, exploit-protection-reference). The default output (Characteristics
0x22,.relockept, noDYNAMIC_BASE) is such an image, so on such a machine the imports.txt titles fault again;--windows-fixed-baseoutput is not rebased, but the sub-option "Do not allow stripped images" refuses it. Worth one sentence in USAGE.md's Runtime layout.main's default was rebased the same way, so this is not a regression. - "Otherwise nothing changes" also omits: secondary executables converted through
--executableget the new preferred base too (main.cpp:67passes no fixed base for them), the alignment/>= 0x10000check inWindowsPeWriter::Writenow applies to every image instead of only a fixed one, and the exception text changed. All harmless as far as I can see. - Note: on Windows "0x400000" is where the lowest ELF load page lands (
WindowsLoadImagesubtractsfirstAddress), on LinuxConversion.cppplaces virtual address 0 there; the two agree only for ELFs whose firstPT_LOADstarts at vaddr 0, which PS5 executables do.
Not checked: no retail title (no dumps here, so oneandonlydean's request for a first-frame run at 0x3f0000 stands); no macOS or Linux execution; relocation of the default executable when 0x3f0000 is actually occupied could not be provoked; Mandatory ASLR was not enabled; the full (non relinker-only) build with windows_import_identity was not run; Windows 10 not measured.
I found no blocking issue; the code change is correct and complete as far as I can tell, the two should-fix items are a rebase hazard outside this diff and a documentation correction. For disclosure: I have open relinker PRs (#10390, #10391, #10401, #10402, #10403); none shares a file with this one.
Written with AI assistance (Claude Code). I ran the builds, tests and native checks above on my Windows 11 machine.
| - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. | ||
| - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. | ||
| - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. | ||
| - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) and [prefer `0x400000`](../../../core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp) for their lowest ELF load page by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). Relocations are kept, so if that range is taken, as by an image that reaches the shared user data page at `0x7ffe0000`, Windows relocates the image, possibly above that window. |
There was a problem hiding this comment.
Measured on Windows 11 build 26200: the loader refuses any converted image with SizeOfImage above 0x77000000 (CreateProcess error 193, STATUS_INVALID_IMAGE_FORMAT), at 0x3f0000 as at 0x140000000, while 0x77000000 itself loads. From 0x3f0000 an image would need SizeOfImage ≥ 0x7fbf0000 to reach 0x7ffe0000, so this example cannot occur: such an image is refused, not relocated. I also found no run that observed a relocation of the executable at either base, and the old "Verified only on Windows 10" is gone without a replacement.
| - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) and [prefer `0x400000`](../../../core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp) for their lowest ELF load page by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). Relocations are kept, so if that range is taken, as by an image that reaches the shared user data page at `0x7ffe0000`, Windows relocates the image, possibly above that window. | |
| - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) and [prefer `0x400000`](../../../core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp) for their lowest ELF load page by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). Relocations are kept, so if that range is taken Windows relocates the image, possibly above that window; no relocation has been observed at either base. Images with `SizeOfImage` above `0x77000000` are refused by the loader (`STATUS_INVALID_IMAGE_FORMAT`; measured on Windows 11 build 26200), so the default executable ends below `0x773f0000` and never reaches the shared user data page at `0x7ffe0000`. |
| ## Runtime layout | ||
|
|
||
| For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. | ||
| The Windows executable has no `DYNAMIC_BASE` and prefers to load with its lowest ELF load page at `0x400000`, where Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) expect their image when they read their `app0/imports.txt`; the PE headers occupy the preceding 64 KiB (preferred base `0x3f0000`). Relocations are kept, so Windows relocates it if that range is taken. When the executable requires an exact address, use `relinker --windows --windows-fixed-base <address> source/input.elf app.exe`, which maps the lowest ELF load page at that address without relocations, so an occupied address fails to load. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. |
There was a problem hiding this comment.
Nit: the realistic case for "if that range is taken" is Exploit Protection's "Force randomization for images (Mandatory ASLR)", which Microsoft documents as forcibly rebasing EXEs that have not opted in to ASLR; with it on, titles that read their GOT at 0x400000 fault again, and --windows-fixed-base 0x400000 is the way out (unless "Do not allow stripped images" is also on). One sentence here would save a user that search. Also "This option" at the end now has a distant antecedent since the paragraph opens with the default.
| image[0x200:0x200 + len(code)] = code | ||
| image[0x240:0x246] = bytes.fromhex("b8 2a 00 00 00 c3") | ||
| struct.pack_into("<q", image, 0x710, 0x240) | ||
| run(convert("sce-dynexec-imports-offset-default", image)) |
There was a problem hiding this comment.
Nit: the fixture code on line 84 is mov eax,0x300; cmp qword ptr [rax+0x400000],0; je ...; mov eax,42; ret, so exit 42 only proves that the read at 0x400300 succeeded and was non-zero (on main it faults because 0x400300 falls into the reserved main-thread stack region here). Comparing the slot with the resolved 0x400240, or jumping through it as absolute_jump() does below, would also catch a wrong non-zero value. Same for the fixed-base variant on line 89.
92fbd0a to
f65de44
Compare
|
@roboterspinne thanks for the careful review. Head
The relinker-only matrix on this head (https://github.com/rooklz/AnyPS5/actions/runs/40098933979) passes 240 of 241. Its one failure is |
|
@rooklz Thanks. I checked f65de44 against the previous head: apart from the rebase, the only code change is WindowsImportIdentityTests.cpp:77, and |
|
@rooklz @oneandonlydean Thanks, this matches what I see. Re-checked f65de44 merged onto
Written with AI assistance (Claude Code). I ran the build, tests and native checks above on my Windows 11 machine; the full Windows build result is from the fork's Actions log. |
f65de44 to
39df8e0
Compare
|
@roboterspinne thanks for the re-check and for tracing the |
|
A catalog title that needs this PR: wiliwili (PPSA99099, 1.6.0.1, homebrew, so not the retail check asked for above). It crashes on Windows at the current default base and runs with this head. The title resolves its own imports: it reads Windows 11, Ryzen 7 5800X, WinLibs GCC 15.2, Release,
Linux ( The frame rate is low (about 7 FPS) with and without this PR, so that's unrelated to the base. AI-assisted: yes (Claude Code) |
|
@babyhuey thanks, that's a clear case: the title adds |
|
@rooklz thanks for adding the new run. The retail-title check I asked for on 23 Oct isn't needed any more: the homebrew runs and the full Windows CI on this head cover it. This PR is ready, and it will merge as soon as the current merge pause on main lifts. |
boykopovar#9332 made 0x400000 the default base of Windows executables, as boykopovar#8973 did on Linux, so the three titles that read their GOT at that base no longer need fixed_base in titles.json: with a plain relink their guest image starts at 0x400000 and all three render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
boykopovar#9332 made 0x400000 the default base of Windows executables, as boykopovar#8973 did on Linux, so the three titles that read their GOT at that base no longer need fixed_base in titles.json: with a plain relink their guest image starts at 0x400000 and all three render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
boykopovar#9332 made 0x400000 the default base of Windows executables, as boykopovar#8973 did on Linux, so the three titles that read their GOT at that base no longer need fixed_base in titles.json: with a plain relink their guest image starts at 0x400000 and all three render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
boykopovar#9332 made 0x400000 the default base of Windows executables, as boykopovar#8973 did on Linux, so the three titles that read their GOT at that base no longer need fixed_base in titles.json: with a plain relink their guest image starts at 0x400000 and all three render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What
On Windows, Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) only start when they're converted with
--windows-fixed-base 0x400000. They read their GOT slots at0x400000plus the offsets inapp0/imports.txt(#4013), but the default executable puts its lowest ELF load page at0x140010000, so the first read faults. babyhuey on #5732, about VLC onmain64a9caa62: with the option it "runs the full 60 s at about 119 fps with no fault. Without--windows-fixed-baseit still faults at start-up as before (#4013)". dantdmdarealguy measured the same for Ruffle on #4013. The web converter has no way to pass the option.This makes
0x400000the preferred address of the executable's lowest ELF load page, which is a PE image base of0x3f0000, since the headers take the 64 KiB before it. Otherwise nothing changes:DYNAMIC_BASE, as since fix(relinker): load Windows executables at a fixed low base #4237. Windows maps the executable before anything else, so the range is normally free and it loads there. If the range were taken, the kept relocations let Windows move it; no run has shown that happen at either base.--windows-fixed-baseis unchanged: an exact address, no relocations, and an occupied address fails to load.0x200000000. No image can reach the shared user data page at0x7ffe0000from here: Windows refuses aSizeOfImageabove0x77000000(@roboterspinne measured it), and the debt entry now says so.DYNAMIC_BASEand their preferred base0x140000000. macOS output is unchanged.WindowsPeWriter::Writenow takes the image base and whether it's fixed, instead of an optional fixed base, so the patcher can pass a preferred base. Its alignment and lower-bound check now applies to every image, secondary executables converted with--executableget the new preferred base too, andWindowsImportIdentityTests(added onmainsince) passestrue, falseto keep its fixed-base probe.#8973 (merged) does the same on Linux.
Tested
Windows 11, Ryzen 7 5800X, WinLibs GCC 15.2, Release, by @babyhuey: PPSA99099 wiliwili (homebrew), which reads its own import table at its offset plus
0x400000, faults with0xC0000005at 2.5 s onmain(0x140000000base) and runs for the 45 s window with this head and withmainplus--windows-fixed-base 0x400000.No title run on my side. These runs were on the pre-rebase head
14ffbf945(same diff) unless noted. ycbuzi ran VLC (PPSA85300) on this head merged ontomaina849f2516e(comment), Windows 11, RTX 4070 SUPER: onmainthe default conversion crashed at start-up with0xc0000005and 0 presents (2 runs); with this PR the default conversion rendered at 72.6 to 118.9 fps (4 of 5 runs; the fifth stopped presenting after about 20 s, which also happened once with--windows-fixed-base 0x400000). ccleste (PPSA59322) and DriftCheck (PPSA98766) render on the new default too. Ruffle and XPSemu weren't run.windows_fixed_basenow converts itsimports.txt-style fixture without any option and runs it. The fixture reads the slot at0x400300and returns 42 when the slot is filled. With only that check added tomain950388bfd, the converted executable exits with0xC0000005on a GitHub-hosted Windows runner (verifyrun on my fork), which is the fault in Ruffle, VLC and XPSemu read their GOT at a fixed 0x400000 base from app0/imports.txt and fault once all imports load #4013. With this change it passes. The header check there and inwindows_tls_protected_entriesnow expects0x3f0000.Windows Server 2025 (GitHub-hosted), WinLibs GCC 15.2.0, relinker-only build, run on my fork: 233/233 with the change and 233/233 on its base
950388bfd. That includeswindows_address_space, which runs the default executable 8 times and checks that its main thread stack stays below the arena.macOS 26.2 (Apple M5), AppleClang 21, relinker-only build: 229/229.
Windows 11 Pro 10.0.26300, WinLibs GCC 15.2.0 r7, relinker-only build, by @BKlaasWerkman on
92fbd0adc1merged intomain990f45e17d: 235 of 235 pass,windows_fixed_baseandwindows_tls_protected_entrieswith their changed expectations among them, and no result differs frommainon the same machine.check_conventions.pypasses.pr_overlap.pyagainst currentmainlists only shared docs files it could not compare.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)