mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2062 字
5 分钟
代码重构:模块化架构设计
2021-05-18

内核能运行了,但代码还挤在一个文件里。随着功能增加,每次改动都要在几百行代码中翻找,稍有不慎就会引入新问题。这种状况无法持续——是时候重构了。

代码现状与重构目标#

当前内核代码存在几个明显的问题:

  • 代码重复:多处使用相同的 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_tuint16_t 等标准类型。如果使用 unsigned charunsigned 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 驱动采用分层设计,从底层硬件操作到高层格式化输出:

graph TD A[vga_printf<br/>格式化输出] --> B[vga_write<br/>字符串输出] B --> C[vga_putc<br/>单字符输出] C --> D[硬件显存操作] D --> E[VGA 显存<br/>0xB8000] C --> F[vga_sync_cursor<br/>光标同步] F --> G[VGA CRT 控制器<br/>端口 0x3D4/0x3D5] C --> H[滚动处理<br/>屏幕内容上移]

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 控制器的寄存器:

flowchart TD A[开始同步光标] --> B[计算线性位置<br/>row * 80 + col] B --> C[写地址端口<br/>VGA_CRTC_CURSOR_LOC_HI] C --> D[写数据端口<br/>位置高字节] D --> E[写地址端口<br/>VGA_CRTC_CURSOR_LOC_LO] E --> F[写数据端口<br/>位置低字节] F --> G[完成]
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 驱动的核心逻辑:

flowchart TD A[调用 vga_putc] --> B{字符类型?} B -->|换行符| C[row++, col=0] B -->|普通字符| D[写入显存<br/>color << 8 | char] D --> E[col++] E --> F{列>=80?} F -->|是| G[col=0, row++] F -->|否| H[检查滚动] G --> H C --> H H --> I{row>=25?} I -->|是| J[屏幕上移一行] J --> K[清空最后一行] K --> L[row=24] I -->|否| M[同步硬件光标] L --> M M --> N[完成]
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 地址空间(与内存地址空间分开),需要通过专用的 inout 指令访问硬件端口。直接使用内联汇编虽然可行,但代码冗余(每次都要编写相同的汇编代码)、类型不安全(容易误用数据类型)、可读性差(汇编代码难以理解)。封装端口 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.refactoring
make all # 编译所有模块
make run # 在 QEMU 中运行
make clean # 清理编译产物

预期输出#

Started in 16-bit real mode (BIOS)
Now in 32-bit protected mode (direct video)
Now Enable Page
Hello, kernel world!
Value: 1234, Hex: 0x4d2, Char: A, String: VGA printf OK!

验证要点#

  1. 类型系统:确保编译器正确识别自定义类型(如 uint8_tuintptr_t

  2. VGA 驱动

    • 文本正常显示,颜色正确
    • 光标跟随输出移动
    • 屏幕满时自动滚动
  3. 格式化输出

    • 整数正确显示
    • 十六进制正确显示
    • 字符串正确显示
  4. 端口 I/O:光标位置正确同步到硬件

踩坑记录#

  1. VGA 输出乱码

    • 原因:颜色属性或字符编码错误
    • 解决方案:检查 (color << 8) | c 的位操作,确保颜色在高位,字符在低位
  2. 格式化输出错误

    • 原因:可变参数处理不当,va_startva_end 不匹配
    • 解决方案:确保每个 va_start 都有对应的 va_end
  3. 光标位置不正确

    • 原因:滚动后未更新光标位置,或未调用 vga_sync_cursor
    • 解决方案:在每次输出后调用 vga_sync_cursor,特别是滚动操作后
  4. 类型大小不一致

    • 原因:在不同平台上,类型大小可能不同
    • 解决方案:使用 types.h 中定义的固定宽度类型(如 uint32_t),而不是原生类型
  5. 编译警告或错误

    • 原因:内联汇编语法不正确,或编译器不支持某些扩展
    • 解决方案:确保使用 GCC 兼容的编译器,检查汇编约束语法

小结#

重构后的内核代码结构清晰,各模块职责分明。基础类型系统提供了可移植的类型定义,VGA 驱动封装了显存操作细节,格式化输出让调试信息更加丰富,端口 I/O 抽象统一了硬件访问接口。下一章将基于这个模块化架构,实现中断系统——让内核获得响应硬件事件的能力。

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

代码重构:模块化架构设计
https://blog.souloss.com/posts/os/refactoring-modular-architecture/
作者
Souloss
发布于
2021-05-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时