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 2f6a47efe..e11786333 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