用课余时间重新拾起JOS,作为一个码农通过了解不同技术层面的机制对自己的技术水平提高非常大,而JOS作为一个MIT的开放课程,可让我们从一无所有构造一个自己的操作系统,这无疑是学习OS的一个非常好的方法。
然而不可否认,操作系统本身是非常复杂的,即使是一个简化过的、只有基本功能的OS,里面的代码也够我研究好久,所以我在学习之余写这么几篇博客,当作学习笔记。
我使用的是6.828版本。地址http://pdos.csail.mit.edu/6.828/2011/schedule.html
1、环境搭建
(1)建立一只ubuntu11.10虚拟机,刚做好的系统是裸系统并,没有装任何东西。
(2) 装git,vim,cscope,qemu,eclipse(本来想用vim+cscope看代码的,结果因为本人太低端,vim还是玩不转,所以又装了eclipse用来看代码)
2、start
首先让我们从lab1开始,lab1的目的也就是让我们熟悉一下os的启动过程,所以这篇笔记也就不拘泥于里面的excerise了而直接尝试去理解里面的代码。
先按lab1的pdf里的说明将jos的代码git下来:
git clone http://pdos.csail.mit.edu/6.828/2011/jos.git lab
然后拖到eclipse里,我们就可以阅读代码了。
通过讲义(或者是经验)我们知道,当计算机加电,首先会把bios加载进内存执行,然后bios从硬盘加载mbr,之后由mbr来加载操作系统或者grab之类的东西。
那么这个过程我们就会面临很多问题
(1)bios加载进了内存的什么地方?
直接上图不解释:
+------------------+ <- 0xFFFFFFFF (4GB) | 32-bit | | memory mapped | | devices | | | /\/\/\/\/\/\/\/\/\/\ /\/\/\/\/\/\/\/\/\/\ | | | Unused | | | +------------------+ <- depends on amount of RAM | | | | | Extended Memory | | | | | +------------------+ <- 0x00100000 (1MB) | BIOS ROM | +------------------+ <- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ <- 0x000C0000 (768KB) | VGA Display | +------------------+ <- 0x000A0000 (640KB) | | | Low Memory | | | +------------------+ <- 0x00000000
通过图我们可以看到加载到了960K--1MB的地方,为啥会加载到这个位置,估计是一些约定俗成的规定吧。
(2)mbr被加载到了哪里?
在项目文件夹下make一下,编译好内核然后打开obj/boot/boot.asm,这是编译好的boot的反汇编文件,在刚开始的那几行我们就能很明显的看到有这么一个标志:
00007c00 <start>:
这说明start符号在内存中的位置“应该是”0x7c00,换句话说,这段程序“认为”它所处的位置是内存中的0x7c00,因此为了使mbr程序能正确的执行,bios会将其加载到0x7c00的位置,然后跳转到0x7c00,将控制权给它。这时候我们的内存布局是这个样子的:
+------------------+ <- 0xFFFFFFFF (4GB) | 32-bit | | memory mapped | | devices | | | /\/\/\/\/\/\/\/\/\/\ /\/\/\/\/\/\/\/\/\/\ | | | Unused | | | +------------------+ <- depends on amount of RAM | | | | | Extended Memory | | | | | +------------------+ <- 0x00100000 (1MB) | BIOS ROM | +------------------+ <- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ <- 0x000C0000 (768KB) | VGA Display | +------------------+ <- 0x000A0000 (640KB) | | | Low Memory | | |
| | <-0x00007c00 (boot loader here!)
| | +------------------+ <- 0x00000000
(3)boot loader被加载进来做了些什么?
这就是一个较为复杂的问题,查看源码lab/boot.S,这是一个汇编文件,读起来比较痛苦,当然还是比那个asm的反汇编要强很多。
首先关中断,估计是在执行过程中不希望被打扰,然后开A20线(貌似为了和早期PC相兼容,算了,不要在意这些细节,只关注那些操作系统本身的东西就好),然后加在段表并开保护模式。
为啥要开保护模式?原因有2,首先是只有开了保护模式,才能访问64K以上的地址空间,其次是因为在实模式下程序可以访问整个地址空间的任意区域,太不安全了,因此在x86架构中引入了保护模式。
如何开启保护模式?设置cr0寄存器的某一位即可,代码:
movl %cr0,%eax
orl $cr0_PE_ON,%eax
movl %eax,%cr0
开启保护模式之后,基址:偏移这种寻址方式就变成了段选择子:偏移这种方式,而所谓的段选择子就是段表中的索引,因此为了正确的进行段式地址变换,还需要加载段表。这就是为什么在装在cr0之前需要先使用指令lgdt gdtdesc加载段表的原因。
再看段表的内容,也就是符号gdtdesc的位置,同样在boot.S这个文件下方。
可以发现gdt里面有3个段,第一个段为空段(查相关资料才知道,这是x86中的规定,第一个段均为空段),第二个和第三个段的定义使用了SEG宏,跟踪代码到mmu.h,发现宏的第一个参数是type,第二个是base,第三个是limit,所以我们可知定义的第二个和第三个段均是基质为0,长度是4G的段,也就是整个32位地址空间。
可以看出,jos并没有使用x86的段式地址变换来进行内存管理(起码在lab1里没有用),加载段表只是为了能正确的访问32位地址空间而已。
之后boot.S 设置一些寄存器的值,然后就call bootmain,跳转到boot/main.c这个文件里执行了。
值得注意的是,在任何函数调用前都要初始化栈,boot.S里很巧妙的将start作为栈的基址,因为栈空间是向下增长的,所以内存布局:
+------------------+ <- 0xFFFFFFFF (4GB) | 32-bit | | memory mapped | | devices | | | /\/\/\/\/\/\/\/\/\/\ /\/\/\/\/\/\/\/\/\/\ | | | Unused | | | +------------------+ <- depends on amount of RAM | | | | | Extended Memory | | | | | +------------------+ <- 0x00100000 (1MB) | BIOS ROM | +------------------+ <- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ <- 0x000C0000 (768KB) | VGA Display | +------------------+ <- 0x000A0000 (640KB) | | | Low Memory | | |
|------------------| <-0x00007c00 (boot loader here!)
| bootmain stack | +------------------+ <- 0x00000000
(4) bootmain做了什么:
bootmain终于是c文件了,终于不用再看蛋疼的汇编文件了。
bootmain负责的事情是讲硬盘上的kernel加载到内存里并执行,那么第一个问题是,这个kernel存在硬盘的什么地方。
因为我们现在没有任何文件系统,所以jos就“很友好”的将编译好的kernel就放在mbr的后面,也就是第二个扇区(也可以说是第1个扇区,在这之前还有第0个扇区)的位置。
然后main.c里定义了两个函数,readseg和readsect,从逻辑上将,第一个函数的功能是“将相对于kernel基址便宜offset个自己处之后的count各字节的东西读到物理地址pa处”,而第二个的功能是“将相对于第二个扇区便宜offset字节的扇区里的内容读到dst的位置”。
第一个函数调用第二个函数完成自己的功能,第二个函数牵扯到硬盘数据的读写,当然我没有过多的花精力在这些更底层的内容上,不过看代码貌似是将地址的不同位写出到不同的端口(应该就是地址线吧),然后等待读取。
大致明白了这两个函数,就可以去看bootmain的逻辑了。
首先将8*512B的字节读到ELFHDR处,而ELFHDR是指向Elf结构体的指针,Elf结构代表一个elf头,接着通过elf头里的信息读出这个elf的其它部分,并加载到相应地址上。
ELFHDR的位置是0x10000,所以elf头会被加载到内存的这个位置(确切的说是线性地址(未经过段式变换的地址)的这个位置,但因为段表中的段基址是0,因此实际加载的位置也就是内存的位置,大概在bootloader上面一点点。
+------------------+ <- 0xFFFFFFFF (4GB) | 32-bit | | memory mapped | | devices | | | /\/\/\/\/\/\/\/\/\/\ /\/\/\/\/\/\/\/\/\/\ | | | Unused | | | +------------------+ <- depends on amount of RAM | | | | | Extended Memory | | | | | +------------------+ <- 0x00100000 (1MB) | BIOS ROM | +------------------+ <- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ <- 0x000C0000 (768KB) | VGA Display | +------------------+ <- 0x000A0000 (640KB) | | | Low Memory | |------------------| <-0x00010000 (elf herader here!)
|------------------| <-0x00007c00 (boot loader here!)
| bootmain stack | +------------------+ <- 0x00000000
通过elf header,并读取里面的信息,可以逐段的把elf里面的段加载进来,并加载到相应位置,至于加载到哪里,要由ELF头里面的信息所决定。因此,我们应该先研究一下elf头里读到的信息。
使用objdump -h obj/kern/kernnel可以看一下kernel的obj信息:
bootmain所做的工作就是先读到file off得到在文件中的偏移,然后再读取Size个字节,之后放到LMA所指定的地址处。
之后bootmain找出这个elf文件的entry(也在elf头里),然后跳转到这个头执行。
这样bootloader的工作就算完成了,接下来就是内核的工作了。