MIT 6.828 环境搭建及Lab1运行
最近学了点Windows保护模式对操作系统底层有些感兴趣了,边学Windows边开个新坑吧,6.828的资料在网上不算难找,慢慢填坑
运行环境搭建
搭建环境的系统:Ubuntu18.04.6
安装编译工具
根据6.828要求的环境下载相关压缩包,自行编译
在编译前先设置环境变量,根据官方推荐的路径,将/usr/local/lib
目录添加到LD_LIBRARY_PATH
export PATH=$HOME/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/lib:$HOME/lib:$LD_LIBRARY_PATH #不加这步编译不下去
apt install m4 #安装configure所需的包
# 遇到如下情况
# root@zzz:/home/zzz/6.828/gmp-5.0.2# apt install m4
# E: 无法获得锁 /var/lib/dpkg/lock-frontend - open (11: 资源暂时不可用)
# E: 无法获取 dpkg 前端锁 (/var/lib/dpkg/lock-frontend),是否有其他进程正占用它?
ps -aux | grep apt
kill -9 [查到的PID] #杀死相关进程
# 以下所有步骤都在root用户下进行,懒得打sudo了
tar xjf gmp-5.0.2.tar.bz2
cd gmp-5.0.2
./configure --prefix=/usr/local
make && make install && cd ..
tar xjf mpfr-3.1.2.tar.bz2
cd mpfr-3.1.2
./configure --prefix=/usr/local
make && make install && cd ..
tar xzf mpc-0.9.tar.gz
cd mpc-0.9
./configure --prefix=/usr/local
make && make install && cd ..
tar xjf binutils-2.21.1.tar.bz2
cd binutils-2.21.1
./configure --prefix=/usr/local --target=i386-jos-elf --disable-werror
make && make install && cd ..
i386-jos-elf-objdump -i #运行以下命令看是否成功编译并安装
tar xjf gcc-core-4.6.4.tar.bz2
cd gcc-4.6.4
mkdir build && cd build
../configure --prefix=/usr/local \
--target=i386-jos-elf --disable-werror \
--disable-libssp --disable-libmudflap --with-newlib \
--without-headers --enable-languages=c MAKEINFO=missing
make all-gcc
make install-gcc
make all-target-libgcc
make install-target-libgcc
cd ../..
i386-jos-elf-gcc -v #验证是否安装成功
安装QEMU
git clone https://github.com/mit-pdos/6.828-qemu.git qemu
# root@zzz:/home/zzz/6.828# git clone https://github.com/mit-pdos/6.828-qemu.git qemu
# 正克隆到 'qemu'...
# /usr/lib/git-core/git-remote-https: symbol lookup error: /usr/lib/x86_64-linux-gnu/libhogweed.so.4: undefined symbol: __gmpz_limbs_read
# 接着可能出现上述报错,重新安装libgmp10即可
rm /usr/local/lib/libgmp.so*
apt-get --reinstall install libgmp10
# 重新克隆qemu
git clone https://github.com/mit-pdos/6.828-qemu.git qemu
cd qemu
apt install python2.7 pkg-config zlib* libglib2.0-dev libpixman-1-dev #安装configure所需的环境
mv /usr/bin/python2.7 /usr/bin/python #将python2.7重命名为python,当然也可以用ln
./configure --disable-kvm --disable-werror --target-list="i386-softmmu x86_64-softmmu" #不加prefix默认路径也在/usr/local下
make && make install && cd ..
根据Lab1克隆Lab
git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab
cd lab
make qemu-nox #此步便可进入系统内核
K>
K> kerninfo #打印内核信息
至此,6.828所需的环境全部配置完成
调试环境搭建
这里参考了Anarion-zuo的配置,使用Clion进行开发,目前用的是2022.1版本
配置SSH连接,进入项目,此步就不再赘述
分别配置GDB和远程调试
Anarion-zuo文章里说的远程调试的端口在.gdbinit中查看,我找到的是25000,但实际上为26000,可以先将上图名为GDB的服务运行起来,查看其中的端口
在kern/init.c
第34行下一个断点,先点击运行按钮运行GDB服务,再点击调试按钮运行Remote Debug服务,可以看到已经在系统初始化函数处断了下来
至此,配置基本完成
GDB基本命令
x/i [ADDR]
:查看对应地址的指令,也可使用表达式计算目的地址 SEG*16 + OFFSET
x/Ni [ADDR]
:N代表查看指令条数,x/10i就代表查看10条指令
x/Nx addr
:查看地址处的 N 个十六进制字节
set print pretty
:打印更漂亮
i reg
: 也可以输info register
,查看当前寄存器值
b *ADDR
:在指定地址下断点
c
:运行到断点
si
:单步执行
si N
:N指执行N条指令
Part1. 装载BIOS
在开启系统之前,先新建一个GDB远程调试,将Symbol File指向boot.out,这样就可以在boot时候下断点了
运行系统,开启GDB调试,发现GDB调试打印出来系统的入口0x0000fff0
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual. E.g., run from the shell:
info "(gdb)Auto-loading safe path"
0x0000fff0 in ?? ()
以下写于环境搭建完的第二天
发现直接用Clion的调试打印不出来BIOS入口指令ljmp,还是改用GDB来调试
开两个Terminal,先运行 make qemu-nox-gdb
,再到另一个窗口运行make gdb
,进了gdb大概就下面这样
可以看到第一条指令是ljmp $0xf000, $0xe05b
,这是因为
IBM 规定BIOS入口物理地址为0x000ffff0,是ROM BIOS
64KB
的顶部在系统刚运行起来的时候,CS默认等于0xf000, IP默认等于0xfff0(也就是强制变成这个值)
第一条指令需要用jmp修改CS:IP的值为0xf000:0xe05b
注意第一条指令的地址是[f000:fff0],而16位的寻址方式为16 x Segment + Offset
,这里也就是CS x 16 + IP
= 16 * 0xf000 + 0xfff0 = 0xffff0
而BIOS的总共就1MB大,最大地址为0xfffff,剩下的只有16字节可以装,所以塞个jmp跳到别处去了
当 BIOS 运行时,它会建立一个中断描述符表(interrupt descriptor table)并初始化许多设备(比如 VGA 显示器),在初始化 PCI 总线以及所有重要的设备之后,BIOS 会寻找一个可以启动的设备(比如软盘、硬盘、CD-ROM),当它找到了之后,会把 boot loader从磁盘里面读入,并将控制权转交给 boot loader
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
[f000:e05b] 0xfe05b: cmpl $0x0,%cs:0x6ac8
[f000:e062] 0xfe062: jne 0xfd2e1
[f000:e066] 0xfe066: xor %dx,%dx
[f000:e068] 0xfe068: mov %dx,%ss
[f000:e06a] 0xfe06a: mov $0x7000,%esp
[f000:e070] 0xfe070: mov $0xf34c2,%edx
[f000:e076] 0xfe076: jmp 0xfd15c
[f000:d15c] 0xfd15c: mov %eax,%ecx
以上就是加载Boot代码前的一系列初始化操作,给各个通用寄存器和段寄存器初始值
Part2. The Boot Loader
软盘和硬盘一个扇区有512个字节,扇区是磁盘的最小传输粒度,每次的读写操作都必须与一个扇区或多个扇区的边界对齐。如果该磁盘是可引导的,第一个扇区是引导加载程序代码所在的位置,称为引导扇区。当BIOS找到可引导的磁盘时,它会将512字节的引导扇区加载到物理地址0x7c00到0x7dff的内存中,然后使用jmp指令将CS:IP设置为0000:7c00,将控制权传递给引导加载程序。和BIOS加载地址一样这些地址是瞎定的,而且不能改.
现代BIOS是从CD-ROM启动的,使用的2048字节对齐,但6.828使用的还是从硬盘启动机制。Boot Loader由boot/boot.S
和boot/main.c
两个文件组成,Boot Loader必须执行两个主要功能:
1、Boot Loader将实模式转移到了32位的保护模式,用户态软件只能通过该模式才能访问1MB以上的部分
2、然后Boot Loader使用X86特殊的I/O指令读取硬盘中的内核
引导进入保护模式
参考此处,IBM为了兼容8088的20条地址线(因为20条地址线最大地址为0xfffff),从8042键盘控制器的一个备用引脚变成了一个开关,来启用/禁用0x100000地址位,该信号称为A20,如果为零,则清除所有地址的第20位。以下代码就是因为历史原因,在boot加载的时候,默认A20是禁用的,所以需要找到这个地址并启用它。
seta20.1:
inb $0x64,%al # 从I/O端口读取一个字节
testb $0x2,%al
jnz seta20.1 # 循环,直到读取到0x2
movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64 # 向I/O端口写入一个字节
seta20.2:
inb $0x64,%al
testb $0x2,%al
jnz seta20.2
movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60
以下代码是进入保护模式的一系列工作
lgdt gdtdesc # 从gdtr获取gdt全局描述符表地址
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
# 修改CS和EIP
ljmp $PROT_MODE_CSEG, $protcseg
.code32
protcseg:
# 建立保护模式数据段的寄存器
movw $PROT_MODE_DSEG, %ax # 段选择子
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# 跳转到.c文件中
movl $start, %esp
call bootmain
ELF文件头格式
在进入main.c
文件前,先了解一下ELF文件格式,以下结构体可以在inc/x86.h
中找到
struct Elf {
uint32_t e_magic; // 魔数,必须为7F 45 4C 46
uint8_t e_elf[12]; //作为开头主要用装一些标示信息
uint16_t e_type; //类型包括:可执行文件、可重定向文件、共享目标文件、内核文件等
uint16_t e_machine; //运行此文件的机器类型
uint32_t e_version; //表示文件的版本
uint32_t e_entry; //函数入口
uint32_t e_phoff;
uint32_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize; //表示每个节头表的条目大小
uint16_t e_shnum; //表示有多少个节头表条目
uint16_t e_shstrndx;
};
//一共52个字节
加载ELF文件
进入bootmain函数的第一句代码为readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
,此段代码从硬盘中读取了0x1000长度的数据,存入内存0x10000处,可自行对比执行前后内存的变化
在这个函数里面,包含了一个函数readsect((uint8_t*) pa, offset);
,通过while循环重复调用这个函数,完成文件加载
wait disk
waitdisk
函数是等待磁盘加载完成,代码只有一句话while ((inb(0x1F7) & 0xC0) != 0x40)
,0x1F7
既是命令端口,又是状态端口,当该端口为状态端口时,每一位代表的含义如下
第
7
位BSY 控制器忙碌
第6
位RDY 磁盘驱动器已准备好
第5
位WFT 写入错误
第4
位SKC 搜索完成
第3
位DRQ 为1
时扇区缓冲区没有准备好
第2
位COR 是否正确读取磁盘数据
第1
位 IDX 磁盘每转一周将此位设为1
第0
位ERR 之前的命令因发生错误而结束
为了保证磁盘可读,必须等待扇区缓冲区准备好后才执行下一步,所以需要判断第3位DRQ
是否为0
read disk
再往下走有一个outb的函数,其中调用了outb汇编指令,完整汇编代码为asm volatile("outb %0,%w1" : : "a" (data), "d" (port))
参考此文章asm函数,%w1表示宽度为w的1号占位符,%0表示0号占位符
“a” (data), “d” (port)代表两个输入,分别对应0、1号占位符,意思是将data(1个字节)传输到port
outb(0x1F2, 1); // 0x1F2 -> Sector Count,这里代表读取一个扇区的数据
outb(0x1F3, offset); //
outb(0x1F4, offset >> 8);
outb(0x1F5, offset >> 16);
outb(0x1F6, (offset >> 24) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors