From 3953a8d49eff0f0772aa7550865084826fe3af7b Mon Sep 17 00:00:00 2001 From: codingreapermaps <233954859+codingreapermaps@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:53:04 +0100 Subject: [PATCH] feat(libc): record traced import calls on Windows TraceImport_nid_no_patch wraps a function of a library named in APS5_TRACE_CALLS in a thunk that records the import, the thread and the caller in a ring and counts the call, then jumps to the function. One target keeps one thunk, so function pointers compare equal. The crash and abort reports print the last 64 calls and the 32 most called imports. Nothing calls TraceImport_nid_no_patch yet. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 13 ++ core/libs/prx/libc/include/CallTrace.hpp | 27 +++ .../libc/src/specifics/windows/CallTrace.cpp | 202 ++++++++++++++++++ .../src/specifics/windows/CrashReport.cpp | 26 +++ core/libs/tests/GuestCallTrace.cpp | 129 +++++++++++ docs/dev/debt/libc.md | 1 + 6 files changed, 398 insertions(+) create mode 100644 core/libs/prx/libc/include/CallTrace.hpp create mode 100644 core/libs/prx/libc/src/specifics/windows/CallTrace.cpp create mode 100644 core/libs/tests/GuestCallTrace.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 4cfa6a288..8f2342a25 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/CreationMask.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CallTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp @@ -411,6 +412,18 @@ if(BUILD_TESTING) set_tests_properties(guest_errors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test_executable(guest_call_trace_tests ${LIBS_INCLUDE_DIR}/tests/GuestCallTrace.cpp) + target_include_directories(guest_call_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_call_trace_tests PRIVATE libc) + configure_windows_unwind(guest_call_trace_tests) + add_test(NAME guest_call_trace COMMAND guest_call_trace_tests) + add_test(NAME guest_call_trace_abort COMMAND guest_call_trace_tests abort) + set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_call_trace_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests.exe[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_syslog_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/CallTrace.hpp b/core/libs/prx/libc/include/CallTrace.hpp new file mode 100644 index 000000000..2c1f46c60 --- /dev/null +++ b/core/libs/prx/libc/include/CallTrace.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CALLTRACE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CALLTRACE_HPP + +#include +#include + +struct CallTraceEntry { + std::uint64_t sequence; + std::uint64_t thread; + std::uint64_t returnAddress; + const char* name; + const char* library; +}; + +struct CallTraceCount { + std::uint64_t calls; + const char* name; + const char* library; +}; + +extern "C" { +void* TraceImport_nid_no_patch(void* target, const char* name, const char* library); +std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity); +std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total); +} + +#endif diff --git a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp b/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp new file mode 100644 index 000000000..7e3097145 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp @@ -0,0 +1,202 @@ +#ifdef _WIN32 +#include +#include "prx/libc/include/CallTrace.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" void CallTraceEnter_nid_no_patch(); + +namespace { + +struct ImportRecord { + void* target; + std::string name; + std::string library; + mutable std::atomic calls{0}; +}; + +struct RingSlot { + std::atomic sequence{0}; + std::atomic thread{0}; + std::atomic returnAddress{0}; + std::atomic record{nullptr}; +}; + +constexpr std::size_t RingCapacity = 65536; +constexpr std::size_t ThunkBytes = 24; +constexpr std::size_t ThunkAreaBytes = 1024 * 1024; + +std::mutex g_mutex; +std::deque g_records; +std::unordered_map g_thunkByTarget; +std::unique_ptr g_ring; +std::atomic g_sequence{0}; +std::uint8_t* g_writable = nullptr; +std::uint8_t* g_executable = nullptr; +std::size_t g_thunks = 0; + +bool Selected(std::string_view library) { + const char* value = std::getenv("APS5_TRACE_CALLS"); + if (value == nullptr || *value == '\0') return false; + std::string_view rest(value); + if (library.ends_with(".prx")) library.remove_suffix(4); + while (!rest.empty()) { + const auto comma = rest.find(','); + auto token = rest.substr(0, comma); + rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1); + if (token.ends_with(".prx")) token.remove_suffix(4); + if (token == "all" || token == library) return true; + } + return false; +} + +void MapThunkArea() { + const HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(ThunkAreaBytes), nullptr); + if (section == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "call trace: CreateFileMappingW"); + g_writable = static_cast(MapViewOfFile(section, FILE_MAP_WRITE, 0, 0, ThunkAreaBytes)); + g_executable = static_cast(MapViewOfFile(section, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, ThunkAreaBytes)); + const DWORD code = GetLastError(); + CloseHandle(section); + if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); +} + +void* WriteThunk(const ImportRecord& record) { + if ((g_thunks + 1) * ThunkBytes > ThunkAreaBytes) throw std::runtime_error("call trace: thunk area exhausted"); + const std::size_t offset = g_thunks * ThunkBytes; + const std::uint8_t prefix[] = {0x49, 0xbb}; + const std::uint8_t jump[] = {0xff, 0x25, 0, 0, 0, 0}; + const auto recordAddress = reinterpret_cast(&record); + const auto handler = reinterpret_cast(&CallTraceEnter_nid_no_patch); + std::uint8_t* thunk = g_writable + offset; + std::memcpy(thunk, prefix, sizeof(prefix)); + std::memcpy(thunk + 2, &recordAddress, sizeof(recordAddress)); + std::memcpy(thunk + 10, jump, sizeof(jump)); + std::memcpy(thunk + 16, &handler, sizeof(handler)); + FlushInstructionCache(GetCurrentProcess(), g_executable + offset, ThunkBytes); + ++g_thunks; + return g_executable + offset; +} + +} + +static_assert(offsetof(ImportRecord, target) == 0); + +extern "C" { + +void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t returnAddress) { + static_cast(record)->calls.fetch_add(1, std::memory_order_relaxed); + const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; + RingSlot& slot = g_ring[sequence % RingCapacity]; + slot.sequence.store(0, std::memory_order_release); + slot.thread.store(GetCurrentThreadId(), std::memory_order_relaxed); + slot.returnAddress.store(returnAddress, std::memory_order_relaxed); + slot.record.store(static_cast(record), std::memory_order_relaxed); + slot.sequence.store(sequence, std::memory_order_release); +} + +void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { + if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; + std::lock_guard lock(g_mutex); + if (g_ring == nullptr) { + MapThunkArea(); + g_ring = std::make_unique(RingCapacity); + } + if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; + g_records.emplace_back(target, name, library); + void* thunk = WriteThunk(g_records.back()); + g_thunkByTarget.emplace(target, thunk); + return thunk; +} + +std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity) { + if (entries == nullptr || capacity == 0 || g_ring == nullptr) return 0; + const std::uint64_t last = g_sequence.load(std::memory_order_acquire); + const std::uint64_t span = std::min({last, capacity, RingCapacity}); + std::size_t count = 0; + for (std::uint64_t sequence = last - span + 1; sequence <= last; ++sequence) { + const RingSlot& slot = g_ring[sequence % RingCapacity]; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + const auto* record = slot.record.load(std::memory_order_relaxed); + const CallTraceEntry entry{sequence, slot.thread.load(std::memory_order_relaxed), slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + entries[count++] = entry; + } + return count; +} + +std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { + if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0) return 0; + std::unique_lock lock(g_mutex, std::try_to_lock); + if (!lock.owns_lock()) return 0; + std::size_t count = 0; + for (const auto& record : g_records) { + const std::uint64_t calls = record.calls.load(std::memory_order_relaxed); + if (calls == 0 || (count == capacity && calls <= entries[count - 1].calls)) continue; + std::size_t position = count < capacity ? count++ : count - 1; + while (position > 0 && entries[position - 1].calls < calls) { + entries[position] = entries[position - 1]; + --position; + } + entries[position] = {calls, record.name.c_str(), record.library.c_str()}; + } + return count; +} + +} + +asm(".text\n" + APS5_ASM_FUNCTION("CallTraceEnter_nid_no_patch") + " push %rdi\n" + " push %rsi\n" + " push %rdx\n" + " push %rcx\n" + " push %r8\n" + " push %r9\n" + " push %rax\n" + " push %r10\n" + " push %r11\n" + " sub $128, %rsp\n" + " movdqu %xmm0, 0(%rsp)\n" + " movdqu %xmm1, 16(%rsp)\n" + " movdqu %xmm2, 32(%rsp)\n" + " movdqu %xmm3, 48(%rsp)\n" + " movdqu %xmm4, 64(%rsp)\n" + " movdqu %xmm5, 80(%rsp)\n" + " movdqu %xmm6, 96(%rsp)\n" + " movdqu %xmm7, 112(%rsp)\n" + " mov %r11, %rdi\n" + " mov 200(%rsp), %rsi\n" + " call CallTraceRecord_nid_no_patch\n" + " movdqu 0(%rsp), %xmm0\n" + " movdqu 16(%rsp), %xmm1\n" + " movdqu 32(%rsp), %xmm2\n" + " movdqu 48(%rsp), %xmm3\n" + " movdqu 64(%rsp), %xmm4\n" + " movdqu 80(%rsp), %xmm5\n" + " movdqu 96(%rsp), %xmm6\n" + " movdqu 112(%rsp), %xmm7\n" + " add $128, %rsp\n" + " pop %r11\n" + " pop %r10\n" + " pop %rax\n" + " pop %r9\n" + " pop %r8\n" + " pop %rcx\n" + " pop %rdx\n" + " pop %rsi\n" + " pop %rdi\n" + " jmp *(%r11)\n"); +#endif diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 53d651430..a4671a43e 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -1,6 +1,7 @@ #ifdef _WIN32 #include #include +#include "prx/libc/include/CallTrace.hpp" #include "prx/libc/include/GuestArena.hpp" #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include "Sse4aEmulation.hpp" @@ -32,6 +34,7 @@ void Report(const char* format, ...) { void ReportAllThreads(); void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span); +void ReportCallTrace(); void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) { HMODULE module = nullptr; @@ -325,12 +328,34 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { } } } + ReportCallTrace(); std::fflush(stderr); ReportAllThreads(); std::fflush(stderr); return EXCEPTION_CONTINUE_SEARCH; } +void ReportCallTrace() { + static CallTraceEntry entries[64]; + const std::size_t count = CopyCallTrace_nid_no_patch(entries, std::size(entries)); + if (count == 0) return; + char line[MAX_PATH + 64]; + Report(" last traced system library calls (APS5_TRACE_CALLS):\n"); + for (std::size_t index = 0; index < count; ++index) { + DescribeAddress(entries[index].returnAddress, line, sizeof(line)); + Report(" #%llu thread %llu %s %s from %s\n", static_cast(entries[index].sequence), + static_cast(entries[index].thread), entries[index].library, entries[index].name, line); + } + static CallTraceCount counts[32]; + std::uint64_t total = 0; + const std::size_t counted = CopyCallCounts_nid_no_patch(counts, std::size(counts), &total); + if (counted == 0) return; + Report(" most called traced imports (%llu calls in total):\n", static_cast(total)); + for (std::size_t index = 0; index < counted; ++index) { + Report(" %12llu %s %s\n", static_cast(counts[index].calls), counts[index].library, counts[index].name); + } +} + std::size_t CaptureFrames(const CONTEXT* context, std::uint64_t* frames, std::size_t capacity) { std::size_t count = 0; std::uint64_t frame = context->Rbp; @@ -451,6 +476,7 @@ void ReportBacktrace(const char* what, bool reportThreads) { Report(" #%u %s\n", static_cast(i), line); } ReportStackCandidates(reinterpret_cast(__builtin_frame_address(0)), 32, 0x4000); + ReportCallTrace(); if (reportThreads) ReportAllThreads(); std::fflush(stderr); } diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp new file mode 100644 index 000000000..4be35302d --- /dev/null +++ b/core/libs/tests/GuestCallTrace.cpp @@ -0,0 +1,129 @@ +#include "prx/libc/include/CallTrace.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct Block { + std::uint64_t words[4]; +}; + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Call trace check failed at line %u\n", location.line()); + std::abort(); + } +} + +static std::uint64_t APS5_VABI Integers(std::uint64_t a, std::uint64_t b, std::uint64_t c, std::uint64_t d, std::uint64_t e, std::uint64_t f, std::uint64_t g, std::uint64_t h) { + return a + 2 * b + 3 * c + 4 * d + 5 * e + 6 * f + 7 * g + 8 * h; +} + +static double APS5_VABI Doubles(double a, double b, double c, double d, double e, double f, double g, double h, double i, double j) { + return a + 2 * b + 3 * c + 4 * d + 5 * e + 6 * f + 7 * g + 8 * h + 9 * i + 10 * j; +} + +static double APS5_VABI Mixed(int a, double b, const char* c, float d, std::uint64_t e) { + return a + b + static_cast(std::strlen(c)) + d + static_cast(e); +} + +static double APS5_VABI Variadic(int count, ...) { + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, count); + double sum = 0; + for (int index = 0; index < count; ++index) { + sum += static_cast(__builtin_va_arg(arguments, std::uint64_t)) * (index + 1); + sum += __builtin_va_arg(arguments, double) * (index + 1); + } + __builtin_sysv_va_end(arguments); + return sum; +} + +static Block APS5_VABI Blocks(std::uint64_t seed) { + return {{seed, seed + 1, seed + 2, seed + 3}}; +} + +template +static TFunction* Traced(TFunction* function, const char* name, const char* library) { + return reinterpret_cast(TraceImport_nid_no_patch(reinterpret_cast(function), name, library)); +} + +int main(int argc, char** argv) { + Require(_putenv_s("APS5_TRACE_CALLS", "libTraced,libOther.prx") == 0); + if (argc > 1 && std::strcmp(argv[1], "abort") == 0) { + const auto first = Traced(&Integers, "Integers", "libTraced.prx"); + const auto second = Traced(&Blocks, "Blocks", "libOther.prx"); + Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204 && second(7).words[3] == 10); + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + std::abort(); + } + CallTraceEntry entries[16]; + CallTraceCount counts[8]; + std::uint64_t total = 1; + Require(CopyCallTrace_nid_no_patch(entries, 16) == 0); + Require(CopyCallCounts_nid_no_patch(counts, 8, &total) == 0 && total == 0); + Require(Traced(&Integers, "Integers", "libUnselected.prx") == &Integers); + Require(TraceImport_nid_no_patch(nullptr, "Integers", "libTraced.prx") == nullptr); + Require(CopyCallTrace_nid_no_patch(entries, 16) == 0); + + const auto integers = Traced(&Integers, "Integers", "libTraced.prx"); + const auto doubles = Traced(&Doubles, "Doubles", "libTraced"); + const auto mixed = Traced(&Mixed, "Mixed", "libOther.prx"); + const auto variadic = Traced(&Variadic, "Variadic", "libOther"); + const auto blocks = Traced(&Blocks, "Blocks", "libTraced.prx"); + Require(integers != &Integers && doubles != &Doubles && mixed != &Mixed && variadic != &Variadic && blocks != &Blocks); + Require(Traced(&Integers, "Alias", "libOther.prx") == integers); + + Require(integers(1, 2, 3, 4, 5, 6, 7, 8) == Integers(1, 2, 3, 4, 5, 6, 7, 8)); + Require(doubles(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5) == Doubles(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5)); + Require(mixed(-3, 0.25, "trace", 1.5f, 1ull << 40) == Mixed(-3, 0.25, "trace", 1.5f, 1ull << 40)); + const double expected = Variadic(8, 1ull, 0.5, 2ull, 1.5, 3ull, 2.5, 4ull, 3.5, 5ull, 4.5, 6ull, 5.5, 7ull, 6.5, 8ull, 7.5); + Require(variadic(8, 1ull, 0.5, 2ull, 1.5, 3ull, 2.5, 4ull, 3.5, 5ull, 4.5, 6ull, 5.5, 7ull, 6.5, 8ull, 7.5) == expected); + const Block block = blocks(40); + Require(block.words[0] == 40 && block.words[1] == 41 && block.words[2] == 42 && block.words[3] == 43); + + const char* const names[] = {"Integers", "Doubles", "Mixed", "Variadic", "Blocks"}; + const char* const libraries[] = {"libTraced.prx", "libTraced", "libOther.prx", "libOther", "libTraced.prx"}; + MEMORY_BASIC_INFORMATION image{}; + Require(VirtualQuery(reinterpret_cast(&main), &image, sizeof(image)) == sizeof(image)); + Require(CopyCallTrace_nid_no_patch(entries, 16) == 5); + for (std::uint64_t index = 0; index < 5; ++index) { + Require(entries[index].sequence == index + 1 && entries[index].thread == GetCurrentThreadId()); + Require(std::strcmp(entries[index].name, names[index]) == 0 && std::strcmp(entries[index].library, libraries[index]) == 0); + MEMORY_BASIC_INFORMATION caller{}; + Require(VirtualQuery(reinterpret_cast(entries[index].returnAddress), &caller, sizeof(caller)) == sizeof(caller)); + Require(caller.AllocationBase == image.AllocationBase); + } + Require(CopyCallTrace_nid_no_patch(entries, 2) == 2 && entries[0].sequence == 4 && entries[1].sequence == 5); + Require(CopyCallCounts_nid_no_patch(counts, 8, &total) == 5 && total == 5); + for (std::size_t index = 0; index < 5; ++index) { + Require(counts[index].calls == 1 && std::strcmp(counts[index].name, names[index]) == 0 && std::strcmp(counts[index].library, libraries[index]) == 0); + } + Require(doubles(1, 1, 1, 1, 1, 1, 1, 1, 1, 1) == 55 && blocks(1).words[0] == 1 && blocks(2).words[0] == 2); + Require(CopyCallCounts_nid_no_patch(counts, 2, nullptr) == 2); + Require(counts[0].calls == 3 && std::strcmp(counts[0].name, "Blocks") == 0 && counts[1].calls == 2 && std::strcmp(counts[1].name, "Doubles") == 0); + Require(CopyCallCounts_nid_no_patch(nullptr, 8, &total) == 0 && total == 8); + + constexpr std::uint64_t Workers = 4; + constexpr std::uint64_t Calls = 20000; + std::vector workers; + for (std::uint64_t worker = 0; worker < Workers; ++worker) { + workers.emplace_back([integers, worker] { + for (std::uint64_t call = 0; call < Calls; ++call) Require(integers(worker, call, 1, 2, 3, 4, 5, 6) == Integers(worker, call, 1, 2, 3, 4, 5, 6)); + }); + } + for (auto& worker : workers) worker.join(); + constexpr std::size_t Window = 65536; + const auto recent = std::make_unique(Window); + Require(CopyCallTrace_nid_no_patch(recent.get(), Window) == Window); + Require(recent[Window - 1].sequence == 8 + Workers * Calls && recent[0].sequence == 8 + Workers * Calls - Window + 1); + for (std::size_t index = 0; index < Window; ++index) Require(std::strcmp(recent[index].name, "Integers") == 0); + Require(CopyCallCounts_nid_no_patch(counts, 1, &total) == 1 && total == 8 + Workers * Calls); + Require(counts[0].calls == 1 + Workers * Calls && std::strcmp(counts[0].name, "Integers") == 0); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 576699758..137fdef95 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -60,6 +60,7 @@ - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follows FreeBSD 11.4's [wcsftime.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/wcsftime.c) over this library's `wcsrtombs`, `strftime` and `mbsrtowcs` in the single-byte C locale of [none.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/none.c), with the guest's 16-bit `wchar_t`, without console verification. It has the `strftime` gaps above. A format unit above 255 returns 0 with `EILSEQ`; an empty result, a result that does not fit and a null buffer return 0 with `errno` unchanged, discarding the `ERANGE` or `EINVAL` that the Windows CRT's `strftime` sets. A null format or `tm` throws, where FreeBSD dereferences it (a null `tm` is read there only when the format has a conversion that needs it) - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) +- [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. Nothing passes imports through `TraceImport_nid_no_patch` yet, so no title is traced. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The host's [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) locks also restrict ordinary reads and writes through other handles, unlike FreeBSD's advisory locks, and do not restrict mapped views. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred