diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 62fa131ed..e22f4e581 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -90,6 +90,7 @@ if(BUILD_TESTING) COMMAND $ 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:$") 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 $) - set_tests_properties(guest_module_info PROPERTIES - WORKING_DIRECTORY "$") + 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 $) + set_tests_properties(guest_module_info PROPERTIES + WORKING_DIRECTORY "$") + if(WIN32) + set_tests_properties(guest_module_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_filesystem_tests ${LIBS_INCLUDE_DIR}/tests/GuestFilesystem.cpp) target_include_directories(guest_filesystem_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 926f25ae5..7fa14394a 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -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 +#include +#else #include #include #include @@ -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::max()) + throw std::runtime_error(std::string("sceKernelGetModuleInfoFromAddr: ") + field + " exceeds 32 bits"); + return static_cast(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 +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(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(value); + case 0x10: return field + static_cast(value); + default: throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_hdr encoding"); + } +} + +template +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(header); + if (bytes[0] != 1) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_hdr version"); + return EncodedValue(contains, header + 4, bytes[1]); +} + +template +std::uint64_t EhFrameHeaderSize(const TContains& contains, std::uintptr_t header) { + const auto* bytes = reinterpret_cast(header); + std::uint64_t size = 4 + EncodedSize(bytes[1]); + if (bytes[2] == 0xff) return size; + const auto count = static_cast(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::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 +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(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(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::max()) - throw std::runtime_error(std::string("sceKernelGetModuleInfoFromAddr: ") + field + " exceeds 32 bits"); - return static_cast(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(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(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(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(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(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(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(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(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(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(address), &memory, sizeof(memory)) != sizeof(memory) || memory.Type != MEM_IMAGE) return false; + const auto base = reinterpret_cast(memory.AllocationBase); + const auto* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; + const auto* nt = reinterpret_cast(base + static_cast(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(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(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(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(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(address), result)) return SCE_KERNEL_ERROR_ESRCH; #else Dl_info symbol{}; link_map* native = nullptr; if (!dladdr1(reinterpret_cast(address), &symbol, reinterpret_cast(&native), RTLD_DL_LINKMAP) || !native) return SCE_KERNEL_ERROR_ESRCH; - ModuleInfoEx result{}; - result.st_size = sizeof(ModuleInfoEx); ImageSearch search{static_cast(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 } } diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp index 5921931c9..797064fa9 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -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(&Require), 0); Require(self.id != info.id && Query(reinterpret_cast(&Query), 0).id == self.id); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2abd1d6eb..794895a5f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -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