MIT 6.828环境搭建及Lab1运行


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 #验证是否安装成功

成功安装jos-elf-gcc

安装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和远程调试

qemu-nox-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时候下断点了

Remote Debug 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大概就下面这样

进入GDB

可以看到第一条指令是ljmp $0xf000, $0xe05b,这是因为

IBM 规定BIOS入口物理地址为0x000ffff0,是ROM BIOS64KB的顶部

在系统刚运行起来的时候,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.Sboot/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

文章作者: Kevin。
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kevin。 !
评论
 上一篇
TLB和控制寄存器 TLB和控制寄存器
TLB和控制寄存器TLBTLB(Translation Lookaside Buffer)中文一般翻译为转译后备缓冲区,也就是页表缓存,用来存放线性地址相对应的物理地址及其相关信息。TLB是在MMU中包括的一段小的缓存,比内存快了十几倍不等
2022-04-27
下一篇 
Windows分页 Windows分页
Windows分页Windows32主要有两种分页方式,10-10-12 和 2-9-9-12,加起来就是32位,其中2,9,10,12代表2的x次方 10-10-12分页 根据因特尔手册给出的图可以看出,当前程序的页目录索引存在CR3中,
2022-04-20
  目录