fix(libkernel): report guest unwind tables in sceKernelGetModuleInfoForUnwind on Linux
A title that ships its own libc.prx unwinds C++ exceptions with that libc's libunwind, which asks sceKernelGetModuleInfoForUnwind where each frame's eh_frame is. On Linux the call always reported no tables, so no C++ exception thrown in guest code could be caught. For the main executable and .guest.prx images, report the PT_GNU_EH_FRAME tables that sceKernelGetModuleInfoFromAddr finds, as Windows does from .ehmeta. Other host images still report none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8
This commit is contained in:
parent
a291ae0f88
commit
1bd1923718
|
|
@ -428,6 +428,17 @@ 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:$<TARGET_FILE_DIR:libkernel>;PATH=path_list_prepend:$<TARGET_FILE_DIR:guest_unwind_module_info_fixture>")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_library(guest_unwind_module_info_fixture SHARED ${LIBS_INCLUDE_DIR}/tests/GuestModuleFixture.cpp)
|
||||
target_include_directories(guest_unwind_module_info_fixture PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
set_target_properties(guest_unwind_module_info_fixture PROPERTIES PREFIX "" OUTPUT_NAME guest_unwind_module_info_fixture.prx SUFFIX ".guest.prx")
|
||||
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_unwind_module_info_fixture)
|
||||
add_test(NAME guest_unwind_module_info COMMAND guest_unwind_module_info_tests guest_unwind_module_info_fixture.prx)
|
||||
set_tests_properties(guest_unwind_module_info PROPERTIES
|
||||
WORKING_DIRECTORY "$<TARGET_FILE_DIR:guest_unwind_module_info_fixture>")
|
||||
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})
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#include <mach-o/loader.h>
|
||||
#else
|
||||
#include <fstream>
|
||||
#include <string_view>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
|
|
@ -36,6 +37,44 @@ void FillGuestUnwindInfo(const std::uint8_t* base, ModuleInfoForUnwind* info) {
|
|||
info->seg0_size = nt->OptionalHeader.SizeOfImage;
|
||||
}
|
||||
}
|
||||
#elif !defined(__APPLE__)
|
||||
extern "C" int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info);
|
||||
|
||||
namespace {
|
||||
struct ImageSearch {
|
||||
std::uint64_t address;
|
||||
bool relinked;
|
||||
};
|
||||
|
||||
int FindRelinkedImage(dl_phdr_info* image, std::size_t, void* data) {
|
||||
auto& search = *static_cast<ImageSearch*>(data);
|
||||
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 && search.address >= start && search.address - start < header.p_memsz) contains = true;
|
||||
}
|
||||
if (!contains) return 0;
|
||||
const std::string_view name = image->dlpi_name != nullptr ? image->dlpi_name : "";
|
||||
search.relinked = name.empty() || name.ends_with(".guest.prx");
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool IsRelinkedImage(std::uint64_t address) {
|
||||
ImageSearch search{address, false};
|
||||
dl_iterate_phdr(FindRelinkedImage, &search);
|
||||
return search.relinked;
|
||||
}
|
||||
|
||||
void FillGuestUnwindInfo(std::uint64_t address, ModuleInfoForUnwind* info) {
|
||||
ModuleInfoEx module{};
|
||||
module.st_size = sizeof(ModuleInfoEx);
|
||||
if (sceKernelGetModuleInfoFromAddr(address, 2, &module) != 0) return;
|
||||
info->eh_frame_hdr_addr = module.eh_frame_hdr_addr;
|
||||
info->eh_frame_addr = module.eh_frame_addr;
|
||||
info->eh_frame_size = module.eh_frame_size;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
|
|
@ -147,6 +186,7 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn
|
|||
info->eh_frame_size = 0;
|
||||
info->seg0_addr = start;
|
||||
info->seg0_size = end - start;
|
||||
if (IsRelinkedImage(addr)) FillGuestUnwindInfo(addr, info);
|
||||
return 0;
|
||||
}
|
||||
return SCE_KERNEL_ERROR_ESRCH;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <link.h>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info);
|
||||
void* APS5_VABI dlopen_nid_postfix(const char* path, int flags);
|
||||
void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name);
|
||||
int APS5_VABI dlclose_nid_postfix(void* handle);
|
||||
}
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
struct TableSearch {
|
||||
std::uintptr_t address;
|
||||
std::uintptr_t header;
|
||||
bool found;
|
||||
};
|
||||
|
||||
static int FindTables(dl_phdr_info* image, std::size_t, void* data) {
|
||||
auto& search = *static_cast<TableSearch*>(data);
|
||||
bool contains = false;
|
||||
std::uintptr_t header = 0;
|
||||
for (std::uint16_t i = 0; i < image->dlpi_phnum; ++i) {
|
||||
const auto& segment = image->dlpi_phdr[i];
|
||||
const std::uintptr_t start = image->dlpi_addr + segment.p_vaddr;
|
||||
if (segment.p_type == PT_LOAD && search.address >= start && search.address - start < segment.p_memsz) contains = true;
|
||||
if (segment.p_type == PT_GNU_EH_FRAME) header = start;
|
||||
}
|
||||
if (!contains) return 0;
|
||||
search.header = header;
|
||||
search.found = true;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static ModuleInfoForUnwind Query(const void* address) {
|
||||
ModuleInfoForUnwind info{};
|
||||
Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast<std::uint64_t>(address), 1, &info) == 0);
|
||||
Require(info.st_size == sizeof(ModuleInfoForUnwind));
|
||||
Require(reinterpret_cast<std::uint64_t>(address) - info.seg0_addr < info.seg0_size);
|
||||
return info;
|
||||
}
|
||||
|
||||
static void RequireTables(const void* address) {
|
||||
TableSearch search{reinterpret_cast<std::uintptr_t>(address), 0, false};
|
||||
dl_iterate_phdr(FindTables, &search);
|
||||
Require(search.found && search.header != 0);
|
||||
const auto* header = reinterpret_cast<const std::uint8_t*>(search.header);
|
||||
Require(header[0] == 1 && header[1] == 0x1b);
|
||||
std::int32_t offset = 0;
|
||||
std::memcpy(&offset, header + 4, sizeof(offset));
|
||||
const auto info = Query(address);
|
||||
Require(info.eh_frame_hdr_addr == search.header);
|
||||
Require(info.eh_frame_addr == search.header + 4 + static_cast<std::intptr_t>(offset));
|
||||
Require(info.eh_frame_size != 0);
|
||||
}
|
||||
|
||||
static void RequireNoTables(const void* address) {
|
||||
const auto info = Query(address);
|
||||
Require(info.eh_frame_hdr_addr == 0 && info.eh_frame_addr == 0 && info.eh_frame_size == 0);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Require(argc == 2);
|
||||
RequireTables(reinterpret_cast<const void*>(&Query));
|
||||
void* module = dlopen_nid_postfix(argv[1], 2);
|
||||
Require(module != nullptr);
|
||||
const void* add = dlsym_nid_postfix(module, "GuestModuleAdd_nid_postfix");
|
||||
Require(add != nullptr);
|
||||
RequireTables(add);
|
||||
RequireNoTables(reinterpret_cast<const void*>(&dl_iterate_phdr));
|
||||
RequireNoTables(reinterpret_cast<const void*>(&sceKernelGetModuleInfoForUnwind));
|
||||
Require(dlclose_nid_postfix(module) == 0);
|
||||
}
|
||||
|
|
@ -125,7 +125,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) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the 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. Only the main executable and `.guest.prx` images report tables; other host images report none, as 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue