Operating Systems · Boot and Firmware
Early Console Output Before Drivers Exist
How a kernel prints messages before it has a driver for any display or serial device, why the framebuffer left by firmware is the usual answer, and what early output costs when it is a polling loop.
The most informative moment in a boot is also the moment with the least infrastructure. The kernel has just been decompressed and jumped to, its page tables are provisional, its allocator is not initialized, and no driver has been registered. If it faults here, the fault is silent: no console, no log buffer, no filesystem. Producing even a single line of text at that point requires a path that depends on nothing the kernel has not yet built.
There are two general approaches, and they correspond to two different assumptions about what the firmware left behind. One is to take over a device the firmware already programmed and continue driving it with polling. The other is to keep calling back into firmware services for output. Both work, both have sharp edges, and the choice constrains everything the kernel can do afterwards.
Taking over the firmware’s framebuffer
Firmware that has been asked to set a graphics mode leaves behind a linear framebuffer: a region of physical memory, mapped by the display controller, whose contents are scanned out to the panel. The mode is described by a structure the firmware publishes, containing the physical base address, the horizontal and vertical resolution in pixels, the number of bytes per scan line, the bits per pixel, and the channel positions and widths for the red, green, and blue components.
With that structure, writing a pixel is an ordinary store to a physical address. Drawing a character is writing the bitmap for that character into a rectangle of pixels. The kernel needs a font, which for early output is usually a fixed 8x16 bitmap compiled into the image, and it needs a cursor position and a scroll routine. None of that requires an allocator, an interrupt, or a device driver in the kernel’s sense: the framebuffer is already mapped, already enabled, and already displaying.
The constraint is that the mapping is not there. The kernel’s page tables cover its own text and data and whatever the boot stub mapped; the framebuffer sits at a physical address reported by the firmware, typically in the range just below 4 GiB or, on machines with large memory, in a PCI aperture above 4 GiB. The kernel must add a mapping for it before the first write. On x86-64 that means either extending the existing tables or, more commonly, relying on the fact that the boot stub identity-mapped a large region and the framebuffer happens to be inside it.
The second constraint is the framebuffer format. A 32-bit-per-pixel mode with 8 bits per channel is easy: one 32-bit store per pixel. A 16-bit mode packs the channels into 5, 6, and 5 bits, and the correct conversion requires shifting and masking per channel. A palettized mode requires programming the palette, which the firmware has already done and which the kernel must not disturb. A kernel that assumes 32-bit truecolor and writes 32-bit values into a 16-bit framebuffer produces noise on the display rather than text, and the failure looks like a hardware fault.
Serial output and why it is preferred for debugging
A serial port is a byte-at-a-time device with a small, well-defined register interface. The classic 16550-compatible UART exposes a transmit holding register, a line status register whose bit 5 indicates the transmitter is empty, and a set of divisor latches that set the baud rate from a reference clock. Writing a byte to the holding register queues it; polling the line status bit until it indicates empty is a complete transmit path.
That path needs no interrupt, no DMA, no allocation, and no mapping beyond the port I/O permission the kernel already has. On x86, outb to the port address writes the byte. The same property that makes serial output convenient also makes it slow: at 115200 baud, a byte takes roughly 87 microseconds to leave the shift register, so a 100-character line occupies the transmitter for about 9 milliseconds. A kernel that prints on every page fault with serial output enabled will spend most of its time waiting for the UART.
The trade is worth it because serial output survives things framebuffer output does not. It works when the display mode has not been set, when the framebuffer is not mapped, when the kernel has crashed and the display controller has been reset by firmware, and when the machine is headless. A serial console can also be captured by another machine, which a display cannot.
/* a complete, allocation-free transmit path for a 16550-compatible UART */
#define UART_TX 0
#define UART_LSR 5
#define LSR_THRE 0x20
static inline void outb(uint16_t port, uint8_t value)
{
__asm__ volatile ("outb %0, %1" : : "a"(value), "Nd"(port));
}
static inline uint8_t inb(uint16_t port)
{
uint8_t v;
__asm__ volatile ("inb %1, %0" : "=a"(v) : "Nd"(port));
return v;
}
void early_putc(uint16_t base, char c)
{
while (!(inb(base + UART_LSR) & LSR_THRE))
; /* poll: the transmitter is still busy */
outb(base + UART_TX, (uint8_t)c);
}
The loop has no timeout. A UART whose clock is not configured, or a port address that decodes to nothing, makes inb return 0xFF forever, and the loop never exits. Early console code that polls must either bound the loop or accept that a misconfigured port hangs the boot before any message can explain why.
The firmware services path
The alternative is to leave the firmware’s own output routine callable. On UEFI, the system table carries a console output protocol with a function pointer that writes a string to whichever device the firmware selected, and that protocol remains valid after the kernel has taken over memory management only if the kernel keeps the firmware’s code and data mapped and does not call ExitBootServices until it is done printing.
That path is convenient because it handles the device selection, the font, and the mode. It is also fragile: the firmware’s code runs in the kernel’s address space with the kernel’s page tables, and any firmware bug becomes a kernel fault. The runtime services are specified to be callable, but the console output protocol is a boot service, and the specification says boot services are unavailable after ExitBootServices. A kernel that wants firmware console output after that point cannot have it, which is precisely the point at which a kernel most wants to print.
A middle path, used by loaders and by kernels that support it, is to have the loader hand over a description of an already-initialized device and let the kernel drive it directly. The loader has the firmware’s services available, can query the framebuffer geometry or program the UART’s divisor, and can pass the resulting parameters in the boot structure. The kernel then has a device it can poll without any firmware dependency.
Rate, buffering, and the cost of a log line
Early output competes with the work the kernel is trying to do. A print statement in a hot early path, such as per-page initialization during memory setup, can multiply boot time by a large factor because each character costs either a poll loop or a store to uncached memory. Framebuffer writes are worse than serial in one respect: the framebuffer is typically mapped as uncached, so each pixel store is a bus transaction, and a single character cell at 8x16 pixels is 128 stores.
The usual mitigation is a log buffer. Messages are formatted into a ring buffer in ordinary memory, and a console that drains the buffer is registered later, once the relevant driver exists. Early messages are then available retroactively, and the cost during boot is a memory copy rather than a device transaction. The buffer itself must be statically allocated, because the allocator is not running when the first message is written.
The other mitigation is level filtering. A kernel parameter controls which messages reach the early console, and the default is quiet enough that boot is not dominated by output. The messages that do print are the ones that indicate progress through the stages where a hang would otherwise be silent.
What early output cannot report
A console that depends on the kernel’s own page tables cannot report a fault in those tables. If the mapping for the framebuffer or the UART is wrong, the first write faults, the fault handler tries to print, and the print faults. The result is a reset or a hang with no output, and the absence of output is indistinguishable from a hang before the console was initialized.
The escape from that class of failure is hardware assistance: a debug port, a management controller’s serial redirection, or a platform-specific trace facility that the processor writes to without any kernel involvement. Those are outside the kernel’s software path and survive its faults, which is why they exist.
Once the kernel can print, it can begin initializing the subsystems that will eventually own the console, the scheduler, and the filesystem. The step from a single running thread to the first user-space process is covered in from kernel thread to PID 1, and the point at which firmware output stops being available is the ExitBootServices transition whose effect on the memory map is described in how firmware describes physical memory.
References
- VESA BIOS Extension (VBE) Core Functions Standard, Version 3.0 — Video Electronics Standards Association
- Unified Extensible Firmware Interface Specification — UEFI Forum
- 16550D Universal Asynchronous Receiver/Transmitter with FIFOs — Texas Instruments
- Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3: System Programming Guide — Intel Corporation