Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/libs/prx/libkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:$<TARGET_FILE_DIR:libkernel>;PATH=path_list_prepend:$<TARGET_FILE_DIR:guest_unwind_module_info_fixture>")
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 $<TARGET_FILE_NAME:guest_module_fixture>)
set_tests_properties(guest_unwind_module_info PROPERTIES WORKING_DIRECTORY "$<TARGET_FILE_DIR:guest_module_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})
Expand Down
101 changes: 60 additions & 41 deletions core/libs/prx/libkernel/Module/src/Module.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <algorithm>
#include <cstdint>
#include <cstddef>
#include <cstring>
Expand All @@ -15,11 +16,9 @@
#include <dlfcn.h>
#include <mach-o/getsect.h>
#include <mach-o/loader.h>
#else
#include <fstream>
#endif

#ifdef _WIN32
#ifndef __APPLE__
namespace {
std::uint64_t ReadEncoded(const std::uint8_t*& p, std::uint8_t encoding) {
const auto application = encoding & 0x70;
Expand All @@ -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<const std::uint8_t*>(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<std::uint64_t>(header);
info->eh_frame_addr = frames;
info->eh_frame_size = static_cast<std::uint64_t>(record + 4 - reinterpret_cast<const std::uint8_t*>(frames));
}

#ifdef _WIN32
void FillGuestUnwindInfo(const std::uint8_t* base, ModuleInfoForUnwind* info) {
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header");
Expand All @@ -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<const std::uint8_t*>(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<std::uint64_t>(header);
info->eh_frame_addr = frames;
info->eh_frame_size = static_cast<std::uint64_t>(record - reinterpret_cast<const std::uint8_t*>(frames));
FillEhFrameInfo(base + rva, base, base + nt->OptionalHeader.SizeOfImage, info);
info->seg0_addr = reinterpret_cast<std::uint64_t>(base);
info->seg0_size = nt->OptionalHeader.SizeOfImage;
return;
}
}
#endif
}
#endif

Expand Down Expand Up @@ -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<Search*>(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<std::uint64_t>(begin, start);
end = std::max<std::uint64_t>(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<const std::uint8_t*>(image->dlpi_addr + frames->p_vaddr), reinterpret_cast<const std::uint8_t*>(begin), reinterpret_cast<const std::uint8_t*>(end), info);
}
search.found = true;
return 1;
}, &search);
if (search.found) return 0;
return SCE_KERNEL_ERROR_ESRCH;
#endif
}
Expand Down
2 changes: 1 addition & 1 deletion core/libs/tests/GuestUnwindModuleInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ int main() {
Require(info.st_size == sizeof(ModuleInfoForUnwind));
Require(info.eh_frame_hdr_addr == reinterpret_cast<std::uint64_t>(&fixture->header));
Require(info.eh_frame_addr == reinterpret_cast<std::uint64_t>(&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<const IMAGE_DOS_HEADER*>(base);
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS64*>(base + dos->e_lfanew);
Expand Down
118 changes: 118 additions & 0 deletions core/libs/tests/GuestUnwindModuleInfoLinux.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/KernelErrors.hpp"
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <dlfcn.h>
#include <filesystem>
#include <link.h>
#include <string>

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<Image*>(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<std::string, std::uint64_t> search{suffix, 0};
dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) {
auto& search = *static_cast<std::pair<std::string, std::uint64_t>*>(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<const std::uint8_t*>(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<std::int64_t>(relative);
Require(info.eh_frame_addr == frames);
auto record = reinterpret_cast<const std::uint8_t*>(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<std::uint64_t>(record + 4 - reinterpret_cast<const std::uint8_t*>(frames)));
Require(info.seg0_addr == image.firstLoad && info.seg0_size == image.firstSize);
}

}

int main(int argc, char** argv) {
Require(argc == 2);
CheckGuest(reinterpret_cast<std::uint64_t>(&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::uint64_t>(&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);
}
2 changes: 1 addition & 1 deletion docs/dev/TechnicalDebt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading