diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1fe4e1e79b43..305b16f1b57b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -318,6 +318,13 @@ if(BUILD_TESTING) add_test(NAME guest_unwind_module_info COMMAND guest_unwind_module_info_tests) set_tests_properties(guest_unwind_module_info PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;PATH=path_list_prepend:$") + else() + add_test_executable(guest_unwind_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestUnwindModuleInfoLinux.cpp) + target_include_directories(guest_unwind_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_unwind_module_info_tests PRIVATE libkernel libc) + add_dependencies(guest_unwind_module_info_tests guest_module_fixture) + add_test(NAME guest_unwind_module_info COMMAND guest_unwind_module_info_tests $) + set_tests_properties(guest_unwind_module_info PROPERTIES WORKING_DIRECTORY "$") endif() add_test_executable(guest_sync_on_address_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyncOnAddress.cpp) target_include_directories(guest_sync_on_address_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 466329d8c729..30d2f81cf427 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -15,11 +16,9 @@ #include #include #include -#else -#include #endif -#ifdef _WIN32 +#ifndef __APPLE__ namespace { std::uint64_t ReadEncoded(const std::uint8_t*& p, std::uint8_t encoding) { const auto application = encoding & 0x70; @@ -36,6 +35,26 @@ std::uint64_t ReadEncoded(const std::uint8_t*& p, std::uint8_t encoding) { return value; } +void FillEhFrameInfo(const std::uint8_t* header, const std::uint8_t* begin, const std::uint8_t* end, ModuleInfoForUnwind* info) { + if (header[0] != 1) NotImplemented_nid_no_patch("sceKernelGetModuleInfoForUnwind eh_frame_hdr version other than 1"); + const auto* p = header + 4; + const auto frames = ReadEncoded(p, header[1]); + const auto* record = reinterpret_cast(frames); + if (record < begin || record >= end) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: eh_frame_ptr outside the image"); + for (;;) { + if (record + 4 > end) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: eh_frame has no terminator inside the image"); + std::uint32_t length = 0; + std::memcpy(&length, record, 4); + if (length == 0) break; + if (length == 0xffffffffu) NotImplemented_nid_no_patch("sceKernelGetModuleInfoForUnwind eh_frame record with a 64-bit length"); + record += 4 + length; + } + info->eh_frame_hdr_addr = reinterpret_cast(header); + info->eh_frame_addr = frames; + info->eh_frame_size = static_cast(record + 4 - reinterpret_cast(frames)); +} + +#ifdef _WIN32 void FillGuestUnwindInfo(const std::uint8_t* base, ModuleInfoForUnwind* info) { const auto* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); @@ -46,29 +65,13 @@ void FillGuestUnwindInfo(const std::uint8_t* base, ModuleInfoForUnwind* info) { if (std::memcmp(sections[i].Name, ".ehmeta", 8) != 0) continue; std::uint32_t rva = 0; std::memcpy(&rva, base + sections[i].VirtualAddress, 4); - const auto* header = base + rva; - if (header[0] != 1) NotImplemented_nid_no_patch("sceKernelGetModuleInfoForUnwind eh_frame_hdr version other than 1"); - const auto* p = header + 4; - const auto frames = ReadEncoded(p, header[1]); - const auto* record = reinterpret_cast(frames); - const auto* end = base + nt->OptionalHeader.SizeOfImage; - if (record < base || record >= end) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: eh_frame_ptr outside the image"); - for (;;) { - if (record + 4 > end) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: eh_frame has no terminator inside the image"); - std::uint32_t length = 0; - std::memcpy(&length, record, 4); - if (length == 0) break; - if (length == 0xffffffffu) NotImplemented_nid_no_patch("sceKernelGetModuleInfoForUnwind eh_frame record with a 64-bit length"); - record += 4 + length; - } - info->eh_frame_hdr_addr = reinterpret_cast(header); - info->eh_frame_addr = frames; - info->eh_frame_size = static_cast(record - reinterpret_cast(frames)); + FillEhFrameInfo(base + rva, base, base + nt->OptionalHeader.SizeOfImage, info); info->seg0_addr = reinterpret_cast(base); info->seg0_size = nt->OptionalHeader.SizeOfImage; return; } } +#endif } #endif @@ -157,32 +160,48 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn info->seg0_size = textSize; return 0; #else - std::ifstream maps("/proc/self/maps"); - if (!maps) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: failed to open /proc/self/maps"); - std::string line; - while (std::getline(maps, line)) { - std::uint64_t start = 0; + struct Search { + std::uint64_t address; + ModuleInfoForUnwind* info; + bool found; + } search {addr, info, false}; + dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) { + auto& search = *static_cast(data); + const Elf64_Phdr* first = nullptr; + const Elf64_Phdr* frames = nullptr; + std::uint64_t begin = UINT64_MAX; std::uint64_t end = 0; - char perms[8] = {}; - std::uint64_t offset = 0; - unsigned int devMajor = 0; - unsigned int devMinor = 0; - std::uint64_t inode = 0; - char path[4096] = {}; - int parsed = std::sscanf(line.c_str(), "%llx-%llx %7s %llx %x:%x %llu %4095s", - (unsigned long long*)&start, (unsigned long long*)&end, perms, - (unsigned long long*)&offset, &devMajor, &devMinor, (unsigned long long*)&inode, path); - if (parsed < 7 || addr < start || addr >= end) continue; + bool contains = false; + for (std::uint16_t index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + const auto start = image->dlpi_addr + header.p_vaddr; + if (header.p_type == PT_LOAD) { + if (first == nullptr) first = &header; + begin = std::min(begin, start); + end = std::max(end, start + header.p_memsz); + if (search.address >= start && search.address - start < header.p_memsz) contains = true; + } + if (header.p_type == PT_GNU_EH_FRAME) frames = &header; + } + if (!contains) return 0; + auto* info = search.info; + const std::string name = image->dlpi_name != nullptr ? image->dlpi_name : ""; info->st_size = sizeof(ModuleInfoForUnwind); - std::strncpy(info->name, parsed >= 8 ? path : "", sizeof(info->name) - 1); + std::strncpy(info->name, name.c_str(), sizeof(info->name) - 1); info->name[sizeof(info->name) - 1] = '\0'; info->eh_frame_hdr_addr = 0; info->eh_frame_addr = 0; info->eh_frame_size = 0; - info->seg0_addr = start; - info->seg0_size = end - start; - return 0; - } + info->seg0_addr = image->dlpi_addr + first->p_vaddr; + info->seg0_size = first->p_memsz; + const bool guest = name.empty() || name.ends_with(".guest.prx"); + if (guest && frames != nullptr) { + FillEhFrameInfo(reinterpret_cast(image->dlpi_addr + frames->p_vaddr), reinterpret_cast(begin), reinterpret_cast(end), info); + } + search.found = true; + return 1; + }, &search); + if (search.found) return 0; return SCE_KERNEL_ERROR_ESRCH; #endif } diff --git a/core/libs/tests/GuestUnwindModuleInfo.cpp b/core/libs/tests/GuestUnwindModuleInfo.cpp index b5af722f4936..5a3e65be6aa8 100644 --- a/core/libs/tests/GuestUnwindModuleInfo.cpp +++ b/core/libs/tests/GuestUnwindModuleInfo.cpp @@ -33,7 +33,7 @@ int main() { Require(info.st_size == sizeof(ModuleInfoForUnwind)); Require(info.eh_frame_hdr_addr == reinterpret_cast(&fixture->header)); Require(info.eh_frame_addr == reinterpret_cast(&fixture->frames)); - Require(info.eh_frame_size == 4 + 12); + Require(info.eh_frame_size == 4 + 12 + 4); Require(info.seg0_addr == base); const auto* dos = reinterpret_cast(base); const auto* nt = reinterpret_cast(base + dos->e_lfanew); diff --git a/core/libs/tests/GuestUnwindModuleInfoLinux.cpp b/core/libs/tests/GuestUnwindModuleInfoLinux.cpp new file mode 100644 index 000000000000..25bb525c8f05 --- /dev/null +++ b/core/libs/tests/GuestUnwindModuleInfoLinux.cpp @@ -0,0 +1,118 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info); + +static void Require(bool value) { if (!value) std::abort(); } + +namespace { + +struct Image { + std::uint64_t address = 0; + std::string name; + std::uint64_t firstLoad = 0; + std::uint64_t firstSize = 0; + std::uint64_t frameHeader = 0; + bool found = false; +}; + +Image Find(std::uint64_t address) { + Image image; + image.address = address; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + auto& image = *static_cast(data); + const ElfW(Phdr)* first = nullptr; + const ElfW(Phdr)* frames = nullptr; + bool contains = false; + for (ElfW(Half) index = 0; index < info->dlpi_phnum; ++index) { + const auto& header = info->dlpi_phdr[index]; + const auto start = info->dlpi_addr + header.p_vaddr; + if (header.p_type == PT_LOAD && first == nullptr) first = &header; + if (header.p_type == PT_LOAD && image.address >= start && image.address - start < header.p_memsz) contains = true; + if (header.p_type == PT_GNU_EH_FRAME) frames = &header; + } + if (!contains) return 0; + image.name = info->dlpi_name != nullptr ? info->dlpi_name : ""; + image.firstLoad = info->dlpi_addr + first->p_vaddr; + image.firstSize = first->p_memsz; + image.frameHeader = frames != nullptr ? info->dlpi_addr + frames->p_vaddr : 0; + image.found = true; + return 1; + }, &image); + Require(image.found); + return image; +} + +std::uint64_t FirstLoadOf(const std::string& suffix) { + std::pair search{suffix, 0}; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + auto& search = *static_cast*>(data); + const std::string name = info->dlpi_name != nullptr ? info->dlpi_name : ""; + if (!name.ends_with(search.first)) return 0; + for (ElfW(Half) index = 0; index < info->dlpi_phnum; ++index) { + if (info->dlpi_phdr[index].p_type != PT_LOAD) continue; + search.second = info->dlpi_addr + info->dlpi_phdr[index].p_vaddr; + return 1; + } + return 0; + }, &search); + Require(search.second != 0); + return search.second; +} + +void CheckGuest(std::uint64_t address) { + const auto image = Find(address); + Require(image.frameHeader != 0); + ModuleInfoForUnwind info{}; + Require(sceKernelGetModuleInfoForUnwind(address, 1, &info) == 0); + Require(info.st_size == sizeof(ModuleInfoForUnwind)); + Require(info.eh_frame_hdr_addr == image.frameHeader); + const auto* header = reinterpret_cast(image.frameHeader); + Require(header[0] == 1 && header[1] == 0x1b); + std::int32_t relative = 0; + std::memcpy(&relative, header + 4, 4); + const auto frames = image.frameHeader + 4 + static_cast(relative); + Require(info.eh_frame_addr == frames); + auto record = reinterpret_cast(frames); + for (;;) { + std::uint32_t length = 0; + std::memcpy(&length, record, 4); + if (length == 0) break; + record += 4 + length; + } + Require(info.eh_frame_size == static_cast(record + 4 - reinterpret_cast(frames))); + Require(info.seg0_addr == image.firstLoad && info.seg0_size == image.firstSize); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2); + CheckGuest(reinterpret_cast(&main)); + + const auto guest = std::filesystem::absolute("anyps5-unwind-info-module-for-test.guest.prx"); + std::filesystem::copy_file(argv[1], guest, std::filesystem::copy_options::overwrite_existing); + void* module = ::dlopen(guest.c_str(), RTLD_NOW | RTLD_LOCAL); + Require(module != nullptr); + CheckGuest(FirstLoadOf(".guest.prx")); + Require(::dlclose(module) == 0); + std::filesystem::remove(guest); + + const auto host = reinterpret_cast(&std::abort); + const auto hostImage = Find(host); + Require(!hostImage.name.empty() && hostImage.frameHeader != 0); + ModuleInfoForUnwind info{}; + Require(sceKernelGetModuleInfoForUnwind(host, 1, &info) == 0); + Require(info.eh_frame_hdr_addr == 0 && info.eh_frame_addr == 0 && info.eh_frame_size == 0); + Require(info.seg0_addr == hostImage.firstLoad && info.seg0_size == hostImage.firstSize); + + Require(sceKernelGetModuleInfoForUnwind(0x10000, 1, &info) == SCE_KERNEL_ERROR_ESRCH); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8f2178189d5b..fa36dcbb118c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -117,7 +117,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDcbPrimeUtcl2](../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [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. +- [sceKernelGetModuleInfoForUnwind](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - `flags` is ignored and `name` is filled with the host path. On a PS5 (firmware 13.60), only flags 1 succeeds for an address in a loaded module: 0, 3 and 8 return `0x80020016`, 2 returns `0x80020003`. A failed call zeroes the whole structure, and a successful one leaves `name` empty. - [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 - [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