官网:https://pdos.csail.mit.edu/6.828/2018/schedule.html

参考资料:

  • https://blog.csdn.net/bysui/category_6232831.html

  • https://github.com/SmallPond/MIT6.828_OS

  • https://www.cnblogs.com/fatsheep9146/category/769143.html

前期准备

配置环境

  • VMware Workstation虚拟机

  • Ubuntu 16.04

  • 安装MIT给的Tool Chain:https://pdos.csail.mit.edu/6.828/2018/tools.html

  • 安装MIT打Patch的Qemu(我安装的时候用的./configure --disable-kvm --disable-werror --target-list=“i386-softmmu x86_64-softmmu”)

可能出现的错误

  • 上面的链接中gmp-5.0.2可能下载不了,用https://mirrors.sjtug.sjtu.edu.cn/gnu/gmp/gmp-5.0.2.tar.bz2
  • mpc-0.9可能需要加–no-check-certificate选项
  • 安装gmp时出现No usable m4 in $PATH or /usr/5bin:apt-get install m4
  • 安装gcc时出现cannot compute suffix of object files: cannot compile:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
  • 安装gdb时出现no termcap library found:去https://ftp.gnu.org/gnu/termcap/安装termcap后即可
  • git clone qemu时出现git-remote-https: symbol lookup error: /usr/lib/x86_64-linux-gnu/libhogweed.so.4: undefined symbol: __gmpz_limbs_read: rm /usr/local/lib/libgmp.so*

Lab1:Booting a PC

Part 1: PC Bootstrap

mkdir 6.828
cd 6.828
git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab
cd lab
make
make qemu

// 在两个终端中分别输入make qemu-gdbmake gdb,观察ljmp那一行。

Exercise2

使用si命令去追踪ROM BIOS几条指令

Part 2: The Boot Loader

观察boot/boot.S, boot/main.c, obj/boot/boot.asm文件。

boot.S,main.c

cli关中断。

cld指定串处理操作的指针的移动方向。

使用异或操作将ax寄存器中设为0,并清空ds、es、ss三个段寄存器的数据。

观察seta20.1代码:

由图看出0x64端口为控制器读取状态寄存器,属于keyboard controller键盘控制器804x

inb指令为读取端口的一字节到al寄存器中;

testb指令将0x2和al寄存器中的数据作与运算;

jnz表示结果不为0则继续到inb指令;

前三行指令表示当bit 1为0时才跳转到下一指令。(bit1的值代表输入缓冲区是否满了,即CPU传送给键盘控制器的数据是否已经取走,如果CPU想向控制器传送新数据的话,必须先保证这一位为0。)

movb与outb表示将0xd1数据写入0x64端口。

d1指令表示下一次写入0x60端口的数据将被写入给804x控制器的输出端口。可以理解为下一个写入0x60端口的数据是一个控制指令。

seta20.2代码类似,df指令表示进入到保护模式。


lgdt gdtdesc,指将gdtdesc标识符的值送入全局映射描述符表寄存器GDTR中,把关于GDT表的一些重要信息存放到CPU的GDTR寄存器中,其中包括GDT表的内存起始地址,以及GDT表的长度。有关gdt和gdtdesc的具体数据在boot.S最后有:

movl、orl、movl指令表示把cr0寄存器的bit0置1,代表保护模式启动。

跳转指令,把当前的运行模式切换成32位地址模式。

加载这些段寄存器,使GDTR的值生效。

设置当前的esp寄存器的值,并正式跳转到main.c文件中的bootmain函数处。然后来看main.c文件:

前面的注释基本和前面所说差不多,bootloader主要由boot.S与main.c来控制。下面来看bootmain函数。

bootmain函数首先调用readseg函数(如下),从注释来看,以距离内核起始地址offset个偏移量的存储单元为起始,将之后count字节的数据送入以pa为起始地址的内存物理地址处。这一行函数把内核的第一个页的内容读取的内存地址ELFHDR处。相当于把操作系统映像文件的elf头部读取出来放入内存中。

if条件语句判断这个输入文件是否是合法的elf可执行文件。

ph被指定为Program Header Table表头,这个表存放着程序中所有段的信息。通过这个表才能找到要执行的代码段,数据段等等。

eph指向该表末尾。

for循环就是在把操作系统内核的各个段从外存读入内存中。

e_entry字段指向的是这个文件的执行入口地址。这里相当于开始运行这个内核文件。 自此把控制权从boot loader转交给了操作系统的内核。

Exercise3.1

在0x7c00处设置断点,观察boot.S代码的运行过程,同时使用boot.S文件和系统反汇编出来的文件obj/boot/boot.asm。也可以使用GDB的x/i指令来获取任意一个机器指令的反汇编指令,把源文件boot.S文件和boot.asm文件以及在GDB反汇编出来的指令进行比较。

首先设置断点并运行到断点处。

使用x/i反汇编。

查看obj/boot/boot.asm文件,基本一致。

Exercise3.2

追踪到boot/main.c中的bootmain(),然后追踪到readsect()。确定与readsect()中的每个语句相对应的确切汇编指令。跟踪readsect()的其余部分 并返回bootmain() ,并确定从磁盘读取内核剩余扇区的for循环的开始和结束。找出循环结束时将运行的代码,在此处设置断点,然后继续执行该断点。然后逐步完成引导加载程序的其余部分。

略,详情可见https://www.cnblogs.com/fatsheep9146/p/5115086.html

questions

1、处理器在什么时候开始执行 32 位代码?究竟是什么导致从 16 位模式切换到 32 位模式?

boot.S中,运行完 " ljmp $PROT_MODE_CSEG, $protcseg " 语句后,正式进入32位工作模式。

2、boot loader中执行的最后一条语句是什么?内核被加载到内存后执行的第一条语句又是什么?

boot loader执行的最后一条语句是bootmain中的最后一条语句 " ((void (*)(void)) (ELFHDR->e_entry))(); "。内核被加载到内存后的第一句为 movw $0x1234, 0x472。

3、内核的第一条指令在哪里?

/kern/entry.S文件中

4、boot loader是如何知道它要读取多少个扇区才能把整个内核都送入内存的呢?在哪里找到这些信息?

在Program Header Table表中有操作系统有多少个段,每个段有多少个扇区的信息;

该表在操作系统内核映像文件的ELF头部信息中。

Loading the Kernel

Exercise4

熟悉C语言指针(略)

ELF文件可以理解为由三大部分组成:一个是带有加载信息的文件头,然后紧跟着程序段表,然后紧跟着几个程序段。其中每一个段都是一块连续的代码或者数据。它们在被运行时要首先被加载到内存中。boot loader的工作就是把它们加载到内存中。

在6.828中对ELF的三个段感兴趣:

  • .text段:存放所有程序的可执行代码
  • .rodata段:存放所有只读数据的数据段
  • .data段:存放所有被初始化过的数据段

当链接器计算程序的内存布局时,它会为未初始化的全局变量,在.data段后的.bss段中保留空间,C 要求未初始化的全局变量以零值开头。因此,无需 在 ELF 二进制文件中存储.bss段的内容;相反,链接器只记录.bss段的地址和大小。加载程序时必须将 .bss段清零。

使用objdump -h obj/kern/kernel来查看jos中所有段的名称、大小和链接地址。

在每一个段中都有两个比较重要的字段,VMA(链接地址),LMA(加载地址)。其中加载地址代表这个段被加载到内存中后的内存地址,链接地址则指的是这个段希望被存放到的内存地址。通常两者是相等的。

通过输入下述指令来获取kernel的Program Headers Table的信息:

objdump -x obj/kern/kernel

需要被加载到内存的段被标记为LOAD。

BIOS通常会把boot sector加载到内存地址0x7c00处。

Exercise5

追踪boot loader一开始的几句指令,找到第一条满足如下条件的指令处:修改了boot loader的链接地址,这个指令就会出现错误。(在boot/Makefrag文件中修改链接地址,修改完成后运行 make clean, 然后通过make指令重新编译内核)

首先make clean,然后修改boot/Makefrag文件中的链接地址,重新make,观察boot.asm文件:

可以看到入口变为了7e00。

继续按照之前在7c00处打断点,可以看到在切换到保护模式的语句中出现了错误。

记得修改回来。

Exercise6

使用GDB的x命令:x/Nx ADDR(这个指令将打印出从ADDR地址开始之后的N个字的内容)。重启一下Qemu,在Bios进入boot loader之前,内存地址0x00100000处8个字的内容,然后在boot loader运行到内核开始处停止,再看下这个地址处的值。为什么二者不同?第二次这个内存处所存放的值的含义是什么?

首先刚进入gdb观察:

全为0

从boot.asm看到,bootmain中加载内核的地址在0x7d63,设置断点并观察0x100000的值:

可以推测,这里面存放的应该是指令段,即.text段的内容。

Part 3: The Kernel

Using virtual memory to work around position dependence

一个问题:我们应该把操作系统放在高地址处,但是在实际的计算机内存中却没有那么高的地址,这该怎么办?

解决方案:在虚拟地址空间中,把操作系统放在高地址处0xf0100000,但在实际的内存中把操作系统放在一个低的物理地址空间处,如0x00100000。当用户程序想访问一个操作系统内核时,首先给出的高的虚拟地址,然后计算机通过某个机构把这个虚拟地址映射为真实的物理地址。那么这种机构通常通过分段管理,分页管理来实现。

使用 kern/entrypgdir.c中手写的、静态初始化的页目录和页表来执行操作(不必了解细节)。

Exercise7

使用Qemu和GDB追踪JOS内核文件,在movl %eax, %cr0指令前查看内存地址0x00100000以及0xf0100000处的内存。然后使用stepi命令执行完这条命令,再次检查这两个地址处的内容。

通过注释entry.S中的这条指令,如果该指令并没有执行,而是被跳过,那么第一个会出现问题的指令是什么?

从boot.asm文件可以看到,调用内核文件的地址在0x7d63,打断点后发现内核代码的入口地址为0x10000C,单步执行后发现movl %eax, %cr0指令在地址0x100025处,观察此时的0x100000地址和0xf0100000地址的内容:

执行命令后再查看此时地址的内容:

可以看到将0xf0100000的内容映射到了0x100000中。

接下来在entry.S中注释这一语句,make clean并make:

再进行调试:

gdb窗口中,原本0x10025的语句被注释后,可以看到下面一句是将f010002c地址写入eax并跳转,因为没有映射地址,所以这里出错。

qemu窗口中的错误。

Formatted Printing to the Console

阅读kern/printf.c, lib/printfmt.c, 和kern/console.c

大致观察printf.c:

观察注释,主要是printfmt及cputchar函数。可以看到vcprintf和cprintf都调用了vprintfmt函数(在lib/printfmt.c中),而cputchar函数是在console.c中定义的。接下来来看console.c中的cputchar函数:

由注释可以看到,cputchar是高等级的console I/O,调用的cons_putc函数是将字符输出到控制台上。

下面来看lib/printfmt.c:

vprintfmt函数:

总体是一个大的循环,首先输出%之前的所有字符,然后通过switch处理%后面的格式化输出。

Exercise8

我们省略了一小部分代码—即当我们在printf中指定输出"%o"格式的字符串,即八进制格式的代码。尝试去完成这部分程序。

类似case d,将基数base改为8即可。

questions

1、解释一下printf.c和console.c两个之间的关系。console.c输出了哪些子函数?这些子函数是怎么被printf.c所利用的?

printf.c调用console.c的接口cputchar,将这个函数封装在putch中,并将这个封装好的函数作为参数传给vprintfmt函数,用于向屏幕上输出一个字符。

2、解释console.c的如下代码:

1      if (crt_pos >= CRT_SIZE) {2              int i;
3              memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
4              for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
5                      crt_buf[i] = 0x0700 | ' ';
6              crt_pos -= CRT_COLS;
7      }

crt_post是当前光标位置,CRT_SIZE是屏幕上总共的可以输出的字符数(其值等于行数乘以每行的列数),这段代码的意思是当屏幕输出满了以后,将屏幕上的内容都向上移一行,即将第一行移出屏幕,同时将最后一行用空格填充,最后将光标移动到屏幕最后一行的开始处。

3、对以下代码

int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);

(1)对cprintf函数来说,fmt和ap指针分别指向哪里?

(2)按照执行的顺序列出对cons_putc, va_arg和vcprintf的调用。对于cons_putc,列出它所有的输入参数。对于va_arg,列出ap在执行这个函数前后的变化。对于vcprintf,列出它的两个输入参数的值。

(1)fmt指向的是显示信息的格式字符串,即"x %d, y %x, z %d\n"。而ap是可变参数,指向所有输入参数的集合。

(2)先调用vcprintf,参数值为fmt和ap的值;然后按照字符串来分别调用cons_putc和va_arg。

tips:可以将该代码添加到kern/monitor.c中,来观察实际输出。

4、运行下列代码:

unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);

可以看到输出为He110 World

因为57616对应八进制为e110,i所表示的内存地址处存储了rld。(小端大端)

5、运行下列代码,y会输出什么?

cprintf("x=%d y=%d", 3);

由于有两个%d但只有一个3,y并没有参数被指定,所以会输出一个不确定的值。

The Stack

Exercise9

判断操作系统内核是从哪条指令开始初始化它的堆栈空间的,以及这个堆栈在内存的哪个地方?内核是如何给它的堆栈保留一块内存空间的?堆栈指针又是指向这块被保留的区域的哪一端的呢?

​ 前面分析过boot.S和main.c,它们并不属于操作系统的内核。main.c文件中bootmain函数运行到最后时,执行的最后一条指令是跳转到entry.S文件中的entry地址处。此时控制权已经被转交给了entry.S。在跳转到entry之前,并没有对%esp,%ebp寄存器的内容进行修改,所以在bootmain中并没有初始化堆栈空间的语句。

​ 下面进入entry.S,最后一条指令是要调用i386_init函数。这个子程序位于init.c文件之中,已经开始对操作系统进行一些初始化工作,所以在这之前,内核的堆栈应该已经设置好了。所以设置内核堆栈的指令就是call i386_init 指令之前的两条语句:

所以栈顶为bootstacktop指针的地址,利用反汇编来查看:

另外也可以通过gdb来打印寄存器esp的值:

可以看到栈的空间在f010f000-f0117000的位置。栈顶指针即esp,为f0117000。

esp指向栈顶位置,栈的生长由高向低,所以当计算机将一个值压入堆栈时,需要先把esp中的值减1(有时候是减4,由机器字长决定),然后把值存入内存单元。从堆栈中弹出一个值反之。

ebp则是记录每一个程序的栈帧的相关信息的一个非常重要的寄存器。

Exercise10

为更好的了解在x86上C调用的细节,找到在obj/kern/kern.asm中test_backtrace子程序的地址,设置断点,并且探讨一下在内核启动后,这个程序被调用时发生了什么。对于这个循环嵌套调用的程序test_backtrace,它一共压入了多少信息到堆栈之中。并且它们都代表什么含义?

kernel.asm中,test_backtrace的地址在f0100040,test_backtrace如下:

void
test_backtrace(int x)
{cprintf("entering test_backtrace %d\n", x);if (x > 0)test_backtrace(x-1);elsemon_backtrace(0, 0, 0);cprintf("leaving test_backtrace %d\n", x);
}

设置断点

在test_backtrace执行前,esp为0xf0116fdc,ebp为0xf0116ff8。

执行push %ebp后:

将值压入ebp中,esp变为fd8。

执行mov %esp,%ebp后:

esp与ebp均变为fd8。

执行push %ebx后:

保存ebx的值,esp变为fd4。

执行sub后:

esp减掉0x14,变为fc0。后续以此类推。(以下借鉴https://github.com/clpsz/mit-jos-2014/tree/master/Lab1/Exercise10)

f0100040:       55                      push   %ebp                             ;压入调用函数的%ebp
f0100041:       89 e5                   mov    %esp,%ebp                        ;将当前%esp存到%ebp中,作为栈帧
f0100043:       53                      push   %ebx                             ;保存%ebx当前值,防止寄存器状态被破坏
f0100044:       83 ec 14                sub    $0x14,%esp                       ;开辟20字节栈空间用于本函数内使用
f0100047:       8b 5d 08                mov    0x8(%ebp),%ebx                   ;取出调用函数传入的第一个参数
f010004a:       89 5c 24 04             mov    %ebx,0x4(%esp)                   ;压入cprintf的最后一个参数,x的值
f010004e:       c7 04 24 e0 19 10 f0    movl   $0xf01019e0,(%esp)               ;压入cprintf的倒数第二个参数,指向格式化字符串"entering test_backtrace %d\n"
f0100055:       e8 27 09 00 00          call   f0100981 <cprintf>               ;调用cprintf函数,打印entering test_backtrace (x)
f010005a:       85 db                   test   %ebx,%ebx                        ;测试是否小于0
f010005c:       7e 0d                   jle    f010006b <test_backtrace+0x2b>   ;如果小于0,则结束递归,跳转到0xf010006b处执行
f010005e:       8d 43 ff                lea    -0x1(%ebx),%eax                  ;如果不小于0,则将x的值减1,复制到栈上
f0100061:       89 04 24                mov    %eax,(%esp)                      ;接上一行
f0100064:       e8 d7 ff ff ff          call   f0100040 <test_backtrace>        ;递归调用test_backtrace
f0100069:       eb 1c                   jmp    f0100087 <test_backtrace+0x47>   ;跳转到f0100087执行
f010006b:       c7 44 24 08 00 00 00    movl   $0x0,0x8(%esp)                   ;如果x小于等于0,则跳到这里执行,压入mon_backtrace的最后一个参数
f0100072:       00
f0100073:       c7 44 24 04 00 00 00    movl   $0x0,0x4(%esp)                   ;压入mon_backtrace的倒数第二个参数
f010007a:       00
f010007b:       c7 04 24 00 00 00 00    movl   $0x0,(%esp)                      ;压入mon_backtrace的倒数第三个参数
f0100082:       e8 68 07 00 00          call   f01007ef <mon_backtrace>         ;调用mon_backtrace,这是这个练习需要实现的函数
f0100087:       89 5c 24 04             mov    %ebx,0x4(%esp)                   ;压入cprintf的最后一个参数,x的值
f010008b:       c7 04 24 fc 19 10 f0    movl   $0xf01019fc,(%esp)               ;压入cprintf的倒数第二个参数,指向格式化字符串"leaving test_backtrace %d\n"
f0100092:       e8 ea 08 00 00          call   f0100981 <cprintf>               ;调用cprintf函数,打印leaving test_backtrace (x)
f0100097:       83 c4 14                add    $0x14,%esp                       ;回收开辟的栈空间
f010009a:       5b                      pop    %ebx                             ;恢复寄存器%ebx的值
f010009b:       5d                      pop    %ebp                             ;恢复寄存器%ebp的值
f010009c:       c3                      ret                                     ;函数返回

Exercise11

略(参考https://www.cnblogs.com/fatsheep9146/p/5070145.html)

Exercise12

略(有点复杂)

可能出现的错误

  • git clone时出现fatal: unable to access ‘https://pdos.csail.mit.edu/6.828/2018/jos.git/’: server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none:git config --global http.sslverify false

MIT JOS 6.828 Lab1学习笔记相关推荐

  1. ucore lab1学习笔记整理

    前置知识 ucore是运行在80386这一32位x86架构的CPU,汇编采用的格式是AT&T, lab1 ucore开始执行makefile会生成磁盘映像,这个磁盘映像就是对应着现实计算机中的 ...

  2. MIT 6.824 学习笔记(一)--- RPC 详解

    从本文开始,将记录作者学习 MIT 6.824 分布式系统的学习笔记,如果有志同道合者,欢迎一起交流. RPC 的定义和结构 RPC 全称为 Remote Procedure Call,他表示一种远程 ...

  3. JOS学习笔记(一)

    起初 神创造天地.  地是空虚混沌.渊面黑暗. BIOS运行在水面上.  神说.要有mbr.就加载了mbr.  神看mbr是好的.就用mbr加载了kernel.  神称mbr为引导程序.称kernel ...

  4. MIT 6.828 lab1 part2

    Part 2: The Boot Loader 加载内核 为了理解boot/main.c,你需要知道ELF二进制文件是什么.当你编译和链接一个C程序(如JOS内核)时,编译器将每个C源文件('. C ...

  5. MIT 操作系统实验 MIT JOS lab1

    JOS lab1 首先向MIT还有K&R致敬! 没有很好的开源环境我不可能拿到这么好的东西. 向每一个与我一起交流讨论的programmer致谢!没有道友一起死磕,我也可能会中途放弃. 跟丫死 ...

  6. gram矩阵的性质_第十七课:正交矩阵和GramSchmidt正交化——MIT线性代数课程学习笔记...

    公众号关注  "DL_NLP" 设为 "星标",重磅干货,第一时间送达! ◎ 原创 | 深度学习算法与自然语言处理 ◎ 作者 | 丁坤博 一. 知识概要 这一节 ...

  7. [MIT]微积分重点学习笔记 目录

    先介绍下自己的情况,大学的时候学习不认真,很多概念都忘记了,工作中有时要用到微积分,碰到不会的在网上查询,感觉这样学习的比较零散,也不能建立系统的认识.多次想要从头看一遍同济版<高等数学> ...

  8. MIT 6.s081学习笔记

    MIT 6.s081学习笔记 introduction 计算机组织结构: 最底部是一些硬件资源,包括了CPU,内存,磁盘,网卡 最上层会运行各种应用程序,比如vim,shell等,这些就是正在运行的所 ...

  9. JOS学习笔记(十)

    神说.进程要有多个.可以分片.切换.互不影响.  并要支持多任务.多处理器并行.事就这样成了.   于是 神造了多个内核栈,又开辟多个寄存器.   就把这些摆列在内存里.内核空间里.  管理多核,分别 ...

最新文章

  1. 一种NVMe SSD友好的数据存储系统设计
  2. IBM Watson:用人工智能提升美国零售业消费体验
  3. SAP ECC6.0-中建信息版
  4. Linux调试——gdb调试器的简单使用调试coredump文件
  5. Windows 8 应用开发 - 应用栏
  6. 4.1.9 文件系统的层次结构
  7. Intel 酷睿i5 6300HQ与Intel 酷睿i7 6700HQ哪个好
  8. xp系统中的隐藏文件不能显示 解决方案
  9. python爬取贴吧_Python爬取贴吧(简洁版)
  10. livevent的几个问题
  11. 章节3.4----队列的实现与应用
  12. 小程序毕设作品之微信小程序点餐系统毕业设计(5)任务书
  13. 物体检测学习笔记-3D相机成像原理简介
  14. ip扫描命令 linux,如何使用Linux扫描网络上的IP地址
  15. 内存规格的解释(Unbuffered DIMM,Registered DIMM和SODIMM)
  16. 数据结构 第七章 图(图的概念和存储)
  17. ftp 工具 绿色,四款将会让你爱不释手的绿色 ftp 工具
  18. html5是什么意思
  19. MySql快速复习,看这一篇就够了!
  20. SpringMVC @RequestBody和@ResponseBody原理解析

热门文章

  1. 【Sybase】常用SQL
  2. 设计原则—YAGNI
  3. 西邮Linux兴趣小组2021纳新试题
  4. bwh: s,fk_gfw,s
  5. Microsoft Message Queue(MSMQ:微软消息队列)简介
  6. Python 教你识别淘宝刷单,买到称心如意的商品
  7. pyautogui 滑动页面_pyautogui 使用方法简记
  8. 牛客 -- 小M和天平(简单dp)
  9. 面向服务的体系架构(SOA)—入门篇
  10. 【AD】元件,导线,电气符号放置与操作