从零打造你的第一个操作系统内核:mkernel极简实现全指南
【免费下载链接】mkernel A minimalist kernel 项目地址: https://gitcode.com/gh_mirrors/mk/mkernel
你是否曾好奇操作系统内核(Kernel)如何启动?想亲手编写能在真实硬件上运行的代码却被复杂概念吓退?本文将带你通过mkernel项目(一个仅200行代码的极简内核),从0到1掌握内核开发核心原理,最终让你的代码在计算机启动时显示自定义信息。
读完本文你将掌握:
- 内核启动的完整流程(从BIOS到GRUB再到内核执行)
- 实模式与保护模式的切换原理
- 32位汇编与C语言的混合编程技术
- 显存操作与字符显示的底层实现
- 内核编译、链接与GRUB配置全步骤
- 在QEMU模拟器与真实硬件中测试内核的方法
项目概述:mkernel是什么?
mkernel是一个遵循Multiboot规范的极简内核(Kernel)实现,核心功能是在屏幕上打印"my first kernel"并挂起系统。其代码量不足200行,却完整展示了操作系统内核的启动流程和最基础的硬件交互方式。
核心特性与技术栈
| 特性 | 说明 | 技术细节 |
|---|---|---|
| 代码规模 | 汇编30行 + C语言80行 | 无外部依赖,纯手工编写 |
| 启动方式 | GRUB Multiboot兼容 | 符合Multiboot v1规范 |
| 目标架构 | x86 32位 | 支持i386及兼容处理器 |
| 显示输出 | VGA文本模式 | 直接操作0xB8000显存地址 |
| 编译工具链 | NASM + GCC + LD | 生成ELF32格式可执行文件 |
内核启动流程深度解析
从加电到内核执行的四阶段模型
关键技术点:实模式到保护模式的切换
x86处理器启动后默认工作在实模式(Real Mode),只能访问1MB内存。GRUB负责完成到保护模式(Protected Mode)的切换,使处理器能够访问4GB内存空间并启用内存分页机制。
代码实现详解:汇编与C的协同工作
1. 汇编入口文件(kernel.asm)
汇编代码负责最底层的初始化工作,是连接GRUB与C语言内核的桥梁。
; kernel.asm - 内核启动入口点
bits 32 ; 告诉NASM生成32位代码
section .text ; 代码段开始
; Multiboot规范要求的魔术数和校验和
align 4
dd 0x1BADB002 ; Multiboot魔术数(Magic Number)
dd 0x00 ; 标志位(Flags)
dd - (0x1BADB002 + 0x00) ; 校验和(Checksum) = -(魔术数 + 标志位)
global start ; 导出start符号,供链接器使用
extern kmain ; 声明外部函数kmain(在kernel.c中定义)
start: ; 内核执行的第一个指令
cli ; 关闭中断(CLI = Clear Interrupts)
mov esp, stack_space ; 设置栈指针(ESP)到栈空间顶部
call kmain ; 调用C语言内核主函数
hlt ; 当kmain返回后挂起CPU(HLT = Halt)
section .bss ; 未初始化数据段
resb 8192 ; 预留8KB内存作为栈空间
stack_space: ; 栈空间顶部地址(栈向下生长)
核心功能解析:
- Multiboot头部:前12字节是GRUB识别内核的必要信息,必须放在代码段开始
- 栈初始化:
mov esp, stack_space设置栈指针,为C函数调用准备栈空间 - 中断控制:
cli指令关闭中断,防止内核执行时被外部中断干扰 - 系统挂起:
hlt指令使CPU进入暂停状态,减少功耗
2. C语言内核实现(kernel.c)
C语言部分负责实现具体功能——清屏和显示字符串,展示了内核如何直接操作硬件。
/* kernel.c - mkernel主功能实现 */
void kmain(void) {
const char *str = "my first kernel"; // 要显示的字符串
char *vidptr = (char*)0xb8000; // 文本模式显存起始地址
unsigned int i = 0; // 显存偏移量
unsigned int j = 0; // 字符串索引
unsigned int screensize = 80 * 25 * 2; // 屏幕总字节数(80列×25行×2字节/字符)
/* 第一步:清屏操作 */
while (j < screensize) {
vidptr[j] = ' '; // 空格字符(ASCII 32)
vidptr[j+1] = 0x07; // 属性字节(黑底0x0 + 灰字0x7)
j += 2; // 移动到下一个字符位置
}
/* 第二步:显示字符串 */
j = 0; // 重置字符串索引
while (str[j] != '\0') {
vidptr[i] = str[j]; // 字符ASCII值
vidptr[i+1] = 0x07; // 保持黑底灰字属性
j++; // 下一个字符
i += 2; // 显存地址+2(字符占2字节)
}
return;
}
显存操作核心原理:
VGA(Video Graphics Array)文本模式下,显存(Video Memory)从地址0xB8000开始,每个字符占用2字节:
- 低字节:字符的ASCII码
- 高字节:属性值(背景色4位 + 前景色4位)
常见属性值组合:
0x07:黑底灰字(默认)0x0F:黑底白字(高亮)0x70:灰底黑字(反显)0x42:红底绿字(错误提示)
3. 链接脚本(link.ld)
链接脚本控制链接器如何组织最终的可执行文件,指定内核加载地址和各段布局。
OUTPUT_FORMAT(elf32-i386) ; 输出ELF32格式
ENTRY(start) ; 程序入口点为start符号
SECTIONS {
. = 0x100000; ; 内核加载地址(1MB处)
.text : { *(.text) } ; 代码段:包含所有.text节
.data : { *(.data) } ; 数据段:包含所有.data节
.bss : { *(.bss) } ; BSS段:未初始化数据
}
为什么内核从0x100000(1MB)开始加载?
- 低端内存(0x00000~0xFFFFF)被BIOS、GRUB和各种硬件占用
- 1MB以上内存区域通常对内核可用
- 符合Multiboot规范的推荐加载地址
编译与测试全流程
开发环境准备
| 工具 | 作用 | 安装命令(Ubuntu) |
|---|---|---|
| NASM | 汇编器,编译.asm文件 | sudo apt install nasm |
| GCC | C编译器,生成32位目标文件 | sudo apt install gcc-multilib |
| LD | 链接器,组合目标文件 | 通常随GCC安装 |
| QEMU | 模拟器,测试内核 | sudo apt install qemu-system-i386 |
| GRUB | 引导加载程序 | sudo apt install grub2 |
编译步骤详解
第一步:汇编编译
nasm -f elf32 kernel.asm -o kasm.o
-f elf32:指定输出格式为32位ELF目标文件kernel.asm:输入汇编源文件-o kasm.o:输出目标文件名(Object File)
第二步:C代码编译
gcc -m32 -c kernel.c -o kc.o -ffreestanding -nostdlib
关键参数解析:
-m32:生成32位代码-c:只编译不链接-ffreestanding:告诉GCC这是独立环境程序(无libc支持)-nostdlib:不链接标准库
⚠️ 注意:内核开发不能使用标准C库(如printf、malloc等),因为这些函数依赖操作系统提供的系统调用,而内核本身就是提供这些功能的程序。
第三步:链接生成内核
ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o
-m elf_i386:指定链接器生成32位ELF格式-T link.ld:使用自定义链接脚本kasm.o kc.o:输入目标文件(顺序无关)-o kernel:输出内核文件名
模拟器测试
使用QEMU(Quick Emulator)测试内核,无需写入硬盘:
qemu-system-i386 -kernel kernel
成功运行后将看到:
- QEMU窗口中显示"my first kernel"
- 屏幕其余区域为黑色背景
- 系统无进一步响应(内核执行完后挂起)
真实硬件部署指南
GRUB配置详解
GRUB(Grand Unified Bootloader)是大多数Linux发行版使用的引导加载程序,需要正确配置才能启动自定义内核。
1. 重命名内核文件
GRUB要求内核文件名为kernel-<version>格式:
mv kernel kernel-701 # 701可替换为任意版本号
2. 复制到启动分区
sudo cp kernel-701 /boot/kernel-701 # 需要root权限
3. 配置GRUB菜单项
编辑/etc/grub.d/40_custom文件,添加以下内容:
menuentry 'myKernel' {
set root='(hd0,msdos1)' # 启动分区(根据实际情况修改)
multiboot /boot/kernel-701 ro # 加载内核,ro表示只读
}
⚠️ 注意:
set root的值需要根据你的磁盘分区情况调整:
hd0表示第一块硬盘msdos1表示MBR分区表的第一个主分区- 如果使用GPT分区表,应为
gpt1
4. 更新GRUB配置
sudo update-grub # 更新/boot/grub/grub.cfg文件
5. 重启系统
sudo reboot
启动时选择"myKernel"菜单项,即可看到你的内核运行!
常见问题与解决方案
编译错误排查
| 错误信息 | 可能原因 | 解决方案 |
|---|---|---|
undefined reference to 'kmain' | 汇编未正确声明extern kmain | 检查kernel.asm中是否有extern kmain |
ld: i386 architecture required | 未指定32位模式 | 编译时添加-m32参数 |
error: unknown type name 'uint32_t' | 使用了标准库类型 | 替换为基本类型或手动定义 |
启动失败处理
如果系统无法启动,可通过以下方法恢复:
- 重启并按住Shift键,进入GRUB菜单
- 选择原有操作系统(如Ubuntu)
- 检查/修正GRUB配置文件
- 重新运行
sudo update-grub
调试技巧
- 使用QEMU调试:
qemu-system-i386 -s -S -kernel kernel # 启动GDB服务器
gdb -ex "target remote localhost:1234" # 连接调试器
- 打印调试信息: 修改kernel.c,在显存不同位置显示状态信息:
// 在屏幕右上角显示"DBG"
vidptr[80*2*2] = 'D'; vidptr[80*2*2+1] = 0x0F;
vidptr[80*2*2+2] = 'B'; vidptr[80*2*2+3] = 0x0F;
vidptr[80*2*2+4] = 'G'; vidptr[80*2*2+5] = 0x0F;
进阶方向与学习路径
mkernel只是内核开发的起点,以下是值得探索的进阶方向:
推荐学习资源
-
官方文档:
-
扩展项目:
- mkeykernel:mkernel的进阶版本,添加了键盘支持
- Linux内核源码:学习工业级内核实现
总结与下一步
通过本文,你已经掌握了:
- 内核启动流程:从BIOS到GRUB再到内核执行的完整链路
- 汇编与C混合编程:如何用汇编处理底层初始化,用C实现业务逻辑
- 显存操作:直接访问硬件地址实现字符显示
- 内核编译与部署:从源代码到在真实硬件上运行的全过程
下一步行动建议:
- 修改显示字符串,让内核显示你的名字
- 尝试不同的属性字节值,改变文字颜色和背景
- 实现滚动显示或居中对齐文本
- 添加简单的键盘输入响应功能
希望本文能点燃你对操作系统开发的兴趣!如果觉得有帮助,请点赞收藏本文,并关注后续的内核高级特性讲解。下一篇我们将实现键盘中断处理和简单的shell交互,敬请期待!
【免费下载链接】mkernel A minimalist kernel 项目地址: https://gitcode.com/gh_mirrors/mk/mkernel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



