从零编写操作系统 10 - 物理页分配:哪些内存真正可以交出去
内核已经能处理时钟中断和键盘事件,但新增数据仍依赖静态数组。 页表、任务栈或文件缓存需要动态取得内存时,必须知道哪些物理地址可以使用、哪些已经分配,以及释放是否有效。 本篇把第 04 篇取得的 E820 内存图转换成 4 KiB 物理页分配器。 实验仍使用单 CPU、64 MiB RAM 的 qemu32 模拟机,分页保持关闭。 分配器管理固定上限内的页框,返回物理地址;它不建立虚拟地址映射,也不提供任意字节大小的 malloc。 实测初始空闲页数为 16087,客体耗尽全部页面、验证每页数据并释放后,空闲数恢复为 16087。 E820 可用内存还需要扣除内核占用 E820 描述固件报告的系统地址范围。 内核已经装入内存,却不意味着固件会把这段地址从 type 1 区域里扣掉。 若直接把所有 type 1 页面交给调用者,分配结果可能覆盖正在运行的代码或栈。 ACPI 6.5 第 15 章定义了 BIOS 内存图接口和范围类型。 其中 type 1 表示可用 RAM;其他类型不能在本实验中直接当作普通空闲页。 低地址区域也可能有固件或引导程序用途,规范 §15.2要求调用方考...
从零编写操作系统 09 - 键盘与事件队列:把中断接收和字符处理分开
键盘接入后,内核需要同时处理两种节奏:设备产生扫描码时必须及时接收,字符转换、退格和整行输出则可以留在前台执行。把打印放进 IRQ1 会延长关中断时间,也会让输入设备直接依赖控制台。 本篇在已有异常入口和 PIC 重映射上接入 PS/2 键盘。IRQ1 只接收原始字节并放进队列;主循环负责 US set 1 解析与简单行编辑。输入回车后显示 LINE: ...,尚未执行命令,也没有 shell。 IRQ1 接到已有中断入口 第 08 篇把主 PIC 的向量起点设为 0x20。键盘接主片 IRQ1,因此向量为 0x21,即十进制 33。 123456789PS/2 键盘扫描码 ↓i8042 输出缓冲区 → IRQ1 ↓主 PIC → IDT[33] → 公共汇编入口 ↓trap_dispatch → keyboard_irq ↓ 原始字节入队,主 PIC EOIiret 恢复前台 → 出队 → 解析 → 行编辑 kernel/exceptions.c 现在初始化前 34 个 IDT 表项,0—31 为异常,32 为时钟,33 为键盘。其余表项仍不在场,不能...
从零编写操作系统 08 - 时钟中断:让外部事件打断当前执行
上一篇的异常由正在执行的指令触发。时钟中断来自处理器外部:即使内核没有执行 int,定时器也能发出请求,经中断控制器送到 CPU,再通过 IDT 进入处理函数。 本篇接入传统 PC 的 8259A PIC 与 8254 PIT,只开放 IRQ0。正常内核收到 20 次时钟中断后关闭外部 IRQ,继续原有控制台自检;两个独立诊断镜像验证屏蔽、恢复和漏发 EOI 的后果。入口仍沿用上一篇的保存与恢复路径,尚未加入调度或任务切换。 从 PIT 输出到 IDT 第 32 项 PIT 的 channel 0 产生周期信号,连接主 PIC 的 IRQ0 输入。PIC 根据屏蔽位和优先级判断是否请求 CPU 响应;CPU 接受请求后获得向量,再查 IDT。 12345678910111213PIT channel 0 周期输出 ↓主 PIC 的 IRQ0 输入 ↓ IMR、优先级及 CPU 当前 IF 允许CPU 接受可屏蔽外部中断 ↓IDT[0x20] → isr32 → exception_common ↓trap_dispatch(frame) → timer...
从零编写操作系统 05 - 跳进 C:入口、链接脚本与内存布局
第 04 篇的保护模式入口停在一段 32 位汇编里。本篇从这里继续,把磁盘中的真实内核复制到物理地址 0x100000,准备 BSS 和栈,再调用 kernel_main。 屏幕上的 KERNEL_MAIN OK 有几个前提:已初始化全局变量保持原值,未初始化全局变量为零,普通函数调用能返回,BootInfo 指针和调用栈符合约定。这些条件都在本篇镜像里检查;另一个故障镜像故意跳过 BSS 清零,输出 KERNEL BSS FAIL。 编译器不负责启动内核 GCC 生成指令,链接器安排地址,磁盘读取和运行环境由启动代码提供。-ffreestanding 选择独立环境,不能代替加载器,也不会附带完整标准库。普通应用依赖的启动对象、进程栈和零初始化内存,在这里都需要有明确来源。 第 03、04 篇的 payload 是 20000 字节测试数据。本篇将它替换为 C 和汇编编译得到的 kernel.elf,再经 objcopy -O binary 得到平坦文件。第二阶段仍读取固定 LBA 的镜像头与 payload,不解析 ELF 的 program headers。 1234567...
从零编写操作系统 04 - 进入保护模式:建立第一份运行环境
第 03 篇已经让第一阶段启动扇区加载第二阶段,也让第二阶段读取载荷并检查边界。它仍然停留在实模式。实模式下可以方便调用 BIOS,也能通过操作数前缀使用 32 位寄存器;后续内核需要的平坦地址空间和权限隔离仍要建立在保护模式上。 本篇完成第一次处理器模式切换:在进入保护模式前收集 E820 内存图,打开 A20,建立 GDT,设置 CR0.PE,再用远跳转刷新代码段。切换完成后,32 位代码直接写 VGA 文本显存,GDB 能观察到保护模式状态。BIOS 调用到这里为止,后面的内核不能再按实模式方式直接调用 int 10h 或 int 13h。 为什么先把 BIOS 调用做完 BIOS 服务是实模式时代的接口。第 03 篇依赖它读盘;本篇还要依赖它查询内存图。进入保护模式后,CPU 的段解释、地址宽度和中断入口都变了。没有虚拟 8086 模式、实模式回跳或专门 BIOS 调用桥时,内核不能继续把 int 15h 当作普通函数使用。 切换前后的准备顺序如下: 1234567891011real mode ├─ read disk through BIOS EDD ├─ pr...
从零编写操作系统 03 - 两阶段引导:加载比一个扇区更大的程序
第 02 篇的启动扇区已经能被 BIOS 执行,并在屏幕上显示启动盘编号。继续增加读盘、内存图和模式切换代码时,第一扇区的空间很快就会用完。本篇让第一阶段读取一个更大的第二阶段,再由第二阶段读取并校验载荷。 这一版读取 20000 字节的确定性测试数据,分两次放到低端内存,验证首尾标识与校验和后停机。载荷仍是数据,没有执行 C 内核,也没有进入保护模式。 本系列当前阶段在 QEMU pc-i440fx + SeaBIOS 环境开发与验证;对物理硬件或其他虚拟机的适配将在后续篇章讨论。 两次加载分别解决什么 BIOS 先读取 LBA 0。第一阶段只有 512 字节,末尾保留字节 55 aa,偏移 446–509 仍留给未来分区表。第二阶段占用 8 个扇区,第一阶段将其装入物理地址 0x8000 后远跳转。 12345678910BIOS -> MBR at 0000:7c00 | | INT 13h AH=42h: LBA 1..8 v stage2 at 0000:8000 | ...
从零编写操作系统 07 - 异常入口:CPU 出错后把哪些信息交给内核
串口和 panic 可以报告代码主动检测到的错误,但前提是程序执行到了检查位置。除数为零、指令无效、段选择子越界,会让处理器直接转入异常处理。没有异常入口,上一篇建立的输出能力也无从调用。 本篇安装 IDT,把异常现场整理成 C 结构,打印向量、错误码和寄存器。一个受控 INT3 探针验证返回路径,除零、非法指令和一般保护异常分别验证故障路径。范围限定在 32 位保护模式、同一特权级、固定 QEMU 教学配置;设备 IRQ 尚未启用。 IDT 决定异常从哪里进入 处理器通过异常向量查找中断描述符表 IDT。向量 0 对应除法错误 #DE,3 对应断点 #BP,6 对应无效操作码 #UD,13 对应一般保护异常 #GP。IDT 中的门描述符给出目标代码段和入口地址。 1234567891011指令触发异常 ↓CPU 按向量查 IDT,检查门与目标代码段 ↓CPU 保存返回现场,转入对应汇编 stub ↓stub 补齐向量与错误码,公共入口保存寄存器 ↓trap_dispatch(frame) ├─ 受控 INT3:返回公共入口,恢复现场,IRETD ...
从零编写操作系统 02 - 第一个启动扇区:让 BIOS 执行自己的指令
第 01 篇得到了一块全零磁盘。QEMU 能运行固件,GDB 能控制 CPU,但 BIOS 最终只显示 No bootable device.。磁盘里没有可执行的引导入口,工具链生成的 ELF 文件也没有被加载。 本篇把一段 16 位机器码写进磁盘第一个扇区。BIOS 读取它,程序准备自己的段寄存器和栈,输出固定标识,再停在可检查的循环中。改坏末尾签名后,同一份机器码将不再得到执行机会。 从第一个扇区交出执行权 在本系列固定的 QEMU PC 和 SeaBIOS 环境里,固件尝试从 IDE 硬盘启动时,会读取第一个逻辑扇区,把内容放到物理地址 0x7c00。通过有效性检查后,执行流转移到这块内存。 磁盘上的扇区编号、文件偏移和内存地址是三个不同的量: 123456789镜像文件偏移 0 … 511:第一个 512 字节扇区 │ BIOS 读取 ▼客体内存0x7c00 … 0x7dff:刚读入的同一组字节 │ CPU 取指 ▼设置段与栈 → 输出标识 → 停止 0x7c00 不是 NASM 决定的磁盘位置。固件先决定加载位置,汇编代...
从零编写操作系统 06 - 输出与故障现场:建立串口、控制台和 panic
第 05 篇已经完成从保护模式到 kernel_main 的交接,栈、参数、已初始化数据和 BSS 都有检查。内核还需要把检查结果输出成可保存的日志,并在无法继续时留下调用位置。 第 06 篇增加 COM1 串口、VGA 文本控制台和 panic。正常镜像会演示换行、滚屏与格式化;独立故障镜像触发断言,同时输出文件名、行号和表达式,随后循环执行 hlt。这些功能仍运行在单核、未开启 IRQ、没有分页的早期内核里。 输出模块在自检之前初始化 启动器仍按第 05 篇的契约装载内核。stage2 完成 E820、A20 和保护模式切换,把内核复制到 0x100000;入口汇编清 BSS、建立栈,再把 BootInfo 指针传给 C。新增的控制台没有改变这条装载路径。 kernel_main 的第一步是 console_init(),之后才检查 BootInfo、data、BSS 和最小运行时函数。这个顺序让自检失败能输出具体原因。若先验证 BSS,输出模块却依赖尚未初始化的状态,坏 BSS 实验就可能连错误信息也无法显示。 因此,console_init() 显式把 row、colu...
从零编写操作系统 01 - 准备工具链:源代码怎样变成磁盘里的字节
在普通应用项目里,编译成功后通常可以直接运行可执行文件。自制操作系统还缺少一段工作:谁把文件里的指令放进内存,谁准备栈,CPU 又从哪里开始执行? 本篇先把这些问题拆开验证。第一条实验生成 ELF32/i386 文件,检查源代码、符号、链接地址和机器指令的对应关系。第二条实验把一块全零磁盘接到 QEMU,观察固件启动失败,再用 GDB 停住并单步执行 CPU。 两条实验还没有连接起来。sample.elf 不会被写进空磁盘,也不会由 BIOS 加载。本篇完成后得到的是可用的编译与调试环境;第 02 篇才开始写 BIOS 能执行的启动扇区。 宿主、目标与固件 本次实验的宿主链路是 ARM macOS 上的 Podman Linux 虚拟机,容器内运行 Debian 12 aarch64。交叉编译器本身是一段 ARM Linux 程序,它生成的指令却属于 32 位 x86。QEMU 再通过 TCG 动态翻译运行这个 x86 客体,不要求宿主 CPU 支持直接执行 x86 指令,机制见 QEMU TCG 文档。 12345678ARM macOS └─ Podman Linux V...




