Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pastaboy12345/NKLegacy/llms.txt

Use this file to discover all available pages before exploring further.

kprintf is NKLegacy’s kernel-level formatted print function. Every call outputs simultaneously to both the VGA text console and the COM1 serial port (0x3F8), making it the primary debug channel during kernel development. There is no buffering or locking — output is written character-by-character in the order it is produced.

Function Signature

void kprintf(const char *fmt, ...);
Declared in ntoskrnl/kernel/kprintf.h and implemented in ntoskrnl/kernel/kprintf.c. Include the header anywhere you need formatted output:
#include "kernel/kprintf.h"

Format Specifiers

kprintf does not support padding, field-width specifiers, precision, length modifiers (%ld, %zu, etc.), or floating-point types. It is intentionally minimal for a freestanding kernel environment where the C standard library is unavailable.

%s — string

Prints a null-terminated C string. If the pointer is NULL, prints the literal text (null) instead of faulting.

%d — signed integer

Prints a signed decimal int. Negative values are preceded by a minus sign (-).

%u — unsigned integer

Prints an unsigned decimal unsigned int. Use this for tick counters, sizes, and other non-negative quantities.

%x — hexadecimal

Prints an unsigned int in uppercase hexadecimal, always prefixed with 0x (e.g., 0x1F4).

%p — pointer

Identical to %x — the value is formatted as an unsigned hex integer with a 0x prefix. Useful for printing addresses.

%c — character

Prints a single char.

%% — literal percent

Emits a single % character in the output.

Dual Output Behaviour

Internally, kprintf routes every character through the private kput helper:
static void kput(char c) {
    if (c == '\n') {
        serial_putchar(COM1, '\r');   /* prepend CR for CRLF on terminals */
    }
    vga_putchar(c);
    serial_putchar(COM1, c);
}
Key behaviours:
  • VGA: Each character is written at the current cursor position using vga_putchar. The VGA driver advances the cursor and scrolls when the bottom row is reached.
  • Serial (COM1): Each character is sent to 0x3F8. Newline characters (\n) automatically emit a carriage return (\r) before the newline so that serial terminals receive proper CRLF line endings.
  • Order: VGA and serial writes happen within the same kput call, so both outputs stay in sync even if the kernel crashes mid-print.
When running NKLegacy in QEMU with make run, all kprintf output is visible on the host terminal via the emulated serial port. This makes serial output the most reliable crash-debugging channel — it survives situations where VGA scrolling or state is corrupted.

Usage Examples

Basic string and integer

kprintf("tick: %u\n", pit_get_ticks());
tick: 1024

Hexadecimal address

kprintf("entry: %x\n", entry_point);
entry: 0x100000

Multiple arguments

kprintf("[%s] loaded at %x, size %u\n", name, addr, size);
[kernel] loaded at 0x100000, size 49152

NULL pointer guard

const char *label = NULL;
kprintf("label=%s\n", label);
label=(null)

Signed integer

int delta = -42;
kprintf("delta: %d\n", delta);
delta: -42

Single character

kprintf("magic byte: %c\n", 'K');
magic byte: K

Internal Implementation Reference

The full kprintf.c source shows each specifier’s handling path:
void kprintf(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);

    while (*fmt) {
        if (*fmt == '%') {
            fmt++;
            switch (*fmt) {
                case 's': {
                    const char *s = va_arg(args, const char *);
                    if (!s) s = "(null)";
                    while (*s) kput(*s++);
                    break;
                }
                case 'd': {
                    int val = va_arg(args, int);
                    print_int(val);
                    break;
                }
                case 'u': {
                    unsigned int val = va_arg(args, unsigned int);
                    print_uint(val);
                    break;
                }
                case 'x':
                case 'p': {
                    unsigned int val = va_arg(args, unsigned int);
                    print_hex(val);
                    break;
                }
                case 'c': {
                    char c = (char)va_arg(args, int);
                    kput(c);
                    break;
                }
                case '%':
                    kput('%');
                    break;
                default:
                    kput('%');
                    kput(*fmt);
                    break;
            }
        } else {
            kput(*fmt);
        }
        fmt++;
    }

    va_end(args);
}
Unknown specifiers (the default branch) are passed through literally — kprintf("%q", ...) would emit %q verbatim rather than silently discarding the character.

Build docs developers (and LLMs) love