Files
AnyPS5/core/libs/tests/UnwindCallbackAbi.cpp
Itamar Medyoni 3168727fa4 fix(libc): call guest unwind callbacks with the System V ABI on Windows
_Unwind_Backtrace and _Unwind_ForcedUnwind call the trace and stop
functions a title passes them, and _Unwind_DeleteException calls the
exception_cleanup of the exception it deletes. Their types in
UnwindAbi.hpp had no APS5_VABI, so on Windows these guest functions were
called with the Microsoft x64 convention: the context and the argument
went to rcx and rdx while the guest reads rdi and rsi. vkQuake
(PPSA99010) imports _Unwind_Backtrace and reads each frame in its
callback (db3ec02c5).

The three types are now APS5_VABI, like the callbacks of __cxa_vec_ctor
and the other libc exports, and so is Cleanup, the exception_cleanup of
the exceptions libc throws. An exception thrown by libstdc++ on Windows
(class GNUCC++) has a host cleanup, so _Unwind_DeleteException passes it
to libgcc's _Unwind_DeleteException, which calls that cleanup as before.
Linux and macOS are unchanged.
2026-10-16 16:48:22 +03:00

133 lines
5.3 KiB
C++

#include "prx/libc/include/exceptions/Unwind.hpp"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>
extern "C" _Unwind_Reason_Code APS5_VABI _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn trace, void* argument);
extern "C" _Unwind_Reason_Code APS5_VABI _Unwind_ForcedUnwind_nid_postfix(_Unwind_Exception* exception, _Unwind_Stop_Fn stop, void* argument);
extern "C" _Unwind_Ptr APS5_VABI _Unwind_GetRegionStart_nid_postfix(_Unwind_Context* context);
extern "C" void* APS5_VABI __cxa_begin_catch_nid_postfix(void* exception);
extern "C" void APS5_VABI __cxa_end_catch_nid_postfix();
extern "C" [[gnu::noinline]] void BacktraceFromHere();
extern "C" [[gnu::noinline]] void ForceUnwindFromHere();
namespace {
constexpr std::uint64_t ForeignClass = 0x54455354464f5200;
constexpr std::uint64_t GnuClass = 0x474e5543432b2b00;
using Cleanup = decltype(_Unwind_Exception::exception_cleanup);
struct Call {
int count;
int version;
_Unwind_Action actions;
std::uint64_t exceptionClass;
_Unwind_Exception* exception;
void* argument;
_Unwind_Ptr region;
_Unwind_Reason_Code reason;
};
Call trace;
Call stop;
Call cleanup;
int traceArgument;
int stopArgument;
void Require(bool condition, const char* message) {
if (condition) return;
std::fprintf(stderr, "unwind callback check failed: %s\n", message);
std::exit(1);
}
_Unwind_Reason_Code APS5_VABI Trace(_Unwind_Context* context, void* argument) {
++trace.count;
trace.argument = argument;
trace.region = argument == &traceArgument ? _Unwind_GetRegionStart_nid_postfix(context) : 0;
return _URC_NORMAL_STOP;
}
_Unwind_Reason_Code APS5_VABI Stop(int version, _Unwind_Action actions, std::uint64_t exceptionClass, _Unwind_Exception* exception,
_Unwind_Context* context, void* argument) {
++stop.count;
stop.version = version;
stop.actions = actions;
stop.exceptionClass = exceptionClass;
stop.exception = exception;
stop.argument = argument;
stop.region = argument == &stopArgument ? _Unwind_GetRegionStart_nid_postfix(context) : 0;
return _URC_NORMAL_STOP;
}
void APS5_VABI GuestCleanup(_Unwind_Reason_Code reason, _Unwind_Exception* exception) {
++cleanup.count;
cleanup.reason = reason;
cleanup.exception = exception;
}
void HostCleanup(_Unwind_Reason_Code reason, _Unwind_Exception* exception) {
++cleanup.count;
cleanup.reason = reason;
cleanup.exception = exception;
}
void CheckCleanup(const _Unwind_Exception& exception) {
Require(cleanup.count == 1, "exception_cleanup is called once");
Require(cleanup.reason == _URC_FOREIGN_EXCEPTION_CAUGHT, "exception_cleanup gets _URC_FOREIGN_EXCEPTION_CAUGHT");
Require(cleanup.exception == &exception, "exception_cleanup gets the deleted exception");
}
void CheckDeleteException() {
_Unwind_Exception exception {};
exception.exception_class = ForeignClass;
exception.exception_cleanup = GuestCleanup;
_Unwind_DeleteException_nid_postfix(&exception);
CheckCleanup(exception);
}
void CheckEndCatch(std::uint64_t exceptionClass, Cleanup function) {
_Unwind_Exception exception {};
exception.exception_class = exceptionClass;
exception.exception_cleanup = function;
__cxa_begin_catch_nid_postfix(&exception);
__cxa_end_catch_nid_postfix();
CheckCleanup(exception);
}
}
extern "C" void BacktraceFromHere() {
Require(_Unwind_Backtrace_nid_postfix(Trace, &traceArgument) == _URC_NORMAL_STOP, "_Unwind_Backtrace stops when the callback asks");
Require(trace.count == 1, "_Unwind_Backtrace calls the callback once");
Require(trace.argument == &traceArgument, "the trace callback gets the argument given to _Unwind_Backtrace");
Require(trace.region == reinterpret_cast<_Unwind_Ptr>(&BacktraceFromHere), "the trace callback gets the context of the caller of _Unwind_Backtrace");
}
extern "C" void ForceUnwindFromHere() {
_Unwind_Exception exception {};
exception.exception_class = ForeignClass;
_Unwind_ForcedUnwind_nid_postfix(&exception, Stop, &stopArgument);
Require(stop.count == 1, "_Unwind_ForcedUnwind calls the stop function once");
Require(stop.version == 1, "the stop function gets version 1");
Require(stop.actions == (_UA_CLEANUP_PHASE | _UA_FORCE_UNWIND), "the stop function gets _UA_CLEANUP_PHASE | _UA_FORCE_UNWIND");
Require(stop.exceptionClass == ForeignClass, "the stop function gets the exception class");
Require(stop.exception == &exception, "the stop function gets the exception");
Require(stop.argument == &stopArgument, "the stop function gets the argument given to _Unwind_ForcedUnwind");
Require(stop.region == reinterpret_cast<_Unwind_Ptr>(&ForceUnwindFromHere), "the stop function gets the context of the caller of _Unwind_ForcedUnwind");
}
int main(int argc, char** argv) {
const std::string mode = argc > 1 ? argv[1] : "";
if (mode == "backtrace") BacktraceFromHere();
else if (mode == "forced_unwind") ForceUnwindFromHere();
else if (mode == "delete_exception") CheckDeleteException();
else if (mode == "end_catch") CheckEndCatch(ForeignClass, GuestCleanup);
else if (mode == "end_catch_gnu") CheckEndCatch(GnuClass, reinterpret_cast<Cleanup>(&HostCleanup));
else Require(false, "unknown mode");
std::printf("unwind callback ABI %s test passed\n", mode.c_str());
return 0;
}