feat(relinker): write the output of a Windows executable without standard handles to anyps5.log
A --windows-gui build started from Explorer, or any Windows executable started without a standard output or error handle, lost everything it printed: the entry stub's messages, the libraries' stdout and stderr, uncaught exception messages and crash reports. Before it enters its directory or loads a library, the entry stub now checks the standard output and error handles, and gives each missing one anyps5.log beside the executable. The C runtime of the libraries it then loads adopts the file. Executables started with handles are unchanged, and one that cannot create the file starts as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5fe4bebec6
commit
ab3e926058
|
|
@ -100,6 +100,9 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
|
|||
const auto argumentCount = reserve(4);
|
||||
const auto wideArguments = reserve(8);
|
||||
const auto shellHandle = reserve(8);
|
||||
const auto logHandle = reserve(8);
|
||||
const std::u16string logFileName = u"anyps5.log";
|
||||
const auto logName = addWideString(logFileName);
|
||||
const auto shellLibrary = addString("shell32.dll");
|
||||
const auto parseArguments = addString("CommandLineToArgvW");
|
||||
const auto handles = reserve(libraries.size() * 8);
|
||||
|
|
@ -325,6 +328,48 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
|
|||
code.PatchBranch(converted, code.GetRva());
|
||||
};
|
||||
|
||||
const auto logMissingHandle = [&](const std::uint32_t standardHandle) {
|
||||
const auto units = CheckedRva(logFileName.size() + 1);
|
||||
code.Emit({0xb9});
|
||||
code.U32(standardHandle);
|
||||
call("GetStdHandle");
|
||||
code.Emit({0x48, 0x85, 0xc0});
|
||||
const auto missing = code.Branch({0x0f, 0x84});
|
||||
code.Emit({0x48, 0x83, 0xf8, 0xff});
|
||||
const auto present = code.Branch({0x0f, 0x85});
|
||||
code.PatchBranch(missing, code.GetRva());
|
||||
code.Rip({0x48, 0x8b, 0x05}, logHandle);
|
||||
code.Emit({0x48, 0x85, 0xc0});
|
||||
const auto opened = code.Branch({0x0f, 0x85});
|
||||
code.Emit({0x4c, 0x89, 0xe0, 0x48, 0x29, 0xd8, 0x48, 0x05});
|
||||
code.U32(units * 2);
|
||||
code.Emit({0x48, 0x3d});
|
||||
code.U32(PathCapacity * 2);
|
||||
const auto tooLong = code.Branch({0x0f, 0x87});
|
||||
code.Emit({0x4c, 0x89, 0xe7});
|
||||
code.Rip({0x48, 0x8d, 0x35}, logName);
|
||||
code.Emit({0xb9});
|
||||
code.U32(units);
|
||||
code.Emit({0x66, 0xf3, 0xa5});
|
||||
code.Rip({0x48, 0x8d, 0x0d}, modulePath);
|
||||
code.Emit({0xba, 0, 0, 0, 0x40, 0x41, 0xb8, 3, 0, 0, 0, 0x45, 0x31, 0xc9});
|
||||
code.Emit({0x48, 0xc7, 0x44, 0x24, 0x20, 2, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x28, 0x80, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x30, 0, 0, 0, 0});
|
||||
call("CreateFileW");
|
||||
code.Emit({0x66, 0x41, 0xc7, 0x04, 0x24, 0x00, 0x00});
|
||||
code.Emit({0x48, 0x83, 0xf8, 0xff});
|
||||
const auto failed = code.Branch({0x0f, 0x84});
|
||||
code.Rip({0x48, 0x89, 0x05}, logHandle);
|
||||
code.PatchBranch(opened, code.GetRva());
|
||||
code.Emit({0xb9});
|
||||
code.U32(standardHandle);
|
||||
code.Emit({0x48, 0x89, 0xc2});
|
||||
call("SetStdHandle");
|
||||
const auto done = code.GetRva();
|
||||
code.PatchBranch(present, done);
|
||||
code.PatchBranch(tooLong, done);
|
||||
code.PatchBranch(failed, done);
|
||||
};
|
||||
|
||||
code.Emit({0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x48, 0x83, 0xec, 0x60});
|
||||
code.Emit({0x31, 0xc9});
|
||||
code.Rip({0x48, 0x8d, 0x15}, programPath);
|
||||
|
|
@ -351,6 +396,8 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
|
|||
code.Rip({0x0f, 0x85}, findSeparator);
|
||||
code.Emit({0x49, 0x83, 0xc4, 0x02});
|
||||
code.Emit({0x66, 0x41, 0xc7, 0x04, 0x24, 0x00, 0x00});
|
||||
logMissingHandle(0xfffffff5u);
|
||||
logMissingHandle(0xfffffff4u);
|
||||
code.Rip({0x48, 0x8d, 0x0d}, modulePath);
|
||||
call("SetCurrentDirectoryW");
|
||||
requireNonzero(directoryError, 0xc000000du);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
namespace Elfpatcher::Windows {
|
||||
|
||||
WindowsImports WindowsImportBuilder::Build(const std::uint32_t sectionRva) const {
|
||||
const std::vector<std::string> names = {"ExitProcess", "FormatMessageA", "FreeLibrary", "GetCommandLineW", "GetFileAttributesW", "GetLastError", "GetModuleFileNameW", "GetModuleHandleA", "GetModuleHandleW", "GetProcAddress", "GetStdHandle", "GetSystemDirectoryW", "LoadLibraryExA", "LoadLibraryExW", "LocalFree", "MultiByteToWideChar", "RaiseException", "SetCurrentDirectoryW", "VirtualAlloc", "WideCharToMultiByte", "WriteFile", "lstrcatA", "lstrcmpA", "lstrcmpiA", "lstrcpyA", "lstrlenA"};
|
||||
const std::vector<std::string> names = {"CreateFileW", "ExitProcess", "FormatMessageA", "FreeLibrary", "GetCommandLineW", "GetFileAttributesW", "GetLastError", "GetModuleFileNameW", "GetModuleHandleA", "GetModuleHandleW", "GetProcAddress", "GetStdHandle", "GetSystemDirectoryW", "LoadLibraryExA", "LoadLibraryExW", "LocalFree", "MultiByteToWideChar", "RaiseException", "SetCurrentDirectoryW", "SetStdHandle", "VirtualAlloc", "WideCharToMultiByte", "WriteFile", "lstrcatA", "lstrcmpA", "lstrcmpiA", "lstrcpyA", "lstrlenA"};
|
||||
WindowsImports result{{".idata", sectionRva, SectionRead | SectionWrite | 0x40u, std::vector<std::uint8_t>(40)}, {sectionRva, 40}, {}, {}};
|
||||
auto& bytes = result.Section.Data;
|
||||
const auto lookupOffset = bytes.size();
|
||||
|
|
|
|||
|
|
@ -384,19 +384,22 @@ def main():
|
|||
assert not output.exists()
|
||||
|
||||
if os.name == 'nt':
|
||||
for name, owner, symbol, expected in [
|
||||
('missing-symbol-gui', 'a.prx', 'absent#A#B', 0xc0000139),
|
||||
('missing-dependency-gui', 'missing.prx', 'shared#A#B', 0xc0000135)]:
|
||||
for name, owner, symbol, expected, message in [
|
||||
('missing-symbol-gui', 'a.prx', 'absent#A#B', 0xc0000139, 'unresolved ELF import absent'),
|
||||
('missing-dependency-gui', 'missing.prx', 'shared#A#B', 0xc0000135, 'Failed to load module')]:
|
||||
result, output = convert(name, owner, symbol, windows_gui=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
run_status = exit_code_without_standard_handles(output)
|
||||
assert run_status == expected, (name, hex(run_status), hex(expected))
|
||||
log = output.parent / 'anyps5.log'
|
||||
assert log.is_file() and message in log.read_text(errors='replace'), (name, log.exists())
|
||||
|
||||
result, output = convert('missing-symbol', 'a.prx', 'absent#A#B')
|
||||
assert result.returncode == 0, result.stderr
|
||||
if os.name == 'nt':
|
||||
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
|
||||
assert run.returncode != 0 and 'unresolved ELF import absent' in run.stderr, (run.returncode, run.stderr)
|
||||
assert not (output.parent / 'anyps5.log').exists()
|
||||
result, output = convert('wrong-provider', 'c.prx')
|
||||
assert result.returncode == 0, result.stderr
|
||||
if os.name == 'nt':
|
||||
|
|
|
|||
|
|
@ -119,6 +119,8 @@ python3 tools/pad_input.py /tmp/anyps5-pad release
|
|||
|
||||
Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads and add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled.
|
||||
|
||||
A Windows executable started without a standard output or error handle, such as a `--windows-gui` build started from Explorer, writes that output to `anyps5.log` beside it, replacing the previous run's file. Without write access to that directory it runs without the log.
|
||||
|
||||
### System fonts
|
||||
|
||||
Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese and Chinese). Without either, opening a system font set fails and the game shows no text in those fonts.
|
||||
|
|
|
|||
Loading…
Reference in New Issue