Loading

MIT 6.828 Lab实验记录 —— lab1 Booting PC

实验参考信息

  1. MIT 6.828 lab1 讲义地址
  2. MIT 6.828 课程 Schedule
  3. MIT 6.828 lab 环境搭建参考
  4. MIT 6.828 lab 工具guide
  5. Brennan's Guide to Inline Assembly

实验环境搭建

笔者实验环境:ubuntu 20.02

本实验的实验环境主要包括两部分:

  1. QEMU:x86模拟器
  2. 一整套编译环境

由于实验环境搭建网上已经有很多详尽的资料,这里引用一位大佬的博客作为参考。

实验环境搭建参考链接

实验内容

该实验主要分为3个部分:

  1. 由于我们的实验是基于一个x86模拟器QEMU做的,因此首先要先熟悉一下这个工具,并且借此研究一下PC的开机程序
  2. 在这部分,我们会探究6.828内核的加载过程,探究开机后,是如何将内核加载到内存并运行的。
  3. 最后这部分我们会探究一下6.828内核的基本结构

6.828的实验代码存储在https://pdos.csail.mit.edu/6.828/2018/jos.git代码仓库中,可以通过如下代码拉取到本地:

git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab

6.828一共有6个实验,每个实验对应一个分支,因此需要切换到对应分支,即:

cd lab # lab code 被克隆到了lab目录下
git checkout lab1 # 切换到lab1分支

由于操作系统的主要编程语言是C和汇编,因此需要有一定的汇编基础,为了保证可以顺畅进行后面的实验,可以先阅读一下Brennan's Guide to Inline Assembly

做好这些准备工作后,让我们开始实验内容。

1. PC Bootstrap(初探QEMU)

这里我们会尝试利用QEMU模拟PC的启动过程。首先,我们尝试先将QEMU跑起来,依次执行如下命令:

cd lab
make

运行结果如下:

+ as kern/entry.S
+ cc kern/entrypgdir.c
+ cc kern/init.c
+ cc kern/console.c
+ cc kern/monitor.c
+ cc kern/printf.c
+ cc kern/kdebug.c
+ cc lib/printfmt.c
+ cc lib/readline.c
+ cc lib/string.c
+ ld obj/kern/kernel
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 380 bytes (max 510)
+ mk obj/kern/kernel.img

到此为止,我们已经得到了一个镜像文件(obj/kern/kernel.img),该文件包括了两个部分:

  1. boot loader(启动加载器):obj/boot/boot
  2. kernel(内核):obj/kernel
    这两个部分后面都会分别介绍,拥有了这个镜像文件,我们就可以运行QEMU了。
make qemu-nox # 或者 make qemu,建议使用make qemu-nox,否则在虚拟机环境下还是有一点小麻烦的

然后如下内容将被显示,如果想要退出qemu请依次按下Ctrl+ax

Booting from Hard Disk...
# 下面部分是习题用的print出来的内容
6828 decimal is XXX octal!
entering test_backtrace 5
entering test_backtrace 4
entering test_backtrace 3
entering test_backtrace 2
entering test_backtrace 1
entering test_backtrace 0
leaving test_backtrace 0
leaving test_backtrace 1
leaving test_backtrace 2
leaving test_backtrace 3
leaving test_backtrace 4
leaving test_backtrace 5
# 到此为止
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K>

我们可以看到,QEMU模拟的操作系统打印出了许多类似于debug的信息,这些都是后面的习题要使用的内容,为了我们能够很好的对操作系统debug,以便后面处理上面的信息,我们需要学会使用GDB(调试工具),使用方式也很简单。

  1. 在lab目录下打开两个终端
  2. 第一个终端(终端1)输入命令make qemu-nox-gdb
  3. 第二个终端(终端2)输入命令make gdb

结果如下:

终端1是预览窗口,终端2是debug窗口。我们可以在终端2中输入一些命令来控制程序的运行,或者获取当前计算机中的信息,详细使用方式可以查看MIT 6.828 lab 工具guide,本实验我们只需要使用到3条命令,第一条为si即单句运行。

通过上图我们可以看到,我们对6.828提供的操作系统debug,运行的第一行代码是在内存0xffff0位置的代码ljmp $0xf000,$0xe05b,并且在这行代码上还有一句提示The target architecture is assumed to be i8086。这里有一个问题:

  1. 为何是从0xffff0这个位置开始运行?这里是什么?

我们考察MIT 6.828 lab1 讲义中给出的地址空间布局图:


+------------------+  <- 0xFFFFFFFF (4GB)
|      32-bit      |
|  memory mapped   |
|     devices      |
|                  |
/\/\/\/\/\/\/\/\/\/\

/\/\/\/\/\/\/\/\/\/\
|                  |
|      Unused      |
|                  |
+------------------+  <- depends on amount of RAM
|                  |
|                  |
| Extended Memory  |
|                  |
|                  |
+------------------+  <- 0x00100000 (1MB)
|     BIOS ROM     |
+------------------+  <- 0x000F0000 (960KB)
|  16-bit devices, |
|  expansion ROMs  |
+------------------+  <- 0x000C0000 (768KB)
|   VGA Display    |
+------------------+  <- 0x000A0000 (640KB)
|                  |
|    Low Memory    |
|                  |
+------------------+  <- 0x00000000

可以看到0xffff0对应BIOS ROM的最后16个字节。继续查看讲义,我们发现,原来这里是沿用了早起8088的设计,PC中的BIOS是烧录进入0x000f0000-0x000fffff位置的,这使得在PC启动时BIOS总能第一时间控制PC,毕竟此时,内存中除了BIOS的部分,都是随机的数据。因此,设计者将入口设置到了0xffff0,即CS=0xf000,IP=0xfff0。注意,BIOS只能运行在实模式下,实模式的物理地址计算方式为:

physical address = 16 * segment + offset

根据公式可以看出实模式只能访问前1MB的内存(0x00000-0xfffff)。然而16个字节的内存并不能存储多少代码,因此,真实的处理逻辑被存储在其他地方,这里只负责jump到对应的地点而已。这部分代码主要用于进行上电自检等设备检验和初始化操作,当一切硬件设备都处理好了,就要开始引导并加载操作系统内核了。

2. Boot Loader

传统意义上,操作系统存储在硬盘空间中,而硬盘又被划分为一个个的扇区,每个扇区512bytes。根据冯诺依曼体系结构,操作系统的内核映像需要被装载到内存中才能运行,因此,Boot Loader的职责就是将操作系统内核映像加载到内存中,并且将控制权限交给内核。

然而这里存在一个问题,回顾物理内存的布局结构,可以看到前1MB的内存基本已经被占用满了。

+------------------+  <- 0x00100000 (1MB)
|     BIOS ROM     |
+------------------+  <- 0x000F0000 (960KB)
|  16-bit devices, |
|  expansion ROMs  |
+------------------+  <- 0x000C0000 (768KB)
|   VGA Display    |
+------------------+  <- 0x000A0000 (640KB)
|                  |
|    Low Memory    |
|                  |
+------------------+  <- 0x00000000

这里的Low Memory也需要做保留用于他用,可见实模式的寻址已经不足以满足当前的需求,我们需要一种方式能够访问更多的内存,以便可以将内核加载到其中。因此,就需要将实模式切换到32-bit的保护模式,在这个模式下,可以访问4GB内存(32bit 30=1GB 2=4)。处理这部分的代码在boot/boot.S中。

了解了内存寻址不够的处理方案,那么还有一个问题,我们需要从哪里加载操作系统内核?怎么让机器了解这个位置?

事实上,为了从硬盘或者软盘上启动,必须把它们用于启动的第一个扇区中所存放的指令(boot/boot.S)装载到内存中执行,而这些指令再把包含内核映像的其他所有扇区拷贝到内存中。处理这部分的代码在boot/main.c中。第一个扇区的装载位置同PC的最初启动位置一样,是一个固定值,在 0x7c00 到 0x7dff中。这里我们学习GDB的第二条和第三条命令:

  1. b *ADDR 在ADDR位置设置断点
  2. c 将程序运行到断点处

这里我们尝试在0x7c00位置设置断点,即b *0x7c00,然后使用c命令,将程序运行到断点处。

对比右侧和左侧的代码可以发现,boot/boot.S被加载到了0x7c00中,我们也可以通过obj/boot/boot.asm查看代码和它的内存空间中的物理地址分布。

考察boot.S的第44行(boot.asm的第61行):

通过注释,可以看到,ljmp $PROT_MODE_CSEG, $protcseg指令后,跳转到了32-bit保护模式,如果我们将断点打在这里,并跳过这一行代码可以发现:

然而,真正造成实模式到保护模式转变的包括两个部分:

  1. 使能A20总线

  2. 设置保护模式flag

完成这两步之后,还需要对保护模式下的各大段寄存器进行初始化,并为C语言运行设置esp,保证C语言运行有栈可用:

最后调用call bootmain进入到boot/main.c中,在boot/main.c中将会读取整个内核镜像到内存中。编译后的内核镜像是一个ELF格式的文件,整个装载过程就是装载该ELF文件的过程(这个文件很复杂,我们只简单看一下装载过程),查看boot/main.c文件:

#define SECTSIZE	512
#define ELFHDR		((struct Elf *) 0x10000) // scratch space

void readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);

void
bootmain(void)
{
	struct Proghdr *ph, *eph;

	// 读取磁盘中的第一页(4096 bytes)
	readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

	// is this a valid ELF?
	if (ELFHDR->e_magic != ELF_MAGIC)
		goto bad;

	// load each program segment (ignores ph flags)
	ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
    // 根据ELFHeader中的信息,每次读取一个segment,直到读取完毕为止。
	eph = ph + ELFHDR->e_phnum;
	for (; ph < eph; ph++)
		// p_pa is the load address of this segment (as well
		// as the physical address)
		readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

	// call the entry point from the ELF header
	// note: does not return!
    // 进入内核
	((void (*)(void)) (ELFHDR->e_entry))();

bad:
	outw(0x8A00, 0x8A00);
	outw(0x8A00, 0x8E00);
	while (1)
		/* do nothing */;
}

可以看到,ELF文件的Header中存放了各个segment的信息,boot/main.c根据这些信息将整个内核加载入内存中,最后进入内核。

3. The kernel

扩展内容

通过本次实验,我们了解了6.828的使用的QEMU的整个BOOT流程,真实的Linux是如何启动的呢?笔者考察了《深入理解linux内核》这一著作,其附录一《系统启动》描述了该问题:

  1. BIOS:在开始启动时,有一个特殊的硬件电路在CPU的一个引脚上产生一个RESET逻辑值,在RESET产生以后,就把处理器的一些寄存器(包括cs和eip)设置成固定的值,并执行在物理地址0xfffffff0处找到的代码。硬件把这个地址映射到某个只读、持久的存储芯片(ROM),ROM中存放的程序集在80x86体系中通常叫做基本输入/输出系统(BIOS),因为它包括几个终端驱动的低级过程。所有的操作系统在启动时,都要通过这些过程对计算机硬件进行设备初始化。BIOS的启动过程主要包括4个操作:
    1. 上电自检(POST),对计算机硬件执行一系列的测试,用来检测现在都有什么设备以及这些设备是否正常工作。
    2. 初始化硬件设备。这个阶段在现代基于PCI的体系结构中相当重要,因为它可以保证所有的硬件设备操作不会引起IRQ线和IO端口的冲突。
    3. 搜索一个操作系统来启动,根据BIOS的设置,这个阶段可能要试图访问系统中软盘、硬盘和CD-ROM的第一个扇区(引导扇区)
    4. 只要找到一个有效的设备,就把第一个扇区的内容拷贝到RAM中从物理地址0x00007c00开始的位置,然后跳转到这个地址处,开始执行刚才装载进来的代码。
  2. 引导装入程序:
    1. 为了从软盘启动,必须把第一个扇区中所有存放的指令装载到RAM中,这些指令再把包含内核映像的其他所有扇区拷贝到RAM中。
    2. 从硬盘启动:硬盘的第一个扇区成为主引导记录(Master Boot Record, MBR),该扇区中包括分区表和一个小程序,该小程序用来装载被启动的操作系统所在分区的第一个扇区。(注意:在Linux早期版本(一直到2.4系列),在第一个512字节有一个最小的引导程序,因此在第一个扇区拷贝一个内核映像就可以使软盘可启动,但是到2.6中不再有这样的引导装入程序)。
    3. 从磁盘启动:从磁盘启动Linux内核需要一个两步的引导装入程序,在80x86体系中,众所周知的linux引导装入程序叫Linux Loader(LILO)。LILO引导装入程序被分为两部分,因为不划分的话,它就太大无法装进单个扇区。MBR或者分区引导扇区包括一个小的引导装入程序,由BIOS把这个小程序装入从地址0x00007c00开始的RAM中,这个小程序又把自己移到地址0x00096a00,建立实模式栈(0x00098000-0x000969ff),并把LILO的第二部分装入到从地址0x00096c00开始的RAM中。第二部分又一次从磁盘读取可用操作系统的映射表,并提供给用户一个提示符,用户就可以从中选择一个操作系统,最后用户选择了的操作系统被装入内核后,引导装入程序就可以把相应分区的引导扇区拷贝到RAM并执行它,或者直接把内核映像拷贝到RAM中。
      注意,这里调用BIOS过程从磁盘装载内核映像时,对于小内核映像,装入低地址到0x00010000上,对于大内核映像,则装载到0x00100000上。
  3. 进入内核进行初始化。
posted @ 2023-07-16 12:58  啊哈哈哈哈312  阅读(69)  评论(0编辑  收藏  举报