首页 / 💻 技术交流 / 如何自制一个Hobby OS(兴趣爱好操
💻 技术交流

如何自制一个Hobby OS(兴趣爱好操作系统) —— 第一篇 Bootloader与Kernel,GDT64与IDT64

自制 Bootloader:从 MBR 到 64 位 C 内核与中断

本文目标:从 BIOS 加载 MBR 开始,手写一套完整的 Bootloader(MBR -> VBR -> Loader),经过保护模式进入长模式,进入Kernel,并在内核里重建 GDT64、配置 IDT64,最后通过 sti 打开中断,得到时钟和键盘响应。


0. 验收目标

教程结束时,内核在 QEMU 里应做到:

  1. 64 位模式下通过串口(COM1, 0x3F8)打印字符串;

  2. 时钟中断(IRQ0 -> 向量 32)持续计数;

  3. 键盘敲键有响应(IRQ1 -> 向量 33)。

里程碑

位置

验证方式

活着进入长模式

Loader 末尾

串口打出 K!

C 世界接管

entry.asm -> kmain

kmain 里 kprintf 正常输出

中断体系打通

idt + pic 之后

sti 后时钟计数增长、按键有回显

工具链

nasm          # 汇编器
ld.lld        # 链接器(内核 ELF)
zig cc / gcc  # C 编译器
qemu-system-x86_64   # 无头启动 + debugcon 0xE9 回读日志
objcopy       # ELF -> 裸二进制

调试约定:实模式下用 BIOS int 0x10 打字符;进入长模式后 BIOS 不可用,改用 out 0xE9(debugcon)——QEMU 加 -debugcon stdio 就能在终端看到。每个阶段出口输出一个字符,卡住时可快速定位所在阶段。

磁盘布局

扇区 LBA

内容

0

MBR(boot.asm,512 字节)

2048 起

FAT32 分区:保留区 -> FAT -> 数据区

数据区

kernel.bin(内核裸二进制)+ loader


1. MBR:第一段 512 字节

BIOS 把磁盘第一个扇区(512 字节)加载到 0x7C00,检查末尾 0x55 0xAA 后跳进来执行。此时 CPU 处于实模式 16 位,可用的只有 DL 寄存器中的启动驱动器号和 BIOS 中断。

MBR 只做一件事:找到活动分区,把 VBR 读进内存并跳转过去。VBR 加载到 0x600,与 0x7C00 不冲突。

org 0x7C00
    cli
    xor ax, ax
    mov ss, ax
    mov sp, 0x7C00
    mov ds, ax
    mov es, ax
    mov [0x0FFE], dl        ; 保存 BIOS 给的驱动器号,后面还要用

    ; 在 MBR 分区表(0x7BE 起,4 个 16 字节表项)里找活动分区 0x80
    mov si, 0x7C00 + 0x1BE
    mov cx, 4
.find:
    cmp byte [si], 0x80
    je  .have
    add si, 16
    loop .find
    mov si, 0x7C00 + 0x1BE
.have:
    mov eax, [si + 8]       ; 表项偏移 8 = 起始 LBA
    mov [dap + 8], eax      ; 填进 DAP 的起始 LBA 字段

    ; int 13h 扩展读(AH=42h):读 1 个扇区到 0x60:0
    mov si, dap
    mov dl, byte [0x0FFE]
    mov ah, 0x42
    int 0x13
    jc  err
    jmp word 0x0000:0x0600  ; 跳进 VBR

dap:    db 0x10, 0, 1, 0
        dw 0x0060
        dd 0, 0

    times 510-($-$$) db 0
    db 0x55, 0xAA

踩坑

  • 不要依赖 BIOS 留下的任何段寄存器值,先 cli + 全部清零自设,栈也自己设。

  • org 0x7C00 告诉汇编器"我运行时在 0x7C00",与实际加载地址不一致会造成难以定位的错误。

  • 用 AH=42h(LBA 扩展读)而不是 AH=02h(CHS 读),现代盘的 CHS 几何是模拟值,不可靠。

  • 末尾 0x55 0xAA 缺失时 BIOS 不加载。


2. VBR:从 FAT32 读取 Loader

VBR 位于分区第一个扇区,被 MBR 加载到 0x600。它自带一份 FAT32 的 BPB(BIOS Parameter Block),从里面能算出文件系统布局:

FAT_LBA  = 分区起始 + 保留扇区数
DATA_LBA = FAT_LBA + FAT份数 × 每FAT扇区数

VBR 的任务:在根目录簇里逐扇区扫描 8.3 短文件名,匹配到 LOADER ASM 之类的文件后,顺着 FAT 簇链把整个文件读到 0xC200,然后 jmp 0xC200

核心逻辑(简化骨架):

ORG 0x0600
LOAD_ADDR equ 0xC200

start:
    cli
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00
    sti
    mov al, byte [0x0FFE]   ; 从 MBR 那里接过的驱动器号
    mov byte [drive], al

    mov word [curclu], ROOT_CLUSTER
.dirscan:                   ; 逐簇读根目录
    mov ax, word [curclu]
    sub ax, 2
    add ax, DATA_LBA
    call readsec            ; 读一个扇区到 DIR_BUF
    jc  fail
    mov bx, DIR_BUF
.entry:                     ; 16 个 32 字节目录项/扇区
    cmp byte [bx], 0x00     ; 0x00 = 后面没有更多项
    je  .nextroot
    cmp byte [bx], 0xE5     ; 0xE5 = 已删除
    je  .next
    ; —— 用 11 字节短文件名逐字符比较,匹配则转读文件 ——
    ; 读文件:起始簇 -> FAT_LBA 表里查下一簇 -> 循环读到 0xFFFF(EOF)
    ...
.next:  add bx, 32
    ; 扫完本扇区 -> 下一扇区;扫完本簇 -> FAT 查下一簇

踩坑

  • 目录项首字节 0x00 表示其后无目录项,可提前结束;0xE5 是已删除,要跳过。

  • 长文件名(LFN)是若干 0x0F 属性项的串,更可靠的做法是给文件起规范的 8.3 短名(如 LOADER ASM),只匹配短名。

  • FAT 簇号从 2 开始计数:LBA = DATA_LBA + (簇号 - 2) × 每簇扇区数

  • 读扇区务必自己封一个 readsec(内部走 int 13h AH=42h),DAP 里的传输缓冲区必须按 16 字节对齐的段:偏移给出。

到这里,0xC200 处已经有一个功能完整的 Loader。此后 BIOS 只剩一个用途:设置显卡模式。


3. Loader:VBE 设屏 + 保存启动信息

Loader 开头仍是 16 位代码(org 0xC200),在退出实模式前做三件事:

① VBE 设图形模式int 0x10; AX=4F01 查模式信息块,AX=4F02 + BX=模式号|0x4000 设置线性帧缓冲。把宽、高、VRAM 物理地址、总字节数存到约定内存(如 0x0FF0 附近),将来内核直接来取。失败就退化到 VGA mode 13h(320×200)。这一步是固定的"查—设—存"流程。

② 拷贝内核到最终物理地址。Loader 此前已经把 kernel.bin 也读进了低端内存临时区,这里 memcpy 到最终位置(如 0x280000)。做 KASLR 的话,用两次 rdtsc 异或出一个随机槽位。

③ 组装启动信息结构(类 Multiboot2 风格:magic + 总长 + 一串 tag),写到一个双方约定好的物理地址(如 0x5000),C 内核按约定解析。


4. 进保护模式:GDT32 + CR0.PE

现在关中断(cli),开始切换模式。先讲清楚段描述符

段描述符是 8 字节的一条记录,描述"一段内存从哪开始、多长、能不能执行、特权级多少"。实模式的"段寄存器存基址"到保护模式变成了"段寄存器存描述符在 GDT 里的下标(选择子)"。操作系统根本不用分段,所以全部用平坦模型:所有段都从 0 开始、盖满 4GB、只是代码段可执行/数据段可写这个区别。

; GDTR 结构:2 字节长度 + 4 字节基地址
GDTR0:
    dw GDT0_LEN - 1
    dd GDT0

GDT0:
    dq 0x0000000000000000           ; 0x00: 必须的空描述符
    dq 0x000092000000FFFF           ; 0x08: 数据段 base=0 limit=4GB 可读写
    dq 0x00CF9A000000FFFF           ; 0x10: 代码段 base=0 limit=4GB 可执行

注意描述符字节序是故意打散的:limit 拆两截、base 拆三截,属性夹在中间。0x9A = Present + Ring0 + Code,0x92 = Present + Ring0 + Data,0xCF 里的 G=1 表示 limit 单位是 4KB。

切换动作三步:lgdt -> 置 CR0.PE -> 远跳转

    cli
    call waitkbdout
    mov al, 0xD1            ; 8042 键盘控制器命令:发 A20
    out 0x64, al
    call waitkbdout
    mov al, 0xDF            ; A20 = 1,地址线 21 根可用
    out 0x60, al

    lgdt [GDTR0]
    mov eax, cr0
    or  eax, 0x00000001     ; CR0.PE = 1,进入保护模式
    mov cr0, eax
    jmp DWORD 2*8:pipelineflush   ; 关键:远跳转刷新流水线

pipelineflush:
    bits 32
    mov ax, 1*8             ; 重载数据段寄存器
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, STACK_PHYS

踩坑

  • lgdt 之后必须远跳转jmp 2*8:label)。CS 不能用 mov 改,远跳转让 CPU 用新选择子重新取指、清空预取队列——不做这步,CPU 还按旧的 16 位语义解码,直接崩。

  • A20 没开的话,0x100000 以上的地址会回绕到 0,内核一进高地址就诡异挂掉。

  • 置 CR0.PE 之前 GDT 必须已在内存里。

  • 从这一刻起 BIOS 中断全部作废。


5. 进长模式:页表 + EFER + GDT64

保护模式只是中间状态。长模式强制要求分页——不建页表无法进入,这也是整个流程中最容易出错的一步,分四步走。

5.1 建页表:恒等映射 + 高半区

x86_64 用四级页表:PML4 -> PDPT -> PD -> PT,每级 512 项,每项 8 字节,虚拟地址 48 位有效。教程里把页表页固定在物理 0x90000 一带,共 4 个 4KB 页。用 1GB 大页(PDTE 的 PS 位)最简洁:一个表项映射 1GB。

    mov edi, 0x90000        ; 清零页表区
    mov ecx, 0x9000 / 4
    xor eax, eax
    rep stosd

    ; PML4[0] -> PDPT@0x91000:覆盖低 512GB
    mov dword [0x90000], 0x91007        ; 属性 0x007 = Present|RW|User

    ; PDPT[0] -> PD@0x92000,用 1GB 页恒等映射前 10GB
    mov dword [0x91000], 0x92007
    mov dword [0x92000 + 0*8], 0x000083 ; 0x083 = Present|RW|PS(1GB)
    mov dword [0x92000 + 1*8], 0x200083 ; 1GB @ 0x40000000
    mov dword [0x92000 + 2*8], 0x400083
    mov dword [0x92000 + 3*8], 0x600083
    mov dword [0x92000 + 4*8], 0x800083
    mov dword [0x92000 + 5*8], 0xA00083

    ; 高半区:让 0xC0000000 处也能摸到内核物理页
    ; (把 PDPT 里指向内核物理地址的 1GB 页,挂到虚拟 3GB 位置)
    mov dword [0x91000 + 3*8], <内核物理 1GB 页描述符>

设计意图:恒等映射(虚拟地址 = 物理地址)保证切换页表时当前代码仍可执行;高半区0xC0000000 起)是内核最终链接地址——KERNEL_VIRT = 0xC0000000 + KERNEL物理地址,所有内核符号链接在高地址,用户态无法访问。

5.2 GDT64

64 位的 GDT 反而更简单:分段机制基本弃用,起作用的只剩 L 位和兼容位:

GDT64:
    dq 0x0000000000000000       ; 0x00 空
    dq 0x00209A0000000000       ; 0x08 代码64: L=1(长模式) D=0
    dq 0x00CF92000000FFFF       ; 0x10 数据
    dq 0x00CF9A000000FFFF       ; 0x18 代码32(兼容模式用)
GDT64_LEN equ $ - GDT64

GDTR64:
    dw GDT64_LEN - 1
    dq GDT64                    ; 64 位模式下 GDTR 基地址是 8 字节

代码64 描述符 0x00209A0000000000 拆开看:type=0xA(可执行可读)、L 位=1(长模式)、D 位=0(长模式下必须 D=0)。base/limit 全 0——64 位下段基址强制为 0,limit 不检查。

5.3 切换序列

    lgdt [GDTR64]

    mov eax, cr4
    or  eax, 0x20           ; CR4.PAE = 1,长模式前提
    mov cr4, eax

    mov eax, 0x90000        ; CR3 = PML4 物理地址
    mov cr3, eax

    mov ecx, 0xC0000080     ; EFER MSR
    rdmsr
    or  eax, 0x100          ; EFER.LME = 1,允许长模式
    wrmsr

    mov eax, cr0
    or  eax, 0x80000000     ; CR0.PG = 1,开分页
    mov cr0, eax            ; PG+PAE+LME 齐活 -> CPU 自动进入长模式

    jmp 0x08:lg64           ; 用 64 位代码选择子远跳,正式落位

    bits 64
lg64:
    mov ax, 0x10            ; 重载全部数据段
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov rsp, STACK_PHYS

踩坑

  • 顺序锁死:PAE -> CR3 -> EFER.LME -> CR0.PG。LME 没置就开 PG 是非法组合,直接 triple fault。

  • 页表必须在 mov cr3 之前建好、内存写完;切页表那一刻,当前正在执行的代码所在区域必须两种映射下都可访问(恒等映射就是为此存在的)。

  • 长模式下段描述符的 L 位和 D 位互斥:L=1 时 D 必须为 0。

  • 32 位代码物理执行在低地址,但链接地址在高地址——链接脚本里要把 ELF 入口段映射成"虚拟高地址 + 物理低地址"双重 VMA/LMA,否则 objcopy 出来的二进制跳不过去。

  • jmp 0x08:lg64 之后紧跟 bits 64,nasm 不自动切。

5.4 跳转内核入口

64 位下最后一段汇编工作:

    mov eax, 0x36D76289     ; 启动 magic(Multiboot2 风格)
    mov ebx, MBI            ; 启动信息结构地址
    mov rcx, KERNEL_VIRT    ; C 入口链接地址
    mov edx, [l_kphys]      ; 内核实际物理加载地址(KASLR 用)
    jmp rcx

四个寄存器携带四项信息,跳转到内核入口。

里程达成判定:串口输出 K!


6. 进入内核

C 代码运行前需要两个前提:栈可用、BSS 段为零。内核入口汇编先处理这两件事(entry.asm):

bits 64
section .text
extern kmain
extern __bss_start
extern __bss_end

global entry_start
entry_start:
    mov r8d, eax            ; 留存 loader 传来的四个寄存器
    mov r9,  rbx
    mov r10d, edx

    ; 初始化 COM1 串口 0x3F8,输出 "K!" 验证串口可用
    mov dx, 0x3F8 + 1
    xor al, al
    out dx, al              ; 关中断
    inc dx
    out dx, al
    inc dx
    out dx, al
    inc dx
    mov al, 0x03
    out dx, al              ; 8N1
    ...
    mov dx, 0x3F8
    mov al, 'K'
    out dx, al
    mov al, '!'
    out dx, al

    ; 清 BSS:C 全局变量区必须从零开始
    movabs rdi, __bss_start
    movabs rax, __bss_end
    sub rax, rdi
    mov rcx, rax
    xor eax, eax
    cld
    rep stosb

    cli                     ; 环境就绪前禁止中断
    mov edi, r8d            ; SysV 调用约定传参
    mov esi, r9d
    mov edx, r10d
    call kmain
.spin:
    hlt
    jmp .spin

对应的链接脚本要保证:.text 的 VMA 在 0xC0000000 高地址,LMA 在低物理地址;导出 __bss_start / __bss_end 符号。

ENTRY(entry_start)
SECTIONS
{
    . = 0xC0000000 + 0x280000;
    __kernel_start = .;
    .text : AT(ADDR(.text) - 0xC0000000) { *(.text*) }
    .rodata : AT(ADDR(.rodata) - 0xC0000000) { *(.rodata*) }
    .data : AT(ADDR(.data) - 0xC0000000) { *(.data*) }
    __bss_start = .;
    .bss : AT(ADDR(.bss) - 0xC0000000) { *(.bss*) *(COMMON) }
    __bss_end = .;
}

AT(ADDR(...) - 0xC0000000) 就是"链接时认为自己住在高地址、实际烧录在低物理地址"的关键。

kmain 接收启动信息并初始化帧缓冲控制台后,kprintf("[OK] C world") ——里程碑 ② 达成


7. 重建 GDT64

Loader 的 GDT 是临时的:位于 loader 内存区,不含 TSS、TLS、per-CPU 表项。内核需要建立自己的正式 GDT:

struct gdt_desc {
    uint16_t limit_low, base_low;
    uint8_t  base_mid, attr_low;
    uint8_t  limit_high_attr_high, base_high;
} __attribute__((packed));

struct gdt_desc gdt[GDT_ENTRIES];

void gdt_init(void) {
    desc_init(&gdt[0], 0, 0, 0, 0);          // 空
    desc_init(&gdt[1], 0, 0, 0x9A, 0x20);    // 内核代码 64 位 (attr_high 0x20 = L位)
    desc_init(&gdt[2], 0, 0xFFFFF, 0x92, 0xCF); // 内核数据
    desc_init(&gdt[3], 0, 0xFFFFF, 0x9A, 0xCF); // 内核代码 32 兼容
    desc_init(&gdt[4], 0, 0xFFFFF, 0xF2, 0xCF); // 用户数据
    desc_init(&gdt[5], 0, 0xFFFFF, 0xFA, 0xCF); // 用户代码 64
    desc_init(&gdt[6], 0, 0, 0xFA, 0x20);    // 用户代码 64 专用

    gdtr0.limit = sizeof(gdt) - 1;
    gdtr0.base  = (uint64_t)gdt;
    asm_lgdt((uint64_t)&gdtr0);
}

选择子编号从此固定为内核约定:0x08 内核代码、0x10 内核数据、0x1B 用户代码、0x23 用户数据——后面 IDT 里的门全用 0x08

踩坑框 ⑤lgdt 之后 CS/SS 等段寄存器还是旧选择子的缓存值,严格来说要远跳 + 重载;实操中由于新旧 GDT 布局一致可暂缓,但 TSS 一旦挂载(ltr),每个核都必须重载。


8. IDT64 + 中断打通

8.1 门描述符:16 字节

64 位的 IDT 门描述符翻倍成 16 字节,因为要装得下 64 位处理程序地址:

struct IDTEntry {
    uint16_t offset_low;    // 处理程序地址 bit 0-15
    uint16_t selector;      // = 0x08 内核代码段
    uint8_t  ist;           // 中断栈表,0 = 不切换
    uint8_t  type_attr;     // 0x8E = Present|Ring0|Interrupt Gate
    uint16_t offset_mid;    // bit 16-31
    uint32_t offset_high;   // bit 32-63
    uint32_t reserved;
} __attribute__((packed));

struct IDTEntry idt[256];

static void idt_set(uint8_t vec, void (*handler)(void), uint8_t type) {
    uint64_t addr = (uint64_t)handler;
    idt[vec].offset_low  = (uint16_t)(addr & 0xFFFF);
    idt[vec].selector    = SELECTOR_KERNEL_CODE;
    idt[vec].ist         = 0;
    idt[vec].type_attr   = type;
    idt[vec].offset_mid  = (uint16_t)((addr >> 16) & 0xFFFF);
    idt[vec].offset_high = (uint32_t)((addr >> 32) & 0xFFFFFFFF);
    idt[vec].reserved    = 0;
}

8.2 stub:为什么异常入口必须用汇编

两个原因:① 中断发生瞬间 CPU 不保存通用寄存器,C 函数不懂保护现场;② 有的异常 CPU 自动压错误码,有的不压——栈布局不一致,C 没法统一处理。所以用宏生成 256 个入口代码:

[bits 64]
%macro ISR_NOERR 1      ; CPU 不压错误码的异常:手动补一个 0
global isr%1
isr%1:
    push qword 0
    push qword %1
    jmp isr_common_stub
%endmacro
%macro ISR_ERR 1        ; CPU 已压错误码(如 #PF(14)、#DF(8)、#GP(13))
global isr%1
isr%1:
    push qword %1
    jmp isr_common_stub
%endmacro

ISR_NOERR 0
ISR_ERR   14
; …… 0~31 按手册逐个标定 ……

isr_common_stub:
    push rax            ; 保存全部易失寄存器
    push rbx
    push rcx
    push rdx
    push rsi
    push rdi
    push rbp
    push r8  .. push r15
    mov  rdi, rsp       ; 把栈帧指针作为第一个参数传给 C
    call isr_handler
    pop  r15 .. pop rax ; 原路恢复
    add  rsp, 16        ; 弹掉 错误码+向量号
    iretq

C 侧注册就非常干净:

void idt_init(void) {
    for (int i = 0; i < 256; ++i)
        idt_set(i, default_handler, IDT_TYPE_INT_GATE64);
    idt_set(0,  isr0,  IDT_TYPE_INT_GATE64);   // 逐个挂 0~31 CPU 异常
    ...
    idt_set(32, irq0,  IDT_TYPE_INT_GATE64);   // IRQ0~15 -> 32~47
    idt_set(33, irq1,  IDT_TYPE_INT_GATE64);

    idtr0.limit = sizeof(idt) - 1;
    idtr0.base  = (uint64_t)idt;
    __asm__ volatile("lidt %0" : : "m"(idtr0) : "memory");
}

初始化顺序上的惯例:先把全部 256 个向量注册到 default_handler,再精细覆盖。任何未处理的异常都会进入"打印向量号 + 停机"的可观测路径,而不是无限 triple fault 重启,排查效率差别很大。

8.3 PIC 重映射:把硬件中断搬出异常区

8259 PIC 默认把 IRQ0~7 映射到向量 8~15——与 CPU 异常区重叠(8 是 #DF,14 是 #PF)。不重映射,缺页异常会与硬件中断冲突。重映射需要依次写入四组 ICW:

int pic_init(void) {
    outb(PIC1_CMD, 0x11);       // ICW1:级联 + 需要 ICW4
    outb(PIC2_CMD, 0x11);
    outb(PIC1_DATA, 0x20);      // ICW2:主片 IRQ0 -> 向量 32
    outb(PIC2_DATA, 0x28);      //        从片 IRQ8 -> 向量 40
    outb(PIC1_DATA, 0x04);      // ICW3:主片 IR2 接从片
    outb(PIC2_DATA, 0x02);
    outb(PIC1_DATA, 0x01);      // ICW4:8086 模式
    outb(PIC2_DATA, 0x01);
    outb(PIC1_DATA, 0xF8);      // OCW1 掩码:只放行时钟/键盘等
    outb(PIC2_DATA, 0xAF);
    return 0;
}

void pic_send_eoi(uint8_t irq) {
    if (irq >= 8) outb(PIC2_CMD, 0x20);   // 从片要先 EOI
    outb(PIC1_CMD, 0x20);
}

8.4 开中断:sti

初始化完成后,在中断处理中计数并打印,然后:

kprintf("[OK] kernel init done, enable IRQs\n");
__asm__ volatile("sti");

里程碑 ③ 达成判定:时钟中断计数每秒增长、键盘按下时 IRQ1 触发。三个里程碑全部达成,从 Bootloader 到长模式、C 内核、中断体系的整条启动链路验证完成。


9. 镜像打包与运行

最后一步是把各文件写入磁盘镜像:

# 1. 汇编
nasm -f bin arch/x86/boot/boot.asm   -o build/boot.bin
nasm -f bin arch/x86/boot/vbr.asm    -o build/vbr.bin
nasm -f bin arch/x86/boot/loader.asm -o build/loader.bin

# 2. C 内核
zig cc -ffreestanding -fno-pic -mcmodel=kernel -c ...
ld.lld -T linker/kernel.ld -o build/kernel.elf ...
objcopy -O binary build/kernel.elf build/kernel.bin

# 3. 打盘
dd if=/dev/zero of=build/disk.img bs=1M count=64
# 用 mtools 或自写脚本把 boot.bin 写入 LBA0、vbr.bin 写入分区头,
# loader.bin 和 kernel.bin 按短文件名放进 FAT32 根目录

# 4. 运行
qemu-system-x86_64 -drive format=raw,file=build/disk.img \
    -display none -serial mon:stdio -debugcon stdio

10. 踩坑速查

#

阶段

后果

1

MBR

依赖 BIOS 遗留段寄存器/栈

随机崩溃

2

MBR

org 与加载地址不一致

指针计算错误

3

VBR

只匹配 LFN、忽略 8.3 短名

格式化方式一变就找不到文件

4

VBR

簇号忘了从 2 起算

读出垃圾数据

5

PM

lgdt 后没远跳刷流水线

按 16 位语义解码,立即出错

6

PM

A20 未开

高地址回绕,系统挂死

7

LM

LME 未置先开 PG

triple fault

8

LM

页表没建好就切 CR3

triple fault

9

LM

L=1 的描述符 D 位没清 0

非法描述符

10

LM

链接脚本 VMA/LMA 没分离

跳转后地址错误

11

entry

BSS 没清零

全局变量初值随机,极难定位

12

IDT

64 位门当 8 字节写

地址截断,返回地址错误

13

stub

错误码有无没归一化

栈错位,返回地址错乱

14

PIC

不重映射就 sti

硬件中断砸进 CPU 异常区

15

PIC

EOI 顺序错(从片先于主片)

中断只来一次


结语

请自行感悟

可以前去https://github.com/lightfl0w/CoreZ-OS查看一个正在做到OS()


🏆 打赏榜(共 1 人)
dYz00 100

💬 全部评论 1

共 1 条 · 按楼层排序
登录 注册 后即可发表评论
dYz00 Lv.2 一砖一瓦
11天前
#1 楼
这么强
👍 0 💰 打赏