最近经常感受到被大佬碾压,想想自己写了几年代码但对操作系统的理解似乎仍然停留在课本上…OTZ,特开此篇来进行实践、总结。感谢大佬们,大佬们的碾压就是我前进的动力。
本系列的目的是通过QEMU模拟计算机硬件,然后在此基础上进行操作系统的实践学习,课程地址:https://pdos.csail.mit.edu/6.828/2017/schedule.html
本文将逐步记录实践操作,并复习相关的知识。
vmware workstation16 player + Ubuntu16.04
使用Ubuntu20.04作为环境会出现以下错误:
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
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 1GCC9.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
至此,环境配置完整,下面将深入探索计算机启动的过程。
打开两个终端,终端1输入:
cd ~/lab
make qemu-gdb
终端2输入:
cd ~/lab
make gdb
在终端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。
到这里,可以得到以下结论:
那么自然会问,为什么开机执行的第一条指令在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 loade主要功能为两个:
说了一堆概念,下面接着用qemu结合GDB进行实践。
在上面的第二个终端内执行:
b *0x7c00
我们在0x7c00处添加一个断点,这里应该是boot loader开始的地方。
然后继续执行:
continue 或者简写 c
这句话表示继续执行程序直到下一个断点,执行后可以看到qemu界面输出如下:
可以看到输出了BIOS信息并且准备初始化操作系统了!
对boot loader文件(obj/boot/boot.out)进行反编译,可以看到boot确实是从0x7c00位置开始:
obj/boot/boot.out: file format elf32-i386
Disassembly of section .text:
00007c00 :
保护模式相关的操作:
7c00: fa cli #关中断
00007c0a : #设置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
上面图中显示kernel文件是一个ELF格式文件,-h参数显示了ELF文件的各个header数据,对ELF格式感兴趣的请参考:https://pdos.csail.mit.edu/6.828/2017/readings/elf.pdf
这里简要回顾几个header的含义:
Boot loader正是通过ELF程序的header来决定如何加载kernel的各个部分到内存里。上图中的LMA一列就表示装载地址,指出该部分应加载到内存的哪个位置;VMA表示链接地址,表示此部分在程序执行时期望从内存何处开始。
从上图中我们看到,VMA和LMA不一样,比如text正文段加载到0x00100000但执行时是从0xf0100000开始,这是由于操作系统将虚拟内存与物理内存进行了映射,程序执行时的地址是用的操作系统的虚拟地址,再有虚地址映射到物理地址。关于虚拟内存后面应该还会涉及到。
我们回过头来看一下boot loader在内存加载和执行地址是什么样的?
执行:
objdump -h ~/lab/obj/boot/boot.out
得到如下结果
可以看到LMA装在地址正是我们上面验证的0x7c00,而且VMA和LMA是相等的,这是因为在boot loader执行的阶段并没有虚拟内存,实际执行的位置就是装载的位置。
到目前为止,本文总结了实验环境的搭建、计算机的第一条指令、BIOS初始化硬件以及boot loader如何加载内核。
下一篇开始将逐步剖析一个小巧的kernel(JOS),将会有不少代码了呢。