diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 939ec911b..b9963ffe5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -161,6 +161,20 @@ jobs: - run: cmake --build build-relinker --parallel 3 - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 + register-context: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-15-intel] + configuration: [Debug, Release] + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + - run: cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} + - run: cmake --build build/register-context --parallel + - run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error + linux: needs: eligibility if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }} diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 67d64bd44..08d015556 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -156,6 +156,11 @@ if(BUILD_TESTING) set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + if(UNIX AND (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$" OR CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64")) + add_subdirectory(tests/register_context) + endif() + add_test_executable(savedata_memory_metadata_tests tests/SaveDataMemoryMetadata.cpp) target_include_directories(savedata_memory_metadata_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(savedata_memory_metadata_tests PRIVATE libSceSaveData.native) diff --git a/core/libs/prx/libc/src/specifics/x86_64/RegisterContext.cpp b/core/libs/prx/libc/src/specifics/x86_64/RegisterContext.cpp index aece550d8..b4538a0cf 100644 --- a/core/libs/prx/libc/src/specifics/x86_64/RegisterContext.cpp +++ b/core/libs/prx/libc/src/specifics/x86_64/RegisterContext.cpp @@ -4,27 +4,39 @@ extern "C" void LibcCaptureRegisters(std::uintptr_t*); extern "C" [[noreturn]] void LibcRestoreRegisters(const std::uintptr_t*); -#if defined(__linux__) && defined(__x86_64__) +#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) asm( ".text\n" +#ifdef __APPLE__ +".private_extern _LibcCaptureRegisters\n" +"_LibcCaptureRegisters:\n" +#else ".hidden LibcCaptureRegisters\n" ".type LibcCaptureRegisters,@function\n" "LibcCaptureRegisters:\n" +#endif "movq %rax,0(%rdi)\nmovq %rdx,8(%rdi)\nmovq %rcx,16(%rdi)\nmovq %rbx,24(%rdi)\n" "movq %rsi,32(%rdi)\nmovq %rdi,40(%rdi)\nmovq %rbp,48(%rdi)\n" "leaq 8(%rsp),%rax\nmovq %rax,56(%rdi)\n" "movq %r8,64(%rdi)\nmovq %r9,72(%rdi)\nmovq %r10,80(%rdi)\nmovq %r11,88(%rdi)\n" "movq %r12,96(%rdi)\nmovq %r13,104(%rdi)\nmovq %r14,112(%rdi)\nmovq %r15,120(%rdi)\n" "movq (%rsp),%rax\nmovq %rax,128(%rdi)\nret\n" +#ifdef __APPLE__ +".private_extern _LibcRestoreRegisters\n" +"_LibcRestoreRegisters:\n" +#else ".size LibcCaptureRegisters,.-LibcCaptureRegisters\n" ".hidden LibcRestoreRegisters\n.type LibcRestoreRegisters,@function\n" "LibcRestoreRegisters:\n" +#endif "movq %rdi,%r10\nmovq 128(%r10),%r11\n" "movq 0(%r10),%rax\nmovq 8(%r10),%rdx\nmovq 16(%r10),%rcx\nmovq 24(%r10),%rbx\n" "movq 32(%r10),%rsi\nmovq 40(%r10),%rdi\nmovq 48(%r10),%rbp\nmovq 56(%r10),%rsp\n" "movq 64(%r10),%r8\nmovq 72(%r10),%r9\nmovq 96(%r10),%r12\n" "movq 104(%r10),%r13\nmovq 112(%r10),%r14\nmovq 120(%r10),%r15\njmp *%r11\n" +#ifndef __APPLE__ ".size LibcRestoreRegisters,.-LibcRestoreRegisters\n" +#endif ); #elif defined(_WIN32) && defined(__x86_64__) asm( diff --git a/core/libs/tests/RegisterContext.cpp b/core/libs/tests/RegisterContext.cpp new file mode 100644 index 000000000..28c22057c --- /dev/null +++ b/core/libs/tests/RegisterContext.cpp @@ -0,0 +1,156 @@ +#include "prx/libc/include/exceptions/Unwind.hpp" +#include "prx/libc/src/specifics/x86_64/RegisterContext.cpp" + +#include +#include +#include +#include +#include + +#if !defined(__x86_64__) || (!defined(__linux__) && !defined(__APPLE__)) +#error Register context probes require an x86-64 System V host +#endif + +static_assert(sizeof(_Unwind_Context::registers) == 17 * sizeof(std::uintptr_t)); +static_assert(offsetof(_Unwind_Context, cfa) == 17 * sizeof(std::uintptr_t)); + +extern "C" void CaptureProbe(std::uintptr_t*); +extern "C" void RestoreProbe(std::uintptr_t*, std::uintptr_t*); + +#ifdef __APPLE__ +#define CONTEXT_SYMBOL(name) "_" #name +#else +#define CONTEXT_SYMBOL(name) #name +#endif + +asm( +".text\n" +".globl " CONTEXT_SYMBOL(CaptureProbe) "\n" +CONTEXT_SYMBOL(CaptureProbe) ":\n" +"pushq %rbp\npushq %rbx\npushq %r12\npushq %r13\npushq %r14\npushq %r15\n" +"subq $24,%rsp\n" +"movq %rsp,136(%rdi)\n" +"leaq 1f(%rip),%rax\nmovq %rax,144(%rdi)\n" +"movabsq $0x1234567800000100,%rax\n" +"movabsq $0x1234567800000101,%rdx\n" +"movabsq $0x1234567800000102,%rcx\n" +"movabsq $0x1234567800000103,%rbx\n" +"movabsq $0x1234567800000104,%rsi\n" +"movabsq $0x1234567800000106,%rbp\n" +"movabsq $0x1234567800000108,%r8\n" +"movabsq $0x1234567800000109,%r9\n" +"movabsq $0x123456780000010a,%r10\n" +"movabsq $0x123456780000010b,%r11\n" +"movabsq $0x123456780000010c,%r12\n" +"movabsq $0x123456780000010d,%r13\n" +"movabsq $0x123456780000010e,%r14\n" +"movabsq $0x123456780000010f,%r15\n" +"call " CONTEXT_SYMBOL(LibcCaptureRegisters) "\n" +"1:\n" +"addq $24,%rsp\n" +"popq %r15\npopq %r14\npopq %r13\npopq %r12\npopq %rbx\npopq %rbp\nret\n" +".globl " CONTEXT_SYMBOL(RestoreProbe) "\n" +CONTEXT_SYMBOL(RestoreProbe) ":\n" +"pushq %rbp\npushq %rbx\npushq %r12\npushq %r13\npushq %r14\npushq %r15\n" +"subq $24,%rsp\n" +"movq %rsi,0(%rsp)\nmovq %rsp,56(%rdi)\n" +"leaq 2f(%rip),%rax\nmovq %rax,128(%rdi)\n" +"call LRegisterContextRestoreNested\nud2\n" +"2:\n" +"movq 0(%rsp),%r10\n" +"movq %rax,0(%r10)\nmovq %rdx,8(%r10)\nmovq %rcx,16(%r10)\nmovq %rbx,24(%r10)\n" +"movq %rsi,32(%r10)\nmovq %rdi,40(%r10)\nmovq %rbp,48(%r10)\nmovq %rsp,56(%r10)\n" +"movq %r8,64(%r10)\nmovq %r9,72(%r10)\n" +"movq %r12,96(%r10)\nmovq %r13,104(%r10)\nmovq %r14,112(%r10)\nmovq %r15,120(%r10)\n" +"leaq 2b(%rip),%r11\nmovq %r11,128(%r10)\n" +"addq $24,%rsp\n" +"popq %r15\npopq %r14\npopq %r13\npopq %r12\npopq %rbx\npopq %rbp\nret\n" +"LRegisterContextRestoreNested:\n" +"subq $200,%rsp\n" +"xorq %rbx,%rbx\nxorq %rbp,%rbp\nxorq %r12,%r12\nxorq %r13,%r13\nxorq %r14,%r14\nxorq %r15,%r15\n" +"call " CONTEXT_SYMBOL(LibcRestoreRegisters) "\nud2\n" +); + +#undef CONTEXT_SYMBOL + +namespace { + +constexpr std::uintptr_t Guard = 0xa1b2c3d4e5f60718; + +struct Capture { + std::uintptr_t before = Guard; + std::array registers{}; + std::uintptr_t expectedStack = 0; + std::uintptr_t expectedInstruction = 0; + std::uintptr_t after = Guard; +}; + +static_assert(offsetof(Capture, expectedStack) - offsetof(Capture, registers) == 136); +static_assert(offsetof(Capture, expectedInstruction) - offsetof(Capture, registers) == 144); + +struct Registers { + std::uintptr_t before = Guard; + std::array values{}; + std::uintptr_t after = Guard; +}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckCapture() { + Capture captured; + CaptureProbe(captured.registers.data()); + Require(captured.before == Guard && captured.after == Guard, "capture wrote outside its register context"); + for (std::size_t i = 0; i < 16; ++i) { + const auto expected = i == 5 ? reinterpret_cast(captured.registers.data()) + : i == 7 ? captured.expectedStack : 0x1234567800000100 + i; + Require(captured.registers[i] == expected, "capture saved the wrong general register"); + } + Require(captured.registers[16] == captured.expectedInstruction, "capture saved the wrong return instruction"); + Require(captured.registers[7] % 16 == 0, "capture saved the callee stack instead of the caller stack"); +} + +void CheckRestore(std::uintptr_t seed) { + Registers restored; + Registers observed; + for (std::size_t i = 0; i < restored.values.size(); ++i) { + restored.values[i] = (0xfedcba9800000000 | (seed << 8) | i); + } + RestoreProbe(restored.values.data(), observed.values.data()); + Require(restored.before == Guard && restored.after == Guard, "restore overwrote its source context"); + Require(observed.before == Guard && observed.after == Guard, "restore probe overwrote its destination context"); + for (std::size_t i = 0; i < restored.values.size(); ++i) { + if (i == 10 || i == 11) continue; + Require(observed.values[i] == restored.values[i], "restore did not install the landing pad context"); + } +} + +void CheckThread(std::uintptr_t seed) { + for (std::uintptr_t i = 0; i < 256; ++i) { + CheckCapture(); + CheckRestore(seed + i); + } +} + +} + +int main(int argc, char** argv) { + try { + Require(argc == 2, "expected capture, restore or threads"); + const std::string_view mode(argv[1]); + if (mode == "capture") CheckCapture(); + else if (mode == "restore") { + for (std::uintptr_t seed = 0; seed < 256; ++seed) CheckRestore(seed); + } else if (mode == "threads") { + std::array threads; + for (std::size_t i = 0; i < threads.size(); ++i) threads[i] = std::thread(CheckThread, i * 256); + for (auto& thread : threads) thread.join(); + } else throw std::runtime_error("unknown register context probe"); + std::printf("register context %s passed\n", argv[1]); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/register_context/CMakeLists.txt b/core/libs/tests/register_context/CMakeLists.txt new file mode 100644 index 000000000..eedbc80e3 --- /dev/null +++ b/core/libs/tests/register_context/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.20) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(RegisterContextTests LANGUAGES CXX) + enable_testing() + set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +endif() + +find_package(Threads REQUIRED) +add_executable(register_context_tests ../RegisterContext.cpp) +target_include_directories(register_context_tests PRIVATE ../..) +target_compile_features(register_context_tests PRIVATE cxx_std_20) +target_link_libraries(register_context_tests PRIVATE Threads::Threads) +set_target_properties(register_context_tests PROPERTIES + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + +foreach(mode capture restore threads) + add_test(NAME register_context_${mode} COMMAND register_context_tests ${mode}) + set_tests_properties(register_context_${mode} PROPERTIES TIMEOUT 15) +endforeach() diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 943f6e03c..823d7f549 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -78,3 +78,15 @@ Set `APS5_PIPELINE_STATS=1` to capture and print driver statistics for each newl ## Shader recompiler The shader recompilation logic in [core/shader/recompiler](../../core/shader/recompiler) is isolated from the rest of the project and is a pure function of its input data, designed for integration into any other project. The current CMake target also includes cache support and links a supplied runtime target, glslang, and optionally SPIRV-Tools. + +## Register context tests + +The x86-64 register capture and restore tests can be built separately on Linux and macOS, without submodules or the prx libraries. On macOS: + +```sh +cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 +cmake --build build/register-context +ctest --test-dir build/register-context --output-on-failure +``` + +On Linux x86-64, omit `CMAKE_OSX_ARCHITECTURES`; the full `BUILD_TESTING` build also registers these tests. Apple silicon runs the x86-64 tests through Rosetta. They cover the unwinder's register transfer, not the complete macOS runtime or guest exception handling.