- C 100%
| idk.c | ||
| irq_pit.md | ||
| README.md | ||
Serial Port (UART 16550A) Deep Dive Guide
Table of Contents
- What is a Serial Port?
- Hardware Level: UART in x86 Architecture
- UART 16550A Register Map
- I/O Port Addresses
- How Serial Port Works in Your Hypervisor
- Detailed Instruction Examples
- Memory Map and Architecture Diagram
- CPU and UART Communication Flow
- Extra UART Ports for Linux Kernel x86
- Virtualization Strategy
What is a Serial Port?
A serial port is a physical interface that transfers data one bit at a time (sequentially). In x86 systems, serial communication is handled by a UART (Universal Asynchronous Receiver/Transmitter) chip, historically the Intel 8250 or the more advanced National Semiconductor 16550A.
Why "UART"?
- Universal: Works with various protocols
- Asynchronous: No clock signal shared between sender/receiver
- Receiver/Transmitter: Handles both incoming and outgoing data
Hardware Level: UART in x86 Architecture
Physical Hardware Evolution
┌─────────────────────────────────────────────────────────────────┐
│ Hardware Evolution │
│ │
│ 1980s: Intel 8250 UART (1-byte buffer) │
│ ↓ │
│ 1987: National Semi 16550 (buggy FIFO) │
│ ↓ │
│ 1989: National Semi 16550A (16-byte FIFO, working) │
│ ↓ │
│ Modern: Integrated into chipset/Super I/O or emulated in QEMU │
└─────────────────────────────────────────────────────────────────┘
Physical Location on Motherboard
In real hardware, the UART exists in one of these forms:
- Discrete Chip (old systems): A physical 16550A chip on the motherboard
- Super I/O Chip (modern legacy): Integrated into a Super I/O controller (e.g., Nuvoton NCT6779D)
- Southbridge/PCH (modern): Part of the Platform Controller Hub
- Virtual/Emulated (VMs/Hypervisors): Emulated in software by QEMU/KVM
┌─────────────────────────────────────────────────────────────────┐
│ Motherboard Layout (Simplified) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ CPU │◄──►│ Northbridge │◄──►│ RAM │ │
│ │ (Core i7) │ │ (Memory │ │ (DDR4) │ │
│ │ │ │ Controller)│ │ │ │
│ └──────┬───────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ │ DMI/LPC Bus │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Southbridge / PCH │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Super I/O Controller │ │ │
│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │
│ │ │ │ UART #1│ │ UART #2│ │ UART │ ... │ │ │
│ │ │ │0x3F8 │ │0x2F8 │ │0x3E8 │ │ │ │
│ │ │ └────────┘ └────────┘ └────────┘ │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Also contains: USB, SATA, PCIe, RTC, etc. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Serial Port Connector (DB9) ──► External Cable │
└─────────────────────────────────────────────────────────────────┘
How CPU Communicates with UART
The CPU talks to UART through I/O ports (not memory-mapped I/O on x86). This uses special instructions:
┌──────────────────────────────────────────────────────────────┐
│ CPU-to-UART Communication Path │
│ │
│ CPU executes: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ IN AL, 0x3F8 ; Read from UART port 0x3F8 │ │
│ │ OUT 0x3F8, AL ; Write to UART port 0x3F8 │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ CPU Bus Interface │ │
│ │ (I/O Space, not Memory Space) │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ │ I/O Read/Write Cycle │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ LPC Bus (Low Pin Count) / ISA Bus │ │
│ │ (Legacy bus for low-speed peripherals) │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ UART 16550A Chip │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ I/O Decoder: Detects 0x3F8-0x3FF range │ │ │
│ │ └──────────────────┬──────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Register File (8 registers) │ │ │
│ │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ... │ │ │
│ │ │ │ RBR │ │ IER │ │ FCR │ │ LCR │ │ │ │
│ │ │ │0x00 │ │0x01 │ │0x02 │ │0x03 │ │ │ │
│ │ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
UART 16550A Register Map
The UART has 8 registers, each 8 bits wide, accessed via I/O ports. The register accessed depends on the port address offset (0-7) and sometimes the LCR register state.
Complete Register Map
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│ UART 16550A Register Map (I/O Port Offsets) │
├──────────┬───────────┬─────────┬─────────────────────────────────────────────────────────┤
│ Offset │ Register │ Access │ Description │
│ (hex) │ Name │ │ │
├──────────┼───────────┼─────────┼─────────────────────────────────────────────────────────┤
│ 0x00 │ RBR │ Read │ Receiver Buffer Register (incoming data) │
│ │ THR │ Write │ Transmitter Holding Register (outgoing data) │
│ │ DLL │ R/W* │ Divisor Latch Low (when LCR[7] = 1, DLAB mode) │
├──────────┼───────────┼─────────┼─────────────────────────────────────────────────────────┤
│ 0x01 │ IER │ Write │ Interrupt Enable Register │
│ │ DLM │ R/W* │ Divisor Latch High (when LCR[7] = 1, DLAB mode) │
├──────────┼───────────┼─────────┼─────────────────────────────────────────────────────────┤
│ 0x02 │ IIR │ Read │ Interrupt Identification Register │
│ │ FCR │ Write │ FIFO Control Register │
├──────────┼───────────┼─────────┼─────────────────────────────────────────────────────────┤
│ 0x03 │ LCR │ R/W │ Line Control Register (data length, parity, stop bits) │
├──────────┼───────────┼─────────┼─────────────────────────────────────────────────────────┤
│ 0x04 │ MCR │ R/W │ Modem Control Register (RTS, DTR, OUT1, OUT2) │
├──────────┼───────────┼─────────┼─────────────────────────────────────────────────────────┤
│ 0x05 │ LSR │ Read │ Line Status Register (TX/RX ready, errors) │
├──────────┼───────────┼─────────┼─────────────────────────────────────────────────────────┤
│ 0x06 │ MSR │ Read │ Modem Status Register (CTS, DSR, RI, DCD) │
├──────────┼───────────┼─────────┼─────────────────────────────────────────────────────────┤
│ 0x07 │ SCR │ R/W │ Scratch Register (general-purpose storage) │
└──────────┴───────────┴─────────┴─────────────────────────────────────────────────────────┘
* DLL/DLM accessed only when LCR bit 7 (DLAB) = 1
Detailed Register Bit Fields
1. RBR/THR/DLL (Offset 0x00)
┌─────────────────────────────────────────────────────────────────┐
│ RBR (Read) - Receiver Buffer Register │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Bit 7 │ Bit 6 │ Bit 5 │ Bit 4 │ Bit 3 │ Bit 2 │ Bit 1 │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ Received Data Byte │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ THR (Write) - Transmitter Holding Register │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Bit 7 │ Bit 6 │ Bit 5 │ Bit 4 │ Bit 3 │ Bit 2 │ Bit 1 │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ Data to Transmit │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
2. IER/DLM (Offset 0x01)
┌─────────────────────────────────────────────────────────────────┐
│ IER (Write) - Interrupt Enable Register │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ Bit7 │ Bit6 │ Bit5 │ Bit4 │ Bit3 │ Bit2 │ Bit1 │ Bit0 │ │
│ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │ 0 │ 0 │ 0 │ 0 │ Sleep│ LSE │ RTLS │ RDA │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ └──────┤ │
│ │ │ │ │ │ │ │ Received │ │
│ │ │ │ │ │ │ │ Data │ │
│ │ │ │ │ │ │ │ Available │ │
│ │ │ │ │ │ │ └──────────────┘ │
│ │ │ │ │ │ └───────────────────────────┤ │
│ │ │ │ │ │ Transmitter Holding Register │ │
│ │ │ │ │ │ Empty (THRE) │ │
│ └──────┴──────┴──────┴──────┴──────────────────────────────────┘ │
│ │
│ Bit 0 (RDA): Received Data Available Interrupt │
│ Bit 1 (THRE): Transmitter Holding Register Empty Interrupt │
│ Bit 2 (RTLS): Receiver Line Status Interrupt │
└─────────────────────────────────────────────────────────────────────┘
3. IIR/FCR (Offset 0x02)
┌─────────────────────────────────────────────────────────────────┐
│ IIR (Read) - Interrupt Identification Register │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ Bit7 │ Bit6 │ Bit5 │ Bit4 │ Bit3 │ Bit2 │ Bit1 │ Bit0 │ │
│ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │ FIFO │ FIFO │ INT │ INT │ INT │ │ │ NO │ │
│ │ 1 │ 0 │ 3 │ 2 │ 1 │ 0 │ 0 │ INT │ │
│ │ │ │ │ │ │ │ │ │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ Bit 0: 0 = Interrupt Pending, 1 = No Interrupt │
│ Bits 1-3: Interrupt Type (if Bit 0 = 0): │
│ 0110 = Receiver Line Status (highest priority) │
│ 0100 = Received Data Available │
│ 0110 = Character Timeout (FIFO mode) │
│ 0010 = THR Empty (transmitter ready) │
│ 0000 = Modem Status (lowest priority) │
│ Bits 6-7: FIFO status (11 = FIFO enabled) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ FCR (Write) - FIFO Control Register │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ Bit7 │ Bit6 │ Bit5 │ Bit4 │ Bit3 │ Bit2 │ Bit1 │ Bit0 │ │
│ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │ RX │ RX │ TX │ TX │ DMA │ Clear│ Clear│ FIFO │ │
│ │ Trig1│ Trig0│ Trig1│ Trig0│ Mode │ RX │ TX │ Enbl │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ Bit 0: 1 = Enable FIFO (must be 1 for 16550A) │
│ Bit 1: 1 = Clear RX FIFO (self-clearing) │
│ Bit 2: 1 = Clear TX FIFO (self-clearing) │
│ Bit 3: 1 = DMA Mode Select │
│ Bits 6-7: RX Trigger Level: │
│ 00 = 1 byte, 01 = 4 bytes, 10 = 8 bytes, 11 = 14 bytes │
└─────────────────────────────────────────────────────────────────┘
4. LCR (Offset 0x03) - Line Control Register
┌─────────────────────────────────────────────────────────────────┐
│ LCR - Line Control Register (R/W) │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ Bit7 │ Bit6 │ Bit5 │ Bit4 │ Bit3 │ Bit2 │ Bit1 │ Bit0 │ │
│ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │ DLAB │ Set │ Stick│ Even │ Parity│ Stop │ Word │ Word │ │
│ │ │ Break│ Parity│Parity│ Enable│ Bits │ Len1 │ Len0 │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ Bit 7 (DLAB): Divisor Latch Access Bit │
│ 0 = Access RBR/THR/IER │
│ 1 = Access DLL/DLM (baud rate divisor) │
│ │
│ Bits 0-1: Word Length │
│ 00 = 5 bits, 01 = 6 bits, 10 = 7 bits, 11 = 8 bits │
│ │
│ Bit 2: Stop Bits │
│ 0 = 1 stop bit, 1 = 2 stop bits (1.5 if 5-bit word) │
│ │
│ Bit 3: Parity Enable │
│ 0 = No parity, 1 = Parity enabled │
│ │
│ Bit 4: Even Parity Select │
│ 0 = Odd parity, 1 = Even parity │
│ │
│ Bit 5: Stick Parity │
│ 0 = Normal, 1 = Stick parity (forced) │
│ │
│ Bit 6: Set Break │
│ 1 = Force TX to logic 0 (break condition) │
│ │
│ Bit 7 (DLAB): Divisor Latch Access │
└─────────────────────────────────────────────────────────────────┘
5. MCR (Offset 0x04) - Modem Control Register
┌─────────────────────────────────────────────────────────────────┐
│ MCR - Modem Control Register (R/W) │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ Bit7 │ Bit6 │ Bit5 │ Bit4 │ Bit3 │ Bit2 │ Bit1 │ Bit0 │ │
│ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │ 0 │ 0 │ 0 │ 0 │ 0 │ LOOP │ OUT2 │ OUT1 │ │
│ │ │ │ │ │ │ BACK │ │ │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ └────────────────────┬─────────────────┬──────┬──────┘ │
│ │ │ └──┐ │
│ Bit 0: OUT1 - Auxiliary Output 1 │
│ Bit 1: OUT2 - Auxiliary Output 2 (enables interrupts) │
│ Bit 4: LOOP - Loopback Mode (for testing) │
└─────────────────────────────────────────────────────────────────┘
6. LSR (Offset 0x05) - Line Status Register ⭐ MOST IMPORTANT
┌─────────────────────────────────────────────────────────────────┐
│ LSR - Line Status Register (Read) ⭐ │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ Bit7 │ Bit6 │ Bit5 │ Bit4 │ Bit3 │ Bit2 │ Bit1 │ Bit0 │ │
│ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │FIFO │ TEMT │ THRE │ BI │ FE │ PE │ OE │ DR │ │
│ │Err │ │ │ │ │ │ │ │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ Bit 0 (DR): Data Ready │
│ 0 = No data in RBR, 1 = Data received in RBR │
│ │
│ Bit 1 (OE): Overrun Error │
│ 1 = Received data overwritten before being read │
│ │
│ Bit 2 (PE): Parity Error │
│ 1 = Parity error detected on received data │
│ │
│ Bit 3 (FE): Framing Error │
│ 1 = No valid stop bit received │
│ │
│ Bit 4 (BI): Break Interrupt │
│ 1 = RX line held low longer than one character │
│ │
│ Bit 5 (THRE): Transmitter Holding Register Empty ⭐ │
│ 0 = THR full (still transmitting), 1 = THR empty (ready) │
│ → Set to 1 immediately after OUT instruction │
│ → Cleared to 0 when CPU writes to THR │
│ │
│ Bit 6 (TEMT): Transmitter Empty │
│ 0 = Transmitting in progress, 1 = Transmitter AND THR empty │
│ → Set to 1 only when both THR and shift register are empty │
│ │
│ Bit 7: FIFO Data Error (16550A FIFO mode only) │
│ 1 = At least one error in FIFO │
└─────────────────────────────────────────────────────────────────┘
Common LSR Values and Meaning:
┌──────────┬────────────────────────────────────────────────┐
│ Value │ Meaning │
├──────────┼────────────────────────────────────────────────┤
│ 0x00 │ All clear, nothing ready │
│ 0x01 │ Data ready in RBR (DR=1) │
│ 0x20 │ THR empty, ready to transmit (THRE=1) │
│ 0x40 │ Transmitter completely empty (TEMT=1) │
│ 0x60 │ THRE + TEMT = 1 (ready & idle) ⭐ │
│ 0x61 │ Data ready AND THR empty │
└──────────┴────────────────────────────────────────────────┘
7. MSR (Offset 0x06) - Modem Status Register
┌─────────────────────────────────────────────────────────────────┐
│ MSR - Modem Status Register (Read) │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ Bit7 │ Bit6 │ Bit5 │ Bit4 │ Bit3 │ Bit2 │ Bit1 │ Bit0 │ │
│ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │ DCD │ RI │ DSR │ CTS │ΔDCD │ΔRI │ΔDSR │ΔCTS │ │
│ │ │ │ │ │ │ │ │ │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ Bits 0-3: Delta bits (changed since last read) │
│ Bits 4-7: Current state of modem control lines │
│ DCD: Data Carrier Detect │
│ RI: Ring Indicator │
│ DSR: Data Set Ready │
│ CTS: Clear To Send │
└─────────────────────────────────────────────────────────────────┘
8. SCR (Offset 0x07) - Scratch Register
┌─────────────────────────────────────────────────────────────────┐
│ SCR - Scratch Register (R/W) │
│ General-purpose 8-bit storage for software use │
│ Often used to test UART presence: │
│ 1. Write value (e.g., 0xA5) to SCR │
│ 2. Read back from SCR │
│ 3. If value matches, UART is present │
└─────────────────────────────────────────────────────────────────┘
I/O Port Addresses
x86 architecture has a separate I/O address space (64KB, ports 0x0000-0xFFFF) distinct from memory. UART uses this I/O space.
Standard COM Port Assignments
┌─────────────────────────────────────────────────────────────────┐
│ Standard x86 COM Port Assignments │
├──────────┬───────────────┬───────────────┬──────────────────────┤
│ Port │ I/O Range │ IRQ │ Description │
├──────────┼───────────────┼───────────────┼──────────────────────┤
│ COM1 │ 0x3F8-0x3FF │ IRQ 4 │ Primary Serial │
│ COM2 │ 0x2F8-0x2FF │ IRQ 3 │ Secondary Serial │
│ COM3 │ 0x3E8-0x3EF │ IRQ 4 │ Tertiary Serial │
│ COM4 │ 0x2E8-0x2EF │ IRQ 3 │ Quaternary Serial │
└──────────┴───────────────┴───────────────┴──────────────────────┘
Note: Your hypervisor handles BOTH 0x3F8-0x3FF AND 0xF8-0xFF
(0xF8 is likely a short-address decode quirk in KVM)
I/O Port to Register Mapping for COM1 (0x3F8 base)
┌─────────────────────────────────────────────────────────────────┐
│ COM1 Register Map (Base 0x3F8) │
├─────────────┬───────────────┬──────────────────────────────────┤
│ I/O Port │ Register │ Access │
├─────────────┼───────────────┼──────────────────────────────────┤
│ 0x3F8 │ RBR/THR/DLL │ Read/Write │
│ 0x3F9 │ IER/DLM │ Read/Write │
│ 0x3FA │ IIR │ Read │
│ 0x3FA │ FCR │ Write │
│ 0x3FB │ LCR │ Read/Write │
│ 0x3FC │ MCR │ Read/Write │
│ 0x3FD │ LSR │ Read │
│ 0x3FE │ MSR │ Read │
│ 0x3FF │ SCR │ Read/Write │
└─────────────┴───────────────┴──────────────────────────────────┘
How Serial Port Works in Your Hypervisor
Architecture Overview
┌──────────────────────────────────────────────────────────────────┐
│ Your Hypervisor Architecture │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Guest OS (Payload) │ │
│ │ │ │
│ │ payload.asm executes: │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ in al, 0x3FD ; Read LSR register │ │ │
│ │ │ out 0x3F8, al ; Write character to THR │ │ │
│ │ └──────────────────────┬───────────────────────────┘ │ │
│ │ │ │ │
│ └─────────────────────────┼────────────────────────────────┘ │
│ │ KVM intercepts I/O instructions │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ KVM │ │
│ │ │ │
│ │ KVM_RUN exits with: │ │
│ │ run->exit_reason = KVM_EXIT_IO │ │
│ │ run->io.port = 0x3F8 or 0x3FD │ │
│ │ run->io.direction = KVM_EXIT_IO_IN or OUT │ │
│ │ run->io.data_offset → points to data buffer │ │
│ │ │ │
│ └─────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Your VMM (vmm.c) │ │
│ │ │ │
│ │ while(1) { │ │
│ │ KVM_RUN │ │
│ │ switch(run->exit_reason) { │ │
│ │ case KVM_EXIT_IO: │ │
│ │ handle_serial_io(run); // serial.c │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ handle_serial_io() in serial.c │ │
│ │ │ │
│ │ uint8_t *io_data = (uint8_t *)run + run->io.data_offset│ │
│ │ int offset = run->io.port & 0x7; // register select │ │
│ │ │ │
│ │ if (OUT) { │ │
│ │ if (offset == 0) putchar(*io_data); // Print char! │ │
│ │ } else { │ │
│ │ switch(offset) { │ │
│ │ case 0: *io_data = 0x00; // RBR: no data │ │
│ │ case 5: *io_data = 0x60; // LSR: THRE+TEMT=1 │ │
│ │ ... │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Host Console (stdout) │ │
│ │ putchar() → "Hello yo\n" displayed on your terminal │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Why This Approach Works
- No Hardware Required: Your hypervisor emulates UART in software
- KVM Intercepts: When guest executes
IN/OUT, KVM traps to your VMM - Software Simulation: You return fake register values that make guest think UART exists
- Character Output: When guest writes to THR (0x3F8), you just
putchar()to host console
Detailed Instruction Examples
Example 1: Reading LSR (Line Status Register)
; From payload.asm:
in al, 0x3FD ; Read LSR register
Step-by-step CPU execution:
┌──────────────────────────────────────────────────────────────────┐
│ Instruction: IN AL, 0x3FD │
│ Machine Code: E5 FD │
│ │
│ Step 1: CPU Decode │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ CPU fetches instruction from memory at RIP │ │
│ │ Decodes as: IN instruction, port=0x3FD, dest=AL │ │
│ └────────────────────┬────────────────────────────────────┘ │
│ │ │
│ Step 2: I/O Access Attempt │
│ ┌────────────────────┴────────────────────────────────────┐ │
│ │ CPU attempts I/O read on port 0x3FD │ │
│ │ KVM has configured I/O trapping for this port │ │
│ │ → VM Exit triggered! │ │
│ └────────────────────┬────────────────────────────────────┘ │
│ │ │
│ Step 3: KVM Exit to VMM │
│ ┌────────────────────┴────────────────────────────────────┐ │
│ │ kvm_run structure populated: │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ run->exit_reason = KVM_EXIT_IO (value: 2) │ │ │
│ │ │ run->io.direction = KVM_EXIT_IO_IN (value: 0) │ │ │
│ │ │ run->io.size = 1 (1 byte access) │ │ │
│ │ │ run->io.port = 0x3FD │ │ │
│ │ │ run->io.count = 1 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────┬────────────────────────────────────┘ │
│ │ │
│ Step 4: VMM Handler (serial.c) │
│ ┌────────────────────┴────────────────────────────────────┐ │
│ │ handle_serial_io(run) called: │ │
│ │ │ │
│ │ offset = run->io.port & 0x7 = 0x3FD & 0x7 = 5 │ │
│ │ direction = KVM_EXIT_IO_IN (read) │ │
│ │ │ │
│ │ Switch case 5: │ │
│ │ *io_data = 0x60 ← THIS IS THE MAGIC VALUE │ │
│ │ │ │
│ │ Memory layout at io_data: │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ Before: [??] (undefined) │ │ │
│ │ │ After: [0x60] = 0110 0000 binary │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 0x60 = 0110 0000 binary │ │
│ │ Bit 6 (TEMT) = 1 → Transmitter empty │ │
│ │ Bit 5 (THRE) = 1 → THR empty, ready to send │ │
│ └────────────────────┬────────────────────────────────────┘ │
│ │ │
│ Step 5: Return to Guest │
│ ┌────────────────────┴────────────────────────────────────┐ │
│ │ KVM copies io_data value (0x60) into guest's AL register│ │
│ │ Guest resumes execution with AL = 0x60 │ │
│ │ │ │
│ │ Guest code continues: │ │
│ │ test al, 0x20 ; Check THRE bit (bit 5) │ │
│ │ ; 0x60 & 0x20 = 0x20 → ZF = 0 (not zero) │ │
│ │ jz .wait ; NOT taken, continue sending! │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Example 2: Writing Character to THR
; From payload.asm:
mov al, 'H' ; Load character 'H' (0x48)
out 0x3F8, al ; Write to THR
Step-by-step execution:
┌──────────────────────────────────────────────────────────────────┐
│ Instruction: OUT 0x3F8, AL │
│ Machine Code: E6 F8 48 (assuming AL=0x48='H') │
│ │
│ Step 1: CPU State Before │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ AL = 0x48 (ASCII 'H' = 0100 1000 binary) │ │
│ │ RIP points to OUT instruction │ │
│ └────────────────────┬────────────────────────────────────┘ │
│ │ │
│ Step 2: CPU Executes OUT │
│ ┌────────────────────┴────────────────────────────────────┐ │
│ │ CPU attempts I/O write to port 0x3F8 with value 0x48 │ │
│ │ → VM Exit triggered! │ │
│ └────────────────────┬────────────────────────────────────┘ │
│ │ │
│ Step 3: KVM Exit to VMM │
│ ┌────────────────────┴────────────────────────────────────┐ │
│ │ run->exit_reason = KVM_EXIT_IO │ │
│ │ run->io.direction = KVM_EXIT_IO_OUT (value: 1) │ │
│ │ run->io.port = 0x3F8 │ │
│ │ run->io.size = 1 │ │
│ │ │ │
│ │ io_data points to data buffer containing 0x48 │ │
│ └────────────────────┬────────────────────────────────────┘ │
│ │ │
│ Step 4: VMM Handler (serial.c) │
│ ┌────────────────────┴────────────────────────────────────┐ │
│ │ handle_serial_io(run) called: │ │
│ │ │ │
│ │ offset = run->io.port & 0x7 = 0x3F8 & 0x7 = 0 │ │
│ │ direction = KVM_EXIT_IO_OUT (write) │ │
│ │ │ │
│ │ if (offset == 0) { // THR register │ │
│ │ putchar((char)*io_data); // *io_data = 0x48 │ │
│ │ fflush(stdout); │ │
│ │ } │ │
│ │ │ │
│ │ Output to host terminal: │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ 'H' character appears on your terminal! │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────┬────────────────────────────────────┘ │
│ │ │
│ Step 5: Return to Guest │
│ ┌────────────────────┴────────────────────────────────────┐ │
│ │ Guest resumes execution │ │
│ │ In real hardware, LSR THRE bit would now be set │ │
│ │ Our emulation always returns THRE=1, so guest thinks │ │
│ │ UART is always ready (simplified but works!) │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Why Value 0x60 Works
┌──────────────────────────────────────────────────────────────────┐
│ LSR = 0x60 = 0110 0000 binary │
│ │
│ Bit 7 (FIFO Error) = 0 → No FIFO errors │
│ Bit 6 (TEMT) = 1 → Transmitter AND THR empty │
│ Bit 5 (THRE) = 1 → THR empty, ready for new data │
│ Bit 4 (Break Int) = 0 → No break condition │
│ Bit 3 (Frame Error) = 0 → No framing error │
│ Bit 2 (Parity Error)= 0 → No parity error │
│ Bit 1 (Overrun Err) = 0 → No overrun error │
│ Bit 0 (Data Ready) = 0 → No data in receive buffer │
│ │
│ This is the "ideal" state: UART is ready to transmit! │
│ Guest code checks THRE (bit 5) with: test al, 0x20 │
│ 0x60 & 0x20 = 0x20 (non-zero) → ZF=0 → transmitter ready! │
└──────────────────────────────────────────────────────────────────┘
Memory Map and Architecture Diagram
Complete x86 I/O and Memory Map
┌──────────────────────────────────────────────────────────────────┐
│ x86 Address Space Overview │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MEMORY SPACE (4GB in 32-bit) │ │
│ │ │ │
│ │ 0x00000000 ┌─────────────────────────────────────┐ │ │
│ │ │ RAM (Your VM: 4MB) │ │ │
│ │ │ 0x00000000 - 0x003FFFFF │ │ │
│ │ │ ┌───────────────────────────────┐ │ │ │
│ │ 0x00100000 │ │ Your payload.bin loaded here │ │ │ │
│ │ │ │ (RIP starts at 0x100000) │ │ │ │
│ │ │ └───────────────────────────────┘ │ │ │
│ │ 0x003FFFFF └─────────────────────────────────────┘ │ │
│ │ ... (rest unused in your simple VM) │ │
│ │ 0xFFFFFFFF │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ I/O SPACE (64KB: 0x0000-0xFFFF) │ │
│ │ │ │
│ │ 0x0000 ┌──────────────────────────────────────────┐ │ │
│ │ │ DMA Controller, PIC, PIT, etc. │ │ │
│ │ 0x0020 │ (Not emulated in your hypervisor) │ │ │
│ │ │ │ │ │
│ │ 0x00F8 │ ┌──────────────────────────────────┐ │ │ │
│ │ 0x00FF │ │ Serial Port (ALT - COM1 alias) │ │ │ │
│ │ │ │ Your hypervisor handles this! │ │ │ │
│ │ │ └──────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ 0x02F8 │ ┌──────────────────────────────────┐ │ │ │
│ │ 0x02FF │ │ COM2 (Not emulated) │ │ │ │
│ │ │ └──────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ 0x03E8 │ ┌──────────────────────────────────┐ │ │ │
│ │ 0x03EF │ │ COM3 (Not emulated) │ │ │ │
│ │ │ └──────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ 0x03F8 │ ┌──────────────────────────────────┐ │ │ │
│ │ 0x03FF │ │ COM1 (Primary Serial) ⭐ │ │ │ │
│ │ │ │ Your hypervisor handles this! │ │ │ │
│ │ │ │ ┌────────────────────────────┐ │ │ │ │
│ │ │ │ │ 0x3F8: THR/RBR │ │ │ │ │
│ │ │ │ │ 0x3F9: IER/DLM │ │ │ │ │
│ │ │ │ │ 0x3FA: IIR/FCR │ │ │ │ │
│ │ │ │ │ 0x3FB: LCR │ │ │ │ │
│ │ │ │ │ 0x3FC: MCR │ │ │ │ │
│ │ │ │ │ 0x3FD: LSR ← 0x60 │ │ │ │ │
│ │ │ │ │ 0x3FE: MSR │ │ │ │ │
│ │ │ │ │ 0x3FF: SCR │ │ │ │ │
│ │ │ │ └────────────────────────────┘ │ │ │ │
│ │ │ └──────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ 0x0378 │ ┌──────────────────────────────────┐ │ │ │
│ │ 0x037F │ │ Parallel Port (LPT1) │ │ │ │
│ │ │ └──────────────────────────────────┘ │ │ │
│ │ 0xFFFF └──────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
UART Internal Architecture (Hardware Level)
┌──────────────────────────────────────────────────────────────────┐
│ UART 16550A Internal Block Diagram │
│ │
│ CPU Side (I/O Bus) Serial Line Side │
│ ┌────────────────────────┐ ┌──────────────┐ │
│ │ │ │ │ │
│ │ ┌──────────────────┐ │ ┌───────────┐ │ TX Pin ├──┼──► To Serial Cable
│ │ │ I/O Decoder │ │ │ │ │ │ │
│ │ │ (Selects Reg) │ │ │ TX │ │ │ │
│ │ └────────┬─────────┘ │ │ Shift │ │ RX Pin │ │
│ │ │ │ │ Register │ │ ├──┼──◄ From Serial Cable
│ │ ┌────────┴─────────┐ │ └─────┬─────┘ │ │ │
│ │ │ Data Bus │ │ │ │ │ │
│ │ │ (8 bits) │ │ ┌─────▼─────┐ └──────────────┘ │
│ │ └────────┬─────────┘ │ │ RX │ │
│ │ │ │ │ Shift │ │
│ │ ┌────────▼─────────┐ │ │ Register │ │
│ │ │ Register File │ │ └─────┬─────┘ │
│ │ │ ┌──┬──┬──┬──┬──┐ │ │ │ │
│ │ │ │RBR│IER│IIR│LCR│ │ │ ┌─────▼─────┐ │
│ │ │ │THR│ │FCR│MCR│ │ │ │ RX │ │
│ │ │ │DLL│DLM│ │ │ │ │ │ FIFO │ │
│ │ │ │LSR│MSR│SCR│ │ │ │ │ (16 bytes) │ │
│ │ │ └──┴──┴──┴──┴──┘ │ │ └─────────────┘ │
│ │ └──────────────────┘ │ │
│ │ │ ┌──────────────────────────┐ │
│ │ ┌──────────────────┐ │ │ Baud Rate Formula: │ │
│ │ │ Baud Rate │ │ │ │ │
│ │ │ Generator │ │ │ Divisor = │ │
│ │ │ (DLL + DLM) │ │ │ 115200 / Baud Rate │ │
│ │ │ │ │ │ │ │
│ │ │ Divisor = │ │ │ Example: 115200 baud │ │
│ │ │ 115200 / Target │ │ │ Divisor = 115200/115200 │ │
│ │ └──────────────────┘ │ │ Divisor = 1 = 0x0001 │ │
│ │ │ │ │ │
│ │ ┌──────────────────┐ │ │ Example: 9600 baud │ │
│ │ │ Interrupt Logic │ │ │ Divisor = 115200/9600 │ │
│ │ │ (IIR + IER) │ │ │ Divisor = 12 = 0x000C │ │
│ │ └──────────────────┘ │ │ │ │
│ │ │ └──────────────────────────┘ │
│ └────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
CPU and UART Communication Flow
Complete Execution Flow for "Hello yo" Output
┌──────────────────────────────────────────────────────────────────┐
│ Complete Execution Flow (First Character 'H') │
│ │
│ Guest (payload.asm) Host (vmm.c + serial.c) │
│ ┌────────────────────────┐ ┌──────────────────────┐ │
│ │ │ │ │ │
│ │ mov esi, hello_str │ │ │ │
│ │ movzx eax, byte [esi] │ │ │ │
│ │ ; AL = 'H' (0x48) │ │ │ │
│ │ │ │ │ │
│ │ ; Check LSR: │ │ │ │
│ │ in al, 0x3FD │ │ │ │
│ │ │ │ │ │ │
│ │ │ VM Exit (KVM) │───────────►│ │ │
│ │ │ │ │ handle_serial_io() │ │
│ │ │ │ │ offset = 0x3FD&7=5 │ │
│ │ │ │ │ *io_data = 0x60 │ │
│ │ │ │ │ (THRE+TEMT set) │ │
│ │ │◄───────────────────│ │ │ │
│ │ │ VM Resume │ │ │ │
│ │ │ │ │ │
│ │ ; AL = 0x60 now │ │ │ │
│ │ test al, 0x20 │ │ │ │
│ │ ; 0x60 & 0x20 = 0x20 │ │ │ │
│ │ jz .wait │ │ │ │
│ │ ; NOT taken (ZF=0) │ │ │ │
│ │ │ │ │ │
│ │ mov al, [esi] │ │ │ │
│ │ ; AL = 'H' (0x48) │ │ │ │
│ │ out 0x3F8, al │ │ │ │
│ │ │ │ │ │ │
│ │ │ VM Exit (KVM) │───────────►│ │ │
│ │ │ │ │ handle_serial_io() │ │
│ │ │ │ │ offset = 0x3F8&7=0 │ │
│ │ │ │ │ direction = OUT │ │
│ │ │ │ │ putchar('H') ★ │ │
│ │ │ │ │ fflush(stdout) │ │
│ │ │ │ │ │ │
│ │ │ │ │ Terminal shows: H │ │
│ │ │◄───────────────────│ │ │ │
│ │ │ VM Resume │ │ │ │
│ │ │ │ │ │
│ │ inc esi │ │ │ │
│ │ jmp .loop │ │ │ │
│ │ ; Repeat for 'e'... │ │ │ │
│ │ │ │ │ │
│ └────────────────────────┘ └──────────────────────┘ │
│ │
│ Final Terminal Output: "Hello yo\n" │
└──────────────────────────────────────────────────────────────────┘
Extra UART Ports for Linux Kernel x86
Linux kernel on x86 expects up to 4 COM ports. To boot Linux, you need to emulate more than just COM1.
Required UART Ports for Full Linux Support
┌──────────────────────────────────────────────────────────────────┐
│ UART Ports Needed for Linux Kernel (x86) │
├─────────┬──────────────┬──────────┬─────────────────────────────┤
│ Port │ I/O Range │ IRQ │ Priority for Linux Boot │
├─────────┼──────────────┼──────────┼─────────────────────────────┤
│ COM1 │ 0x3F8-0x3FF │ IRQ 4 │ ★★★★ Essential (default │
│ │ │ │ console: ttyS0) │
├─────────┼──────────────┼──────────┼─────────────────────────────┤
│ COM2 │ 0x2F8-0x2FF │ IRQ 3 │ ★★★ Useful (ttyS1) │
├─────────┼──────────────┼──────────┼─────────────────────────────┤
│ COM3 │ 0x3E8-0x3EF │ IRQ 4 │ ★★ Optional (ttyS2) │
├─────────┼──────────────┼──────────┼─────────────────────────────┤
│ COM4 │ 0x2E8-0x2EF │ IRQ 3 │ ★ Optional (ttyS3) │
└─────────┴──────────────┴──────────┴─────────────────────────────┘
Note: Linux kernel can boot with just COM1 (ttyS0).
For minimal kernel: COM1 only is sufficient!
What Linux Kernel Expects
When Linux kernel boots with console=ttyS0,115200, it:
┌──────────────────────────────────────────────────────────────────┐
│ Linux Kernel Serial Initialization │
│ │
│ 1. Kernel boot with: console=ttyS0,115200n8 │
│ │ │
│ ▼ │
│ 2. 8250 serial driver probes COM ports │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Probe sequence for each COM port: │ │
│ │ │ │
│ │ a) Write 0xFF to LCR (offset 3) │ │
│ │ b) Read back LCR │ │
│ │ c) If value == 0xFF, UART might exist │ │
│ │ │ │
│ │ d) Write test pattern to SCR (offset 7) │ │
│ │ e) Read back SCR │ │
│ │ f) If value matches, UART confirmed! │ │
│ │ │ │
│ │ g) Try to enable FIFO with FCR (offset 2) │ │
│ │ h) Read IIR to check FIFO status │ │
│ │ │ │
│ │ i) Calculate UART type (8250, 16450, 16550A, etc.) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 3. Register ttyS0 device │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Configure UART: │ │
│ │ - Set baud rate (DLL/DLM) │ │
│ │ - Set line format (LCR: 8N1 = 8 bits, No parity, 1 stop)│
│ │ - Enable interrupts (IER) │ │
│ │ - Enable FIFO (FCR) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 4. Console ready for printk() output │
└──────────────────────────────────────────────────────────────────┘
Implementation Guide for Additional Ports
To support COM2, COM3, COM4, modify your vmm.c:
// Current code handles:
if ((run->io.port >= 0x3F8 && run->io.port <= 0x3FF) ||
(run->io.port >= 0xF8 && run->io.port <= 0xFF)) {
handle_serial_io(run);
}
// Need to add for full Linux support:
if ((run->io.port >= 0x3F8 && run->io.port <= 0x3FF) || // COM1
(run->io.port >= 0x2F8 && run->io.port <= 0x2FF) || // COM2
(run->io.port >= 0x3E8 && run->io.port <= 0x3EF) || // COM3
(run->io.port >= 0x2E8 && run->io.port <= 0x2EF) || // COM4
(run->io.port >= 0xF8 && run->io.port <= 0xFF)) { // COM1 alias
handle_serial_io(run);
}
Note: All COM ports can share the same handler function since they have identical register layouts at different base addresses!
Virtualization Strategy
How Your Hypervisor Virtualizes UART
┌──────────────────────────────────────────────────────────────────┐
│ UART Virtualization Strategy │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ TRAPPING (Current Approach) │ │
│ │ │ │
│ │ Guest executes IN/OUT → VM Exit → VMM handles → Resume │ │
│ │ │ │
│ │ Pros: Simple, works without hardware │ │
│ │ Cons: High overhead (VM Exit per I/O instruction) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SIMULATION (Your serial.c) │ │
│ │ │ │
│ │ No real UART hardware needed! │ │
│ │ Return hardcoded values that make guest happy: │ │
│ │ │ │
│ │ LSR = 0x60 → "I'm ready to transmit" │ │
│ │ RBR = 0x00 → "No input data available" │ │
│ │ IIR = 0x01 → "No interrupt pending" │ │
│ │ │ │
│ │ This is "polling mode" emulation - guest polls LSR │ │
│ │ instead of using interrupts. Perfect for simple boot! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ADVANCED (For Linux Support) │ │
│ │ │ │
│ │ To boot Linux, you'll need: │ │
│ │ │ │
│ │ 1. Proper SCR read/write (UART detection) │ │
│ │ 2. LCR read/write (line configuration) │ │
│ │ 3. DLL/DLM support (baud rate setting) │ │
│ │ 4. FCR write (FIFO enable) │ │
│ │ 5. IER read (interrupt enable) │ │
│ │ 6. MCR read/write (modem control) │ │
│ │ 7. Interrupt injection (KVM_INTERRUPT) │ │
│ │ │ │
│ │ Your current code only handles: │ │
│ │ - THR write (offset 0, OUT) ✓ │ │
│ │ - RBR read (offset 0, IN) ✓ │ │
│ │ - LSR read (offset 5) ✓ │ │
│ │ - IIR read (offset 2) ✓ │ │
│ │ │ │
│ │ Missing for Linux: SCR, LCR, DLL/DLM, FCR, IER, MCR │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Minimal Implementation for Linux Boot
To boot Linux with console=ttyS0, you need to add:
// In serial.c, expand handle_serial_io():
// Add a scratch register (persists across calls)
static uint8_t uart_scr = 0x00;
static uint8_t uart_lcr = 0x00;
static uint8_t uart_ier = 0x00;
static uint8_t uart_mcr = 0x00;
static uint16_t uart_divisor = 0x00C; // Default 9600 baud
void handle_serial_io(struct kvm_run *run) {
uint8_t *io_data = (uint8_t *)run + run->io.data_offset;
int offset = run->io.port & 0x7;
int is_dlab = uart_lcr & 0x80; // Check DLAB bit
if (run->io.direction == KVM_EXIT_IO_OUT) {
switch (offset) {
case 0: // THR or DLL
if (is_dlab) {
uart_divisor = (uart_divisor & 0xFF00) | *io_data;
} else {
putchar((char)*io_data);
fflush(stdout);
}
break;
case 1: // IER or DLM
if (is_dlab) {
uart_divisor = (uart_divisor & 0x00FF) | (*io_data << 8);
} else {
uart_ier = *io_data;
}
break;
case 2: // FCR
// Just acknowledge FIFO enable
break;
case 3: // LCR
uart_lcr = *io_data;
break;
case 4: // MCR
uart_mcr = *io_data;
break;
case 7: // SCR
uart_scr = *io_data;
break;
}
} else { // IN
switch (offset) {
case 0: // RBR or DLL
if (is_dlab) {
*io_data = uart_divisor & 0xFF;
} else {
*io_data = 0x00; // No input
}
break;
case 1: // IER or DLM
if (is_dlab) {
*io_data = (uart_divisor >> 8) & 0xFF;
} else {
*io_data = uart_ier;
}
break;
case 2: // IIR
*io_data = 0x01; // No interrupt
break;
case 3: // LCR
*io_data = uart_lcr;
break;
case 4: // MCR
*io_data = uart_mcr;
break;
case 5: // LSR
*io_data = 0x60; // THRE + TEMT
break;
case 6: // MSR
*io_data = 0x60; // DSR + CTS asserted
break;
case 7: // SCR
*io_data = uart_scr;
break;
}
}
}
Summary
What Your Code Does
- Traps I/O instructions (
IN/OUT) via KVM - Decodes which UART register is accessed (port & 0x7)
- Simulates UART behavior with hardcoded values
- Outputs characters to host console via
putchar()
Key Values Explained
| Register | Value | Binary | Meaning |
|---|---|---|---|
| LSR | 0x60 | 0110 0000 | THRE=1 (ready), TEMT=1 (empty) |
| IIR | 0x01 | 0000 0001 | No interrupt pending |
| RBR | 0x00 | 0000 0000 | No received data |
| MSR | 0x00 | 0000 0000 | No modem status |
Why It Works
- Guest polls LSR (doesn't use interrupts)
- Your hypervisor always returns ready (0x60)
- Guest writes character → KVM exits → VMM calls
putchar()→ Character appears - No real hardware needed, pure software emulation!
Next Steps for Linux
To boot a real Linux kernel, you need to:
- Implement all UART registers (SCR, LCR, DLL/DLM, FCR, IER, MCR)
- Support baud rate configuration
- Handle UART detection probe (SCR test)
- Optionally support interrupts (not just polling)
- Add COM2/COM3/COM4 port ranges
References
- 16550A Datasheet: National Semiconductor (now TI)
- Intel SDM Vol 3: I/O Instructions (IN, OUT)
- KVM Documentation: KVM_EXIT_IO exit reason
- Linux 8250 Driver:
drivers/tty/serial/8250/ - OSDev Wiki: Serial Ports article