The u/i/d conversions shared one branch that read the argument as int
and ran it through the signed decimal path of uart_writenum, so %u
values >= 2^31 printed as negative numbers (0xFFFFFFFF became -1).
Read %u as unsigned int and add an is_signed parameter to
uart_writenum so the sign handling applies only to d/i, matching the
is_negative design of the 64-bit helper.
A negative '*' width passed the upper-bound-only clamp and reached
the zero-pad memset as a huge size_t (stack overflow). Clamp to zero
at the parser, the single source of dynamic widths; the existing
zero-to-8 default then applies.
New test test_uart_printf_negative_star_width covers %0*x and
%0*llu; SIGSEGV pre-fix, green post-fix. Full unit suite green.
The UART formatter recognized 'l' and 'z' length modifiers and %p,
but still consumed the argument as int, truncating long, size_t and
pointers on 64-bit targets (undefined behavior). Track the 'z'
modifier and, where PRINTF_LONG_LONG is enabled, consume long /
unsigned long / void * and format through the 64-bit
uart_writenum_ll() path; the 32-bit path is unchanged (long == int
width there).
New test test_uart_printf_64bit_args covers %ld, %lu, %zu, %p, %lx
with values past 32 bits plus %lld and %d regressions; fails
pre-fix (truncated output), green post-fix. 32-bit compile of
string.c verified with -m32 -Wall -Wextra.