diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index 04917fdeb..e03bf6a2d 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -13,6 +13,14 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/WindowsFormatting.hpp" +#ifdef _WIN32 +#define APS5_VA_BEGIN(last) __builtin_sysv_va_list args; __builtin_sysv_va_start(args, last) +#define APS5_VA_END() __builtin_sysv_va_end(args) +#else +#define APS5_VA_BEGIN(last) std::va_list args; va_start(args, last) +#define APS5_VA_END() va_end(args) +#endif + extern "C" int APS5_VABI iswspace_nid_postfix(std::uint32_t c); extern "C" std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void* state); @@ -122,15 +130,9 @@ Fit FloatFit(std::string_view text) { return Fit::Valid; } -std::size_t IntegerSize(const std::string& length) { - if (length.empty()) return sizeof(std::int32_t); - if (length == "hh") return sizeof(std::int8_t); - if (length == "h") return sizeof(std::int16_t); - if (length == "l" || length == "ll" || length == "j" || length == "z" || length == "t") return sizeof(std::int64_t); - throw std::invalid_argument("swscanf_s: invalid integer length modifier " + length); -} - class WideScanner { + const char* const name; + const bool sized; const char16_t* const start; const char16_t* input; const char16_t* format; @@ -139,8 +141,8 @@ class WideScanner { bool converted = false; public: - WideScanner(const char16_t* buffer, const char16_t* formatText, const void* source) - : start(buffer), input(buffer), format(formatText), args(source) {} + WideScanner(const char* function, bool withSizes, const char16_t* buffer, const char16_t* formatText, const void* source) + : name(function), sized(withSizes), start(buffer), input(buffer), format(formatText), args(source) {} int Run() { while (*format != 0) { @@ -162,6 +164,18 @@ public: } private: + [[noreturn]] void Fail(const std::string& message) const { + throw std::invalid_argument(std::string(name) + ": " + message); + } + + std::size_t IntegerSize(const std::string& length) const { + if (length.empty()) return sizeof(std::int32_t); + if (length == "hh") return sizeof(std::int8_t); + if (length == "h") return sizeof(std::int16_t); + if (length == "l" || length == "ll" || length == "j" || length == "z" || length == "t") return sizeof(std::int64_t); + Fail("invalid integer length modifier " + length); + } + void SkipSpace() { while (*input != 0 && IsSpace(*input)) ++input; } @@ -185,7 +199,7 @@ private: const std::size_t limit = (std::numeric_limits::max() - digit) / 10; directive.width = directive.width > limit ? std::numeric_limits::max() : directive.width * 10 + digit; } - if (format != digits && directive.width == 0) throw std::invalid_argument("swscanf_s: zero field width"); + if (format != digits && directive.width == 0) Fail("zero field width"); if (*format == u'h' || *format == u'l') { directive.length += static_cast(*format++); if (*format == directive.length[0]) directive.length += static_cast(*format++); @@ -193,7 +207,7 @@ private: directive.length += static_cast(*format++); } directive.conversion = *format; - if (directive.conversion == 0) throw std::invalid_argument("swscanf_s: incomplete conversion specification"); + if (directive.conversion == 0) Fail("incomplete conversion specification"); ++format; if (directive.conversion == u'[') { directive.setExcluded = *format == u'^'; @@ -201,10 +215,10 @@ private: directive.setBegin = format; if (*format == u']') ++format; while (*format != 0 && *format != u']') ++format; - if (*format == 0) throw std::invalid_argument("swscanf_s: unterminated scanset"); + if (*format == 0) Fail("unterminated scanset"); directive.setEnd = format++; for (const char16_t* member = directive.setBegin + 1; member + 1 < directive.setEnd; ++member) { - if (*member == u'-') throw std::invalid_argument("swscanf_s: scanset range is implementation-defined"); + if (*member == u'-') Fail("scanset range is implementation-defined"); } } return directive; @@ -230,7 +244,7 @@ private: case u'c': case u'[': return Text(directive); case u'n': return Count(directive); - default: throw std::invalid_argument("swscanf_s: unsupported conversion " + std::to_string(directive.conversion)); + default: Fail("unsupported conversion " + std::to_string(directive.conversion)); } } @@ -271,7 +285,7 @@ private: Outcome Float(const Directive& directive) { static_assert(sizeof(long double) == 16 && std::numeric_limits::digits == 64); if (!directive.length.empty() && directive.length != "l" && directive.length != "L") - throw std::invalid_argument("swscanf_s: invalid floating length modifier " + directive.length); + Fail("invalid floating length modifier " + directive.length); void* target = nullptr; if (!directive.suppress && (target = args.Next()) == nullptr) return Outcome::Violation; SkipSpace(); @@ -312,12 +326,12 @@ private: Outcome Text(const Directive& directive) { if (!directive.length.empty() && directive.length != "l") - throw std::invalid_argument("swscanf_s: invalid string length modifier " + directive.length); + Fail("invalid string length modifier " + directive.length); void* target = nullptr; std::size_t capacity = 0; if (!directive.suppress) { target = args.Next(); - capacity = args.Next(); + if (sized) capacity = args.Next(); if (target == nullptr) return Outcome::Violation; } if (directive.conversion == u's') SkipSpace(); @@ -343,7 +357,7 @@ private: } const std::size_t terminator = directive.conversion == u'c' ? 0 : 1; if (directive.length == "l") { - if (item.size() + terminator > capacity) return Truncated(target, capacity, true); + if (sized && item.size() + terminator > capacity) return Truncated(target, capacity, true); std::memcpy(target, item.data(), item.size() * sizeof(char16_t)); if (terminator != 0) static_cast(target)[item.size()] = 0; } else { @@ -351,10 +365,10 @@ private: for (const char16_t unit : item) { char byte[8]; const std::size_t written = wcrtomb_nid_postfix(byte, unit, nullptr); - if (written == static_cast(-1)) throw std::invalid_argument("swscanf_s: wide character has no single-byte form"); + if (written == static_cast(-1)) Fail("wide character has no single-byte form"); bytes.append(byte, written); } - if (bytes.size() + terminator > capacity) return Truncated(target, capacity, false); + if (sized && bytes.size() + terminator > capacity) return Truncated(target, capacity, false); std::memcpy(target, bytes.data(), bytes.size()); if (terminator != 0) static_cast(target)[bytes.size()] = 0; } @@ -364,7 +378,7 @@ private: } Outcome Count(const Directive& directive) { - if (directive.suppress || directive.width != 0) throw std::invalid_argument("swscanf_s: %n with suppression or width"); + if (directive.suppress || directive.width != 0) Fail("%n with suppression or width"); const std::size_t size = IntegerSize(directive.length); void* target = args.Next(); if (target == nullptr) return Outcome::Violation; @@ -378,29 +392,28 @@ private: extern "C" { -int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, ...) { +int APS5_VABI swscanf_nid_postfix(const char16_t* buffer, const char16_t* format, ...) { if (buffer == nullptr || format == nullptr) return EOF; -#ifdef _WIN32 - __builtin_sysv_va_list args; - __builtin_sysv_va_start(args, format); -#else - std::va_list args; - va_start(args, format); -#endif + APS5_VA_BEGIN(format); try { - const int result = WideScanner(buffer, format, args).Run(); -#ifdef _WIN32 - __builtin_sysv_va_end(args); -#else - va_end(args); -#endif + const int result = WideScanner("swscanf", false, buffer, format, args).Run(); + APS5_VA_END(); return result; } catch (...) { -#ifdef _WIN32 - __builtin_sysv_va_end(args); -#else - va_end(args); -#endif + APS5_VA_END(); + throw; + } +} + +int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, ...) { + if (buffer == nullptr || format == nullptr) return EOF; + APS5_VA_BEGIN(format); + try { + const int result = WideScanner("swscanf_s", true, buffer, format, args).Run(); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); throw; } } diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index fa5b81cac..8cd823acb 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -11,6 +11,7 @@ extern "C" { std::uint64_t APS5_VABI _WStoul_nid_postfix(const char16_t*, char16_t**, int); int APS5_VABI _Iswctype_nid_postfix(std::uint32_t, std::uint64_t); +int APS5_VABI swscanf_nid_postfix(const char16_t*, const char16_t*, ...); int APS5_VABI swscanf_s_nid_postfix(const char16_t*, const char16_t*, ...); int APS5_VABI iswalnum_nid_postfix(std::uint32_t); int APS5_VABI iswalpha_nid_postfix(std::uint32_t); @@ -155,9 +156,66 @@ static void CheckScan() { Require(Throws([&] { swscanf_s_nid_postfix(u"Ā", u"%s", narrow, std::size_t{4}); }), "unconvertible narrow %s throws"); } +static void CheckUnsizedScan() { + int first = 0; + int second = 0; + Require(swscanf_nid_postfix(u" 42 -7", u"%d%d", &first, &second) == 2 && first == 42 && second == -7, "swscanf %d"); + Require(swscanf_nid_postfix(u"x=0x1f", u"x=%i", &first) == 1 && first == 31, "swscanf %i with a hex prefix"); + Require(swscanf_nid_postfix(u"1 2", u"%*d%d", &first) == 1 && first == 2, "swscanf suppressed %d"); + Require(swscanf_nid_postfix(u"12345", u"%3d%d", &first, &second) == 2 && first == 123 && second == 45, "swscanf %d width"); + std::int64_t wide = 0; + Require(swscanf_nid_postfix(u"5000000000", u"%ld", &wide) == 1 && wide == 5000000000LL, "swscanf %ld is 64-bit"); + short half = 0; + Require(swscanf_nid_postfix(u"-2", u"%hd", &half) == 1 && half == -2, "swscanf %hd"); + unsigned value = 0; + Require(swscanf_nid_postfix(u"0xg", u"%x", &value) == 0, "swscanf %x prefix without digits is a matching failure"); + + char16_t text[16] = {u'?'}; + Require(swscanf_nid_postfix(u"hello world", u"%ls", text) == 1 && std::u16string(text) == u"hello", "swscanf %ls"); + Require(swscanf_nid_postfix(u"7 a-long-word", u"%d%ls", &first, text) == 2 && first == 7 && std::u16string(text) == u"a-long-word", "swscanf %ls takes no size"); + Require(swscanf_nid_postfix(u"hello world", u"%3ls", text) == 1 && std::u16string(text) == u"hel", "swscanf %ls width"); + char narrow[8] = {}; + Require(swscanf_nid_postfix(u"abc def", u"%s%d", narrow, &first) == 1 && std::strcmp(narrow, "abc") == 0, "swscanf %s"); + char16_t pair[3] = {u'?', u'?', u'?'}; + Require(swscanf_nid_postfix(u"xyz", u"%2lc", pair) == 1 && pair[0] == u'x' && pair[1] == u'y' && pair[2] == u'?', "swscanf %2lc"); + Require(swscanf_nid_postfix(u"x", u"%2lc", pair) == 0, "swscanf %2lc short input"); + char letter = 0; + Require(swscanf_nid_postfix(u" q7", u"%c%d", &letter, &first) == 1 && letter == ' ', "swscanf %c does not skip white space"); + Require(swscanf_nid_postfix(u"cab12", u"%l[abc]%d", text, &first) == 2 && std::u16string(text) == u"cab" && first == 12, "swscanf %l["); + Require(swscanf_nid_postfix(u"été!", u"%l[^!]", text) == 1 && std::u16string(text) == u"été", "swscanf %l[^"); + Require(swscanf_nid_postfix(u"key=val", u"%[^=]=%s", narrow, narrow + 4) == 2 && std::strcmp(narrow, "key") == 0 && std::strcmp(narrow + 4, "val") == 0, "swscanf %[^"); + std::int64_t count = -1; + Require(swscanf_nid_postfix(u" 12 ab", u"%d %ln", &first, &count) == 1 && count == 4, "swscanf %ln"); + int shortCount = -1; + Require(swscanf_nid_postfix(u"abc", u"%*ls%n", &shortCount) == 0 && shortCount == 3, "swscanf %n after a suppressed conversion"); + Require(swscanf_nid_postfix(u"50% off", u"%d%% off", &first) == 1 && first == 50, "swscanf %%"); + Require(swscanf_nid_postfix(u"50 off", u"%d%%", &first) == 1, "swscanf %% mismatch is a matching failure"); + + double real = 0; + Require(swscanf_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "swscanf %lf"); + Require(swscanf_nid_postfix(u"1e+x", u"%lf", &real) == 0, "swscanf %lf incomplete exponent is a matching failure"); + long double extended = 0; + Require(swscanf_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "swscanf %Lf"); + float single = 0; + Require(swscanf_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits::infinity(), "swscanf %f infinity"); + + Require(swscanf_nid_postfix(u"a", u"b%d", &first) == 0, "swscanf literal mismatch is a matching failure"); + Require(swscanf_nid_postfix(u"3 x", u"%d%d", &first, &second) == 1 && first == 3, "swscanf partial match"); + Require(swscanf_nid_postfix(u"", u"%d", &first) == -1, "swscanf empty input is an input failure"); + Require(swscanf_nid_postfix(u"", u"%ls", text) == -1, "swscanf empty input before %ls"); + Require(swscanf_nid_postfix(u" ", u"%ls", text) == -1, "swscanf white space only is an input failure"); + Require(swscanf_nid_postfix(u"5", u"%d%d", &first, &second) == 1, "swscanf input failure after a conversion"); + Require(swscanf_nid_postfix(nullptr, u"%d", &first) == -1, "swscanf null buffer"); + Require(swscanf_nid_postfix(u"5", nullptr) == -1, "swscanf null format"); + Require(Throws([&] { swscanf_nid_postfix(u"abc", u"%l[a-c]", text); }), "swscanf scanset range throws"); + Require(Throws([&] { swscanf_nid_postfix(u"5", u"%Ld", &wide); }), "swscanf %Ld throws"); + Require(Throws([&] { swscanf_nid_postfix(u"Ā", u"%s", narrow); }), "swscanf unconvertible narrow %s throws"); +} + int main() { CheckStoul(); CheckClassification(); CheckScan(); + CheckUnsizedScan(); return failures == 0 ? 0 : 1; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 740c9c59c..0a6b4a832 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -62,7 +62,7 @@ - [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 +- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../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. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [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 - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. 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