如何从零开始写一个简单的操作系统

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何从零开始写一个简单的操作系统相关的知识,希望对你有一定的参考价值。

题主你好,如何写一个简单的操作系统,不是三言两语能够讲清楚的,也不是一两行代码就可完成的。
要完成一个简单的操作系统,而不是一个裸机下打印一下hello world的引导扇区。这需要有足够的计算机系统的知识。包括但不仅限于汇编,C和操作系统原理。
在自己写操作系统这一领域,也有不少大作。我之前也曾拜读过一部分,曾奉为圭臬,跟随作者的脚步慢慢前进,但是没多久,就做不先去了。。。
当知识经过一段时间的沉淀,在回首,曾经的大作看起来亦有不妥之处。
操作系统系统是一个复杂的技术话题,如何实现一个简单的操作系统,楼上已有回答,题主也可读一些大作来参考实现。
最近我发现一个不错的公众号,就是做自己操作系统这方面的,公众号里的文章,试图通过简单的话语来阐述,那些晦涩的计算机概念。 此公众号的作者也曾写过一个小的内核,可以引导到shell执行。
我正在关注这个公众号,如果题主对这个主题十分感兴趣的画,也可以关注下,我们共同进步。
公众号:操作系统探究
参考技术A 早先写过一个玩具内核,现在正做操作系统课助教,带一伙学弟学妹做这个事情。 假定题主对操作系统有一定的认识(被丢去裸考期末考试不会挂科)。 总体路线图大约是这样的: *以下内容全是干货,试图在不干预题主自己的设计的条件下尽可能指出所有的大坑,此方案不能代替看书/读论文/上课/看手册等* 第一阶段:早期规划 1. 搞清楚自己打算写一个内核干啥:试着设计?练习实现?拿来实用的话打算跑什么呢?打不打算跨平台?打算跨哪些平台,兼容到什么程度? 2. interface大概会长什么样?给用户空间提供哪些服务?(并不一定要和现有系统兼容或者怎么样) 3. 想怎么样boot?兼容一个现有bootloader还是自己写一个?内核被load的时候是什么格式? 第二阶段:方案设计 4. 选个架构吧,推荐IA32或者ARMv7A之类的 5. 找到各种手册翻看一下,脑内构思一下内核里各种机制该用什么方案实现 6. 选个目标设备吧,qemu可以玩玩但是内核都写了,还是上个设备跑一跑比较好。(如果你打算在树莓派上做的话下一步你会比较郁闷) 7. 这个设备的启动流程怎么样,和之前的想法有没有冲突? 8. 找到各种手册看一下,之前想好的实现方案有没有坑?时钟、中断、DMA之类的,大概怎么搞,心里要有数了。 9. 想不想要JTAG之类的调试方案?有没有?方不方便? 10. 提前组织一下模块划分、代码结构、编译管理方案等。 第三阶段:初步实施 11. 选好之后就把设备买下吧,需要的SD卡啦电源啦都买下,按照noobs' kit之类的东西试试好不好用 12. 准备工具链。在linux上你需要gcc和binutils。libgcc要不要加?不加的话有什么坑?(CortexA9没有硬件整数除法我说的就是你)怎么处理? 13. 编写字符输出驱动,不管是UART还是IA32的默认控制台或者是什么的。 14. 编写Hello world并作为最简单的内核或者bootloader运行。 15. (如果自己写bootloader)读启动介质的驱动和你想load的内核格式的支持代码应该写出来了。内核load到哪里去?内核想要load到非常奇怪的地址(比如设备区或者和bootloader重叠)怎么办? 16. 平台初始化要做一做了吧?页表准备一下该开MMU了吧?说道MMU就要说缓存,目标架构上缓存需要内核做哪些维护?如何启用?设备区怎么处理?(MIPS没有硬件table walker这种事情也要在这里处理掉) 17. 要不要跳虚拟地址?虚拟地址空间怎么划分? 第四阶段:各种内核组件 18. 一套完整的线性映射能不能容纳?不能的话怎么办?类似vmalloc和ioremap的机制,要实现的话需要提前规划好。 19. 物理内存怎么管理?连续页的分配和回收使用怎样的算法? 20. 任意大小内存块的分配和释放要怎么处理?有没有什么会要求非常大块的对齐(ARM的一级页表你别跑),怎么办? 21. 虚拟地址空间怎么管理?用户空间的和内核自己的虚拟地址空间管理方案可不一样的。 22. 中断和出错该怎么处理?向量表怎么搞?中断路由怎么设置? 23. 系统调用使用怎样的格式?中断信息获取之类的杂务该怎么做? 24. 上下文保存和恢复应该怎么做?保存哪些内容? 25. 各种驱动程序怎样部署和运行?静态携带还是动态注册?有没有打算做内核模块机制?怎么做?驱动的特权级要不要低于内核?怎么实现?驱动要不要参与未来的调度?和用户空间比有什么特别的? 26. 进程和线程打算怎样实现?依靠用户空间的库来调度还是像linux这样做线程和线程组?进程描述符长什么样? 27. 时钟怎么用?时钟比较器是共享的还是独享的? 28. 调度算法是怎样的?有没有什么好处坏处? 29. 自旋锁该怎么实现?有哪些使用要求? 30. 内核提供哪些同步机制?怎么实现? 31. 超过一个CPU的话,其他核心怎样唤醒?唤醒之后哪些初始化需要另做? 32. 各种系统调用内部该如何实现? 第五阶段:和用户态的配合 33. 怎样把提供的系统调用包成一个C库一类的东西? 34. 做哪些文件系统支持?内核要不要导出虚拟文件系统?控制台输入输出算不算文件? 第六阶段:可选内核组件 35. 管道、共享内存等IPC方案? 36. 动态挂载和卸载文件系统? 37. 前面提到过的内核模块? 38. kexec和kdump一类的机制? 39. ASLR?运行禁止?其他一些被动安全机制? 40. 随机数生成器? 41. 安全内存?处理器的加密解密部件? 42. 缺页和换页? 43. 区分一下用户?组?密码?shadow? 44. 文件权限?SUID/SGID? 45. 用户态的init差不多应该负担一些责任了? 46. 管理一下电源和频率?idle的时候做点节能? 47. 用文件/文件系统的方式和内核做一些交互? 48. 网络?路由?名称服务?防火墙? 49. 动态的设备管理?热插拔? 50. (我想不出更多了)来点图形界面?本回答被提问者采纳 参考技术B 早先写过一个玩具内核,现在正做操作系统课助教,带一伙学弟学妹做这个事情。假定题主对操作系统有一定的认识(被丢去裸考期末考试不会挂科)。总体路线图大约是这样的:*以下内容全是干货,试图在不干预题主自己的设计的条件下尽可能指出所

从零开始写 OS 内核

系列目录

  • 序篇
  • 准备工作
  • BIOS 启动到实模式
  • GDT 与保护模式
  • 虚拟内存初探
  • 加载并进入 kernel
  • 显示与打印
  • 全局描述符表 GDT
  • 中断处理
  • 虚拟内存完善
  • 实现堆和 malloc
  • 创建第一个内核线程
  • 多线程运行与切换
  • 锁与多线程同步
  • 进程的实现
  • 进入用户态
  • 一个简单的文件系统
  • 加载可执行程序
  • 系统调用的实现
  • 键盘驱动
  • 运行 shell

扩展并重载 GDT

本篇我们将在 kernel 中重新定义并扩展全局描述符表 GDT,并再次加载它。本篇的内容也会比较简单,更多的是对 x86 相关手册文档的查阅和熟悉。

GDT 在 loader 阶段我们已经初步定义并加载过一次,在那里我们只定义了 kernel 的 codedata 段,因为到目前为止,以及在后面相当长的一段时间里,我们始终处于 kernel 空间中,以 CPU 特权级 0 进行运行。但是作为一个 OS,最终是要运行并管理用户程序的,因此 GDT 中还需要加入用户态的 codedata 段。

另外我们也希望对前面的 GDT 重新整理一下,毕竟在汇编下比较混乱,很多数据结构管理起来不清晰。

GDT 代码

GDT 以及 segment 相关的知识,是 x86 体系架构的历史遗留产物,非常令人讨厌。但是 Intel 为了历史兼容,又不得不始终保留这些历史包袱。我们也不必花太多心思和脑筋在这上面,只要按照文档规范,把该填都填了,该写的都写了,轻轻带过就可以了。它并不是我们项目的核心部分。

按惯例,先给出代码链接,主要源文件是 src/mem/gdt.c。

关于 GDT 的文档,你可以参考这里。

首先我们需要定义 GDT entry 的数据结构:

struct gdt_entry {
  uint16 limit_low;
  uint16 base_low;
  uint8  base_middle;
  uint8  access;
  uint8  attributes;
  uint8  base_high;
} __attribute__((packed));
typedef struct gdt_entry gdt_entry_t;

它对应的是这样一个 64 bit 的结构:

\\"\\"

\\"\\"

其中 base 是指 segment 的内存基址,limit 则是长度,它可以有 1 或者 4KB 两种单位。

其余部分则是图二中展示的一些标志比特位,这里就不多费笔墨了,还是要对着文档仔细校对。

然后我们定义 GDT 表:

static gdt_entry_t gdt_entries[7];

我们这里分配了 7 个 entry:

  • 第 0 项保留;
  • 第一个是 kernelcode segment
  • 第二个是 kerneldata segment
  • 第三个是 video segment,这个不是必须的,可以无视;
  • 第四个是 usercode segment
  • 第五个是 userdata segment
  • 第六个是 tss

从第四个开始,都是用户态需要用到的。其中第六个 tss 目前不必深究,后面进入用户态时我们会回过来再细看这部分。

然后我们定义设置 GDT entry 的函数:

static void gdt_set_gate(
    int32 num, uint32 base, uint32 limit, uint8 access, uint8 flags) {
  gdt_entries[num].limit_low = (limit & 0xFFFF);
  gdt_entries[num].base_low = (base & 0xFFFF);
  gdt_entries[num].base_middle = (base >> 16) & 0xFF;
  gdt_entries[num].access = access;
  gdt_entries[num].attributes = (limit >> 16) & 0x0F;
  gdt_entries[num].attributes |= ((flags << 4) & 0xF0);
  gdt_entries[num].base_high = (base >> 24) & 0xFF;
}

对照着上面那幅图看就可以了。

将 GDT 表中的这些 entry 都设置上:

  // kernel code
  gdt_set_gate(1, 0, 0xFFFFF, DESC_P | DESC_DPL_0 | DESC_S_CODE | DESC_TYPE_CODE, FLAG_G_4K | FLAG_D_32);
  // kernel data
  gdt_set_gate(2, 0, 0xFFFFF, DESC_P | DESC_DPL_0 | DESC_S_DATA | DESC_TYPE_DATA, FLAG_G_4K | FLAG_D_32);
  // video: only 8 pages
  gdt_set_gate(3, 0, 7, DESC_P | DESC_DPL_0 | DESC_S_DATA | DESC_TYPE_DATA, FLAG_G_4K | FLAG_D_32);

  // user code
  gdt_set_gate(4, 0, 0xBFFFF, DESC_P | DESC_DPL_3 | DESC_S_CODE | DESC_TYPE_CODE, FLAG_G_4K | FLAG_D_32);
  // user data
  gdt_set_gate(5, 0, 0xBFFFF, DESC_P | DESC_DPL_3 | DESC_S_DATA | DESC_TYPE_DATA, FLAG_G_4K | FLAG_D_32);

对比 kerneluser 部分的差别,主要是两点:

  • Access Byte 中的 Privl:一共两个 bit 位,对 kernel 来说它是 00,而对 user 则是 11,它的含义是 DPL (Descriptor Privilege Level),代表的是访问这个 segment 需要的最小 CPU 特权级。

    \\"\\"

  • Limit:因为用户空间限制在了 3GB 以下,所以它的 Limit0xBFFFF,注意 FlagsGr (Granularity) 位是 1,所以 Limit 的 单位是 4KB,可以计算得到 (0xBFFFF + 1) * 4KB = 3GB

有了这两点限制,当 CPU 处于用户态时,它就无法访问 3GB 以上的 kernel 空间,这样 segment 机制的作用就发挥出来了。

以上是关于如何从零开始写一个简单的操作系统的主要内容,如果未能解决你的问题,请参考以下文章

从零开始怎么写android native service?

从零开始怎么写android native service?

Electron: 从零开始写一个记事本app

从零开始实现数据结构 动态数组

从零开始写博客系统——权限校验

从零开始写博客系统——权限校验