到目前为止,我们写的代码都是用汇编写的,汇编的缺点就是,第一遍写完了,回头再过来看,又忘了,晦涩难懂。其实我感觉,写汇编的好处,就是能让自己去多了解一下cpu还有其它硬件的结构。硬件的的架构,决定软件的实现方式,硬件就好比基因,软件就是一个个的动物个体。
本次实践涵盖的内容包括《orange’s一个操作系统的实现》书中的第三章末尾、第四章、第五章。因为这些东西比较紧凑。
捋一下思路:
1. 中断与异常,在保护模式下是非常重要的一部分。
写过java代码的同学一定写过”try{}catch(Exception e)”进行异常捕获,也写过类似各种观察者模式类似”onEvent(event,handler)” 的代码。 中断与异常的处理,其实就是这种思想:a.用一个表格,注册一下各种事件发生的时候应该如何处理;2.当cpu执行时发生异常或者中断,就到这个表格中找到相应的处理程序进行处理。
2.用C语言写内核!
其实掌握了上面提到的异常与中断处理机制,接下去,要实现一个操作系统,完全可以用汇编接着写下去。但是,这实在让人崩溃,因为我们还有更好的方法,用C语言写! 书中第四章和第五章,主要目标就是这个。
有了这个目标,接下来就需要解决几个问题:
a. 内核代码会越来越多,而512字节的引导区明显会不够用,怎么办?
b. 如果使用C语言写代码,原有的汇编代码如何与C代码相互调用?
c. 用gcc或者其它编译工具编译后的目标文件,格式与nasm直接编译的汇编代码文件格式不一样,而我们还要把这些编译后的代码加载到内存然后执行它呢!怎么办?
d. 当我们的工程文件越来越多,怎么管理这么多文件?
书中提出的一种解决方案:
a. 引导区的512字节不够用,那就用这512字节的代码,把“Loader”加载到内存里,而“Loader”的任务就是,把“内核”加载到内存里,然后进入保护模式。我们的实践里,系统最后是刻录进一个1.44M的软盘里,所以书中简单介绍了一些FAT12文件系统的格式,重点的就是,如何把文件存进去,然后如何用代码把文件读出来。实践中,我们就把引导、Loader和内核的代码按照这种格式放入到软盘,然后按照文件系统的格式,把代码或者数据读入内存即可。
b. C代码与汇编代码相互调用的问题比较简单,这里推荐一本书《程序员的自我修养——链接与装载》。其实C代码编译后也是汇编代码,而这个问题跟链接的机制关系很大,了解链接器如何工作就能很好的理解这个问题。
c. 书中是以gcc编译器为例,编译后是ELF格式的文件,作者只讲了一些目前够用的ELF格式的知识,其实ELF格式还挺复杂的。
d. 管理工程文件,书中介绍了Makefile文件的一些基本用法,用make来管理工程中文件的各种复杂依赖关系。
这里看下最终的结果截图:
已经进入内核阶段,并且监听键盘的中断。
因为涉及的具体细节比较多,这里不打算拎出一堆代码,只记录本次实践中遇到的一些问题
1. lgdt 和 lidt 指令,载入的地址其实是一个线性地址,gdt表和idt表里面存的也是线性地址,
如果没注意这个的话,在进入保护模式,分页机制建立起来后,对于gdt表的访问可能会有些疑惑。
“LGDT and LIDT appear in operating system software; they are not used in application programs. They are the only instructions that directly load a linear address (i.e., not a segment relative address) in 80386 Protected Mode.”
2. FAT什么意思?
File Allocation Table ( 文件分配表)
文件开始簇号,这里就限制了文件开始位置为某个扇区的起始位置,所以会有很多碎片,空间利用率不高。
3.文中提到,FAT12的数据区的第一个簇的簇号是2,而不是0或者1。为什么?
书中并没有详细解释,在这里找到了答案。FAT12文件系统 数据存储方式详解FAT表开始扇区的第1字节是存储介质,0f0h代表软盘,0f8代表硬盘;第2、3这两个字节都是0ffh,代表了FAT文件分配表标识符,从第四个字节开始与用户数据区所有的簇一一对应,应该注意的是,用户数据区的第一个簇的序号是002,而不是000,因为储存介质和标识符占用了这两个序号。
4. 如何挂载软驱,怎么把loader、kernel.bin文件拷贝到软盘里去?
书中的例子有点坑爹,因为下载下来的a.img,在freedos下,压根找不着那个edit.ext 工具。其实可以参考这个:【orange】OrangeS一个操作系统的实现:第四章实践方面遇到的一些问题,里面提到的方法,就是把软盘在freedos格式化为FAT12的格式,然后mount到系统的软驱,然后直接把loader和kernel文件拷贝进去即可。而引导部分的代码,已经设计为FAT12的格式,直接覆盖img的头部,这样就ok了。
5. 关于书中的"jmp SELECTOR_KERNEL_CS:csinit" ; 这个跳转指令强制使用刚刚初始化的结构。
在这之前已经初始化了gdt,并且已经开启了分页机制,而这里的csinit是kernel.asm文件中的标号,为什么能直接jmp?
这个问题我现在没有确定的答案,不过在进行代码跟踪的时候发现,csinit这个标号,编译的时候已经被编译为一个线性地址,而不是一个偏移量,猜想这一步变化是发生在链接的时候,也就是进行 “ld -Ttext …”的时候发生变化的。如果读者有更准确的解释,请留言,谢谢。
6.gcc编译书中的例子,链接的时候报错?
我是在Ubuntu 13.10 64位下进行实验的,所以在gcc 编译文件的时候需要加上 -m32参数,而链接的时候,需要加上 -m elf_i386参数。
7."klib.c:(.text+0xe6): undefined reference to `__stack_chk_fail'"
原因:函数里并没有对堆栈进行保护。答案参考这个:CodeDump : StackSmash,里面的解决办法是在gcc编译的时候临时加上 -fno-stack-protector
这个参数,而实际解决办法,应该从代码入手,像
void func(){
这样的代码,有潜在的栈越界问题。
char array[10];
gets(array);
}
8.hlt指令
HLT 使 CPU 进入这么一个状态:既不取指令,也不读写数据,总线上“静悄悄”的。这条指令用的地方不多,一般用于等外部中断。
9.ud2指令
UD2是一种让CPU产生invalid opcode exception的软件指令. 内核发现CPU出现这个异常, 会立即停止运行。