MIT 6.828-jos-xv6-lab1:Booting a PC
lab1实验的主要内容还是熟悉汇编,C语言指针和GDB调试等,以及理解jos的启动过程和启动之中做了哪些处理,为什么要做这些处理
典型的一台PC机的物理内存分配
早起的16位处理器最多只能寻址1MB的内存空间
而真正能被用作RAM的是从0到640KB的区域
而从640KB到1MB的地方是留作特殊用途的,最重要的就是BIOS
后来升级到32位之后,低地址的1MB空间还是留作原来的用途,应为要向前兼容
但是现在的做法是在32位地址空间的最高地址处留作BIOS
The boot loader
BIOS启动之后,首先做一些系统的检查和初始化工作,然后就会去磁盘里寻找boot sector,一旦找到,就将这些区域加载到内存特定位置处,加载完成之后,就跳转到该位置,也就是从此将控制权转交给了bootloader
磁盘的第一个sector存放的是boot.s和boot.c,先启动boot.s,然后启动c文件里的bootmain函数,而内核镜像从磁盘的第二个sector开始
Boot loader主要是做两件事:
- 从16位实模式转换到32位保护模式,这在boot/boot.s文件内实现
- 从硬盘设备读取内核,并将控制权转交给内核
下面是boot.s中的代码
可以看到,从0x7c00地址处开始执行,首先是初始化堆栈段寄存器,而在BIOS阶段是没有内核栈的,所以GDB都没办法使用next和step等命令
在切换到32位保护模式之后重新建立了内核数据段寄存器
boot.S文件就主要做了两件事
- 切换到32位模式
- 加载C文件中的bootmain函数
boot.C文件接管控制之后
从磁盘文件读取内核到物理内存中,然后执行磁盘的入口函数,也就是把控制权交给了内核
注意下面两段代码,宏定义ELFHDR实际上是一个指针,将一个实际的物理地址强行转换成了一个elf型的指针,也就是说,要把一个elf型的结构体放到该地址处
MIT给出的几个问题
- 从什么时候开始执行32位模式的
去boot.asm文件里找,这下面的位置切换到32位模式
从上面这句话之后,前面的地址标识就变了,不再是CS:IP的模式
- 最后一条bootloader指令
在bootmain.c函数中的最后一行代码
- kernel的第一条指令在哪
从上面可以看出,kernel的入口位置是ELFHDR->e_entry函数
而ELFHDR是位于0X10000处的一个结构体,e_entry是个指针,是结构体的一个变量,所以具体是多少不好看,可以通过反汇编来看具体是在哪
起始位置在0X0010000C位置
在该位置打个断点,看一下具体是啥
而在kern/entry.s中也是这样的
我一开始在boot.asm文件中看到的汇编代码是这样的
我一开始傻逼了,总以为跳转到的地址是0x10018,没看到前面还有个*号呢,实际上0x10018位置存放的是一个函数指针。。。
- 这个问题在接下来的章节有回答
生成ELF文件的过程:
- 首先编译所有C文件,生成一系列的.o文件
- 链接器把所有的.o文件组合成一个单独的二进制镜像文件
首先是一个定长的ELF header文件,后面是一些变长的program header,这些header列出了每个需要加载的程序区域,每个都包含了连续的代码和数据结构,而且知名的需要加载到的具体的内存地址
bootloader直接按照给定的内存地址进行加载,不进行修改
下面是objdump到的kernel的信息,这些是头文件里面的内容
尤其需要注意的是VMA和LMA
VMA是虚拟地址,是程序希望被加载到的地方,而LMA是实际加载的地址
在boot中,VMA和LMA就是相同的
那为什么kernel中的link address和load address是不一样的呢
主要是因为现代操作系统通常跑在的虚内存的高地址,而把虚内存的低地址留给其他程序
这么做的原因说是下次实验能够揭晓。。。
比如说,上面的kernel中,某程序段的VMA在0XF0100000位置,但是有些机器实际上没有这么多的内存
也就是说,使用了CPU提供的内存映射的功能
这里还有一个小实验
是要观察一下下面的现象,目的是看在人工映射前和映射后有啥区别
如下图所示,刚刚离开bootloader,进入kernel执行第一条语句,观察ox00100000位置就已经全部都是正确位置了
下面是执行前给定的那一句之前两个位置的内容
可以看到是不一样的,0xf0100000的内容是随机的,无内容
而0x0100000位置的内容是内核代码
在执行了给定的一句代码之后,变成了如下结果
两个位置的内容是一样的,也就是人工的内存映射是成功的了
接下来是补全代码问题
这个代码在lib/printfmt.c里面改
因为里面已经有10进制 16进制等等的写法了,
代码阅读问题
问下面这段代码的意思是啥
实际上挺简单的,crt_pos表示的是屏幕上光标的位置,而CRT_SIZE是指整个屏幕的大小
如果发现当前已经把整个屏幕写满了,就把从第2行到最后一行的内容全部移动到从第一行到倒数第二行,然后最后一行清空,重新写
下面是系统堆栈的问题
在此处初始化
而所在位置就是bootstacktop,这个值已经在后面给出了
接下来是一系列的介绍
万言不如一图,上CASPP!!!!!!
上图看的最清楚不过了,每次调用一个函数的时候,假如说函数有n个参数,那么从第n个参数开始,一直到第一个参数,依次压栈,然后将返回地址压入到栈中
随后将调用者的帧指针压入栈顶,同时被调用者的帧指针指向当前栈顶,剩下的才是其他的特定工作
在i386_init函数里,有一段是测试系统栈的,test_backtrace函数,递归调用这个函数5次,观察出现什么现象
先上代码
调用的时候x为5,递归调用5次
再看对应的汇编代码
和所想是一样的,首先将调用者的帧指针压栈,然后更新帧指针,这里多了一个将ebx也压栈
那么算上函数的参数,返回地址,一共是压了四次栈,32位系统内,栈一共是增长了16个字节
实际运行也验证了这个结论
上下两次的栈指针之差正好是16个字节
补充backtrace程序
补充一个程序,改程序可以显示出自当前程序开始至最初系统启动栈初始化之后第一个函数的所有函数的帧指针和返回地址,以及5个参数
还是和之前CSAPP里的那张图是一样的,当前帧指针指向的地方存储着调用者的帧指针,这样就相当于形成了一条链
所以顺着这条链往回走,一定可以找到最初的调用者
这里有一个问题就是怎么知道最初的调用者是谁,这个需要看汇编代码
也就是说,最开始的时候,帧指针是空指针,赋值为零,这样就好写了
同时,jos提供了一个接口debuginfo_eip(addr, eipdebuginfo*),可以根据给定的指令的地址,找到该条指令属于哪个文件中的哪个函数的第多少行
所以可以写成写成下面这样子
Make grade之后,50分得了40分,好像程序名称的输出格式有问题,但是程序肯定是写对了