【操作系统面经】持续更新ing

发布于:2025-05-20 ⋅ 阅读:(15) ⋅ 点赞:(0)

OS面经

一、整体架构设计

你为什么选择自己写一个操作系统内核?这个项目的目标是什么?

你如何组织整个内核的模块?模块之间是如何解耦的?

你如何调试这个系统?用到了哪些工具?有没有遇到比较棘手的bug?怎么解决的?

✅ 模块一:整体架构设计

🧠 问题1:你为什么选择自己写一个操作系统内核?这个项目的目标是什么?

✅ 回答思路:
展示动机:表达对底层技术的兴趣,想深入理解CPU如何管理内存、调度进程、处理中断等。

说明目标:不仅仅是“跑起来”,而是要掌握一个最小可用的类Unix系统内核的原理。

举例:

“我一直对操作系统底层原理感兴趣,尤其是处理器与内核的交互过程。写一个从Bootloader到进程管理的完整系统,可以系统地掌握内存管理、中断处理、调度机制等核心知识。项目目标是构建一个支持用户进程、系统调用、基本文件IO的轻量级x86内核。”

🧠 问题2:你如何组织整个内核的模块?模块之间是如何解耦的?

✅ 回答思路:
模块划分:比如 Bootloader、内存管理、中断系统、调度器、系统调用、文件系统等。

解耦方式:使用结构体/接口、函数指针、模块间的统一接口、头文件规范、静态链接。

举例:

“我将内核分成多个模块,每个模块有独立的头文件和实现文件,例如memory/, interrupt/, process/, syscall/等。模块之间通过统一的接口函数交互,比如中断模块提供register_interrupt_handler来供其他模块注册异常处理函数。这样设计能方便测试和维护。”

🧠 问题3:你如何调试这个系统?用到了哪些工具?有没有遇到比较棘手的bug?怎么解决的?

✅ 回答思路:
工具:QEMU + GDB 是最常见组合,Bochs 也可用于精细调试。

方法:串口输出、设置断点、单步执行、查看寄存器/栈等。

Bug案例:比如保护模式切换失败、中断不触发、分页错误等。

举例:

“我主要使用 QEMU 结合 GDB 来调试。QEMU 启动后通过 -s -S 选项等待 GDB 连接,我可以设置断点、单步执行、查看页表等信息。有一次分页初始化后发生Page Fault,后来通过查看CR2寄存器和页目录项发现缺失映射,最终修复了地址计算逻辑。”

我们继续进入第二个模块:Bootloader & 实模式到保护模式的切换。这是整个操作系统启动的关键步骤,面试官会很看重你是否真正理解了x86的启动流程和硬件机制。

二、Bootloader & 实模式→保护模式切换

面试讲稿:Bootloader 与实模式 → 保护模式切换实现内容

在我的操作系统中,我实现了一个完整的 Bootloader,它运行在 BIOS 加载的实模式环境中,完成了从上电启动到切换到 32 位保护模式的全过程。具体流程如下:

✅ 1. Bootloader 加载与初始化
Bootloader 被 BIOS 加载到实模式地址 0x7C00;

我使用汇编编写 Bootloader,并以 ORG 0x7C00 开头,最后写入 0x55AA 魔数,确保 BIOS 正确识别;

Bootloader 的体积严格控制在 512 字节以内,完成基础初始化并跳转至我编写的 loader_entry。

✅ 2. 开启 A20 地址线
实模式默认只能访问 1MB 内存,超过 0xFFFFF 会回绕;

为了加载内核到 0x100000,我通过 写端口 0x92 的方式开启了 A20 地址线,避免地址回绕;

该方法简单高效,兼容大多数现代PC。

✅ 3. 构建并加载 GDT
我定义了一个简单的 GDT(全局描述符表),包含:

一个空描述符;

一个 代码段(base=0, limit=4GB, DPL=0, 可执行);

一个 数据段(base=0, limit=4GB, DPL=0, 可读写);

使用 lgdt 加载 GDT,准备切换为保护模式。

✅ 4. 设置 CR0,打开保护模式
我通过 mov eax, cr0 → or eax, 0x1 → mov cr0, eax 设置了 CR0 寄存器的 PE 位(bit 0)为1;

这一步将 CPU 从实模式切换为保护模式。

✅ 5. 使用 ljmp 实现远跳转,刷新 CS
设置 PE 位后,CPU 仍旧沿用旧的 CS 段寄存器;

为刷新 CS,我执行了 ljmp $0x08, $protect_mode_entry,跳转到 GDT 中的代码段;

这是进入保护模式后真正安全的第一条32位指令。

✅ 6. 加载段寄存器 & 设置栈
在 protect_mode_entry 中,我重新设置了所有段寄存器(DS、ES、SS、FS、GS)为数据段选择子(0x10);

设置栈指针 ESP 指向我分配的 .bss 区域,准备调用 C 函数。

✅ 7. 加载内核并进入 kernel_init
最后,我将内核从磁盘加载到内存 0x100000 位置;

使用 call kernel_init 进入内核主函数,开始初始化中断、内存、文件系统、调度器等模块。

🌟 总结亮点
整个实模式到保护模式切换过程,我严格遵循了 x86 架构规范,从 A20 开启、GDT 构建、PE 位设置,到 ljmp 刷新 CS、段寄存器重载、ESP 初始化,完成了一个从裸机到内核 C 环境的过渡,打下了可靠的基础环境,为后续的分页、系统调用和多任务调度做了完整准备。
在这里插入图片描述

Boot:是一段过程(动词/名词)
Bootloader:是执行这段过程的程序(名词)
在这里插入图片描述

你写的Bootloader占用了多少字节?如何加载内核的?是基于哪种文件系统或格式?

从实模式切换到保护模式时,GDT的内容是怎么设计的?你用了多少个段?每个段的作用是什么?

为什么需要开启A20地址线?你是怎么做的?

✅ 模块二:Bootloader & 实模式 → 保护模式切换

🧠 问题1:你写的 Bootloader 占用了多少字节?如何加载内核的?是基于哪种文件系统或格式?

1.Bootloader 占用大小
我的 Bootloader 主体是写在一个512字节的扇区中,大小必须控制在 510 字节以内,最后两字节是 BIOS 识别用的魔数 0x55AA。我用汇编写的 start.S 开头位置就是 _start,并用 ORG 0x7C00 确保放在正确的地址。
0x7C00 这是 MBR 标准限制,这个地址是 BIOS 固定将 启动扇区(MBR) 加载的内存地址;
0x55AA IBM PC兼容性要求,。这两个字节的二进制模式(0b01010101和0b10101010)在早期硬件中易于检测,且能有效避免随机数据误判。0x55(二进制01010101)与0xAA(二进制10101010)是互为按位取反的互补模式。
这种交替的位模式在早期磁盘存储介质中具有较高的信号辨识度,可降低因电磁干扰导致的误读概率。
超过 512 字节要通过 二级加载器(如你的 loader_entry / load_kernel)从磁盘加载后续内容。
📦 2. 如何加载内核?
Bootloader 会在进入保护模式后,跳转执行 load_kernel() 函数。这个函数将磁盘中预设位置的内核映像(通常从第2扇区开始)读取到内存的 0x100000(1MB)处。然后设置栈、段寄存器,跳转执行 kernel_init()。

🧠 你用的是 扇区读取方式,通过 BIOS int 13 或直接 I/O 命令读取扇区 —— 属于裸磁盘访问,不是基于文件系统。
3. 使用哪种文件格式?
我的内核不是通过文件系统(如 FAT)加载的,而是直接使用裸的扇区顺序读入,内核是裸 ELF 格式或自定义 bin 格式。我自己通过链接脚本(kernel.lds)控制内核加载地址,比如把 .text 放在 0x100000 开始。

🧠 可以说:

「目前没有在 Bootloader 中解析文件系统(如 FAT),这部分在内核启动后由文件系统模块处理」;

「这样设计简洁可靠,也符合早期 Linux 的加载流程」。
CR0.PE 的作用? 设置为1后CPU切换到保护模式

在这里插入图片描述

🧠 问题2:从实模式切换到保护模式时,GDT 的内容是怎么设计的?你用了多少个段?每个段的作用是什么?

✅ 回答思路:
GDT 至少需要 3 个段:null 段、代码段、数据段(有时再加 TSS 段)。
每个段都有 base、limit、type 等属性。
保护模式要求 CR0 设置 PE 位,且必须加载 GDT。

举例:
“我的 GDT 包含了3个段描述符:一个是null段,一个是内核代码段(base=0, limit=4GB, DPL=0),一个是内核数据段。我


网站公告

今日签到

点亮在社区的每一天
去签到