玩转 BIOS:从启动到接管硬件的完整指南

"BIOS 不仅是启动代码,更是硬件的终极控制台。
本文将深入 BIOS 的启动流程、中断服务、硬件检测,
为你揭示如何在实模式下完全掌控 x86 硬件。"

引言:BIOS 的现代价值

在 UEFI 时代,许多人认为 BIOS 已经过时。
但对自制操作系统开发者来说,BIOS 仍是最佳的学习起点

  • 简化启动:无需处理复杂的 UEFI 协议
  • 硬件抽象:提供标准中断服务(INT 10h, INT 13h, INT 16h)
  • 调试友好:实模式调试比保护模式简单得多
  • 兼容性好:所有 x86 PC 都支持 BIOS 启动

本文将带你玩转 BIOS,从启动流程到硬件控制,
为自制 OS 构建坚实的启动基础。


第一章:BIOS 启动流程深度解析

1.1 上电自检(POST)阶段

BIOS 启动序列

1. 上电 → CPU 复位 → CS:IP = 0xF000:0xFFF0
2. 执行跳转指令 → BIOS 代码段
3. 执行 POST(Power-On Self Test)
4. 检测硬件(内存、显卡、键盘、磁盘)
5. 初始化硬件(设置中断向量表、配置 DMA)
6. 加载启动设备(MBR)
7. 跳转到 MBR(0x7C00)

关键内存布局

| 地址范围 | 用途 | |———-|——| | 0x0000-0x03FF | 中断向量表(IVT)| | 0x0400-0x04FF | BIOS 数据区(BDA)| | 0x0500-0x7BFF | 可用内存 | | 0x7C00-0x7DFF | 引导扇区 | | 0x7E00-0x9FFF | 引导程序堆栈 | | 0xA000-0xBFFF | 显存 | | 0xC000-0xDFFF | BIOS 扩展 ROM | | 0xE000-0xFFFF | BIOS ROM |

1.2 中断向量表(IVT)

IVT 结构

; 中断向量表(每个中断 4 字节:偏移 + 段)
; 地址 0x0000:0x0000
dw timer_handler    ; INT 08h - 时钟中断
dw 0x0000
dw keyboard_handler ; INT 09h - 键盘中断  
dw 0x0000
dw video_handler    ; INT 10h - 视频服务
dw 0x0000
dw disk_handler     ; INT 13h - 磁盘服务
dw 0x0000
dw serial_handler   ; INT 14h - 串口服务
dw 0x0000
dw mouse_handler    ; INT 33h - 鼠标服务
dw 0x0000

自定义中断处理

; 安装自定义时钟中断处理程序
install_timer_handler:
    cli                     ; 禁用中断
    mov ax, cs
    mov [0x0020], timer_handler  ; INT 08h 偏移
    mov [0x0022], ax             ; INT 08h 段
    sti                     ; 启用中断
    ret

timer_handler:
    push ax
    push dx
    
    ; 更新系统时间
    inc word [timer_ticks]
    
    ; 调用原始 BIOS 中断(可选)
    pushf
    call far [0x0024]       ; 原始 INT 08h
    
    pop dx
    pop ax
    iret

1.3 BIOS 数据区(BDA)

BDA 关键字段

| 偏移 | 大小 | 用途 | |——|——|——| | 0x0410 | word | 设备列表(bit 7=DMA, bit 6=FDD, bit 5=LPT2, bit 4=LPT1, bit 3=RS232, bit 2=Game, bit 1=Tape, bit 0=FDD)| | 0x0413 | word | 基础内存大小(KB)| | 0x0449 | byte | 当前视频模式 | | 0x0450 | word | 当前光标位置(高字节=行,低字节=列)| | 0x0463 | dword | BIOS 视频服务入口 |

读取 BDA 信息

// 读取基础内存大小
uint16_t get_base_memory(void) {
    uint16_t *bda_base_mem = (uint16_t*)0x0413;
    return *bda_base_mem;
}

// 读取视频模式
uint8_t get_video_mode(void) {
    uint8_t *bda_video_mode = (uint8_t*)0x0449;
    return *bda_video_mode;
}

第二章:BIOS 中断服务详解

2.1 视频服务(INT 10h)

常用功能

| AH | 功能 | 参数 | |—-|——|——| | 0x00 | 设置视频模式 | AL=模式 | | 0x02 | 设置光标位置 | DH=行, DL=列, BH=页 | | 0x03 | 读取光标位置 | BH=页 | | 0x06 | 滚动窗口向上 | AL=行数, BH=属性, CH=左上行, CL=左上列, DH=右下行, DL=右下列 | | 0x0E | 电传打字输出 | AL=字符, BH=页, BL=前景色 |

文本模式操作

; 设置 80x25 文本模式
mov ah, 0x00
mov al, 0x03
int 0x10

; 设置光标到 (10, 20)
mov ah, 0x02
mov bh, 0x00    ; 页 0
mov dh, 10      ; 行 10
mov dl, 20      ; 列 20
int 0x10

; 输出字符 'A'
mov ah, 0x0E
mov al, 'A'
mov bh, 0x00
mov bl, 0x07    ; 白色前景,黑色背景
int 0x10

图形模式操作

; 设置 320x200 256 色模式
mov ah, 0x00
mov al, 0x13
int 0x10

; 写像素 (x=100, y=50, color=15)
mov ah, 0x0C
mov al, 15      ; 颜色
mov cx, 100     ; X 坐标
mov dx, 50      ; Y 坐标
int 0x10

; 读像素
mov ah, 0x0D
mov cx, 100
mov dx, 50
int 0x10
; 返回颜色在 AL

2.2 磁盘服务(INT 13h)

磁盘参数

  • CHS 寻址:柱面(Cylinder)、磁头(Head)、扇区(Sector)
  • 扇区大小:512 字节
  • 最大容量:8.4GB(1024 柱面 × 256 磁头 × 63 扇区 × 512 字节)

读取扇区

; 读取 1 个扇区到 0x7E00
read_sector:
    pusha
    
    mov ah, 0x02        ; 读取扇区功能
    mov al, 0x01        ; 扇区数
    mov ch, 0x00        ; 柱面
    mov cl, 0x02        ; 扇区(1-63)
    mov dh, 0x00        ; 磁头
    mov dl, 0x80        ; 驱动器(0x80=第一硬盘)
    mov bx, 0x7E00      ; 缓冲区地址
    int 0x13
    
    jc disk_error       ; CF=1 表示错误
    
    popa
    ret

disk_error:
    mov si, error_msg
    call print_string
    hlt

error_msg: db "Disk error!", 0

磁盘参数表(DPT)

// 获取磁盘参数
struct disk_parameter_table {
    uint16_t buffer;        // 缓冲区偏移
    uint16_t count;         // 扇区数
    uint16_t sector;        // 起始扇区
    uint16_t cylinder;      // 起始柱面
    uint8_t head;           // 起始磁头
    uint8_t drive;          // 驱动器号
    uint8_t flags;          // 标志
};

// 扩展磁盘服务(INT 13h, AH=48h)
int get_disk_info(uint8_t drive, struct disk_parameter_table *dpt) {
    struct {
        uint16_t size;      // 结构大小
        uint16_t flags;     // 标志
        uint32_t cylinders; // 柱面数
        uint32_t heads;     // 磁头数
        uint32_t sectors;   // 每磁道扇区数
        uint64_t total_sectors; // 总扇区数
        uint16_t bytes_per_sector; // 每扇区字节数
    } buffer;
    
    buffer.size = sizeof(buffer);
    
    __asm__ volatile (
        "int $0x13"
        : "=a"(buffer)
        : "a"(0x4800), "d"(drive), "S"(&buffer)
        : "memory"
    );
    
    return (buffer.flags & 0x01) ? 0 : -1; // 0=成功
}

2.3 键盘服务(INT 16h)

键盘功能

| AH | 功能 | 返回 | |—-|——|——| | 0x00 | 读取字符 | AL=ASCII, AH=扫描码 | | 0x01 | 检查键盘缓冲区 | ZF=1=无字符, AX=字符 | | 0x02 | 获取移位状态 | AL=移位状态 |

键盘输入处理

; 等待按键
wait_key:
    mov ah, 0x00
    int 0x16
    ; AL = ASCII, AH = 扫描码
    ret

; 检查是否有按键
check_key:
    mov ah, 0x01
    int 0x16
    jz no_key       ; ZF=1 表示无按键
    
    ; AX = 字符
    ret

no_key:
    xor ax, ax
    ret

扫描码处理

// 扫描码到 ASCII 转换表
static const char scan_to_ascii[128] = {
    0, 0x1B, '1', '2', '3', '4', '5', '6',
    '7', '8', '9', '0', '-', '=', '\b', '\t',
    'q', 'w', 'e', 'r', 't', 'y', 'u', 'i',
    'o', 'p', '[', ']', '\n', 0, 'a', 's',
    'd', 'f', 'g', 'h', 'j', 'k', 'l', ';',
    '\'', '`', 0, '\\', 'z', 'x', 'c', 'v',
    'b', 'n', 'm', ',', '.', '/', 0, '*',
    0, ' ', 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, '7',
    '8', '9', '-', '4', '5', '6', '+', '1',
    '2', '3', '0', '.'
};

第三章:BIOS 硬件检测与配置

3.1 内存检测

INT 15h, E820 内存映射

// E820 内存映射条目
struct e820_entry {
    uint64_t base;      // 基地址
    uint64_t length;    // 长度
    uint32_t type;      // 类型(1=可用, 2=保留, 3=ACPI, 4=NVS)
    uint32_t reserved;
};

// 获取内存映射
int get_memory_map(struct e820_entry *map, int max_entries) {
    uint32_t cont_id = 0;
    int count = 0;
    
    do {
        __asm__ volatile (
            "int $0x15"
            : "=a"(cont_id), "=c"(map[count].type), "=d"(map[count].reserved)
            : "a"(0xE820), "b"(map[count].base), "d"(map[count].length), "c"(20), "D"(cont_id)
            : "memory"
        );
        
        if (cont_id == 0) break;
        
        // 检查是否为可用内存
        if (map[count].type == 1 && map[count].length > 0) {
            count++;
            if (count >= max_entries) break;
        }
    } while (cont_id != 0);
    
    return count;
}

内存类型说明

| 类型 | 说明 | |——|——| | 1 | 可用 RAM | | 2 | 保留(硬件使用)| | 3 | ACPI 重声明 | | 4 | NVS(非易失性存储)| | 5 | 有缺陷的内存 |

3.2 CPU 检测

CPUID 指令

// CPUID 结构
struct cpuid_result {
    uint32_t eax, ebx, ecx, edx;
};

// 执行 CPUID
static inline struct cpuid_result cpuid(uint32_t leaf) {
    struct cpuid_result result;
    __asm__ volatile (
        "cpuid"
        : "=a"(result.eax), "=b"(result.ebx), "=c"(result.ecx), "=d"(result.edx)
        : "a"(leaf)
        : "memory"
    );
    return result;
}

// 检测 CPU 功能
int detect_cpu_features(void) {
    struct cpuid_result result = cpuid(0x00000001);
    
    int features = 0;
    if (result.edx & (1 << 15)) features |= CPUID_FPU;
    if (result.edx & (1 << 23)) features |= CPUID_MMX;
    if (result.edx & (1 << 25)) features |= CPUID_SSE;
    if (result.ecx & (1 << 0))  features |= CPUID_SSE3;
    if (result.ecx & (1 << 9))  features |= CPUID_SSSE3;
    
    return features;
}

3.3 PCI 设备检测

PCI 配置空间访问

// PCI 配置空间读取
uint32_t pci_read_config(uint8_t bus, uint8_t slot, uint8_t func, uint8_t offset) {
    uint32_t address = (1 << 31) | (bus << 16) | (slot << 11) | (func << 8) | (offset & 0xFC);
    
    outl(0xCF8, address);
    return inl(0xCFC + (offset & 0x3));
}

// 扫描 PCI 总线
void pci_scan(void) {
    for (int bus = 0; bus < 256; bus++) {
        for (int slot = 0; slot < 32; slot++) {
            uint16_t vendor = pci_read_config(bus, slot, 0, 0x00);
            if (vendor == 0xFFFF) continue; // 无设备
            
            uint16_t device = pci_read_config(bus, slot, 0, 0x02);
            uint32_t class_rev = pci_read_config(bus, slot, 0, 0x08);
            
            printf("PCI %02x:%02x.0 - %04x:%04x (class %06x)\n", 
                   bus, slot, vendor, device, class_rev >> 8);
        }
    }
}

第四章:BIOS 启动扇区开发

4.1 引导扇区结构

MBR 格式

| 偏移 | 大小 | 用途 | |——|——|——| | 0x0000 | 446 | 引导代码 | | 0x01BE | 16 | 分区表项 1 | | 0x01CE | 16 | 分区表项 2 | | 0x01DE | 16 | 分区表项 3 | | 0x01EE | 16 | 分区表项 4 | | 0x01FE | 2 | 签名(0xAA55)|

最小引导扇区

; boot.asm
bits 16
org 0x7C00

start:
    ; 设置段寄存器
    cli
    mov ax, 0x07C0
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00
    sti
    
    ; 清屏
    mov ah, 0x06
    mov al, 0x00
    mov bh, 0x07
    mov cx, 0x0000
    mov dx, 0x184F
    int 0x10
    
    ; 输出消息
    mov si, msg
    call print_string
    
    ; 无限循环
    cli
    hlt
    jmp $

print_string:
    lodsb
    or al, al
    jz .done
    mov ah, 0x0E
    mov bx, 0x0007
    int 0x10
    jmp print_string
.done:
    ret

msg: db "BIOS Bootloader!", 0

; 填充到 510 字节
times 510-($-$$) db 0

; 签名
dw 0xAA55

4.2 磁盘加载器

加载后续扇区

; 加载 32 个扇区到 0x8000
load_kernel:
    mov si, disk_error_msg
    
    ; 重置磁盘
    mov ah, 0x00
    mov dl, 0x80
    int 0x13
    jc disk_error
    
    ; 加载扇区
    mov ax, 0x0800    ; 目标段
    mov es, ax
    xor bx, bx        ; 目标偏移
    mov ah, 0x02      ; 读取功能
    mov al, 32        ; 扇区数
    mov ch, 0x00      ; 柱面
    mov cl, 0x02      ; 起始扇区
    mov dh, 0x00      ; 磁头
    mov dl, 0x80      ; 驱动器
    int 0x13
    jc disk_error
    
    ; 跳转到内核
    jmp 0x0800:0x0000

disk_error:
    call print_string
    hlt

4.3 实模式到保护模式切换

启用 A20 地址线

enable_a20:
    ; 方法 1:键盘控制器
    call a20_wait
    mov al, 0xAD
    out 0x64, al
    call a20_wait
    mov al, 0xD0
    out 0x64, al
    call a20_wait2
    in al, 0x60
    or al, 2
    call a20_wait
    mov al, 0xD1
    out 0x64, al
    call a20_wait
    mov al, al
    out 0x60, al
    call a20_wait
    mov al, 0xAE
    out 0x64, al
    call a20_wait
    
    ret

a20_wait:
    in al, 0x64
    test al, 2
    jnz a20_wait
    ret

a20_wait2:
    in al, 0x64
    test al, 1
    jz a20_wait2
    ret

设置 GDT 并切换到保护模式

; GDT 描述符
gdt_start:
    dq 0x0                    ; 空描述符

gdt_code:
    dw 0xFFFF                 ; 段界限 0-15
    dw 0x0                    ; 基地址 0-15
    db 0x0                    ; 基地址 16-23
    db 10011010b              ; 类型:代码段,可读
    db 11001111b              ; 粒度:4KB,32位
    db 0x0                    ; 基地址 24-31

gdt_
    dw 0xFFFF
    dw 0x0
    db 0x0
    db 10010010b              ; 数据段,可写
    db 11001111b
    db 0x0

gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

; 切换到保护模式
switch_to_pm:
    ; 加载 GDT
    lgdt [gdt_descriptor]
    
    ; 启用保护模式
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    
    ; 远跳转刷新 CS
    jmp 0x08:protected_mode_start

[bits 32]
protected_mode_start:
    ; 设置段寄存器
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    
    ; 调用 C 代码
    call KERNEL_ENTRY_POINT

第五章:BIOS 调试技巧

5.1 实模式调试

使用 Bochs 调试

# bochsrc.txt
megs: 32
romimage: file=/usr/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest
ata0-master: type=disk, path=disk.img, mode=flat
boot: disk
log: bochsout.txt
debug: 1

调试命令

# 启动 Bochs
bochs -f bochsrc.txt

# Bochs 调试器命令
b 0x7c00        # 在引导扇区设置断点
c               # 继续执行
r               # 查看寄存器
xp /10bx 0x7c00 # 查看内存

5.2 硬件模拟调试

QEMU 调试

# 启用调试
qemu-system-i386 -kernel kernel.bin -hda disk.img -s -S

# GDB 连接
gdb kernel.bin
(gdb) target remote :1234
(gdb) set architecture i8086
(gdb) break *0x7c00
(gdb) continue

5.3 BIOS 仿真工具

使用 coreboot 仿真

# 构建 coreboot 仿真
make menuconfig
# 选择 Hardware → Emulation → QEMU
make

# 运行仿真
./build/coreboot.rom

使用 SeaBIOS

# 编译 SeaBIOS
git clone https://github.com/coreboot/seabios.git
make

# 在 QEMU 中使用
qemu-system-i386 -bios out/bios.bin -hda disk.img

第六章:BIOS 与现代启动的对比

6.1 BIOS vs UEFI

特性 BIOS UEFI
模式 16 位实模式 32/64 位保护模式
启动速度 慢(POST 详细) 快(快速启动)
磁盘支持 MBR(2TB 限制) GPT(8ZB 限制)
文件系统 无(直接扇区访问) FAT32(可读文件)
驱动 内置(16 位) 模块化(32/64 位)
安全性 Secure Boot

6.2 Legacy Boot 的优势

对自制 OS 开发者的价值

  • 简化启动:无需处理复杂的 UEFI 协议
  • 硬件控制:直接访问硬件端口
  • 调试友好:实模式调试工具成熟
  • 兼容性:所有 x86 PC 支持

何时转向 UEFI

  • 大磁盘支持:>2TB 磁盘
  • 安全启动:Secure Boot 需求
  • 现代硬件:UEFI-only 系统
  • 性能要求:快速启动需求

6.3 混合启动策略

BIOS 兼容模式

// 检测启动方式
int detect_boot_mode(void) {
    // 检查是否在 UEFI 模式下
    if (*((uint32_t*)0x00000400) == 0x55AA) {
        return BOOT_MODE_UEFI;
    } else {
        return BOOT_MODE_BIOS;
    }
}

双重引导扇区

+------------------+
|  MBR (BIOS)      |  // 446 字节引导代码
+------------------+
|  GPT 头部        |  // UEFI 兼容
+------------------+
|  分区 1          |
+------------------+
|  ...             |
+------------------+

结论:BIOS 的持久价值

尽管 UEFI 已成为现代标准,
BIOS 仍然是自制操作系统开发者的最佳起点

通过深入理解 BIOS:

  • 掌握硬件启动流程
  • 学会实模式编程
  • 理解中断服务机制
  • 构建调试调试技能

这些技能不仅适用于 BIOS 环境,
更为理解 UEFI 和现代操作系统奠定坚实基础。

真正的硬件掌控,
始于对 BIOS 的深刻理解。