feat(libkernel): implement sceKernelGetModuleInfoFromAddr on Windows

Read the module layout from the PE image that holds the address. A
relinked image reports its .elf sections as segments and the
eh_frame_hdr recorded in .ehmeta; any other image reports its sections
with memory access and its .ehfram section. The eh_frame_hdr and
eh_frame helpers are shared with the Linux path, and the guest module
info test now also runs on Windows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MathiasDevelopes 2026-10-07 19:11:36 +02:00 committed by Claude
parent 311c0493bd
commit 77ef75c1da
No known key found for this signature in database
4 changed files with 196 additions and 85 deletions

View File

@ -90,6 +90,7 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:nid_patcher> guest_module_fixture $<TARGET_FILE:guest_module_fixture>
VERBATIM)
add_dependencies(guest_module_fixture nid_patcher)
configure_windows_unwind(guest_module_fixture)
set_target_properties(guest_module_fixture PROPERTIES BUILD_WITH_INSTALL_RPATH ON INSTALL_RPATH "$ORIGIN")
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND AND NOT WIN32)
@ -109,14 +110,17 @@ if(BUILD_TESTING)
set_tests_properties(guest_dynamic_loader PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
if(NOT WIN32)
add_test_executable(guest_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestModuleInfo.cpp)
target_include_directories(guest_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_module_info_tests PRIVATE libkernel libc)
add_dependencies(guest_module_info_tests guest_module_fixture)
add_test(NAME guest_module_info COMMAND guest_module_info_tests $<TARGET_FILE_NAME:guest_module_fixture>)
set_tests_properties(guest_module_info PROPERTIES
WORKING_DIRECTORY "$<TARGET_FILE_DIR:guest_module_fixture>")
add_test_executable(guest_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestModuleInfo.cpp)
target_include_directories(guest_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_module_info_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_module_info_tests)
add_dependencies(guest_module_info_tests guest_module_fixture)
add_test(NAME guest_module_info COMMAND guest_module_info_tests $<TARGET_FILE_NAME:guest_module_fixture>)
set_tests_properties(guest_module_info PROPERTIES
WORKING_DIRECTORY "$<TARGET_FILE_DIR:guest_module_fixture>")
if(WIN32)
set_tests_properties(guest_module_info PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_filesystem_tests ${LIBS_INCLUDE_DIR}/tests/GuestFilesystem.cpp)
target_include_directories(guest_filesystem_tests PRIVATE ${LIBS_INCLUDE_DIR})

View File

@ -8,7 +8,13 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/KernelErrors.hpp"
#ifndef _WIN32
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <filesystem>
#else
#include <dlfcn.h>
#include <link.h>
#include <unistd.h>
@ -18,12 +24,104 @@ extern "C" std::int32_t ModuleIdForImage_nid_no_patch(const void* native);
namespace {
#ifndef _WIN32
constexpr char GuestModuleSuffix[] = ".guest.prx";
constexpr std::int32_t ProtRead = 1;
constexpr std::int32_t ProtWrite = 2;
constexpr std::int32_t ProtExecute = 4;
std::uint32_t ToU32(std::uint64_t value, const char* field) {
if (value > std::numeric_limits<std::uint32_t>::max())
throw std::runtime_error(std::string("sceKernelGetModuleInfoFromAddr: ") + field + " exceeds 32 bits");
return static_cast<std::uint32_t>(value);
}
std::string ModuleName(std::string fileName) {
if (fileName.size() > sizeof(GuestModuleSuffix) - 1 && fileName.ends_with(GuestModuleSuffix))
fileName.resize(fileName.size() - (sizeof(GuestModuleSuffix) - 1));
return fileName;
}
void SetName(ModuleInfoEx& info, const std::string& name) {
if (name.size() >= sizeof(info.name))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: module name too long: " + name);
std::memcpy(info.name, name.c_str(), name.size() + 1);
}
std::uint64_t EncodedSize(std::uint8_t encoding) {
switch (encoding & 0x0fu) {
case 0x00: case 0x04: case 0x0c: return 8;
case 0x02: case 0x0a: return 2;
case 0x03: case 0x0b: return 4;
default: throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_hdr encoding");
}
}
template<typename TContains>
std::uintptr_t EncodedValue(const TContains& contains, std::uintptr_t field, std::uint8_t encoding) {
const auto size = EncodedSize(encoding);
if (!contains(field, size)) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame_hdr outside the image");
const auto* bytes = reinterpret_cast<const void*>(field);
std::int64_t value = 0;
switch (encoding & 0x0fu) {
case 0x02: { std::uint16_t data; std::memcpy(&data, bytes, sizeof(data)); value = data; break; }
case 0x0a: { std::int16_t data; std::memcpy(&data, bytes, sizeof(data)); value = data; break; }
case 0x03: { std::uint32_t data; std::memcpy(&data, bytes, sizeof(data)); value = data; break; }
case 0x0b: { std::int32_t data; std::memcpy(&data, bytes, sizeof(data)); value = data; break; }
default: std::memcpy(&value, bytes, sizeof(value)); break;
}
switch (encoding & 0x70u) {
case 0x00: return static_cast<std::uintptr_t>(value);
case 0x10: return field + static_cast<std::uintptr_t>(value);
default: throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_hdr encoding");
}
}
template<typename TContains>
std::uintptr_t EhFrameAddress(const TContains& contains, std::uintptr_t header) {
if (!contains(header, 4)) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame_hdr outside the image");
const auto* bytes = reinterpret_cast<const std::uint8_t*>(header);
if (bytes[0] != 1) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_hdr version");
return EncodedValue(contains, header + 4, bytes[1]);
}
template<typename TContains>
std::uint64_t EhFrameHeaderSize(const TContains& contains, std::uintptr_t header) {
const auto* bytes = reinterpret_cast<const std::uint8_t*>(header);
std::uint64_t size = 4 + EncodedSize(bytes[1]);
if (bytes[2] == 0xff) return size;
const auto count = static_cast<std::uint64_t>(EncodedValue(contains, header + size, bytes[2] & 0x0fu));
size += EncodedSize(bytes[2]);
if (bytes[3] == 0xff) return size;
const auto entry = 2 * EncodedSize(bytes[3]);
if (count > (std::numeric_limits<std::uint64_t>::max() - size) / entry || !contains(header, size + count * entry))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame_hdr table outside the image");
return size + count * entry;
}
template<typename TContains>
std::uint64_t EhFrameSize(const TContains& contains, std::uintptr_t frame) {
std::uintptr_t position = frame;
for (;;) {
if (!contains(position, 4))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame outside the image");
std::uint32_t length;
std::memcpy(&length, reinterpret_cast<const void*>(position), sizeof(length));
position += 4;
if (length == 0) return position - frame;
std::uint64_t recordLength = length;
if (length == 0xffffffffu) {
if (!contains(position, 8))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame outside the image");
std::memcpy(&recordLength, reinterpret_cast<const void*>(position), sizeof(recordLength));
position += 8;
}
if (!contains(position, recordLength))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame record outside the image");
position += recordLength;
}
}
#ifndef _WIN32
struct ImageSearch {
std::uintptr_t address;
ModuleInfoEx* info;
@ -40,63 +138,6 @@ bool Contains(const dl_phdr_info& image, std::uintptr_t begin, std::uint64_t siz
return false;
}
std::uint32_t ToU32(std::uint64_t value, const char* field) {
if (value > std::numeric_limits<std::uint32_t>::max())
throw std::runtime_error(std::string("sceKernelGetModuleInfoFromAddr: ") + field + " exceeds 32 bits");
return static_cast<std::uint32_t>(value);
}
std::uintptr_t EhFrameAddress(const dl_phdr_info& image, std::uintptr_t header) {
if (!Contains(image, header, 8))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame_hdr outside the image");
const auto* bytes = reinterpret_cast<const std::uint8_t*>(header);
if (bytes[0] != 1) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_hdr version");
const std::uintptr_t field = header + 4;
switch (bytes[1]) {
case 0x1b: {
std::int32_t offset;
std::memcpy(&offset, bytes + 4, sizeof(offset));
return field + static_cast<std::intptr_t>(offset);
}
case 0x03: {
std::uint32_t value;
std::memcpy(&value, bytes + 4, sizeof(value));
return value;
}
case 0x00: {
std::uint64_t value;
if (!Contains(image, header, 12))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame_hdr outside the image");
std::memcpy(&value, bytes + 4, sizeof(value));
return static_cast<std::uintptr_t>(value);
}
default:
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_ptr encoding");
}
}
std::uint64_t EhFrameSize(const dl_phdr_info& image, std::uintptr_t frame) {
std::uintptr_t position = frame;
for (;;) {
if (!Contains(image, position, 4))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame outside the image");
std::uint32_t length;
std::memcpy(&length, reinterpret_cast<const void*>(position), sizeof(length));
position += 4;
if (length == 0) return position - frame;
std::uint64_t recordLength = length;
if (length == 0xffffffffu) {
if (!Contains(image, position, 8))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame outside the image");
std::memcpy(&recordLength, reinterpret_cast<const void*>(position), sizeof(recordLength));
position += 8;
}
if (!Contains(image, position, recordLength))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame record outside the image");
position += recordLength;
}
}
std::string ImageName(const dl_phdr_info& image) {
std::string path = image.dlpi_name ? image.dlpi_name : "";
if (path.empty()) {
@ -105,17 +146,12 @@ std::string ImageName(const dl_phdr_info& image) {
if (length < 0) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: cannot resolve the executable path");
path.assign(executable, static_cast<std::size_t>(length));
}
std::string name = path.substr(path.find_last_of('/') + 1);
if (name.size() > sizeof(GuestModuleSuffix) - 1 && name.ends_with(GuestModuleSuffix))
name.resize(name.size() - (sizeof(GuestModuleSuffix) - 1));
return name;
return ModuleName(path.substr(path.find_last_of('/') + 1));
}
void Fill(const dl_phdr_info& image, ModuleInfoEx& info) {
const auto name = ImageName(image);
if (name.size() >= sizeof(info.name))
throw std::runtime_error("sceKernelGetModuleInfoFromAddr: module name too long: " + name);
std::memcpy(info.name, name.c_str(), name.size() + 1);
const auto contains = [&](std::uintptr_t begin, std::uint64_t size) { return Contains(image, begin, size); };
SetName(info, ImageName(image));
info.tls_index = ToU32(image.dlpi_tls_modid, "TLS module index");
for (std::uint16_t i = 0; i < image.dlpi_phnum; ++i) {
const auto& header = image.dlpi_phdr[i];
@ -133,8 +169,8 @@ void Fill(const dl_phdr_info& image, ModuleInfoEx& info) {
} else if (header.p_type == PT_GNU_EH_FRAME) {
info.eh_frame_hdr_addr = address;
info.eh_frame_hdr_size = ToU32(header.p_memsz, "eh_frame_hdr size");
info.eh_frame_addr = EhFrameAddress(image, address);
info.eh_frame_size = ToU32(EhFrameSize(image, info.eh_frame_addr), "eh_frame size");
info.eh_frame_addr = EhFrameAddress(contains, address);
info.eh_frame_size = ToU32(EhFrameSize(contains, info.eh_frame_addr), "eh_frame size");
} else if (header.p_type == PT_DYNAMIC) {
for (const auto* entry = reinterpret_cast<const ElfW(Dyn)*>(address); entry->d_tag != DT_NULL; ++entry) {
if (entry->d_tag == DT_INIT) info.init_proc_addr = image.dlpi_addr + entry->d_un.d_ptr;
@ -152,6 +188,76 @@ int FindImage(dl_phdr_info* image, std::size_t, void* data) {
search.found = true;
return 1;
}
#else
bool SectionNamed(const IMAGE_SECTION_HEADER& section, const char* name) {
return std::strncmp(reinterpret_cast<const char*>(section.Name), name, IMAGE_SIZEOF_SHORT_NAME) == 0;
}
std::int32_t SectionProtection(const IMAGE_SECTION_HEADER& section) {
const auto characteristics = section.Characteristics;
return ((characteristics & IMAGE_SCN_MEM_READ) ? ProtRead : 0) | ((characteristics & IMAGE_SCN_MEM_WRITE) ? ProtWrite : 0) | ((characteristics & IMAGE_SCN_MEM_EXECUTE) ? ProtExecute : 0);
}
std::string ImageName(HMODULE module) {
std::wstring path(MAX_PATH, L'\0');
for (;;) {
const auto length = GetModuleFileNameW(module, path.data(), static_cast<DWORD>(path.size()));
if (length == 0) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: cannot resolve the module path");
if (length < path.size()) {
path.resize(length);
break;
}
path.resize(path.size() * 2);
}
const auto fileName = std::filesystem::path(path).filename().u8string();
return ModuleName(std::string(fileName.begin(), fileName.end()));
}
bool Fill(std::uintptr_t address, ModuleInfoEx& info) {
MEMORY_BASIC_INFORMATION memory{};
if (VirtualQuery(reinterpret_cast<LPCVOID>(address), &memory, sizeof(memory)) != sizeof(memory) || memory.Type != MEM_IMAGE) return false;
const auto base = reinterpret_cast<std::uintptr_t>(memory.AllocationBase);
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS64*>(base + static_cast<std::uintptr_t>(dos->e_lfanew));
if (nt->Signature != IMAGE_NT_SIGNATURE || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) return false;
const std::uint64_t imageSize = nt->OptionalHeader.SizeOfImage;
const auto contains = [&](std::uintptr_t begin, std::uint64_t size) { return begin >= base && begin - base <= imageSize && size <= imageSize - (begin - base); };
SetName(info, ImageName(reinterpret_cast<HMODULE>(base)));
const auto* sections = IMAGE_FIRST_SECTION(nt);
const auto sectionCount = nt->FileHeader.NumberOfSections;
bool relinked = false;
for (unsigned i = 0; i < sectionCount; ++i) relinked = relinked || SectionNamed(sections[i], ".elf0");
for (unsigned i = 0; i < sectionCount; ++i) {
const auto& section = sections[i];
const auto start = base + section.VirtualAddress;
if (SectionNamed(section, ".ehmeta")) {
if (!contains(start, 4)) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: .ehmeta outside the image");
std::uint32_t headerRva;
std::memcpy(&headerRva, reinterpret_cast<const void*>(start), sizeof(headerRva));
info.eh_frame_hdr_addr = base + headerRva;
info.eh_frame_addr = EhFrameAddress(contains, info.eh_frame_hdr_addr);
info.eh_frame_hdr_size = ToU32(EhFrameHeaderSize(contains, info.eh_frame_hdr_addr), "eh_frame_hdr size");
info.eh_frame_size = ToU32(EhFrameSize(contains, info.eh_frame_addr), "eh_frame size");
continue;
}
if (SectionNamed(section, ".ehfram") && info.eh_frame_addr == 0) {
info.eh_frame_addr = start;
info.eh_frame_size = ToU32(EhFrameSize(contains, start), "eh_frame size");
continue;
}
if (relinked && std::strncmp(reinterpret_cast<const char*>(section.Name), ".elf", 4) != 0) continue;
const auto prot = SectionProtection(section);
if (prot == 0 || info.segment_count >= std::size(info.segments)) continue;
auto& segment = info.segments[info.segment_count++];
segment.address = start;
segment.size = ToU32(section.Misc.VirtualSize, "segment size");
segment.prot = prot;
}
info.id = ModuleIdForImage_nid_no_patch(reinterpret_cast<const void*>(base));
info.ref_count = 1;
return true;
}
#endif
}
@ -163,24 +269,22 @@ int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, M
if (flags != 2) throw std::invalid_argument("sceKernelGetModuleInfoFromAddr: unsupported flags " + std::to_string(flags));
if (info->st_size != sizeof(ModuleInfoEx))
throw std::invalid_argument("sceKernelGetModuleInfoFromAddr: unsupported st_size " + std::to_string(info->st_size));
ModuleInfoEx result{};
result.st_size = sizeof(ModuleInfoEx);
#ifdef _WIN32
(void)address;
NotImplemented_nid_no_patch(__func__);
return 0;
if (!Fill(static_cast<std::uintptr_t>(address), result)) return SCE_KERNEL_ERROR_ESRCH;
#else
Dl_info symbol{};
link_map* native = nullptr;
if (!dladdr1(reinterpret_cast<const void*>(address), &symbol, reinterpret_cast<void**>(&native), RTLD_DL_LINKMAP) || !native)
return SCE_KERNEL_ERROR_ESRCH;
ModuleInfoEx result{};
result.st_size = sizeof(ModuleInfoEx);
ImageSearch search{static_cast<std::uintptr_t>(address), &result, false};
dl_iterate_phdr(FindImage, &search);
if (!search.found) return SCE_KERNEL_ERROR_ESRCH;
result.id = ModuleIdForImage_nid_no_patch(native);
#endif
*info = result;
return 0;
#endif
}
}

View File

@ -45,7 +45,10 @@ int main(int argc, char** argv) {
executable = (segment.prot & 5) == 5;
}
Require(executable);
Require(info.eh_frame_hdr_addr != 0 && info.eh_frame_hdr_size != 0 && info.eh_frame_addr != 0 && info.eh_frame_size != 0);
Require(info.eh_frame_addr != 0 && info.eh_frame_size != 0);
#ifndef _WIN32
Require(info.eh_frame_hdr_addr != 0 && info.eh_frame_hdr_size != 0);
#endif
Require(info.init_proc_addr == 0 || info.init_proc_addr >= info.segments[0].address);
const auto self = Query(reinterpret_cast<const void*>(&Require), 0);
Require(self.id != info.id && Query(reinterpret_cast<const void*>(&Query), 0).id == self.id);

View File

@ -107,7 +107,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceAgcDcbQueueEndOfShaderActionGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - the builder is inline in the SDK and not exported; the size is assumed to be that of the RELEASE_MEM packet `sceAgcCbReleaseMem` writes (32 bytes), the same as `sceAgcAcbQueueEndOfShaderActionGetSize`, as in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [REmu](https://github.com/xnetcat/REmu/blob/main/crates/remu-runtime/src/agc_dcb.rs)
- [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature
- [sceKernelGetModuleInfoForUnwind](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - `eh_frame_size` is assumed to cover the records up to, not including, the zero terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way; what the console reports is not known.
- [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted, as is only the full `st_size`. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. Not implemented on Windows
- [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted, as is only the full `st_size`. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0
- [sceKernelReleaseFlexibleMemory](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified
- [Memory protection bits 0x100 and 0x200](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits
- [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored