尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

手写操作系统实战:从引导扇区到内核的完整修炼之路

手写操作系统实战:从引导扇区到内核的完整修炼之路 简介《自己动手写操作系统》于渊版完整教学资料包面向对操作系统底层机制感兴趣、希望从零实践构建内核的开发者与学习者系统覆盖启动引导、内存管理、文件系统、进程调度等核心主题。压缩包共2000个文件约121.84MB包含956个C源码、748个头文件、119个汇编文件、82个inc包含文件以及Bochs模拟器配置文件、磁盘镜像、Makefile、脚本和PDF文档等构成一套可编译、可运行、可调试的完整开发环境。书中理论与实践紧密结合源码部分涵盖内核编写、系统调用实现、中断处理程序设计等关键功能读者可自行解压后运行示例代码修改和测试不同模块加深对操作系统构建过程的理解。资源同时涉及多核处理、虚拟化技术、安全机制等进阶主题适合系统学习或项目参考。目前已有197人学习下载对于希望深入底层开发的读者具有实用价值。 把《自己动手写操作系统》这本书完整过一遍是种什么体验我是在上一家公司离职后的空窗期干的这件事前后大概两周每天雷打不动五个小时。这本书网上传的资源不少带源码的RAR包也容易找到但真正打开压缩包、把代码逐行读懂、然后自己敲一遍编译运行的人其实是少数。多数人卡在第一步不知道从哪儿入手觉得操作系统是个高不可攀的东西。这篇文章我就以于渊这本书为主线讲讲我实际跑通《自己动手写操作系统》源码的经历包括环境搭建、引导扇区的运行、代码里那些容易看迷糊的底层细节以及我踩过的几个挺有代表性的坑。如果你是非科班转行、系统学操作系统只停留在书本概念、或者看完《30天自制操作系统》还不过瘾的人这篇内容应该能给你一条相对顺的路线。我尽量把每个步骤讲清楚不光是“怎么编译”更会讲“为什么这么做”。1. 内容整体设计与思路拆解1.1 从“用操作系统”到“造操作系统”我自己上大学时操作系统课用的教材是经典的那本恐龙书讲进程、内存、文件系统概念背得滚瓜烂熟考试也能拿高分。但你问我“系统是怎么启动的”我脑子里只有一行字引导程序加载内核。至于引导程序怎么写、内核加载到内存后做了什么、保护模式是怎么切进去的完全一头雾水。于渊这本书为什么让我觉得“相见恨晚”因为它走的是另一条路线不按教科书的结构讲原理而是从零开始、一行一行地写代码最终跑出一个带着终端窗口、能响应键盘输入的小系统。这正好补上了“理论”和“实践”之间那条巨大的沟。这个“自己动手写操作系统”内含代码的资源包本质上是一整套可编译的工程代码。书里源码分章节组织从引导扇区一直到进程调度都有对应的汇编和C代码。拿到手以后别急着盲目编译先把章节结构理清楚明确每一章到底做了什么这样在阅读源码时你才知道当前这一小段代码在整个系统的启动流程里处于哪个位置。1.2 这本书的路线图和学习建议于渊这本书大致分这么几个阶段引导扇区直接在屏幕上输出“Hello, OS world!”。从实模式进入保护模式配置全局描述符表GDT。开启分页机制建立页表和页目录。加载并解析ELF格式的内核映像。安装中断描述符表IDT处理时钟中断和键盘中断。实现简单的进程调度和内存管理。给你打个比方如果把操作系统比作一幢楼引导扇区就是地基的第一块砖保护模式是给楼里装了电梯让32位程序能跑起来分页是给每一层楼分好了房间号虚拟内存。这个进阶顺序很重要每一章都是下一章的前提所以建议按章节顺序来千万不要跳着读。学习方法上我的建议是“三遍读代码”第一遍宏观浏览把该章代码文件结构记下来知道哪些函数是入口哪些是工具函数。第二遍逐行精读遇到不确定的指令去查手册或搜索不放过任何一行。第三遍自己动手写一遍不照抄源码凭记忆和笔记重新实现功能。写不出来再回去查。2. 环境搭建与开发工具链2.1 装好NASM、GCC和一个虚拟机说起来简单但当年劝退我的第一关其实是环境。这本书的代码是32位实模式和保护模式汇编代码初学者建议在Linux环境下运行因为GCC、Make这些工具链本就齐全NASM用包管理器轻轻松松一键安装如果你用的是Windows装WSL或者直接用虚拟机跑个Ubuntu也完全没问题。常用的工具NASM汇编器把汇编源码编译成二进制文件。GCC配合-m32参数编译C语言内核模块。链接器ld把目标文件链接成内核映像。Bochs一款x86模拟器自带调试功能适合观察寄存器状态。QEMU功能更强、速度更快但调试能力相对弱一些适合跑最终产物。用Ubuntu的话一条命令搞定sudo apt-get install build-essential nasm bochs bochs-x qemu-system-x86提示Bochs有带调试器和不带调试器两个版本。一定要装带调试器的版本否则遇到问题只能靠猜。启动时若提示缺少romimage或vgaromimage在bochsrc配置文件里手动指定Bochs安装目录下的BIOS文件路径即可。上手这套环境之后我建议你先用QEMU跑一下最简单的引导扇区样例确保汇编器、虚拟机、镜像生成这3个环节都能正常走通。毕竟如果连最小环境都跑不起来后面所有带中断、带内存管理的代码就更不用想了。2.2 编译链接与格式约定的关键点这本书前几章的代码是纯汇编写的。以一个引导扇区为例用NASM编译成二进制nasm -f bin boot.asm -o boot.bin看起来很简单但如果你在前三章犯迷糊多半是因为对几个“格式”问题没概念-f bin生成未加工的二进制文件不包含任何ELF或PE格式的文件头BIOS能直接把它从磁盘读到内存。引导扇区有512字节限制且最后两字节必须是0x55 0xAA否则BIOS不承认它是合法的启动扇区。引导扇区加载到内存地址是0x7c00代码第一行要用org 0x7c00告诉汇编器所有内存地址都基于这个偏移量来计算。于渊书的第2章提供了一个很经典的代码片段精简下来大概是这样org 0x7c00 mov ax, 0xb800 mov es, ax mov byte [es:0], H mov byte [es:1], 0x07 hang: jmp hang times 510 - ($ - $$) db 0 dw 0xaa55这段代码的逻辑很简单0xb800是文本模式显存起始地址往这个内存地址写字符就会显示在屏幕左上角最后用times 510 - ($ - $$) db 0填充剩余字节再写入魔数0xaa55凑满512字节。编译完用dd命令把它写入一个虚拟软盘镜像dd if/dev/zero offloppy.img bs512 count2880 dd ifboot.bin offloppy.img bs512 count1 convnotrunc然后用QEMU跑qemu-system-i386 -fda floppy.img如果屏幕上出现了一个H说明你的“最小操作系统”已经能启动了。别小看这一步它意味着你完整走了一遍“编写-编译-生成镜像-运行”的流程。往后所有章节的代码都建立在这个链条上。2.3 Bochs配置与调试入门QEMU跑起来方便但真正调试的时候Bochs更可靠。它的调试器可以设置断点查看内存和寄存器的实时状态非常直观。一份最简bochsrc.txt参考配置romimage: file/usr/share/bochs/BIOS-bochs-latest vgaromimage: file/usr/share/bochs/VGABIOS-lgpl-latest floppya: 1_44floppy.img, statusinserted boot: a megs: 32 display_library: x运行bochs -f bochsrc.txt后会进入调试模式。常用命令b 0x7c00在引导扇区入口设置断点。c继续执行直到触发断点。info registers查看寄存器值。x /10bx 0x7c00查看内存地址0x7c00处的内容。info gdt查看全局描述符表的内容。实操心得如果你在真实机器上验证启动过程需要把生成的.img镜像写入U盘或烧录到微控制器里注意事项会多一些比如磁盘类型、分区表等都会影响启动。初学者先在QEMU和Bochs里跑通即可没必要一开始就折腾真机硬件。3. 核心模块拆解与实操要点3.1 引导扇区整个系统的最前端引导扇区是操作系统启动的第一段代码由BIOS负责加载到内存0x7c00处然后把控制权交给它。于渊这本书里的引导代码完整实现的功能比“打印一个字符”复杂一些但它其实做了这么几件事设置栈指针通过mov sp等指令给后面调用C函数腾出可用栈空间。重置磁盘控制器把软盘/硬盘上的内核映像读取到内存指定位置。跳转到内核入口地址。读磁盘的环节这里特别容易出问题因为要调BIOS中断int 0x13。调用前需要逐个设置好寄存器参数ah 0x02读扇区功能号al 要读取的扇区数cx 柱面号和扇区号dx 磁头号和驱动器号es:bx 存放数据的缓冲区地址这是典型的“约定式编程”BIOS规定了寄存器含义你必须严格按规矩来。书里的代码会在读取后马上检查CF进位标志位判断读取是否成功。如果失败一般要重试甚至直接死循环报错。这里有个小细节值得留意引导扇区本身只有512字节不可能把整个内核放进去所以它只负责加载一小段“加载器”代码再由这段代码去读取完整内核。也就是说真正的启动过程是一个“三级跳”BIOS → Boot Sector → Loader → Kernel。理解了这条链你就知道为什么书里要把代码拆分成多个文件而不是一个文件写到天荒地老。3.2 从实模式到保护模式的切换GDT配置这一步必须清楚到了第3章代码就开始进入真正的“硬核环节”了从16位实模式切换到32位保护模式。实模式的内存模型是“段地址乘以16加偏移”20位地址空间最多访问1MB。而保护模式引入了描述符表和段选择子寻址方式完全不同且可以访问4GB的内存空间。实现切换到保护模式核心步骤是在内存某处定义好GDT表包含一个空的描述符、一个代码段描述符、一个数据段描述符。用lgdt指令把GDT表的基址和界限加载到GDTR寄存器。打开A20地址线解除对第21根地址线的屏蔽。将cr0寄存器的PE位第0位置1开启保护模式。通过一个远跳转jmp dword selector_code:start32刷新CPU流水线让后续指令以32位模式执行。GDT表项的结构非常精确定义“基址、界限、属性”。每个描述符8字节拆开来看是这样的; 代码段描述符示例 dw 0xffff ; 段界限低16位 dw 0x0000 ; 段基址低16位 db 0x00 ; 段基址中间8位 db 10011010b ; 存在位、DPL、代码段、可读 db 11001111b ; 粒度、32位、段界限高4位 db 0x00 ; 段基址高8位这段代码里10011010b和11001111b两个字节是很多初学者的噩梦。实在记不住属性位的含义也没关系关键是理解每个段描述符向CPU描述了“这段内存从哪里开始、到哪里结束、以什么权限访问”。实操上保护模式下访问数据段需要用段选择子。代码里要先加载数据段选择子到数据段寄存器否则访问内存会触发异常。我当时就在这一步踩了大坑后面第4节详细说。3.3 分页机制和中断处理并行推进的两个大难点开启保护模式后后面的章节就开始处理分页和中断了。分页机制的核心目的在于让每个进程看到一份完整、独立的虚拟地址空间操作系统再把这些虚拟地址映射到物理内存。于渊这本书的实现不算复杂主要步骤建立页目录表Page Directory和页表Page Table。把这两个表放在特定的物理地址并把表项指向真实物理页框。将页目录表地址写入cr3寄存器。将cr0的PG位置1开启分页。页目录表项和页表项的结构也很统一高20位是页框物理地址低12位是属性位。首次映射内存时常见做法是“恒等映射”——也就是虚拟地址直接等于物理地址等进程管理实现后再做独立地址空间切换。中断部分需要配置IDT把0到255号中断向量和处理函数关联起来。这里必须注意IDT表项的格式和GDT表项不一样它有16字节包含段选择子和偏移地址以及类型属性中断门或陷阱门。书上会先屏蔽所有中断把IDT项全部注册好再打开中断。CPU的sti指令开中断cli指令关中断调试时可以优先通过这两个指令来判断是不是中断发生异常。我当时读到中断这章时最大的感受是“CPU是傻瓜你得好心把每一步都给它安排好”。中断到来时CPU先检查有没有对应的门描述符没有直接报双重异常有它根据门描述符里的段选择子跳转到对应处理函数。任何一步没配好系统就崩了。4. 实操过程与核心环节实现4.1 完整编译并运行一个带内核的镜像到了第5章代码就不再是只有一个引导扇区了而是由多个汇编和C文件组成。以书中的Makefile风格为基础完整编译过程大概分这样几步分别用NASM编译各个汇编文件生成目标文件.o。用GCC的-m32 -ffreestanding -c编译C源码生成目标文件。-ffreestanding告诉编译器这个环境没有标准库不要生成依赖libc的代码。用ld把这些目标文件链接起来指定-Ttext为入口地址比如0x8000生成最终的内核镜像文件。把引导扇区、加载器、内核镜像都按顺序写入虚拟软盘镜像的特定扇区。一个很典型的Makefile片段CC gcc CFLAGS -m32 -ffreestanding -Wall -c LD ld LDFLAGS -m elf_i386 -Ttext 0x8000 all: nasm -f elf boot.asm -o boot.o $(CC) $(CFLAGS) kernel.c -o kernel.o $(LD) $(LDFLAGS) boot.o kernel.o -o kernel.bin这里有个容易踩坑的地方链接地址-Ttext必须和加载器里跳转的目标地址一致。如果代码编译时按0x8000计算地址但加载时却放到了别的物理地址程序跳过去之后读到的第一条指令就是错的。4.2 内核主函数里写了什么当控制权从加载器跳到内核一个典型的C语言入口函数长这样void kernel_main(void) { clear_screen(); print_string(OS is running...); init_gdt(); init_idt(); init_timer(); init_memory(); init_keyboard(); asm volatile (sti); while (1) { // 主循环处理输入输出 } }这个入口函数思路比较常规清一下屏幕输出两行文字然后依次初始化几个子系统最后开中断进入主循环。每次加一个模块就编译跑一遍看看有没有产生异常。这种“做一个小步就跑一次”的方式是我给所有初学者最强烈的一个建议不要等所有代码都写完再一次性跑否则面对满屏报错你根本不知道错在哪。4.3 中断处理的“最小实现”样例键盘中断和时钟中断是本书后期最能给你“成就感”的部分。按下键盘屏幕上有字符出现屏保程序也能在时钟中断里定时刷新。以时钟中断为例处理器每隔约10毫秒或按你设置的频率触发一次int 0x20中断。中断处理程序必须先发送EOIEnd of Interrupt信号给8259A中断控制器告诉它“这个中断我处理完了可以放行下一个”然后才能继续执行主流程extern timer_handler timer_interrupt: pusha call timer_handler mov al, 0x20 out 0x20, al popa iret用C代码来写中断处理逻辑时要注意对应的函数得放在中断无法打断的关键区里否则数据可能会混乱。于渊的代码里用了cli和sti来开关中断很多新手会想“只是切换上下文不关中断行不行”我的经验是不行。先关中断再对共享数据操作是设备驱动里的铁律。5. 常见问题与排查技巧实录5.1 现象汇总与解决方向实际操作中我遇到过的几类典型问题整理成表格方便你排查现象可能原因排查思路启动后屏幕全黑引导扇区编译结果不是预期代码用Bochs断点检查0x7c00处的机器码打印的内容是乱码显示缓冲区地址或属性字节设置错误检查es段寄存器是否指向0xb800属性字节是否正确保护模式跳转后无限重启GDT表基址/界限设置错误A20线未打开用Bochs的info gdt查看GDT是否加载成功检查A20地址线状态开启分页后内核跑飞页表没有恒等映射或cr3写错用Bochs检查cr3、页目录表首项内容确认虚拟地址和物理地址的映射关系执行某个中断时死机IDT项属性错误或中断处理函数返回地址错误检查IDT项格式确认iret是否在正确时机执行屏幕有显示但键盘无响应键盘中断未注册或8259A未正确初始化检查IDT表第33号向量检查主/从8259A的屏蔽寄存器状态5.2 最容易翻车的GDT切换这件事我必须单独拿出来说因为卡了我整整一个下午。当时我照着书上的GDT代码抄写了一遍编译也没报错但一执行到远跳转指令时Bochs直接重启模拟器显示“CPU reset”。排查了一圈才发现是GDT表上的段界限写错了。段界限是20位如果设为0xfffff那在4KB粒度的段模式下段大小是0xfffff * 4KB 4GB这是正确的但如果我在粒度为1字节的模式下也填0xfffff段大小就只有1MB访问高地址内存时CPU会认为“越界”直接触发异常。另一个细节是lgdt指令加载的不只是段基址还有一个“界限”字段表示GDT表有多少字节。如果这个界限值比实际表大小还大lgdt之后一旦CPU尝试读取表外地址马上会出问题。避坑技巧建议你每次修改GDT相关代码先打印基址和界限值然后用Bochs的info gdt确认硬件里存的确实是这两个值。很多时候代码逻辑没问题但寄存器里读出来的值和你想象的不一样。5.3 链接脚本和内存布局的坑随着代码量增大你不太可能把所有代码一次性放进一个文件、用同一套编译参数来完成。这时候你会开始接触链接脚本link.ld它决定各个段被放到哪些地址。书里某些扩展版本会使用一个简单的链接脚本SECTIONS { . 0x8000; .text : ALIGN(4) { *(.text) } .data : ALIGN(4) { *(.data) } .bss : ALIGN(4) { *(.bss) } }我当时不知道ALIGN(4)的作用删掉后代码也能跑但后来内核越来越大就莫名出现了数据错乱。原因在于如果没有对齐结构体或数组的起始地址可能没法被4整除而C语言的某些指令假设了4字节对齐。这种问题非常隐蔽只能靠经验积累了。5.4 调试技巧与效率工具Bochs调试器最强的功能之一是可以直接查看内存和寄存器的变化。遇到“代码跑到一半挂掉”的情况我一般这样做先在异常点附近设置断点比如b 0x8010。执行c跑到断点处看寄存器是否和预期一致。如果寄存器异常就往前查用x命令查看某段内存的内容确认数据有没有被改坏。在怀疑点前后用info registers对比寄存器变化。如果问题只在QEMU上出现在Bochs上不出现那八成是时序或硬件初始化差异比如8259A初始化时机太快设备没来得及响应。这种时候我建议你把QEMU的显示输出和Bochs的调试日志对比着看找到差异发生在哪一步。原因往往是代码里漏掉了“停机延时”或者对某个状态寄存器的等待。6. 资源包内的代码结构建议6.1 解压后先看目录再决定从哪一章开始拿到“自己动手写操作系统于渊-含代码.rar”这个包后里面大致应该是这么一套结构chapter2/引导扇区示例chapter3/保护模式切换chapter4/分页机制chapter5/中断处理chapter6/定时器chapter7/进程调度chapter8/内存管理我的建议是不要先打开chapter8的代码不然很容易被一堆复杂的数据结构和汇编宏击退。应该按顺序来先看chapter2编译运行成功之后再去看chapter3了解保护模式如此循环前进。每完成一章就在自己的笔记里记录“这一章主要解决什么问题”“怎么验证结果”。6.2 常见文件类型和阅读顺序参考理解代码文件类型能帮助你更快定位问题。通常来说.asm/.s汇编源文件负责启动、中断入口等底层逻辑。.c内核逻辑比如进程调度、内存管理算法。.h共享的数据结构定义和函数声明。Makefile编译规则。阅读单个源文件时我习惯先找入口函数比如引导扇区的start、内核的kernel_main然后找到第一个被调用的子函数一层一层往后跟。遇到底层指令不太懂的就停下来查一下指令集的手册不要硬啃过去。6.3 要不要看注释于渊这本书的源码里注释不算多但关键步骤都会有说明。尤其汇编代码里那些“魔术数字”比如0x10、0x8e、0x92旁边如果没有注释建议你在自己的笔记里标一下来源比如0x10是数据段选择子0x8e是中断门属性。这些数字靠猜是猜不出来的必须结合段选择子结构和中断门描述符的格式去推导。实操心得我建议你复制一份源码作为“注释版”专门用来加自己的理解和笔记另一份保持原样用来对照和编译。不要直接在原稿上涂改否则后期想恢复原文就非常麻烦了。7. 从这本书还能延伸到哪于渊这本书覆盖的知识已经能帮你打通“从按下电源键到内核跑起来”这条链路。但真的想在操作系统领域再往前走后续还有不少扩展方向多核与并发书里的进程调度是单核场景你可以尝试给系统加入多核支持了解核间中断IPI和锁机制。文件系统书中用到的简单文件系统比较初级可以进一步研究FAT、EXT2的磁盘布局。网络协议栈在有中断和内存管理的基础上实现网卡驱动和ARP/IP/UDP协议栈是很多学院派项目的方向。内核态与用户态的分离权限模式和系统调用机制是操作系统安全性的根基也是你从“小玩具OS”走向“真正OS”的必经之路。我自己在这本书之后又去翻了很多实验操作系统比如xv6、Minix的源码。对比之后你会更清楚于渊这本书“从简入繁”的取舍它把所有不必要的复杂度都削掉了一路保留主干让你在几个星期内就有机会写出一个能跑起来的内核。这种“先跑通再重新造轮子”的方式反而是打基础最快的方法。最后分享两条实用经验。第一不要在自己电脑上反复刷“当前操作系统的启动流程”来验证书里的东西用虚拟机模拟已经足够了真机启动容易破坏磁盘引导记录代价太大。第二学习和调试操作系统这块一定要保持耐心。它不像写一个网页改错一行刷新就能看结果操作系统内核一旦跑飞往往要花不少时间来定位异常点。我至今还记得自己第一次在屏幕上看到自己写的内核打印出字符串时那股难以形容的快感。一个看起来简单得要命的“H”背后是从引导扇区、保护模式、中断到内存管理的完整链路每一步我都亲手写了一遍。这种体验比背十遍“操作系统十大原理”都来得踏实。本文还有配套的精品资源点击获取
返回列表