当前位置: 代码迷 >> 综合 >> Ucore Lab1 系统软件启动过程
  详细解决方案

Ucore Lab1 系统软件启动过程

热度:28   发布时间:2023-12-29 18:33:17.0

1.实验目的

操作系统是一个软件,也需要通过某种机制加载并运行它。在这里我们将通过另外一个更加简单的软件-bootloader来完成这些工作。为此,我们需要完成一个能够切换到x86的保护模式并显示字符的bootloader,为启动操作系统ucore做准备。lab1提供了一个非常小的bootloader和ucore OS,整个bootloader执行代码小于512个字节,这样才能放到硬盘的主引导扇区中。通过分析和实现这个bootloader和ucore OS,读者可以了解到:

(1)计算机原理

  • CPU的编址与寻址: 基于分段机制的内存管理
  • CPU的中断机制
  • 外设:串口/并口/CGA,时钟,硬盘

(2)Bootloader软件

编译运行bootloader的过程

  • 调试bootloader的方法
  • PC启动bootloader的过程
  • ELF执行文件的格式和加载
  • 外设访问:读硬盘,在CGA上显示字符串

(3)ucore OS软件

  • 编译运行ucore OS的过程
  • ucore OS的启动过程
  • 调试ucore OS的方法
  • 函数调用关系:在汇编级了解函数调用栈的结构和处理过程
  • 中断管理:与软件相关的中断处理
  • 外设管理:时钟

2.实验内容

lab1中包含一个bootloader和一个OS。这个bootloader可以切换到X86保护模式,能够读磁盘并加载ELF执行文件格式,并显示字符。而这lab1中的OS只是一个可以处理时钟中断和显示字符的幼儿园级别OS。

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

在此练习中,大家需要通过静态分析代码来了解:

1. 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

首先执行指令make "V=" 可得到如下结果:

 

 从上图看出,Makedile的过程主要是三部分:

(1)

  • 调用gcc,将C的源代码编译成.o目标文件
  • 调用ld,将一系列目标文件链接成可执行程序
  • 调用dd,将bootblock和kernel内容放入到虚拟硬盘ucore.img中
(2) 此处的几条指令即对应着(1) 中所述的 3 dd 指令,即先创建一个大小为 10000 字节的内存块儿,然后再将bootblock kernel 的内容拷贝过去,但这 3 条指令执行时需要有 bootblock kernel ,故需要先生成bootblock kernel
(3) 生成 kernel ,如下图。可以看到要生成 kernel ,需要用 GCC 编译器将 kern 目录下所有的 .c .s 文件全 部编译生成的 .o 文件:

(4) 生成bootblock,如下图。可以看到要生成bootblock,首先需要生成bootasm.obootmain.o

sign ,用宏定义批量实现生成 bootasm.o bootmain.o

 2.  一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

主引导扇区位于整个硬盘的 0 磁头 0 柱面 1 扇区,包括硬盘主引导记录 MBR MasterBootRecord )和分
区表 DPT DiskPartitionTable )。规范的主引导扇区特征如下:
[1] 总大小为 512 字节,由主引导程序、分区表、结束标志三部分构成;
[2] 引导程序,从 0x0 位置起共 446 字节(隐含 windows 磁盘签名);
[3] 分区表,占用 64 字节,是 MBR 中的重要结构;
[4] 结束标志,扇区的最后两个字节 “55AA” MBR 的结束标志。

查看lab1/tools/sign.c源代码,由3132行代码看出、符合规范的硬盘主引导扇区的大小为512个字节,且最后两个字节为0x550xAA

练习2 理解通过make生成执行文件的过程

[1]CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。

首先修改 gdbinit 文件如上所示,由于 BIOS 启动过程是从实模式( 16 位模式)开始的,故需要将此时的架构修改为 i8086 方能正常调试;

[2] 在初始化位置0x7c00设置实地址断点,测试断点正常

        用b*0x7c00 设置实地址断点,运行 continue 后,在断点处停止,同时执行 x/10i $pc 可以看到 后面的10 条汇编指令。
        

 [3]从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.Sbootblock.asm进行比较

 

 

[4]自己找一个bootblock或内核中的代码位置,设置断点并进行测试

使用命令 b *0x7c14 在0x00007c14处设置断点,输入命令 c 使其运行至断点处停止:

 在0x7c14处使用si和x/i $pc 进行单步调试并反汇编:

 

 bootblock.S 中的代码为:

 bootblock.asm 中的代码为:

 对比发现,上述反汇编指令与两个汇编文件相同

练习3:分析bootloader进入保护模式的过程

1.为何开启A20,以及如何开启A20?

【1】A20的存在是为了保持向下兼容性,一开始A20地址线控制是被屏蔽的,直到系统软件通过一 定的IO操作打开它,开启之后才能访问高端内存,即保护模式下开关必须开启。换句话说,未开启 A20时,此时A20为0,软件可访问的物理内存空间不能超过1MB,并且无法发挥Intel 80386以上级 别的32位CPU的4GB内存管理能力,而开启A20,将其置为1,才可访问4G内存。

【2】A20开启方式如下:

等待8042 Input buffer为空 发送Write 8042 Output Port命令到8042 Input buffer 等待8042 Input buffer为空 将8042 Output Port得到的字节的第2位置为1,然后写入8042 Input buffer

【3】由bootasm.S中的Enable A20部分代码可查看具体关于开启A20的实现

 2. 如何初始化GDT表

【1】初始化GDT表直接通过加载全局描述符的lgdt指令来直接完成,如下面第一个图所示; gdtdesc对应一个内存地址,由于GDTR寄存器6个字节,存放GDT表的内存起始地址和表长度,所 以此条指令是将gdtdesc指向的内存地址开始的6个字节存到GDTR中。

 3. 如何使能并进入保护模式

【1】将CR0中的PE位置1,即可使能并从实模式切换到保护模式;

 【2】执行一系列指令,重置段寄存器,分配堆栈空间,完成后调用bootmain将kernel加载进内存 之中

 练习4:分析bootloader加载EIF格式的OS过程

1. bootloader如何读取硬盘扇区的

【1】EIF文件格式是Linux系统下的一种常用目标文件格式,本实验中的EIF类型为用于执行的可执 行文件(executable file),用于提供程序的进程映像,加载到内存执行,读取硬盘扇区关联的函数是 bootmain.c中的readsect函数,整个加载过程即为对应的bootmain函数;

【2】指导书中已经给出了磁盘IO地址和对应功能以及一个扇区的流程,如下图所示;

 【3】查看bootmain.c中的readsect函数,开启过程如上图所示,对0x1F2的操作指定了每次读取1 个扇区,对0x1F3-0x1F6的操作共同指定了读取的扇区号,对0x1F7的操作中0x20的指令用来读取 扇区。

 2. bootloader是如何加载EIF格式的OS

【1】EIF header在文件开始处描述了整个文件的组织,EIF的文件头包含整个执行文件的控制结 构,定义在elf.h中,由上面的分析可得:加载整个kernel的过程本质上就是循环读取扇区的过程, 涉及radseg函数和bootmain函数;

 【2】下图为readseg函数,secno从1开始,0扇区对应的是主引导区,从1扇区开始是kernel部分;

 【3】bootmain主函数如下图所示,首先读取EIF的头部,然后由EIF文件头格式要求,要先对比 e_magic是否等于EIF_MAGIC来确认EIF的合法性,确认后按照头格式中加载位置、入口信息等值 将EIF加载进内存并找到内核入口,完成后即加载过程结束,进入内核执行。

 

 练习5:实现函数调用堆栈跟踪函数

【1】通过下图所示的函数print_stackframe来跟踪函数调用堆栈中记录的返回地址,即从0-当前深度将 调用的所有函数全部打印出来,包括ebp、eip和参数列表args;

 

 【2】执行make qemu,得到关于堆栈调用部分的代码,每一层调用均会输出ebp、eip和args的值,并 且指明调用函数的位置和关系,结果如下;

 【3】最后一行实际上对应的是最开始的调用call bootmain,由于初始化后堆栈为空,栈顶在0x7c00位 置,所以调用后压入栈,栈顶指针变为0x7BF8,即ebp的值,如下图,eip指向0x7d64,即调用 kern_init后的指令地址,由于kern_init()无参数传入,故此处输出bootloader的二进制代码。

 练习6:完善中断初始化和处理

1. 中断描述符表(保护模式下的中断向量表)中一个表项占多少字节,其中哪几位代表中断处理代码 的入口

【1】由指导书可知,中断描述符表IDT是一个8字节的描述符数组,所以一个表项占8个字节, CPU把中断异常号乘以8作为IDT的索引;

【2】由指导书给出的三种描述符知,除Task-gate未使用外,另外两个均可写成统一形式:

  • 2-3字节作为段选择子
  • 6-7字节和0-1字节拼接成4字节的偏移量
  • 4-5字节决定描述符类型
  • 除去4-5字节外剩余的6个字节联合给出中断处理代码的入口

2. 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init,在idt_init函数中,依次对所 有中断入口进行初始化,使用mmu.h中的SETGATE宏,填充idt数组内容,每个中断入口由 tools/vectors.c生成,使用trap.c中声明的vectors数组即可

 【1】按照要求编写代码如下图,循环对idt内所有的中断入口进行初始化,完成后即可通过LIDT指 令来加载IDT;

 3. 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分的填写trap函数中处理时钟 中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字 “100 ticks”

【1】编写代码如下图,当面对中断时对中断类型进行判断,实验中时钟中断和键盘中断易出现, 串口中断由于无相关设备连接所以未触发;

 

 【2】利用make qemu得到结果如下图,当没有键盘输入时,由于时钟中断,每隔100 ticks会触发 中断并打印出100 ticks,当有键盘输入时会立刻触发键盘中断。

 扩展练习 Challenge 1:

扩展proj4,增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内 核初始完毕后,从内核态返回用户态的函数,而用户态的函数又通过系统调用得到内核态的服务

【1】内核初始化完毕后执行lab1_switch_test函数,发生两次切换,首先输出当前状态,内核态,然后 切换到用户态,输出状态后再切换回内核态并输出当前状态,内核态,编码及结果如下;

 【2】上图中lab1_switch_to_kernel和lab1_switch_to_user函数采用内嵌汇编的写法,加入了volatile限 定符确保汇编指令不被优化修改,‘:’用来分隔不同部分,本处分隔汇编语句和输入部分,%0用作占位 符,输入部分用‘i’代表立即数,将中断号填入INT触发中断;

【3】下图为中断处理切换状态的代码,起始判断是否处在目的状态,若不是,则修改存储状态的相应 寄存器的值,据trapframe,需改CS/DS/ES/ESP等,并且根据trapframe中padding可验证前方的55AA 的小尾存储问题;

 【4】EFLAGS寄存器的第12、13位为IOPL,指示当前运行任务的IO特权级,仅当运行任务的CPL不大 于IOPL时,才可访问IO地址,代码中| =0x3000和& =~0x3000分别置11(最小)和00(最大)。实验的 评测结果如下:

 扩展练习 Challenge 2:

用键盘实现用户模式内核模式切换,具体目标:键盘输入3时切换到用户模式,输入0时切换到内核模式 【1】代码实现如下图,考虑键盘中断的分支,先输出当前键入字符,若输入为0\3且需要发生模式转 换,则按照T_SWITCH_TOU/TOK的方式来切换;

 【2】实验结果如下图,可看出不需要状态切换时只是单纯的键盘中断,需要状态切换时调用 print_trapframe打印寄存器状态,可通过EFLAGS寄存器的值看状态切换结果,并且多次切换时仍可保 证正常工作。

 

 实验感想

【1】本实验与以往的实验不同,实验总体难度大,需要大量的时间来详细阅读ucore指导书,关键对实 验代码的含义以及实验结果的理解,此外,本实验要求对Linux系统有一定的了解,并且会应用指令运 行调试代码;

【2】为了完成实验的相关要求,除了对实验本身有一定的理解外,还要有自己在网上能够搜索相关知 识并应用到实验上的整合能力;

【3】通过本次实验,收获了OS的很多相关知识,初步了解了make生成执行文件的过程以及如何查看 make具体执行的命令;知道了硬盘主引导扇区的作用和具体特征;了解了系统软件启动过程以及相关 代码的编写,掌握了利用make qemu设断点调试查看的方法;明白了与进入保护模式相关的A20、GDT 表等的设置方法;通过函数调用堆栈跟踪函数观察了函数调用时出栈、入栈的情况并且通过中断代码的 简单编写,实现了中断的初始化和处理,另外,通过对中断初始化和处理过程的直接观察,对中断的内 在执行原理有了更深的了解。

【4】本实验中的两个challenge均是针对OS中重要的状态转换细节而设计的。challenge1增加syscall, 使得可以从用户态转换到内核态,采用中断来处理状态转换的方法,在switch语句对应的case中填写相 应转换状态所需的代码来完成;challenge2更进一步地实现“人机交互”,通过键盘键入来完成指定的状 态转换,编写的代码需要对应到键盘中断对应的case之中,再根据输入的字符来判断具体的状态转换方 向,要注意硬件中断执行的时候本身就是内核态的事实!