MIT6828操作系统实践记录(一)

MIT6828操作系统实践记录(一)

最近经常感受到被大佬碾压,想想自己写了几年代码但对操作系统的理解似乎仍然停留在课本上…OTZ,特开此篇来进行实践、总结。感谢大佬们,大佬们的碾压就是我前进的动力。

本系列的目的是通过QEMU模拟计算机硬件,然后在此基础上进行操作系统的实践学习,课程地址:https://pdos.csail.mit.edu/6.828/2017/schedule.html
本文将逐步记录实践操作,并复习相关的知识。

环境准备

vmware workstation16 player + Ubuntu16.04

使用Ubuntu20.04作为环境会出现以下错误:

  1. ERROR: Cannot use ‘python’, Python 2.4 or later is required.
    Note that Python 3 or later is not yet supported.
    Use --python=/path/to/python to specify a supported Python.

    QEMU不支持python3

  2. ld: obj/kern/printfmt.o: in function printnum': lib/printfmt.c:41: undefined reference to__udivdi3’
    ld: lib/printfmt.c:49: undefined reference to `__umoddi3’
    make: *** [kern/Makefrag:71: obj/kern/kernel] Error 1

    GCC9.3版本过高

安装依赖项:

sudo apt install libsdl1.2-dev libtool-bin libglib2.0-dev libz-dev libpixman-1-dev libfdt-dev gcc-multilib

获取JOS与QEMU,JOS是一个类unix的操作系统,QEMU是一个计算机硬件的模拟器:

cd ~
git clone https://pdos.csail.mit.edu/6.828/2017/jos.git lab
git clone http://web.mit.edu/ccutler/www/qemu.git -b 6.828-2.3.0

编译QEMU:

cd ~/qemu
./configure --disable-kvm
make 
sudo make install

编译JOS:

cd ~/lab
make

编译完成JOS后得到的kernel.img包含bootloader( lab/obj/boot/boot)以及系统内核(~/lab/obj/kern/kernel),文件结构如下:

lab
├── obj
│   ├── boot
│   │   ├── boot
│   │   ├── boot.asm
│   │   ├── boot.o
│   │   ├── boot.out
│   │   └── main.o
│   └── kern
│       ├── console.o
│       ├── entry.o
│       ├── entrypgdir.o
│       ├── init.o
│       ├── kdebug.o
│       ├── kernel
│       ├── kernel.asm
│       ├── kernel.img
│       ├── kernel.sym
│       ├── monitor.o
│       ├── printfmt.o
│       ├── printf.o
│       ├── readline.o
│       └── string.o

启动qemu虚拟环境:

cd ~/lab
make qemu

启动后弹出新的窗口,如下图:
MIT6828操作系统实践记录(一)

至此,环境配置完整,下面将深入探索计算机启动的过程。

计算机启动

打开两个终端,终端1输入:

cd ~/lab
make qemu-gdb

终端2输入:

cd ~/lab
make gdb

得到下图的结果
MIT6828操作系统实践记录(一)

在终端2中可以看到这一行

[f000:fff0]    0xffff0:	ljmp   $0xf000,$0xe05b

前面的[:]表示代码段寄存器(CS)的值为f000,指令寄存器(IP)的值为fff0,CS存放当前正在运行的程序代码所在段的段基址,表示当前使用的指令代码可以从该段寄存器指定的存储器段中取得,相应的偏移量则由IP提供。

指令所在物理内存地址 = 16* (CS) + (IP),可以算出第一条指令的物理地址为:

//8086CPU的寄存器宽度为16位,地址总线20位,所要寻址需要
//用基址左移四位,也就是十六进制乘以16,注意此时CPU处于实模式
//(real mode)
    16 * 0xf000 + 0xfff0 
   =0xf0000 + 0xfff0
   =0xffff0 

可以看到算出来的结果正好是[:]后面的 0xffff0,说明应该是没算错的。0xffff0后面是一个ljmp,表示要执行的是一条***转移指令***,跳转目的地址是(0xf000,0xe05b),用上面的算法同样可以算出跳转的物理地址是0xfe05b。

到这里,可以得到以下结论:

  • 8086计算机开机后开始执行第一条指令的物理地址位于0x000ffff0
  • 开机执行的第一条指令是跳转指令,跳转目的地址是(0xf000:0xe05b)

那么自然会问,为什么开机执行的第一条指令在0xffff0?为什么第一条指令就要跳转到别的地方?这里复习一下32位系统映射的物理内存结构:

+------------------+  <- 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

可以看到内存从低地址开始保留了1MB(0x00000000~0x00100000)的空间,0x000ffff0处于BIOS中,这样保证开机总是能够先执行BIOS程序。
BIOS第一句指令向前跳转,随后执行中断描述符表(IDT)并初始化显示器等设备,接着寻找启动设备(如硬盘,CD等)。
这里BIOS默认采用的是磁盘引导,磁盘扇区大小为512B,一旦BIOS识别到某个磁盘是引导设备,就将该磁盘的第一个扇区(boot sector)内的主引导记录(MBR)读取到内存0x7C00~0x7DFF地址上。至于为什么是0x7C00,可以理解为计算机一开始是这么设计的,后面为了兼容就都这么做。BIOS程序随后执行jmp 0000:7C00将控制权移交给boot loader。

至此计算机完成了硬件的准备,下面就是操作系统的工作了。

Boot loader

Boot loade主要功能为两个:

  • 切换CPU模式从实模式到保护模式,简单来说就是前面的物理地址转换是乘以16,现在由于操作系统负责管理,物理地址转换变成了乘以32,这样以便于操作系统访问更大的内存空间。
  • 从磁盘里读取操作系统内核(kernel),读取的方式通过IDE驱动器,这里不用去关注如何读取的。
    说了一堆概念,下面接着用qemu结合GDB进行实践。
    在上面的第二个终端内执行:
b *0x7c00

我们在0x7c00处添加一个断点,这里应该是boot loader开始的地方。
然后继续执行:

continue 或者简写 c

这句话表示继续执行程序直到下一个断点,执行后可以看到qemu界面输出如下:
MIT6828操作系统实践记录(一)

可以看到输出了BIOS信息并且准备初始化操作系统了!
对boot loader文件(obj/boot/boot.out)进行反编译,可以看到boot确实是从0x7c00位置开始:

obj/boot/boot.out:     file format elf32-i386
Disassembly of section .text:
00007c00 <start>:  

保护模式相关的操作:

7c00:	fa                  cli       #关中断
00007c0a <seta20.1>:                  #设置A20地址线,使用所有地址线
...
  lgdt    gdtdesc                     #加载gdt寄存器的数据,获取GDT基址
  movl    %cr0, %eax                  #置cr0寄存器的保护允许位,开启保护模式
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0
  ljmp    $PROT_MODE_CSEG, $protcseg  #跳转指令,开启保护模式以后就是32位代码了
...

保护模式开启完毕后boot loader将加载kernel,我们首先来分析下这个kernel文件。
执行以下面的命令:

objdump -h ~/lab/obj/kern/kernel

输出如下:
MIT6828操作系统实践记录(一)

上面图中显示kernel文件是一个ELF格式文件,-h参数显示了ELF文件的各个header数据,对ELF格式感兴趣的请参考:https://pdos.csail.mit.edu/6.828/2017/readings/elf.pdf
这里简要回顾几个header的含义:

  • .text:程序执行的指令(代码段)
  • .data:保存程序的初始化的数据,比如已经初始化了的全局变量(数据段)
  • .bss:存放程序里未初始化或初始为0的全局变量和静态变量

Boot loader正是通过ELF程序的header来决定如何加载kernel的各个部分到内存里。上图中的LMA一列就表示装载地址,指出该部分应加载到内存的哪个位置;VMA表示链接地址,表示此部分在程序执行时期望从内存何处开始。
从上图中我们看到,VMA和LMA不一样,比如text正文段加载到0x00100000但执行时是从0xf0100000开始,这是由于操作系统将虚拟内存与物理内存进行了映射,程序执行时的地址是用的操作系统的虚拟地址,再有虚地址映射到物理地址。关于虚拟内存后面应该还会涉及到。
我们回过头来看一下boot loader在内存加载和执行地址是什么样的?
执行:

objdump -h ~/lab/obj/boot/boot.out

得到如下结果
MIT6828操作系统实践记录(一)
可以看到LMA装在地址正是我们上面验证的0x7c00,而且VMA和LMA是相等的,这是因为在boot loader执行的阶段并没有虚拟内存,实际执行的位置就是装载的位置。

总结

到目前为止,本文总结了实验环境的搭建、计算机的第一条指令、BIOS初始化硬件以及boot loader如何加载内核。
下一篇开始将逐步剖析一个小巧的kernel(JOS),将会有不少代码了呢。

上一篇:Lab Report怎么写?


下一篇:设置 jupyter lab、jupyter notebook 的默认启动路径