内核能运行了,但代码还挤在一个文件里。随着功能增加,每次改动都要在几百行代码中翻找,稍有不慎就会引入新问题。这种状况无法持续——是时候重构了。
代码现状与重构目标
当前内核代码存在几个明显的问题:
- 代码重复:多处使用相同的 VGA 显存操作代码,硬编码的地址和常量散落在各处
- 缺乏抽象:直接操作硬件寄存器,没有统一接口;没有类型定义,代码可读性差
- 难以扩展:添加新功能需要修改多处代码,没有清晰的模块边界
重构的目标是建立清晰的模块化架构,提高代码的可读性、可维护性和可扩展性。重构后的文件结构如下:
06.refactoring/├── boot/│ ├── mbr.S # 主引导记录(扇区 0)│ └── loader.S # 加载器(扇区 1)├── kernel/│ ├── include/│ │ ├── types.h # 基础类型定义(类型系统)│ │ ├── vga.h # VGA 驱动接口声明│ │ ├── io.h # 端口 I/O 接口(inb/outb 等)│ │ ├── ports.h # 硬件端口常量定义│ │ ├── gdt.h # GDT 结构定义│ │ ├── interrupt.h # 中断结构定义│ │ ├── memory.h # 内存管理接口│ │ └── task.h # 任务结构定义│ ├── drivers/│ │ └── vga.c # VGA 驱动实现(核心文件)│ ├── kernel.c # 内核入口函数│ └── link.ld # 链接脚本└── Makefile # 构建配置模块化设计
基础类型定义系统
在裸机环境中,没有标准库支持(如 stdint.h),无法直接使用 uint8_t、uint16_t 等标准类型。如果使用 unsigned char、unsigned short 等原生类型,会导致可读性差(unsigned short 无法直观表达其用途)、移植性差(不同平台上类型大小可能不同)、易出错(难以保证类型大小的精确性)。
采用分层设计,先定义简短的内部类型名,再为它们创建兼容 stdint 风格的别名:
/* 固定宽度整数类型(便于可读性) */typedef unsigned char u8;typedef unsigned short u16;typedef unsigned int u32;typedef unsigned long long u64;
/* 兼容 stdint 风格的别名(便于代码移植) */typedef u8 uint8_t;typedef u16 uint16_t;typedef u32 uint32_t;typedef u64 uint64_t;
/* 指针相关与大小类型(针对 32-bit) */typedef unsigned int uintptr_t;typedef unsigned int size_t;
/* 物理/虚拟地址类型(便于代码可读性) */typedef uintptr_t phys_addr_t;typedef uintptr_t virt_addr_t;除了类型定义,还提供了大量实用宏:
/* 计算数组元素数量 */#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
/* 位操作 */#define BIT(n) (1U << (n))
/* 最小/最大 */#define MIN(a, b) (((a) < (b)) ? (a) : (b))#define MAX(a, b) (((a) > (b)) ? (a) : (b))
/* 对齐:向上对齐到 align(align 必须是 2 的幂) */#define ALIGN_UP(x, align) ((((uintptr_t)(x)) + ((align) - 1)) & ~((uintptr_t)((align) - 1)))这些宏在内核开发中经常使用,例如内存对齐、位标志操作等。
有了类型定义的基础,接下来处理内核中重复最多的代码——VGA 显存操作。
VGA 驱动封装
直接操作 VGA 显存虽然简单,但缺乏光标管理(无法自动移动光标)、没有滚动支持(屏幕满时无法自动滚动)、颜色控制困难(无法方便地设置文本颜色)、接口不统一(每次输出都要重复相同的代码)。封装 VGA 驱动可以提供更高级、更易用的接口。
VGA 驱动采用分层设计,从底层硬件操作到高层格式化输出:
VGA 文本模式使用 80×25 的字符网格,每个字符占用 2 字节:第 1 字节是 ASCII 字符码,第 2 字节是颜色属性(背景色 4 位 + 前景色 4 位)。颜色属性结构:
+--------+--------+| BLINK | BG | FG | I || 1bit | 3bits | 3bits |1bit |+--------+--------+--------+VGA 驱动使用静态变量维护当前状态:
static uint16_t *const VGA_BUF = (uint16_t *)VGA_MEMORY; // 显存地址(0xB8000)static uint8_t cursor_row = 0; // 当前行(0-24)static uint8_t cursor_col = 0; // 当前列(0-79)static uint8_t current_color = VGA_WHITE_ON_BLACK; // 当前颜色属性其中用到的颜色常量和硬件端口定义:
#define VGA_COLOR_BLACK 0#define VGA_COLOR_BLUE 1#define VGA_COLOR_GREEN 2#define VGA_COLOR_CYAN 3#define VGA_COLOR_RED 4#define VGA_COLOR_MAGENTA 5#define VGA_COLOR_BROWN 6#define VGA_COLOR_LIGHT_GREY 7#define VGA_COLOR_DARK_GREY 8#define VGA_COLOR_LIGHT_BLUE 9#define VGA_COLOR_LIGHT_GREEN 10#define VGA_COLOR_LIGHT_CYAN 11#define VGA_COLOR_LIGHT_RED 12#define VGA_COLOR_LIGHT_MAGENTA 13#define VGA_COLOR_LIGHT_BROWN 14#define VGA_COLOR_WHITE 15
#define VGA_WHITE_ON_BLACK (VGA_COLOR_WHITE | (VGA_COLOR_BLACK << 4))#define VGA_MEMORY 0xB8000#define VGA_WIDTH 80#define VGA_HEIGHT 25
#define VGA_CRTC_ADDR 0x3D4#define VGA_CRTC_DATA 0x3D5#define VGA_CRTC_CURSOR_LOC_HI 0x0E#define VGA_CRTC_CURSOR_LOC_LO 0x0F光标同步是 VGA 驱动的基础操作,需要将软件维护的光标位置写入 VGA CRT 控制器的寄存器:
static void vga_sync_cursor(){ uint16_t cursorLocation = cursor_row * 80 + cursor_col; outb(VGA_CRTC_ADDR, VGA_CRTC_CURSOR_LOC_HI); outb(VGA_CRTC_DATA, cursorLocation >> 8); outb(VGA_CRTC_ADDR, VGA_CRTC_CURSOR_LOC_LO); outb(VGA_CRTC_DATA, cursorLocation);}将二维坐标(行、列)转换为一维位置,然后分两次写入 VGA CRT 控制器:先向地址端口 0x3D4 写入寄存器索引,再向数据端口 0x3D5 写入对应的数据。这种分两次写入的方式是因为 VGA CRT 控制器使用 16 位的光标位置寄存器,但只能通过 8 位端口访问。
字符输出与滚动是 VGA 驱动的核心逻辑:
void vga_putc(char c){ if (c == '\n') { cursor_row++; cursor_col = 0; } else { VGA_BUF[cursor_row * VGA_WIDTH + cursor_col] = (current_color << 8) | c; cursor_col++; if (cursor_col >= VGA_WIDTH) { cursor_col = 0; cursor_row++; } }
// 滚动处理 if (cursor_row >= VGA_HEIGHT) { // 将所有行上移一行 for (int i = 0; i < (VGA_HEIGHT - 1) * VGA_WIDTH; i++) { VGA_BUF[i] = VGA_BUF[i + VGA_WIDTH]; } // 清空最后一行 for (int i = (VGA_HEIGHT - 1) * VGA_WIDTH; i < VGA_HEIGHT * VGA_WIDTH; i++) { VGA_BUF[i] = (current_color << 8) | ' '; } cursor_row = VGA_HEIGHT - 1; } vga_sync_cursor();}换行符将光标移到下一行开头;普通字符写入显存,颜色属性在高 8 位,字符在低 8 位。当列号超过 79 时自动换行。当行号超过 24 时触发滚动:将第 1-24 行的内容复制到第 0-23 行,清空第 24 行,光标回到最后一行。这种滚动方式类似于终端的行缓冲,确保屏幕始终显示最新的 25 行内容。
VGA 驱动封装好了,但调试内核时只靠逐字符输出远远不够,还需要格式化输出能力。
格式化输出实现
调试内核时,经常需要输出变量的当前值、内存地址、函数执行状态、错误信息。如果没有格式化输出函数,每次都需要手动转换类型,非常繁琐。vga_printf 提供类似标准 printf 的功能,方便调试工作。
通过 va_list 遍历可变参数列表,实现格式化输出:
void vga_printf(const char *fmt, ...){ va_list args; va_start(args, fmt); // 初始化参数列表
for (const char *p = fmt; *p; p++) { if (*p != '%') { vga_putc(*p); // 普通字符直接输出 continue; }
p++; // 跳过 '%'
switch (*p) { case 'c': // 字符 char c = (char)va_arg(args, int); vga_putc(c); break; case 's': // 字符串 const char *s = va_arg(args, const char *); vga_write(s); break; case 'd': // 十进制整数 int v = va_arg(args, int); vga_itoa(v, buf, 10); vga_write(buf); break; case 'x': // 十六进制整数 v = va_arg(args, int); vga_itoa(v, buf, 16); vga_write(buf); break; } } va_end(args); // 清理参数列表}支持的格式符:%c(单个字符)、%s(字符串)、%d/%i(十进制整数)、%x/%X(十六进制整数)、%%(百分号)。
格式化输出的底层依赖整数转字符串函数 vga_itoa:
static void vga_itoa(int value, char *buf, int base){ char *p = buf; unsigned int v = (base == 10 && value < 0) ? -value : value;
// 生成逆序的数字字符串 do { int digit = v % base; *p++ = (digit < 10) ? '0' + digit : 'a' + (digit - 10); v /= base; } while (v);
// 处理负号 if (base == 10 && value < 0) *p++ = '-';
*p = '\0';
// 反转字符串 for (char *a = buf, *b = p - 1; a < b; a++, b--) { char tmp = *a; *a = *b; *b = tmp; }}如果是十进制且值为负数,先转为正数处理,最后添加负号。使用模运算逐位提取数字,由于从低位到高位提取,生成的字符串是逆序的,最后用双指针法反转。0-9 的数字用 '0' + digit 转换,10-15 用 'a' + (digit - 10) 转换。这个算法支持十进制(base=10)和十六进制(base=16)转换。
格式化输出用到了 outb 函数来同步光标,而这个函数本身也需要封装——接下来处理端口 I/O。
端口 I/O 封装
x86 架构使用独立的 I/O 地址空间(与内存地址空间分开),需要通过专用的 in 和 out 指令访问硬件端口。直接使用内联汇编虽然可行,但代码冗余(每次都要编写相同的汇编代码)、类型不安全(容易误用数据类型)、可读性差(汇编代码难以理解)。封装端口 I/O 操作可以提供类型安全、易于使用的接口。
利用 GCC 的扩展内联汇编语法,封装字节级的端口读写:
static inline uint8_t inb(uint16_t port){ uint8_t result; asm volatile( "inb %1, %0" // 输入字节指令 : "=a"(result) // 输出操作数(AL 寄存器) : "Nd"(port) // 输入操作数(立即数或 DX 寄存器) ); return result;}
static inline void outb(uint16_t port, uint8_t data){ asm volatile( "outb %0, %1" // 输出字节指令 : // 无输出 : "a"(data), "Nd"(port) // 输入操作数(AL 寄存器和端口) );}汇编模板中,%0、%1 是操作数占位符。"=a"(result) 是输出操作数,= 表示只写,a 表示使用 EAX/AX/AL 寄存器。"Nd"(port) 是输入操作数,N 表示立即数(0-255),d 表示 DX 寄存器。volatile 告诉编译器不要优化这段汇编。
运行与验证
编译运行
cd 06.refactoringmake all # 编译所有模块make run # 在 QEMU 中运行make clean # 清理编译产物预期输出
Started in 16-bit real mode (BIOS)Now in 32-bit protected mode (direct video)Now Enable PageHello, kernel world!Value: 1234, Hex: 0x4d2, Char: A, String: VGA printf OK!验证要点
-
类型系统:确保编译器正确识别自定义类型(如
uint8_t、uintptr_t) -
VGA 驱动:
- 文本正常显示,颜色正确
- 光标跟随输出移动
- 屏幕满时自动滚动
-
格式化输出:
- 整数正确显示
- 十六进制正确显示
- 字符串正确显示
-
端口 I/O:光标位置正确同步到硬件
踩坑记录
-
VGA 输出乱码:
- 原因:颜色属性或字符编码错误
- 解决方案:检查
(color << 8) | c的位操作,确保颜色在高位,字符在低位
-
格式化输出错误:
- 原因:可变参数处理不当,
va_start和va_end不匹配 - 解决方案:确保每个
va_start都有对应的va_end
- 原因:可变参数处理不当,
-
光标位置不正确:
- 原因:滚动后未更新光标位置,或未调用
vga_sync_cursor - 解决方案:在每次输出后调用
vga_sync_cursor,特别是滚动操作后
- 原因:滚动后未更新光标位置,或未调用
-
类型大小不一致:
- 原因:在不同平台上,类型大小可能不同
- 解决方案:使用
types.h中定义的固定宽度类型(如uint32_t),而不是原生类型
-
编译警告或错误:
- 原因:内联汇编语法不正确,或编译器不支持某些扩展
- 解决方案:确保使用 GCC 兼容的编译器,检查汇编约束语法
小结
重构后的内核代码结构清晰,各模块职责分明。基础类型系统提供了可移植的类型定义,VGA 驱动封装了显存操作细节,格式化输出让调试信息更加丰富,端口 I/O 抽象统一了硬件访问接口。下一章将基于这个模块化架构,实现中断系统——让内核获得响应硬件事件的能力。
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






