##实验目的
完成一个能够切换到x86的保护模式并显示字符的bootloader,为启动操作系统ucore做准备。
lab1中包含一个bootloader和一个OS。这个bootloader可以切换到X86保护模式,能够读磁盘并加载ELF执行文件格式,并显示字符。而这lab1中的OS只是一个可以处理时钟中断和显示字符的幼儿园级别OS
lab1主要分为6个练习
###练习1:理解通过make生成执行文件的过程。
- 操作系统镜像文件 ucore.img 是如何一步一步生成的?
- 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
操作系统镜像文件 ucore.img 是如何一步一步生成的?
调用GCC把一些C的源代码编译成.O文件(目标文件)
ld把编译成的目标文件转换成一个执行程序,上图中bootblock.out即为BootLoader的一个执行程序
dd将BootLoader放到一个虚拟硬盘中去,上图中ucore.img count就是生成的虚拟硬盘
硬件模拟器就会基于这个虚拟硬盘中的数据执行相应代码
一共生成两个软件,一个是BootLoader,一个是kernel,kernel是ucore的组成部分
一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
磁盘主引导扇区只有512字节
- 磁盘最后两个字节为0x55AA
- 由不超过466字节的启动代码和不超过64字节的硬盘分区表加上两个字节的结束符组成
###练习2使用qemu执行并调试lab1中的软件
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
在初始化位置0x7c00设置实地址断点,测试断点正常。
从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
在makefile中执行lab1-mon
让qemu将他执行的指令记录下来,放到上图位置的q.log处,结合gdb,debug,使可以调试正在执行的BootLoader
GDB能够识别的一些命令,1 加载并kernel,2 与qemu进行连接 6 显示指令指针寄存器的内容
BootLoader第一条指令在0x7c00处
练习3
分析bootloader是如何完成从实模式进入保护模式的。
为何开启A20,以及如何开启A20
如何初始化GDT表
如何使能和进入保护模式
分析BootLoader:
1、关闭中断,将各个段寄存器重置
首先将各个寄存器置0
2、开启A20
什么是A20:当 A20 地址线控制禁止时,则程序就像在 8086 中运行,1MB 以上的地址是不可访问的。而在保护模式下 A20 地址线控制是要打开的,所以需要通过将键盘控制器上的A20线置于高电位,使得全部32条地址线可用。
3、加载GDT表(全局描述符表)
4、将CR0的第0位置1
5、长跳转到32位代码段,重装CS和EIP
6、重装DS、ES等段寄存器等
7、转到保护模式完成,进入boot主方法
练习4
分析bootloader加载ELF格式的OS的过程
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载 ELF格式的 OS?
bootloader读取硬盘扇区
根据上述bootmain函数分析,首先是由readseg函数读取硬盘扇区,而readseg函数则循环调用了真正读取硬盘扇区的函数readsect来每次读出一个扇区
bootloader加载 ELF格式的 OS
读取完磁盘之后,开始加载ELF格式的文件。
练习1——练习4都不需要写代码,只是对流程进行分析
练习5与练习6就要开始写代码了
练习5实现函数调用堆栈跟踪函数 (需要编程)
1、函数调用的原理:
(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
eip寄存器存储着我们cpu要读取指令的地址
函数调用大概包括以下几个步骤:
- 1、参数入栈:将参数依次压入系统栈中。
- 2、返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
- 3、代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
- 4、栈帧调整
- 4.1保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)。
- 4.2将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。
- 4.3给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。
而函数返回大概包括以下几个步骤:
- 1、保存返回值,通常将函数的返回值保存在寄存器EAX中。
- 2、弹出当前帧,恢复上一个栈帧。
- 2.1在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间
- 2.2将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧。
- 2.3将函数返回地址弹给EIP寄存器。
- 3、跳转:按照函数返回地址跳回母函数中继续执行。
2、print_stackframe函数的实现
函数注释:
根据注释及上面梳理的函数调用返回过程编写代码:
实验结果如上。练习6:
1、请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。
一共对应三步:
第一步:声明__vertors[],其中存放着中断服务程序的入口地址。
第二步,填充中断描述符表IDT。
第三部,加载中断描述符表。最关键的是第二步:填充中断描述符表IDT。主要使用SETGATE
各参数的意义:
gate:为相应的idt[]数组内容,处理函数的入口地址
istrap:系统段设置为1,中断门设置为0
sel:段选择子
off:为__vectors[]数组内容
dpl:设置特权级。这里中断都设置为内核级,即第0级2、请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
这一部分相对还是比较简单的,只需实现每一百次时钟信号便调用print_ticks()函数打印
实验结果截图如下,当用键盘输入a时,屏幕成功予以回显
lab1 至此完成。