feat(libcinternal): run thread_local destructors at thread exit
A title's own libc.prx implements __cxa_thread_atexit and the destructor pass of exit() through _sceLibcInternalThreadAtexit and _sceLibcInternalThreadDtors. Its __cxa_finalize calls _sceLibcInternalForceTlsDestructor. Destructors are kept per thread and run newest first, including those registered while they run, as FreeBSD's __cxa_thread_call_dtors does. A null destructor or one outside every loaded image throws instead of being skipped. The first registration installs the pass as the libkernel thread exit hook, so it runs on every guest thread before the thread can be joined. _sceLibcInternalForceTlsDestructor throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e248f404ef
commit
2dea7164e3
|
|
@ -0,0 +1,23 @@
|
|||
add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp)
|
||||
target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS})
|
||||
configure_windows_unwind(libSceLibcInternal)
|
||||
|
||||
if(MINGW)
|
||||
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
|
||||
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_thread_atexit_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadAtexit.cpp)
|
||||
target_include_directories(guest_thread_atexit_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_thread_atexit_tests PRIVATE libSceLibcInternal libkernel libc)
|
||||
configure_windows_unwind(guest_thread_atexit_tests)
|
||||
add_test(NAME guest_thread_atexit COMMAND guest_thread_atexit_tests)
|
||||
set_tests_properties(guest_thread_atexit PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_thread_atexit PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibcInternal>")
|
||||
endif()
|
||||
endif()
|
||||
|
|
@ -1,5 +1,69 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include "prx/libc/include/HeapDiagnostics.hpp"
|
||||
#include "prx/libc/include/HostThreadLocal.hpp"
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
extern "C" void APS5_VABI sceKernelSetThreadDtors(thread_dtors_func_t dtors);
|
||||
|
||||
namespace {
|
||||
|
||||
using ThreadDestructorFunction = void (APS5_VABI*)(void*);
|
||||
|
||||
struct ThreadDestructor {
|
||||
ThreadDestructorFunction function;
|
||||
void* object;
|
||||
void* dsoSymbol;
|
||||
};
|
||||
|
||||
struct ThreadDestructorsTag {};
|
||||
|
||||
std::vector<ThreadDestructor>& ThreadDestructors() {
|
||||
return HostThreadLocal<std::vector<ThreadDestructor>, ThreadDestructorsTag>();
|
||||
}
|
||||
|
||||
bool IsInLoadedImage(const void* address) {
|
||||
if (address == nullptr)
|
||||
return false;
|
||||
#ifdef _WIN32
|
||||
HMODULE module = nullptr;
|
||||
return GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, static_cast<LPCSTR>(address), &module) != 0;
|
||||
#else
|
||||
Dl_info info{};
|
||||
return dladdr(address, &info) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void APS5_VABI RunThreadDestructors_nid_no_patch() {
|
||||
auto& destructors = ThreadDestructors();
|
||||
while (!destructors.empty()) {
|
||||
const ThreadDestructor destructor = destructors.back();
|
||||
destructors.pop_back();
|
||||
const auto* function = reinterpret_cast<const void*>(destructor.function);
|
||||
if (!IsInLoadedImage(function)) {
|
||||
std::ostringstream message;
|
||||
message << "thread_local destructor " << function << " of dso " << destructor.dsoSymbol << " is not in a loaded image";
|
||||
throw std::runtime_error(message.str());
|
||||
}
|
||||
destructor.function(destructor.object);
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterThreadExitHook() {
|
||||
[[maybe_unused]] static const bool registered = [] {
|
||||
sceKernelSetThreadDtors(RunThreadDestructors_nid_no_patch);
|
||||
return true;
|
||||
}();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
|
|
@ -13,4 +77,20 @@ void APS5_VABI sceLibcHeapGetTraceInfo_nid_postfix(Info* info) {
|
|||
LibcHeapTraceInfo_nid_no_patch(info);
|
||||
}
|
||||
|
||||
int APS5_VABI _sceLibcInternalThreadAtexit_nid_postfix(ThreadDestructorFunction destructor, void* object, void* dsoSymbol) {
|
||||
RegisterThreadExitHook();
|
||||
ThreadDestructors().push_back({destructor, object, dsoSymbol});
|
||||
return 0;
|
||||
}
|
||||
|
||||
void APS5_VABI _sceLibcInternalThreadDtors_nid_postfix() {
|
||||
RunThreadDestructors_nid_no_patch();
|
||||
}
|
||||
|
||||
int APS5_VABI _sceLibcInternalForceTlsDestructor_nid_postfix(KernelModule handle) {
|
||||
(void)handle;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name);
|
||||
int APS5_VABI scePthreadJoin(Pthread thread, void** retval);
|
||||
Pthread APS5_VABI scePthreadSelf();
|
||||
int APS5_VABI _sceLibcInternalThreadAtexit_nid_postfix(void (APS5_VABI* destructor)(void*), void* object, void* dsoSymbol);
|
||||
void APS5_VABI _sceLibcInternalThreadDtors_nid_postfix();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using Destructor = void (APS5_VABI*)(void*);
|
||||
|
||||
void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
int dsoHandle = 0;
|
||||
std::vector<std::intptr_t> calls;
|
||||
std::vector<Pthread> callers;
|
||||
|
||||
void* Object(std::intptr_t value) { return reinterpret_cast<void*>(value); }
|
||||
|
||||
int Register(Destructor destructor, std::intptr_t object) {
|
||||
return _sceLibcInternalThreadAtexit_nid_postfix(destructor, Object(object), &dsoHandle);
|
||||
}
|
||||
|
||||
void APS5_VABI Record(void* object) {
|
||||
calls.push_back(reinterpret_cast<std::intptr_t>(object));
|
||||
callers.push_back(scePthreadSelf());
|
||||
}
|
||||
|
||||
void APS5_VABI RecordAndRegister(void* object) {
|
||||
Record(object);
|
||||
Require(Register(Record, 9) == 0);
|
||||
}
|
||||
|
||||
void* APS5_VABI Worker(void*) {
|
||||
Require(Register(Record, 4) == 0);
|
||||
Require(Register(Record, 5) == 0);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool DestructorsThrow() {
|
||||
try {
|
||||
_sceLibcInternalThreadDtors_nid_postfix();
|
||||
} catch (const std::runtime_error&) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
const Pthread mainThread = scePthreadSelf();
|
||||
Require(Register(Record, 1) == 0);
|
||||
Require(Register(RecordAndRegister, 2) == 0);
|
||||
Require(Register(Record, 3) == 0);
|
||||
_sceLibcInternalThreadDtors_nid_postfix();
|
||||
Require(calls == std::vector<std::intptr_t>{3, 2, 9, 1});
|
||||
Require(callers == std::vector<Pthread>(4, mainThread));
|
||||
_sceLibcInternalThreadDtors_nid_postfix();
|
||||
Require(calls.size() == 4);
|
||||
|
||||
calls.clear();
|
||||
callers.clear();
|
||||
Pthread worker = nullptr;
|
||||
Require(scePthreadCreate(&worker, nullptr, Worker, nullptr, "ThreadAtexit") == 0);
|
||||
Require(scePthreadJoin(worker, nullptr) == 0);
|
||||
Require(calls == std::vector<std::intptr_t>{5, 4});
|
||||
Require(callers == std::vector<Pthread>(2, worker));
|
||||
|
||||
calls.clear();
|
||||
std::vector<unsigned char> heap(64);
|
||||
Require(Register(Record, 6) == 0);
|
||||
Require(Register(nullptr, 7) == 0);
|
||||
Require(Register(reinterpret_cast<Destructor>(heap.data()), 8) == 0);
|
||||
Require(DestructorsThrow());
|
||||
Require(DestructorsThrow());
|
||||
Require(calls.empty());
|
||||
_sceLibcInternalThreadDtors_nid_postfix();
|
||||
Require(calls == std::vector<std::intptr_t>{6});
|
||||
}
|
||||
|
|
@ -44,6 +44,8 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 unknown; only the sceKernelMapNamedFlexibleMemory flags are accepted
|
||||
- [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature
|
||||
- [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature
|
||||
- [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw
|
||||
- [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - unknown return type and behaviour; a title's `libc.prx` calls it from `__cxa_finalize` with the module handle of a non-null dso
|
||||
- [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export
|
||||
- [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED throws: FreeBSD 9.0 libthr rejects it with EINVAL and 11.0 accepts it, and which one the console follows is unknown
|
||||
- [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature
|
||||
|
|
@ -101,3 +103,4 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it.
|
||||
- [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified.
|
||||
- [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware.
|
||||
- [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped
|
||||
|
|
|
|||
Loading…
Reference in New Issue