在前几天生日之际,趁着心血来潮,打算实现一个操作系统,算是实现大学毕业的最后一个目标了,在入职的前的火车上,记录下来这几天的工作。

目前实现的部分有:

  • 从BIOS启动Bootloader
  • 在Bootloader中利用中断来调用BIOS提供的打印程序、加载磁盘扇区数据程序
  • 将C语言编写的内核程序加载到内存,Bootloader从16位实模式进入32位保护模式
  • 执行内核程序
  • 实现一些库函数:vsprintf/printf/memcpy/memset
  • 重置GDT段表和IDT中断向量表。

项目地址:https://github.com/netcan/NetcanOS

实现一个操作系统只是为了加深自己的基础,所以我使用了模拟器qemu/bochs来仿真x86环境。(写到U盘拿到真机上试了一下,并未启动起来= =后面有时间再试试吧)

NetcanOS.png

Bootloader

计算机启动的时候,首先启动BIOS程序进行自检,然后将磁盘的启动扇区(512字节)加载到内存0x7c00位置,没问题后会跳转到内存0x7c00位置执行指令。 所以在用C语言编写内核之前,首先需要用汇编实现Bootloader让BIOS运行,再将内核程序加载到内存执行。

至于启动扇区,BIOS很简单的判断,就是扇区最后两个字节是否为0xaa55。与此同时,BIOS还提供了一些中断处理程序供调用,例如int 0x10触发打印字符的中断,int 0x13触发读取扇区的中断。

需要注意一点的是,由于历史原因,在Bootloader中,CPU处于16位实模式,而后面要是想调用C语言编写的内核,那么需要切换到32位保护模式,在32位保护模式下,BIOS将失效,也就是无法使用BIOS提供的功能了,所以进入32位保护模式之前,需要关中断(BIOS的中断也没用了),设置GDT段表,然后告诉CPU段表的位置,设置cr0寄存器进入保护模式,最后jump far(处理掉CPU流水线残留的任务)到内核代码处。

实模式vs保护模式

32位保护模式相对于16位实模式来说,不同点有:

  • 寄存器宽度从16位扩展到32位
  • 增加了几个通用段寄存器,fsgs
  • 内存地址扩展到32位,能够寻址4G内存空间
  • 在内存管理方面,也有很大不同
    • 代码有权限,能够设置ring 0-3四种级别
    • CPU实现了虚拟内存技术

还有一个不同的是,在实模式下,内存地址计算为(段寄存器 * 0x10 + 偏移量),而在保护模式下,段寄存器为GDT表的索引,索引指向的元素包含了该段的基址。

驱动程序

在实模式下,可以直接触发0x10中断来打印一个个字符,而在保护模式下,由于显示设备映射到内存,可以通过对显存(位于0xb8000)写,来显示一个字符。显示设备有控制端口、数据端口,通过它们可以获取当前光标的位置,从而实现滚屏。

重置GDT表

虽然在Bootloader中已经设置过GDT段表了,考虑代码除了内核态运行,还有用户态,所以实现了一下设置GDT段表的函数(位于descriptor_tables.c:init_gdt),增加了两个段:

  1. NULL段(必须)
  2. 内核态代码段
  3. 内核态数据段
  4. 用户模式代码段
  5. 用户模式数据段

设置IDT表

CPU一共能处理256种中断,保留了前32种系统中断(ISR)。需要定义一个IDT表,存放这些中断的信息,例如中断向量(中断处理程序地址),中断属性。而系统保留的32种中断中,有些中断会将错误码入栈,有些不会,为了统一栈结构,所以定义这32种中断处理程序的时候,不会入栈错误码的那些中断手动push一个数据,然后交给C语言版本的isr_handler统一处理这些中断,根据不同的中断码来辨别不同的中断,还需要在汇编程序push一个中断号。

栈push/pop

栈结构在x86中非常重要,毕竟寄存器数量有限,所以需要将一些数据暂存到栈中来腾出空间。同时在函数调用中,C语言使用栈来传递参数。每个栈元素大小都是固定为一个机器字长,所以在后面C调用汇编程序的时候,直接读取栈内容,就能获取参数,非常方便。例如在中断处理程序中,中断程序执行前会将寄存器入栈以保存环境,同时调用isr_handler,利用这个特性就能在C语言isr_handler函数中轻而易举地获取中断程序保存的寄存器信息。

交叉编译器

由于我的开发环境是Mac OSX,内置的llvm编译器无法正常工作(例如链接器无法生成纯二进制文件,也没有binutils提供objcopy等方便工具)。所以需要用brew安装gcc编译器,接着下载gcc/gdb/binutils源码包,用brew的gcc编译它们(i386版本),然后安装。

调试

对于OS开发,调试也是一件非常重要的事情,不然出现什么问题都很难定位了。在编译内核代码的时候,通过-g选项将符号信息包含进elf文件中,最后利用objcopy从elf文件抽出存二进制文件,然后让qemu运行OS,配合gdb就能调试了,下图为我采用gdbgui调试的情况:

gdbgui.png

曾经我分别生成带符号的elf内核文件,和生成存二进制内核文件,后来调试的时候发现全局变量地址不对,一度怀疑是ld把符号信息搞错了,后来查资料发现,正确调试姿势是,用objcopy从带符号信息的elf文件中抽出纯二进制文件。

参考资料