实验参考信息

  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. 进入内核进行初始化。
内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/yanlishao/p/17557668.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!