user/printf.c
About this file
Formatted output for user programs: printf writes to standard output,
fprintf to any file descriptor (most often 2, standard error, for error
messages), and vprintf does the work for both.
The formatting code is almost a copy of the kernel’s printk
(kernel/printk.c). The difference is where the characters go. The kernel can hand
each character to the console driver directly. A user program cannot touch any device;
it can only ask the kernel to write bytes to a file descriptor. So putc here makes
one write system call per character: printf("hello\n") enters the kernel six
times. That is slow, and it has a visible effect: when two processes print at the same
time, their output is mixed character by character, because nothing groups a whole
printf into one write.
Supported directives, in both files: %d, %u, %x (32-bit), the same with l or ll
(64-bit), %p, %c, %s and %%. There is no field width, padding or precision, and
no floating point. See format string and variadic function.
Read before: kernel/printk.c. Read next: user/umalloc.c.
Headers
kernel/types.h and user/user.h provide the types and the write
declaration. kernel/stat.h is not used by this file.
<stdarg.h> is the one header that does not come from xv6. It defines va_list,
va_start and va_arg, which a function with ... needs to read its extra
arguments. It belongs to the compiler rather than to a C library: the C standard
requires it even in a freestanding (vs. hosted) C environment, and GCC ships it, so it is
available although xv6 is built with -nostdlib.
Digit characters
The characters for digit values 0 to 15; digits[x % base] turns a digit value into
its character for base 10 or 16. Unlike the kernel’s copy in kernel/printk.c:26,
the letters are upper case, so user programs print hexadecimal as 0x1A2B while the
kernel prints 0x1a2b.
putc(): one character, one system call
Writes the single byte c to file descriptor fd. write takes a pointer to the
bytes, so the function passes the address of its own parameter. The call goes
through the write stub (user/usys.S) to sys_write and
filewrite; for the console it ends in consolewrite, which hands
the byte to the UART driver (uartwrite).
The result of write is ignored, so output to a closed descriptor or a full disk is
lost silently. The function is static, private to this file, so its name does not
clash with anything in a program. (The Makefile’s -fno-builtin-putc would stop GCC
treating it as the standard library’s putc, but -ffreestanding already does that
for every name.)
One system call for one byte: write(fd, &c, 1). This is where every
character printed by a user program enters the kernel.
printint(): print an integer in base 10 or 16
Prints xx in base. If sgn is set, xx is treated as signed.
Digits come out of the %// loop lowest first, the reverse of the order they must
be printed, so they are collected in buf and printed backwards. 20 bytes are
enough: the largest 64-bit unsigned number, 18446744073709551615, has 20 decimal
digits, and the most negative signed one has 19 digits plus the -.
The function takes a long long so the same code serves int and 64-bit values: an
int argument is converted to long long with its sign preserved, and an unsigned
32-bit value is converted without becoming negative.
Room for the digits of the largest value plus a minus sign; no terminating zero is needed because the characters are printed one by one.
For a negative signed value, remember the sign and work with the magnitude. -xx is
computed in long long and then stored in the unsigned x. For the most negative
value, -2^63, the negation overflows (undefined in C, but in practice it wraps to the
same bits), and those bits read as unsigned are exactly 2^63, so the right digits are
printed anyway. Otherwise x takes the value as unsigned.
Peel off digits, lowest first: x % base is the last digit, x /= base removes it.
A do/while runs at least once, so 0 prints as 0 instead of nothing.
The sign goes after the digits in buf, which puts it first when the buffer is
printed backwards.
Print the buffer from its last filled byte down to buf[0], i.e. in reading order.
printptr(): print a 64-bit value as 16 hex digits
Prints 0x and then all 16 hexadecimal digits of x, including leading zeros, so
every pointer is printed with the same width. Each round takes the top 4 bits of
x as one digit and then shifts x left by 4, bringing the next 4 bits to the top.
16 rounds, one per hex digit (sizeof(uint64) * 2). x >> 60 is the top 4 bits, a
value from 0 to 15; x <<= 4 then discards them.
vprintf(): walk the format string
Reads the format one byte at a time. Ordinary characters are printed as they are; a
% starts a directive, which consumes one argument from ap.
ap is a va_list, a cursor over the caller’s extra arguments, set up by
va_start in fprintf or printf. Taking a va_list instead of ... is what
lets both of them share this one function; the standard C library has a vprintf
for the same reason.
The comment on line 51 is out of date: the code below also handles %u, the l and
ll forms of %d, %u and %x, and %%.
Start outside a directive.
Loop until the terminating zero of the format string.
Take the next format byte as a value from 0 to 255. On RISC-V char is already
unsigned, so the mask changes nothing there; on a machine with signed char it keeps
bytes above 0x7f from becoming negative.
State 0: outside a directive
state remembers whether the previous character was a %. In state 0 a % only
switches to state '%' (the character’s value is used as the state number); the
directive is handled on the next round of the loop. Any other character is printed.
printk does the same without a state variable, by incrementing i itself
when it sees %. The two are equivalent.
Look ahead for l and ll
In state '%', c0 is the character after the %. To recognize %ld and %lld the
function also needs the next one or two characters, c1 and c2. They are read only
if the previous one was not the terminating zero, so the look-ahead never reads past
the end of the string. (c0 is never 0 here, because the loop condition on line 59
stops at the zero; the test on line 69 is only a safeguard.)
Handle one directive
One branch per directive. Each takes the next argument with va_arg(ap, T), where T
must match what the caller passed, and prints it:
| Directive | Argument read as | Printed as |
|---|---|---|
%d |
int |
signed decimal |
%ld, %lld |
uint64 |
signed decimal |
%u |
uint32 |
unsigned decimal |
%lu, %llu |
uint64 |
unsigned decimal |
%x |
uint32 |
unsigned hex, upper case |
%lx, %llx |
uint64 |
unsigned hex, upper case |
%p |
uint64 |
0x and 16 hex digits |
%c |
uint32 |
one character |
%s |
char * |
the string, or (null) |
%% |
nothing | % |
For the two- and three-letter forms, i += 1 or i += 2 skips the extra letters, so
the loop’s i++ lands on the character after the directive.
This is the same set, in the same order, as printk
(kernel/printk.c:86). There are two small differences: hex digits are upper case
here, and a % at the very end of the format prints nothing here (the loop ends
while still in state '%'), where printk stops explicitly
(kernel/printk.c:121).
Because user/user.h marks printf and fprintf with a format attribute, GCC
checks every call against this table’s C equivalents, and with -Werror a mismatch
such as printf("%d", "text") stops the build.
%d: read an int. Variadic arguments smaller than int are promoted to int, so
%d also works for char and short values.
%ld: read 64 bits. The value is declared uint64 here but passed to printint as a
long long with sgn = 1, so it is printed as signed. i += 1 skips the d.
%u: read 32 bits and print them unsigned (sgn = 0).
%x: 32 bits in base 16. To print a 64-bit value such as an address in hex, a program
must write %lx or %p; %x would show only the low 32 bits.
%p: a pointer, read as a 64-bit integer and printed with all 16 digits.
%c: a character arrives promoted to an int-sized value; putc converts it back to
char.
%s: print the string up to its zero byte. A null pointer prints (null) instead of
crashing. (In xv6 a user program could in fact read address 0, which holds its own
code; see user/user.ld.)
%% prints one % and reads no argument.
Any other character after % is printed with the %, unchanged, so that a mistake in
a format string shows up in the output. No argument is consumed.
Back to state 0
After any directive, known or not, go back to printing ordinary characters. Lines 115–117 close the state test, the loop and the function.
fprintf(): print to a chosen file descriptor
The ... makes this a variadic function. va_start(ap, fmt) sets ap to the
first argument after fmt, and vprintf reads them from there.
Programs use it for error messages, fprintf(2, "cat: cannot open %s\n", ...) in
user/cat.c, so that errors go to standard error (descriptor 2) and do not end up
in a pipe or file that standard output is redirected to. In xv6 both are the
console unless redirected (standard input, output and error).
Neither fprintf nor printf calls va_end, which the C standard requires after
va_start. With GCC on RISC-V va_end generates no code (checked by compiling a test with
GCC for riscv64), so omitting it causes no harm here; the kernel’s printk does call it. Unlike the standard functions, these return
nothing, not the number of characters printed.
Point ap at the arguments after fmt.
Do the formatting and output.
printf(): print to standard output
The same as fprintf(1, ...): file descriptor 1 is standard output by convention.
This is the function that every printf(...) in a user program calls; it is
unrelated to the kernel’s printk, although the formatting code is shared
by copy.
Descriptor 1: standard output.