##实验目的
完成一个能够切换到x86的保护模式并显示字符的bootloader,为启动操作系统ucore做准备。
lab1中包含一个bootloader和一个OS。这个bootloader可以切换到X86保护模式,能够读磁盘并加载ELF执行文件格式,并显示字符。而这lab1中的OS只是一个可以处理时钟中断和显示字符的幼儿园级别OS
lab1主要分为6个练习

###练习1:理解通过make生成执行文件的过程。

  1. 操作系统镜像文件 ucore.img 是如何一步一步生成的?
  2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
    操作系统镜像文件 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

dockefile里面指定镜像名字与tag_操作系统


让qemu将他执行的指令记录下来,放到上图位置的q.log处,结合gdb,debug,使可以调试正在执行的BootLoader

dockefile里面指定镜像名字与tag_保护模式_02


GDB能够识别的一些命令,1 加载并kernel,2 与qemu进行连接 6 显示指令指针寄存器的内容

BootLoader第一条指令在0x7c00处

dockefile里面指定镜像名字与tag_保护模式_03


练习3

分析bootloader是如何完成从实模式进入保护模式的。

为何开启A20,以及如何开启A20

如何初始化GDT表

如何使能和进入保护模式

分析BootLoader:

1、关闭中断,将各个段寄存器重置

dockefile里面指定镜像名字与tag_保护模式_04


首先将各个寄存器置0

2、开启A20

什么是A20:当 A20 地址线控制禁止时,则程序就像在 8086 中运行,1MB 以上的地址是不可访问的。而在保护模式下 A20 地址线控制是要打开的,所以需要通过将键盘控制器上的A20线置于高电位,使得全部32条地址线可用。

dockefile里面指定镜像名字与tag_加载_05


3、加载GDT表(全局描述符表)

dockefile里面指定镜像名字与tag_加载_06


4、将CR0的第0位置1

dockefile里面指定镜像名字与tag_内核_07


5、长跳转到32位代码段,重装CS和EIP

dockefile里面指定镜像名字与tag_内核_08


6、重装DS、ES等段寄存器等

dockefile里面指定镜像名字与tag_内核_09


7、转到保护模式完成,进入boot主方法

dockefile里面指定镜像名字与tag_寄存器_10


练习4

分析bootloader加载ELF格式的OS的过程

  1. bootloader如何读取硬盘扇区的?
  2. 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),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

dockefile里面指定镜像名字与tag_内核_11


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函数的实现

函数注释:

dockefile里面指定镜像名字与tag_内核_12


根据注释及上面梳理的函数调用返回过程编写代码:

dockefile里面指定镜像名字与tag_内核_13

dockefile里面指定镜像名字与tag_寄存器_14


实验结果如上。练习6:

1、请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。

dockefile里面指定镜像名字与tag_内核_15


一共对应三步:

第一步:声明__vertors[],其中存放着中断服务程序的入口地址。

第二步,填充中断描述符表IDT。

第三部,加载中断描述符表。最关键的是第二步:填充中断描述符表IDT。主要使用SETGATE

dockefile里面指定镜像名字与tag_操作系统_16


各参数的意义:

gate:为相应的idt[]数组内容,处理函数的入口地址

istrap:系统段设置为1,中断门设置为0

sel:段选择子

off:为__vectors[]数组内容

dpl:设置特权级。这里中断都设置为内核级,即第0级2、请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

dockefile里面指定镜像名字与tag_寄存器_17


dockefile里面指定镜像名字与tag_操作系统_18


这一部分相对还是比较简单的,只需实现每一百次时钟信号便调用print_ticks()函数打印

实验结果截图如下,当用键盘输入a时,屏幕成功予以回显

dockefile里面指定镜像名字与tag_保护模式_19


lab1 至此完成。