MIT操作系统课程CS6.828实验(3) —— 启动PC(Lab1)

该实验分为三部分,

第一部分熟悉x86汇编语言,QEMU x86模拟器,和PC上电启动程序。

第二部分学习6.828内核的boot loader部分。

第三部分深入研究6.828内核JOS的初始化部分,该部分代码在kernel目录下


本实验使用git版本控制系统来进行代码管理和开发, 更多的关于git,参考git用户使用手册, 若之前熟悉其他版本控制系统(像SVN)的用法,会发现面向CS的Git概述非常的有用。

本实验的Git仓地址:https://pdos.csail.mit.edu/6.828/2014/jos.git

1. 实验代码下载

下载代码命令
$ git clone https://pdos.csail.mit.edu/6.828/2014/jos.git lab
然后进入lab目录
$ cd lab

Git工具可以跟踪代码的修改。
例如,若完成了代码的修改,以及检查进度,可以使用如下命令提交修改
$ git commit -am 'my solution for lab1 exercise 9'

在提交修改之前,可以使用如下命令查看修改点
$ git diff

以下命令可以查看当前修改相对于本实验最开始提供的代码的差异
$ git diff origin/lab1
origin/lab1是从服务器下载的初始代码的git分支名称

2. PC启动过程

第一个练习的目的是介绍x86汇编语言和PC启动过程,以及熟悉QEMU和QEMU/GDB调试。本实验的该部分不需要编写代码,但是需要理解该部分内容,同时回答下面提出的问题。

2.1. 熟悉x86汇编语言

若还不熟悉x86汇编语言,在本程序需要快速的熟悉起来,可以从 PC Assembly Language Book开始学习,该书提供了新的和老的材料用于学习。

注意:本书提供的例子是用nasm汇编编写的,而我们使用的是GNU汇编器。 NASM使用的是所谓的Intel语法格式的汇编语言,而GNU使用的是AT&T语法格式的汇编语言。但是语义上是等同的。 然而这两种语法格式的汇编转换相对比较简单,参考: Brennan's Guide to Inline Assembly.

练习1
熟悉6.828参考页的关于汇编语言的材料, 不需要立即去阅读,但是当在读写x86汇编代码时,确保可以从这些材料中找到相应的引用。

推荐阅读Brennan's Guide to Inline Assembly的“The Syntax”部分, 该部分对AT&T汇编语法进行了非常好的描述,在JOS的GNU汇编中将会使用到。

2.2. 仿真x86架构

本实验不会在实际的,物理的个人电脑(PC)上进行操作系统开发,我们使用软件来仿真一个完整的PC,对于基于软件仿真的PC上开发的代码也可以启动一个真是的PC。使用仿真器可以简化调试,例如,可以在仿真的x86 PC上设置断点,然而这在正式的x86上很难做到。

在6.828实验中,使用QEMU仿真器,一个现代的和相对快速的仿真器,然而QEMU内置的monitor提供的是有限的调试支持。QEMU可以作为GNU调试器(GDB)的一个远程调试目标,我们在该实验中将用来调试PC启动的早期启动过程。

如上下载完实验代码后,进行代码所在目录,使用make命令来构建最小的6.828 boot loader和kernel。如下


若编译的过程中,出现如下错误:“undefined referenece to '__udivdi3'”, 可能是没有32-bit的gcc multilib,安装该软件后重新编译即可。

编译完成后,即可启动QEMU,通过编译生成的obj/kern/kernel,作为仿真的PC的虚拟硬盘的内容,该硬盘镜像包含boot loader(obj/boot/boot)和kernel(obj/kernel)。

通过如下命令启动QEMU
$ make qemu

执行qemu附加一些设置硬盘的选项,直接把串口输出到终端,如下


“Boot from Hard Disk”之后的打印都是JOS内核框架打印出来的。 K>是提示符是通过一个小的监视器或者交互式控制程序(包含在内核中)打印出来的。 从上图可以,内核的这些行打印不仅打印到VGA窗口,同时也打印到普通的shell窗口(仿真的PC虚拟串口),这是为了测试和实验评分目的。

同时,JOS内核将从键盘和串口输入,因此,既可以从VGA显示窗口输入命令,也通过从串口输入。

目前,JOS内核只有两条命令来监视内核。help和kerninfo


help命令很明显了
以下简短介绍下kerninfo命令打印的意思,尽管简单,但是注意到,该kernel监视器直接运行在仿真的PC的“原始(虚拟)硬件”上。这即意味着可以把obj/kern/kernel.img内容复制到实际的硬盘的第一个分区上,把硬件插入实际的PC中,打开PC,将从实际PC的屏幕上看到和QEMU窗口中一样的输出内容,(但是不推荐这么做,因为复制kernel.img到硬件的开始处会丢弃Master Boot Record和第一个分区,会导致硬盘之前的所有内容丢失)。

2.3. PC物理地址空间

以下研究PC如何启动的更多细节内容, 一个PC的物理地址空间是“硬连线”的,具有如下的通用布局。

第一个PC,是基于16-bit的Intel 8088处理器, 只能寻址1MB的物理内存。因此,早期的PC的物理地址空间从0x00000000开始,到0x000FFFFF结束(而不是0xFFFFFFFF)。其中640KB表示为“Low Memory” ,是早期的PC仅有的可以使用的RAM内存,实际上更早的PC仅仅可以配置为16KB,32KB,或64KB的RAM。

从0x000A0000到0x000FFFFF之间的384KB区域保留作为特殊的用途,像视频显示buffers和保存在非易失的内存的firmware。该保留的区域中最重要的部分是基本输入输出系统(BIOS),其占用从0x000F0000到0x000FFFFF的64KB部分。在早期的PC中,BIOS保存在只读内存(ROM)中。但是,现代的PC保存BIOS在可更新的flash内存中。BIOS负责执行基本系统的初始化,像激活视频卡和检测安装的内存大小。初始化完成后,BIOS从软驱,硬盘, CD-ROM或网络的一些合适的位置加载操作系统。然后把机器的控制权交给操作系统。

但Intel最后打破1MB的限制后(80286和80386处理器, 80286支持16M, 80386支持4G 物理地址空间),然而PC架构保留了原始的1MB物理内存空间的布局。为了确保和已经存在的软件的向后兼容,因此,现代的PC在从0x000A0000到0x0010000的物理内存中存在一个“空洞”。把RAM分为“low”或者“传统内存”和”扩展内存“, 另外,对于32-bit 的PC的非常顶端的一些物理地址空间,在所有的物理RAM中,这部分物理空间通常被BIOS保留作用32-bit的PCI设备。

当前的x86已经支持了多于4G的物理RAM,因此,RAM地址扩展到0xFFFFFFFF以上。在这种情况下,BIOS必须留出第二个”空洞“, 在32-bit RAM的地址空间的顶端,留出该部分用于32-bit的设备被映射。由于设计限制,JOS内核仅适用PC物理内存的第一个256MB部分,因为,我们假设所有的PC”仅仅“是32-bit的物理地址空间。
但是,经过多年的发展后,处理复杂的物理地址空间和其他的硬件组织是OS开发实践中重要的挑战之一。

2.4. ROM BIOS

该部分通过使用QEMU的调试功能来学习IA-32兼容的计算机是如何启动的。
打开两个终端窗口,在一个窗口中输入make qemu(或者make qemu-nox-gdb), 该命令将启动QEMU,但QEMU停在处理器执行第一条指令之前,等待GDB连接。在第二个窗口中,在相同目录下,运行gdb,如下:


在实验目录下,提供了一个.gdbinit文件,用来建立GDB来调试早期启动期间的16-bit的代码,直接来连接到监听的QEMU,(若不能正常工作,需要加入一个add-auto-load-safe-path到home目录下.gdbinit文件中, 让GDB处理我们提供的.gdbinit文件,gdb将会告诉你是否需要这么做。

[f000:fff0] 0xffff0:	ljmp   $0xf000,$0xe05b
该行表示是第一条执行的指令的GDB反汇编。从该输出,可以得到如下结论:
  • IBM PC从物理地址0x000ffff0地址开始执行。它在64KB内存的最上面保留用于ROM BIOS
  • PC从CS=0xf000, IP=0xfff0开始执行
  • 第一条执行的指令是jmp,将跳转到段地址CS=0xf000,IP=0xe05b地址处执行

为什么QEMU会如此执行呢?这与IBM使用在他们的PC中的8088处理的设计相关,由于PC中的BIOS是“硬连线”的,地址范围0x000f0000-0x000fffff, 这样设计可以确保在上电启动或者电脑重启时,BIOS总是可以首先获得机器的控制,之所以这样,是因为在上电时,在机器的RAM中没有其他任何软件可以执行。QEMU仿真自带BIOS,放在处理器仿真的物理地址空间的对应位置。当处理器复位时,(仿真的)处理器进入实模式,并设置CS=0xf000,IP=0xfff0,所以从CS:IP地址开始执行。那么CS:IP=0xf000:0xfff0的段地址如何转为物理地址呢?

为此,需要知道一些关于实模式寻址。在实模式(PC启动最开始处于的模式)下,地址转换按如下方式完成
physical address = 16 * segment + offset
所以当PC设置CS:IP=0xf000:0xfff0时,指向的物理地址为:
16 * 0xf000 + 0xfff0
= 0xf0000 + 0xfff0
= 0xffff0

0xffff0是BIOS结束地址(0x100000)前的16字节。 因此,不要惊讶BIOS的第一条指令是BIOS地址空间前的向后跳转,因为0xffff0 ~0x100000之间只有16字节的空间,太小了,干不了什么事情。

练习2
熟悉GDB的si(Step Instruction)命令来跟踪ROM BIOS的更多的执行指令,根据执行的指令猜测下BIOS干了什么,可以参考 Phil Storrs I/O Ports Description, 以及其他的一些6.828参考资料,不需要知道BIOS的所有细节,只要知道BIOS的大概用途就可以了。

当BIOS运行时,建立一个中断描述符表和初始化各种设备,像VGA显示器,在VGA中将会显示“Starting SeaBIOS”信息。

当完成PCI总线初始化和BIOS知道的其他的设备的初始化后,BIOS搜索一个可启动的设备像软盘,硬盘,或者CD-ROM。

当找到一个可启动的磁盘时,BIOS从磁盘读取Boot Loader, 并把控制权交给Boot Loader,接下来介绍Boot Loader部分内容

3. Boot Loader

PC的软盘和硬盘都是分为512字节的区——称为分区(sectors),一个分区是磁盘的最小传输粒度。读写操作必须以一个或多个分区为大小,且在分区边界上对齐。若磁盘是启动盘,这其第一个分区称为启动分区(boot sector),因此该分区放的是Boot Loader代码。

当BIOS找到一个启动软盘或硬盘时,BIOS加载512字节的boot sector到物理地址为0x7C00~0x7dff范围的内存中。然后使用一个jmp指令转到CS:IP=0x0000:0x7C00的位置处执行。并把控制权交给Boot Loader。

跟BIOS加载地址一样,Boot Loader的地址也是相当的任意,但是对于PC他们是固定的和标准化的。

从CD-ROM启动的能力在PC的进化过程出现的比较晚,结果PC架构重新思考了启动过程。以致现代的BIOS从CD-ROM启动非常的复杂(更加的强大),CD-ROM使用2048字节的分区大小来代替512字节。因此BIOS可以从磁盘加载更加大的Boot镜像到内存中(不止一个sector)。更多信息参考: "El Torito" Bootable CD-ROM Format Specification

在6.828课程中,将使用传统的从硬盘启动的机制,即意味着我们的Boot Loader必须满足512字节的限制。

Boot Loader程序由如下两部分代码组成:
  • 汇编语言源代码: boot/boot.S
  • C源代码:boot/main.c
认真阅读这部分代码,理解它们做了什么。

Boot Loader必须实现两个主要功能:
  1. 第一,Boot Loader切换处理器从实模式到32-bit的保护模式,因为只有在保护模式下,软件才能访问处理器物理地址空间1MB以上的内存,保护模式的描述可以参考PC Assembly Language的1.2.7和1.2.8节。更多的细节参考Intel架构手册。此时只需要理解保护模式下,段地址(CS:IP对)转换为物理地址与实模式的差异即可。保护模式下的地址转换后为32bit,而不是16bit
  2. 第二,Boot Loader从硬盘读取内核,通过x86特殊的I/O指令直接访问IDE磁盘设备读取。若想更多的更好的理解特殊的I/O的意思,参考the 6.828 reference page部分的“IDE hard drive controller”部分。在本课程中不需要学习更多的特定设备的编程。编写设备驱动是OS开发的非常重要的一部分,但从OS的概念和架构来看,并不需要太多关注设备驱动。
当理解了Boot Loader源代码后,查看文件obj/boot/boot.asm,该文件是在编译boot loader后由GNUmakefile创建的boot loader反汇编文件。从该反汇编文件可以很容易的看到所有的boot loader代码在物理内存的位置。这样在用GDB逐步调试boot loader时更加容易跟踪。类似的,obj/kern/kernel.asm 包含JOS内核的反汇编。在kernel调试时非常有用。

在GDB调试中,可以使用 b命令设置断点,例如
$ b *0x7c00
表示在地址0x7c00处设置一个断点。到达一个断点后,可以使用 csi命令继续执行。 c让QEMU继续执行直到下一个断点(或者直到在GDB中按下Ctrl-C), si N可以一次执行N条指令。

使用x/i命令可以检查内存中的指令(除了下一条立即执行的指令,GDB会自动打印)。该命令语法如下
$ x/Ni ADDR
其中N是连续执行的指令的反汇编代码数目, ADDR是开始反汇编的起始内存地址。

练习3
参考lab tools guide,尤其GDB命令部分,就算已经熟悉了GDB的用法,还是需要看下该部分内容,因为包含了一些对OS开发调试非常有用的不常见的GDB命令。

在0x7c00地址设置断点,该地址是boot sector加载的位置。继续执行直到该断点。跟踪boot/boot.S代码,使用源码和反汇编文件obj/boot/boot.asm来跟踪当前代码执行的位置。也可以用GDB中的x/i命令来反汇编boot loader中的指令序列。对比boot loader源代码和obj/boot/boot.asm和GDB的反汇编代码。

跟踪boot/main.c中的bootmain()函数,然后进入readsect()函数。对各个汇编指令和readsect()源代码进行对比。继续跟踪调试readsect()剩下的部分,然后返回到bootmain()中,识别从磁盘中读取剩下的kernel部分的for循环的起始和结束地址。找出当for循环执行完后的下一条执行代码,设置断点,然后继续执行到该断点,最后逐步调试boot loader剩下的部分代码。

调试完后,需要回答如下问题:
1. 处理器从哪个点开始执行32-bit代码?究竟是什么原因导致从16-bit模式切换到32-bit模式?
2. boot loader执行的最后一条执行是什么?加载的内核的第一条指令是什么?
3. 内核的第一条指令在哪里?
4. boot loader如何决定有多少个sectors需要去读取,以便从磁盘读取整个的内核,从哪里获取到该信息?

4. 内核



  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实验目标: 本实验的目标是完成一个可以在QEMU仿真器上运行的x86操作系统。具体地说,我们将编写引导扇区代码和内核代码,并将它们组合成一个可引导的磁盘映像。最后,我们将使用QEMU仿真器启动我们的操作系统实验步骤: 1. 准备工作 准备工作包括安装必要的软件和工具、下载实验代码和文档等。 2. 编写引导扇区代码 引导扇区是操作系统的第一个扇区,它需要被放置在磁盘的第一个扇区。引导扇区必须包含一个512字节的主引导记录(MBR),其中包括一个引导程序和分区表。我们需要编写一个能够在引导扇区中运行的汇编代码,它将加载内核并将控制权转交给内核。 3. 编写内核代码 内核是操作系统的核心部分,它负责管理计算机的硬件资源、提供系统调用接口等。我们需要编写一个简单的内核,该内核将输出“Hello, world!”并进入无限循环。我们可以使用C语言编写内核代码,并使用GCC编译器将其编译成汇编代码。 4. 构建磁盘映像 我们需要将引导扇区和内核代码组合成一个可引导的磁盘映像。为此,我们可以使用dd命令将引导扇区和内核代码写入一个空白磁盘映像中。 5. 启动操作系统 最后,我们需要使用QEMU仿真器启动我们的操作系统。我们可以使用以下命令启动QEMU并加载磁盘映像: ``` qemu-system-i386 -hda os.img ``` 实验结果: 经过以上步骤,我们成功地编写了一个简单的操作系统,并使用QEMU仿真器进行了测试。当我们启动操作系统时,它将输出“Hello, world!”并进入无限循环。 实验总结: 本实验让我了解了操作系统的基本概念和架构,并学会了如何编写一个简单的操作系统。通过实验,我更深入地理解了计算机系统的底层原理,对操作系统的工作原理有了更深入的了解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值