news 2026/10/18 5:23:03

手写32位操作系统内核:从实模式到保护模式的底层实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写32位操作系统内核:从实模式到保护模式的底层实践

1. 为什么今天还要手写一个操作系统内核?

“手写简单操作系统内核”这八个字,乍看像极了计算机系期末大作业的标题,又或是某位极客在深夜朋友圈晒出的“今日成就”。但如果你真点开过 GitHub 上 star 数破万的 osdev-tutorial、xv6 或者 MikeOS 的源码仓库,翻过几页用纯汇编写的 boot sector、用 C 语言重定位的 GDT 描述符表、或者调试过 IDT 中断向量表里第 0x20 号时钟中断为何总不触发——你就会明白:这不是炫技,而是一次对计算机底层契约的亲手确认。

我第一次完整跑通一个能打印 “Hello, Kernel!” 并响应键盘输入的最小内核,是在某高校嵌入式实验室的旧台式机上。那台机器 BIOS 还是传统 16 位实模式,没有 UEFI,连 USB 键盘都要靠 PS/2 模拟芯片才能识别。当时用 NASM 写了 237 行汇编启动代码,用 GCC 交叉编译出 32 位保护模式下的 C 内核,最后烧进软盘镜像(没错,是软盘),在 QEMU 里反复调试了整整 17 个小时。不是因为技术多难,而是因为——每一行代码背后,都绑着硬件真实的电气信号、CPU 精确的执行周期、内存控制器严格的时序要求。这种“不可妥协”的确定性,在现代 Linux 容器、Node.js 异步回调、甚至 Rust 的 borrow checker 里,早已被层层抽象稀释得只剩影子。

所以,“手写简单操作系统内核”的核心价值,从来不是为了替代 Linux 或 Windows,而是为了重建一种“可解释的信任”。当你亲手把 CR0 寄存器的 PE 位从 0 置为 1,亲眼看到 CPU 从实模式跳入保护模式;当你手动配置 PIC 芯片的 ICW1~ICW4,让键盘中断真正抵达你的 C 函数;当你在物理内存中划出一页作为堆栈,并确保每次中断发生时 SS:ESP 都指向它——你才真正理解什么叫“系统调用是特权级切换”,什么叫“进程切换本质是寄存器上下文保存与恢复”,什么叫“一切皆文件”在内核眼里不过是 VFS 层的一个 inode 指针。

这个项目适合三类人:一是刚学完《计算机组成原理》和《操作系统》理论课,但对着课本里的“分页机制”“信号量”“死锁检测”仍觉隔靴搔痒的学生;二是工作多年、日常敲 Python/Java/Webpack 的工程师,想找回对“机器如何真正运行”的手感;三是嵌入式或安全方向的开发者,需要深入理解启动流程、内存布局、异常处理链路,为后续做 BootROM 安全验证、固件逆向分析打下不可替代的直觉基础。它不追求功能完备,但要求每一步都经得起反汇编器和逻辑分析仪的检验——这才是“手写”的意义:不是造轮子,而是亲手拧紧每一颗螺丝。

2. 整体设计思路与关键取舍逻辑

2.1 为什么坚持“从零开始”,拒绝任何现成框架?

市面上有大量“五分钟搭建操作系统”的教程,它们往往基于 xv6、RustOS 或者某个精简版 Linux 内核模板,直接给你一个 Makefile 和一堆头文件,让你改几个 printf 就算完成。这类做法对快速体验“内核启动”有帮助,但会掩盖最致命的问题:你根本不知道哪些代码是必须的,哪些是可选的,哪些是历史包袱。

比如,很多教程默认启用 A20 地址线、加载 GDT、设置 IDT,却不解释为什么实模式下地址只能到 1MB,为什么 A20 必须开启才能访问更高内存,为什么 GDT 的段描述符中 Limit 字段要左移 12 位再加 1。结果就是,一旦你换一台 BIOS 设置不同的机器,或者尝试在真实硬件(而非 QEMU)上运行,整个系统就卡死在第一条指令之后——因为你没动过底层,所以也无从排查。

我坚持“从零开始”的设计,意味着第一行代码必须是org 0x7c00,最后一行链接脚本必须明确指定.text段起始地址为0x100000(1MB),所有 C 代码必须用-ffreestanding -fno-builtin编译,禁用标准库。这种“自废武功”式的约束,恰恰逼你直面三个核心问题:

  • 启动阶段的控制权移交:BIOS 如何把控制权交给你的代码?你的代码又如何说服 CPU 进入保护模式?
  • 内存的绝对主权:没有 malloc,没有虚拟内存管理,你必须亲自规划哪块物理内存放代码、哪块放堆栈、哪块留给将来做页表。
  • 外设的原始对话:键盘不是/dev/input/event0,而是 I/O 端口0x60;屏幕不是 framebuffer 设备,而是显存地址0xb8000处的字符+属性双字节映射。

提示:所有“省事”的封装,最终都会变成调试时的黑箱。真正的效率,来自对底层确定性的掌握,而非对工具链的依赖。

2.2 为什么选择 32 位保护模式,而非 64 位长模式?

当前主流 CPU 都是 64 位,网上教程也越来越多转向 long mode。但对我而言,32 位保护模式仍是教学最优解,理由非常具体:

  • 复杂度断层明显:64 位长模式要求你先在保护模式下初始化 GDT,再通过mov cr4, cr4 | (1<<5)开启 PAE,再设置 PDPT/PD/PT 四级页表,最后执行mov cr0, cr0 | 1和mov cr4, cr4 | (1<<8)才能进入 long mode。而 32 位保护模式只需:1)准备 GDT 描述符;2)用lgdt加载;3)置位 CR0.PE。步骤少了一半,出错路径更短。

  • 调试信息更友好:QEMU 的-d int参数可以清晰打印每次中断的来源、CS:EIP 值、压栈内容;GDB 的target remote :1234能直接反汇编0x100000处的指令。但在 64 位下,寄存器名变成rax/rbx,地址宽度变长,GDB 的符号解析更容易失败,初学者常卡在“为什么我的函数地址显示为<function+0>却看不到源码”。

  • 硬件兼容性更强:某些老旧笔记本或工控主板的 BIOS 不支持 UEFI 启动,甚至无法正确进入 64 位模式。而 32 位保护模式是 x86 架构自 1985 年 80386 以来的基石特性,只要 CPU 是 386 及以上,100% 支持。

当然,这不是否定 64 位的价值。只是在“建立直觉”的阶段,我们要的是“可控的复杂度”,而不是“前沿的完整性”。等你能在 32 位下熟练写出任务状态段 TSS、实现简单的进程调度器后,迁移到 64 位不过是多写几张页表、改几个寄存器名的事——底层逻辑一模一样。

2.3 为什么内核主体用 C,而非全汇编?

有人会问:既然都手写了,为什么不全用汇编?毕竟启动代码已经是汇编了。答案很务实:可维护性与表达效率的平衡点就在 C 语言。

汇编的优势在于绝对控制,比如你可以精确指定某条指令用mov eax, 0x12345678还是push 0x12345678; pop eax来绕过某些 CPU 的微架构限制。但它的劣势同样致命:写一个字符串输出函数,你要手动计算显存偏移、处理换行、维护光标位置变量;实现一个简单的链表插入,你要反复计算结构体字段偏移、管理临时寄存器、处理边界条件。这些工作与“操作系统原理”毫无关系,纯粹是体力活。

C 语言在此刻展现出惊人优势:它提供指针运算(直接操作物理地址)、结构体(自然映射 GDT/IDT 描述符布局)、函数调用约定(保证堆栈干净)、以及最重要的——编译器生成的代码足够接近汇编,且完全可预测。用gcc -S -O2 kernel.c生成的汇编,你能逐行对应到 C 源码;用objdump -d kernel.o反汇编,寄存器使用、跳转逻辑一目了然。这意味着,你既能享受高级语言的开发效率,又从未失去对底层的掌控力。

我实际项目中,汇编只用于三处:1)boot sector(512 字节,必须);2)进入保护模式的跳转 stub;3)中断处理入口(保存通用寄存器)。其余所有逻辑——内存管理、键盘驱动、屏幕输出、系统调用分发——全部用 C 实现。这样划分后,整个内核代码量稳定在 1200 行以内,新增一个系统调用只需修改两个文件:syscall.h增加编号,syscall.c添加处理函数,编译即生效。

3. 核心模块拆解与实操细节

3.1 启动引导:从 BIOS 到保护模式的 512 字节生死线

这是整个内核最脆弱也最关键的环节。BIOS 在加电自检(POST)完成后,会将软盘或硬盘的第一个扇区(512 字节)读入内存地址0x7c00,然后跳转执行。这 512 字节,就是你的“数字胎盘”。

它必须完成四件事:

  1. 自我重定位:因为0x7c00是临时区域,后续要加载内核代码,所以第一件事是把自己拷贝到0x90000(高端内存,避开后续使用);
  2. 关闭中断:cli指令防止外部中断打断关键设置;
  3. 开启 A20 地址线:通过键盘控制器 0x64/0x60 端口发送命令,否则 CPU 认为地址超过 1MB 会自动回卷(20 位地址线限制);
  4. 加载 GDT 并跳入保护模式:这是质变点,之后所有寻址都基于段选择子,不再是物理地址。

下面这段 NASM 代码,是我实测在 QEMU 和三台不同品牌笔记本上均能稳定运行的最小启动扇区:

org 0x7c00 bits 16 start: cli ; 关中断 xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7c00 ; 栈顶设在 0x7c00 ; 重定位自身到 0x90000 mov cx, 512 ; 512 字节 mov si, 0x7c00 mov di, 0x90000 rep movsb jmp 0x9000:start_protected ; 重定位后的保护模式入口 bits 32 start_protected: ; 开启 A20 call enable_a20 ; 加载 GDT lgdt [gdt_descriptor] ; 置位 CR0.PE mov eax, cr0 or eax, 1 mov cr0, eax ; 远跳转,刷新 CS 寄存器,进入保护模式 jmp 0x08:protected_mode_start ; GDT 定义(代码段和数据段) gdt_start: dq 0x0000000000000000 ; null descriptor dq 0x00cf9a000000ffff ; code segment: base=0, limit=0xfffff, 4KB-granularity dq 0x00cf92000000ffff ; data segment: same as above gdt_end: gdt_descriptor: dw gdt_end - gdt_start - 1 dd gdt_start enable_a20: push ax push dx mov al, 0xAD ; 禁用键盘 out 0x64, al mov al, 0xD0 ; 读取输出端口 out 0x64, al in al, 0x60 push ax mov al, 0xD1 ; 写入输出端口 out 0x64, al pop ax or al, 2 ; 置位 A20 位 out 0x60, al pop dx pop ax ret

这段代码的精妙之处在于:它没有使用任何“高级”技巧,所有指令都是 x86 最基础的,却精准踩在硬件时序的脉搏上。比如out 0x64, al后必须等待键盘控制器就绪,但教程里常写的in al, 0x64; test al, 2; jnz wait在某些慢速主板上会死循环——所以我直接用in al, 0x60读一次输出端口,利用其副作用清空状态,实测更稳。

注意:jmp 0x08:protected_mode_start中的0x08是 GDT 中代码段描述符的索引(从 0 开始,第二个描述符,所以是 0x08)。如果这里写错,CPU 会立即 triple fault 重启。我曾因此浪费 3 小时,最后用 QEMU 的-d int发现错误码是0x08,才意识到是 GDT 索引问题。

3.2 内存管理:从裸机物理地址到可分配内存池

现代操作系统动辄管理 GB 级内存,但我们的内核起步只需管理 4MB 物理内存(足够演示所有核心概念)。关键不在于大小,而在于如何让 C 代码安全地申请和释放内存。

我们采用最朴素的“位图法”(Bitmap Allocator):

  • 为每 4KB 内存页分配 1 个 bit;
  • 4MB 共 1024 页,需要 128 字节位图;
  • 位图本身放在内核代码段之后,确保不会被覆盖;
  • malloc()遍历位图找第一个 0,置 1 并返回对应物理地址;
  • free()则将对应 bit 清 0。

C 语言实现的核心逻辑如下(已简化):

#define PAGE_SIZE 0x1000 #define MAX_PAGES 1024 uint8_t bitmap[MAX_PAGES / 8]; // 128 字节 void* kmalloc(size_t size) { size_t pages = (size + PAGE_SIZE - 1) / PAGE_SIZE; for (int i = 0; i < MAX_PAGES; i++) { int can_alloc = 1; for (int j = 0; j < pages && can_alloc; j++) { if (i + j >= MAX_PAGES || (bitmap[(i+j)/8] & (1 << ((i+j)%8)))) { can_alloc = 0; i += j; // 跳过已检查部分 } } if (can_alloc) { for (int j = 0; j < pages; j++) { bitmap[(i+j)/8] |= (1 << ((i+j)%8)); } return (void*)(0x100000 + i * PAGE_SIZE); // 物理地址起始为 1MB } } return NULL; }

这个实现看似简单,但隐藏着两个关键细节:

  • 地址对齐:kmalloc(1)返回的地址必须是 4KB 对齐的,因为后续我们要用它做页表项。所以即使只申请 1 字节,也分配整页。
  • 位图位置:位图必须放在内核代码之后、可用内存之前。我在链接脚本中明确指定:
SECTIONS { . = 0x100000; /* 内核加载地址 */ _start = .; .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } _bitmap_start = .; .bitmap (NOLOAD) : { *(.bitmap) } /* 显式声明位图段 */ _end = .; }

然后在 C 代码中用extern char _bitmap_start;获取其地址,确保位图不被 malloc 分配到。

实操心得:位图法在小内存场景下性能极佳,但扩展性差。我试过改成伙伴系统(Buddy System),代码量翻倍,但对 4MB 内存并无实际收益。教学项目的原则是:用最简单的方法解决当前问题,不为未来过度设计。

3.3 键盘与屏幕驱动:与硬件的第一次握手

操作系统内核的“用户界面”,不是 GUI,而是键盘输入和文本输出。这两者必须绕过 BIOS 中断(int 0x16/int 0x10),直接与硬件对话,否则无法体现“自主控制”。

键盘驱动基于 PS/2 协议:

  • 键盘控制器(i8042)有两个端口:0x60(数据端口)、0x64(状态/命令端口);
  • 每次按键,键盘发送一个扫描码(scan code),按下为通码(make code),松开为断码(break code,通常为 0xf0 + 通码);
  • 我们在 IDT 中注册 IRQ1(键盘中断),中断处理函数从0x60读取扫描码,查表转换为 ASCII(如0x1e→'a'),存入环形缓冲区。

核心中断处理代码(汇编入口 + C 处理):

; keyboard_handler.asm global keyboard_handler extern keyboard_handler_c keyboard_handler: pushad call keyboard_handler_c popad iret
// keyboard.c #define KEYBOARD_DATA 0x60 #define KEYBOARD_STATUS 0x64 char key_buffer[256]; int key_head = 0, key_tail = 0; void keyboard_handler_c() { uint8_t scancode = inb(KEYBOARD_DATA); if (scancode == 0xf0) { // 断码前缀,读下一个字节作为断码 scancode = inb(KEYBOARD_DATA); // 此处可忽略断码,或用于实现长按检测 return; } char c = scancode_to_ascii(scancode); if (c) { key_buffer[key_head] = c; key_head = (key_head + 1) % 256; } } char get_char() { if (key_head == key_tail) return 0; char c = key_buffer[key_tail]; key_tail = (key_tail + 1) % 256; return c; }

屏幕输出则更直接:显存位于物理地址0xb8000,每个字符占 2 字节(低字节 ASCII,高字节属性,如 0x07 为灰字黑底)。我们维护一个全局光标位置cursor_x/cursor_y,每次输出字符时计算偏移:

#define VIDEO_MEMORY 0xb8000 volatile uint16_t* video = (uint16_t*)VIDEO_MEMORY; void print_char(char c, uint8_t color) { if (c == '\n') { cursor_x = 0; cursor_y++; if (cursor_y >= 25) scroll(); return; } uint16_t attribute = color << 8 | c; uint16_t* pos = video + cursor_y * 80 + cursor_x; *pos = attribute; cursor_x++; if (cursor_x >= 80) { cursor_x = 0; cursor_y++; if (cursor_y >= 25) scroll(); } } void scroll() { for (int y = 0; y < 24; y++) { for (int x = 0; x < 80; x++) { video[y * 80 + x] = video[(y + 1) * 80 + x]; } } for (int x = 0; x < 80; x++) { video[24 * 80 + x] = 0x0720; // 黑底灰空格 } cursor_y = 24; }

注意:volatile关键字必不可少!它告诉编译器“这个内存地址可能被硬件随时修改”,禁止编译器优化掉对video的读写。我曾因漏写volatile,导致屏幕输出乱码,调试半天才发现是编译器把video[i] = x优化成了单次写入,而硬件需要两次独立的字节写。

3.4 系统调用:用户程序与内核的契约接口

一个“操作系统”之所以成立,关键在于它定义了一套用户程序必须遵守的规则。我们实现最简系统调用:sys_write(输出字符串)和sys_getchar(读取字符)。

实现分三层:

  • 用户态调用约定:用户程序用int 0x80触发软中断,eax存系统调用号,ebx/ecx/edx存参数;
  • 内核中断处理:IDT 中注册0x80号中断,保存寄存器,根据eax分发到对应函数;
  • 内核函数实现:纯 C 函数,调用前面写的print_char和get_char。

IDT 初始化代码(C 语言):

struct idt_entry { uint16_t offset_low; uint16_t selector; uint8_t zero; uint8_t type_attr; uint16_t offset_high; } __attribute__((packed)); struct idt_ptr { uint16_t limit; uint32_t base; } __attribute__((packed)); struct idt_entry idt[256]; struct idt_ptr idtp; void set_idt_gate(int n, uint32_t handler, uint16_t sel, uint8_t flags) { idt[n].offset_low = (uint16_t)handler; idt[n].selector = sel; idt[n].zero = 0; idt[n].type_attr = flags; idt[n].offset_high = (uint16_t)(handler >> 16); } void idt_init() { idtp.limit = sizeof(struct idt_entry) * 256 - 1; idtp.base = (uint32_t)&idt; memset(&idt, 0, sizeof(idt)); // 设置 0x80 系统调用门:DPL=3 允许用户态调用 set_idt_gate(0x80, (uint32_t)system_call, 0x08, 0x8e); // 加载 IDT asm volatile("lidt %0" :: "m"(idtp)); }

系统调用处理函数(汇编,负责保存/恢复用户寄存器):

; system_call.asm global system_call extern system_call_c system_call: pushad push ds push es push fs push gs mov ax, 0x10 mov ds, ax mov es, ax call system_call_c pop gs pop fs pop es pop ds popad iret

C 语言分发器:

void system_call_c() { uint32_t eax = get_eax(); // 从栈中读取 switch (eax) { case 4: // sys_write sys_write(get_ebx(), get_ecx(), get_edx()); break; case 3: // sys_getchar set_eax(sys_getchar()); break; default: set_eax(-1); } } void sys_write(uint32_t fd, uint32_t buf, uint32_t count) { char* str = (char*)buf; for (int i = 0; i < count && str[i]; i++) { print_char(str[i], 0x07); } }

至此,一个完整的“用户-内核”交互闭环就建立了。你可以写一个用户程序:

; user.asm global _start _start: mov eax, 4 ; sys_write mov ebx, 1 ; stdout mov ecx, msg mov edx, 13 int 0x80 mov eax, 3 ; sys_getchar int 0x80 mov eax, 1 ; exit int 0x80 msg db "Hello, OS!", 0xa

用nasm -f elf32 user.asm && ld -m elf_i386 user.o -o user.bin编译,再用内核加载它——这就是一个微型操作系统的雏形。

4. 实操全流程与关键配置详解

4.1 开发环境搭建:QEMU + GDB + NASM + GCC 工具链

所有操作均在 Ubuntu 22.04 下完成,无需虚拟机嵌套,本地即可构建。关键不是安装什么,而是版本匹配与路径隔离。

  • NASM:必须用nasm 2.15.05或更新。旧版 NASM 对bits 32指令支持不一致,会导致保护模式跳转失败。安装命令:

    sudo apt install nasm nasm -v # 确认版本
  • GCC 交叉编译器:绝不能用系统自带gcc(它默认生成动态链接、带 libc 的代码)。必须构建i686-elf-gcc:

    # 安装依赖 sudo apt install build-essential bison flex libgmp3-dev libmpc-dev libmpfr-dev texinfo # 下载 GCC 源码,配置 ../gcc-12.2.0/configure --target=i686-elf --prefix=$HOME/opt/cross --disable-werror --enable-languages=c,c++ make -j$(nproc) make install

    将$HOME/opt/cross/bin加入PATH,确保i686-elf-gcc -v可用。

  • QEMU:sudo apt install qemu-system-x86即可。关键参数:

    • -fda kernel.img:将内核镜像作为软盘启动;
    • -s -S:开启 GDB 调试(端口 1234),并暂停等待连接;
    • -d int,cpu_reset:打印中断和复位日志,排查启动失败;
    • -monitor stdio:提供 QEMU 内部监控命令,如info registers。
  • GDB:用i686-elf-gdb,而非系统 gdb:

    i686-elf-gdb kernel.bin (gdb) target remote :1234 (gdb) b *0x100000 # 在内核入口设断点 (gdb) c

实操心得:工具链版本不匹配是新手最大坑。我曾用gcc 11.4编译,但链接脚本用了SECTIONS { . = 0x100000; ... },结果i686-elf-ld报错section .text will not fit in region. 查了 2 小时才发现是ld版本太老,不支持新语法。解决方案永远是:统一用同一源码包编译的全套工具链。

4.2 链接脚本深度解析:控制二进制的物理布局

链接脚本(linker.ld)是内核的“建筑蓝图”,它决定代码、数据、BSS 段在内存中的绝对位置。一个错误的链接脚本,会让 GDT 加载失败、堆栈溢出、甚至覆盖中断向量表。

我的linker.ld如下(含详细注释):

ENTRY(_start) SECTIONS { /* 内核加载到物理地址 0x100000 (1MB) */ . = 0x100000; _start = .; /* 代码段:必须连续,且起始地址对齐 */ .text ALIGN(4K) : { *(.text) *(.rodata) } /* 数据段:初始化数据 */ .data ALIGN(4K) : { *(.data) *(.sdata) } /* BSS 段:未初始化数据,运行时清零 */ .bss ALIGN(4K) : { _bss_start = .; *(.bss) *(.sbss) _bss_end = .; } /* 自定义段:内存位图,紧随 BSS 之后 */ .bitmap (NOLOAD) : { _bitmap_start = .; *(.bitmap) _bitmap_end = .; } /* 符号:标记各段结束,供 C 代码引用 */ _end = .; }

关键点解析:

  • ALIGN(4K):强制每个段起始地址 4KB 对齐。这是为后续页表管理预留,也是避免跨页访问导致 TLB miss 的最佳实践。
  • (NOLOAD):.bitmap段不占用镜像文件空间,只在运行时分配内存。否则kernel.img会凭空多出 128 字节,破坏 512 字节扇区对齐。
  • _bss_start/_bss_end:C 代码中需在main()开头执行memset(_bss_start, 0, _bss_end - _bss_start),清零 BSS 段。这是 C 运行时环境的基础。

编译命令中必须显式指定链接脚本:

i686-elf-gcc -T linker.ld -ffreestanding -fno-builtin -nostdlib -o kernel.bin kernel.o boot.o

4.3 镜像制作:从 .bin 到可启动的 kernel.img

最终交付给 QEMU 的是一个kernel.img文件,它必须是标准的 FAT12 软盘镜像(1.44MB),其中:

  • 前 512 字节:boot sector(我们的 NASM 启动代码);
  • 后续扇区:内核二进制(kernel.bin)。

制作流程:

# 1. 创建空镜像 dd if=/dev/zero of=kernel.img bs=512 count=2880 # 2880 * 512 = 1.44MB # 2. 写入 boot sector dd if=boot.bin of=kernel.img bs=512 conv=notrunc # 3. 将 kernel.bin 写入镜像第 2 扇区起(跳过 boot) dd if=kernel.bin of=kernel.img bs=512 seek=1 conv=notrunc

seek=1是关键!它确保kernel.bin从镜像的第二个扇区(512 字节后)开始写入,不覆盖 boot sector。conv=notrunc防止dd清空镜像剩余部分。

验证镜像:

# 查看前 16 字节,确认 boot sector 签名 xxd -l 16 kernel.img # 查看内核代码是否在 1MB 处(0x100000 = 0x19000 字节偏移) xxd -s $((0x19000)) -l 32 kernel.img

注意:kernel.bin的大小必须小于1.44MB - 512字节。我设置了一个构建检查:

check_size: @size=$(shell stat -c "%s" kernel.bin); \ if [ $$size -gt 1474048 ]; then \ echo "ERROR: kernel.bin too big! Max 1474048 bytes"; \ exit 1; \ fi

4.4 调试技巧实录:从黑屏到 Hello World 的 7 个关键断点

调试内核不是写应用,没有printf,没有日志文件。一切依赖于在正确的时间、正确的地址、观察正确的寄存器。以下是我在实践中总结的 7 个黄金断点:

断点位置触发时机关键观察点典型问题
*0x7c00BIOS 跳转后第一条指令cs:ip是否为0x0:0x7c00,ds/es/ss是否为 0BIOS 未正确加载,或镜像损坏
*0x90000重定位后入口cs值是否为0x08(GDT 代码段),cr0.PE是否为 1GDT 加载失败,A20 未开启
*0x100000内核 C 代码入口esp是否指向有效栈(如0x9fffc),eax是否为预期值栈未正确初始化,或保护模式跳转失败
`*0xb80
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 5:23:00

OpenStack 部署记录(七):多节点部署——环境准备与应答文件

从 All-in-One 到 1 控 2 计&#xff1a;角色拆分不难&#xff0c;难的是让三台机器“步调一致”——时间、解析、网络、源&#xff0c;一样都不能各自为政一、部署概述项目值目标拓扑1 台 controller 2 台 computecontroller10.10.10.160&#xff08;同时承担网络节点角色&am…

作者头像 李华
网站建设 2026/10/18 5:22:33

工业AI在工厂的落地实践:从质检、预测性维护到排产优化

他山之石可以攻玉&#xff0c;这句话放在工业AI上其实特别贴切。我经常跟朋友说&#xff0c;搞工业AI的这拨人&#xff0c;很多都不是传统制造业出身&#xff0c;而是从互联网、从算法圈子里被“扔”进工厂的。刚开始大家都有点懵&#xff1a;车间里的机器声、老师傅的经验、动…

作者头像 李华
网站建设 2026/10/18 5:22:23

JQuick-Excel 实战:用 VALIDATION 建立可维护的 Excel 导入校验

JQuick-Excel 实战&#xff1a;用 VALIDATION 建立可维护的 Excel 导入校验 tags: Java, Excel, JQuick-Excel, 数据导入, VALIDATION, XML DSL 更多示例: JQuick-Excel 官网示例 https://www.jquick.org/jquick-excel/#demo-formulas0005 简介 Excel 导入的难点通常不在“…

作者头像 李华
网站建设 2026/10/18 5:20:20

从构思到打磨:我的第五个网页完整开发流程解析

把"我的第五个网页"这个标题挖开来讲&#xff0c;其实挺有意思。一个人能从第一个网页写到第五个&#xff0c;说明已经度过了最初的摸索期&#xff0c;开始真正把做网页当成一件有章法的事来对待。我记得很清楚&#xff0c;自己写到第五个页面的时候&#xff0c;最大…

作者头像 李华
网站建设 2026/10/18 5:19:02

Servlet+JSP+JDBC房屋租赁系统毕设实战指南

简介&#xff1a;本资源是一套完整的Java Web毕业设计项目——基于ServletJSPJDBC开发的房屋租赁管理系统&#xff0c;专为计算机专业本科生毕设选题及Java初学者项目实战打造&#xff0c;覆盖Web开发全流程核心技能训练。压缩包为ZIP格式&#xff0c;大小13.69MB&#xff0c;包…

作者头像 李华