一、概要

  • 了解x86汇编语言,学会用QEMU模拟x86环境,学会用GDB调试
  • BIOS负责执行基本的系统初始化,从某些适当的位置(如 floppy disk(软盘), hard disk, CD-ROM, or the network)加载操作系统,并将机器的控制权给操作系统(通过jmp指令将CS:IP设置为0000:7c00,0x7c00是the boot sector被加载到的物理地址)
  • the boot loader将处理器从16-bit real model切换到32-bit protected model,使得1M以上的内存地址可以访问(主要boot.S完成。将控制信号传递0x60与0x64端口,加载GDT表,将CR0的bit0位设为1,call bootmain)。还从硬盘中读取内核到内存并跳转到内核入口地址(主要main.c完成。先读取ELF program headers,然后通过ELFHDR内的信息将kernel加载到内存正确位置,最后跳转内核入口地址0x0010000c
  • the boot loader将内核加载到内存的物理地址(加载地址)是0x00100000,但内核在内存中的虚拟地址(链接地址)是0xf0100000
  • CR0中的两个控制位PG (Paging)和PE(Protection Enable)。只有在保护方式下(PE=1)分页机制才可能生效。PE=1, PG=1,分页机制生效,把线性地址转换为物理地址。PE=1, PG=0,分页机制无效,线性地址就直接作为物理地址。
  • 数据在内存中有大端(big-endian)格式–高尾端,尾端放在高地址处和小端(little-endian)格式—低尾端, 尾端放在低地址处
  • backtrace函数实现的关键在于ebp。当前ebp指向当前子程序的栈帧(frame)基地址,地址内存的是caller的栈帧基地址,里面存的又是再外层的caller的栈帧基地址,这样就可以区分出每一层程序的栈帧,从而实现回溯。

二、实验内容

首先是配置环境,下载虚拟机、ubuntu映像文件,安装git,拷贝下jos代码,安装qemu及其所需要的诸多包,./configure,make,然后克隆6.828mit,最后在lab文件目录下make。

1.PC Bootstrap

了解x86汇编语言

使用QEMU模拟器模拟x86的环境

了解PC的物理地址空间

  • 这个图还是很nice的,复制过来再说。
  • 早期PC,16位,只有可用物理地址只有1M(0x00000000~0x000FFFFF)
  • 最重要的部分是Basic Input/Output System (BIOS),它占据从0x000F0000到0x000FFFFF的64KB区域。在早期的pc机中,BIOS保存在read-only memory (ROM)中,现在pc机将BIOS存储在updateable flash memory(可更新的闪存)中。
  • BIOS负责执行基本的系统初始化,如激活显卡和检查已安装的内存数量。在执行此初始化之后,BIOS从某些适当的位置(如 floppy disk(软盘), hard disk, CD-ROM, or the network)加载操作系统,并将机器的控制权传递给操作系统。
  • 虽然现在可以处理器支持4GB的物理地址空间了,但还是保留那1MB的物理地址的设定来向后兼容

The Rom BIOS

  • 实模式下,地址20位(2^20=1MB)而寄存器都是16位,如何表示这20位的地址呢:physical address = 16 * segment + offset(把段地址左移4位加上段内偏移地址)
  • GDB’s disassembly of the first instruction是[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b由上一行的公式可以知道跳转的地址是0xffff0,即BIOS结束(0x100000)前的16个字节

2.The Boot Loader

  • pc机的Floppy and hard disks 被划分为512字节的 sectors(扇区)。扇区是磁盘的最小传输粒度:每个读或写操作的大小必须是一个或多个扇区,并且在扇区边界上对齐。如果磁盘是可引导的,则第一个扇区称为the boot sector(引导扇区)。
  • BIOS找到一个可引导的软盘或硬盘,它将512字节的引导扇区加载到物理地址0x7c00到0x7dff的内存中,然后使用jmp指令将CS:IP设置为0000:7c00,将控制权传递给the boot loader

Exercise 3

学习boot.s,main.c,boot.asm,了解在GDB中执行the boot loader时发生了什么?

  1. 将处理器从16位real model切换到32位protected model,使得1M以上的内存地址可以访问。(主要boot.S完成)
  2. 通过x86特定的I/O指令直接访问 IDE disk device registers,从而从硬盘中读取内核 (主要main.c完成,还会跳转到内核入口地址)

因为三个文件基本差不多,只是运行时有些标识符以地址形式显示,下面就只挂出实验过程

过程
  • 16位,关中断,设置方向标识,初始化段寄存器为0

  • 准备把CPU的工作模式从16位实模式转换为32位保护模式,使得1M以上的内存地址可以访问。D1指令代表下一次写入0x60端口的数据将被写入给804x控制器的输出端口,跳转判断就是看D1是否被取走,DF类似。

  • 加载全局描述符寄存器GDTR,GDT表是处理器在保护模式下非常重要的一个表。后面是在把CR0寄存器的bit0置1,CR0寄存器的bit0是保护模式启动位,把这一位值1代表保护模式启动。之后必须重新加载所有段寄存器的值,GDTR值才能生效

    lgdtw 0x7c64 : 这条指令是把指令后面的值所指定内存地址0x7c64处后6个字节的值输入全局描述符表寄存器GDTR

接下来就是call bootmain,开始分析main.c,因为三个文件基本差不多,只是运行时有些标识符以地址形式显示,下面就只挂出实验过程。

readseg()函数中pa &= ~(SECTSIZE-1) 功能就是把pa重新定向到offset存储单元所在的扇区的起始地址。

waitdisk(),这个函数用于查询当前磁盘的状态是否已经准备好进行操作。如果没有准备好,那么程序就会一直停在这里,直到磁盘准备好。

readseg((uint32_t)ELFHDR, SECTSIZE*8, 0)先把ELF头部信息读入内存pa处,大小大概是512*8,后面的for循环根据ELF头部信息里的信息把内核所有段读入内存,通过段地址定位所在扇区,将所需扇区读入。

boot/main.c中minimal ELF loader,将内核的每个部分从磁盘读取到内存中该部分的加载地址,然后跳转到内核的入口点。

问答
  • At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?{处理器从何时开始执行32位代码?究竟是什么导致了从16位模式到32位模式的转换?)
    答:在ljmp $PROT_MODE_CSEG, $protcseg(截图三中间)处开始执行32位代码。应该是经过64与60端口的控制,加载完GDT表后,CRO的bit0位为1,此时机器已处于保护模式,故处理器从16位模式转为32位模式。
  • What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?(引导加载程序执行的最后一条指令是什么?它刚刚加载完的内核的第一条指令是什么?)
    答:引导加载程序的最后一条指令是boot/main.c中bootmain函数最后的((void (*)(void)) (ELFHDR->e_entry))(); 这个第一条指令位于/kern/entry.S文件中,第一句 movw $0x1234, 0x472
  • Where is the first instruction of the kernel?(内核的第一条指令在哪里?)
    答:位于/kern/entry.S文件中
  • How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?(引导加载程序如何决定必须读取多少扇区才能从磁盘获取整个内核?它在哪里找到这些信息?)
    答:通过ELF program headers决定,他在操作系统内核映像文件的ELF头部信息里找到。

3.Exercise 4

pointers.c

指针的类型标志着这个指针指向数据的类型,有两个作用:

告诉了编译器需要从这个地址开始对多少字节(n)的数据进行操作, 以及操作模式
告诉编译器当对这个指针进行增减操作时,每加(减)一对应实际地址内存移动的字节数(n) 感谢这位仁兄的描述

int是4个字节,char占1个字节。
*c相当于a[1]=400

0x0062FDF0 |0x0062FDF1 |0x0062FDF2 |0x0062FDF3

    90          01           00          00

c = (int *) ((char *) c + 1); *c=500;
0x0062FDF0 |0x0062FDF1 |0x0062FDF2 |0x0062FDF3 |0x0062FDF4

    90         01->F4       00->01        00         2D->00

此时c影响的地址是0x0062FDF1~0x0062FDF4, 而a[1]的地址是0x62FDF0~0x62FDF3, a[2]是0x62FDF4~0x62FDF8, 故都受影响。

objdump 有点像那个快速查看之类的工具,就是以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。

Exercise 5

我把-Ttext 0x7c00 改成了 -Ttext 0x7d00,一直运行到lgdtw 0x7d64前好像都没有什么错误,内存地址全变了,但是机器指令跟汇编代码都没变。可能就是GDT表加载错误才导致后面加载失败,因为加载信息都错掉了。

Exercise 6

在BIOS进入 the boot loader时检查内存0x00100000处的8个字,然后在 the boot loader进入内核时再检查一次。

0x00100000往后8个字前后不一样应该是由于bootmain将内核的某个section存入了该地址处。由于要在进入内核时再看一次,故第二个断点应该设置在call entry处,
b *0x7d6b
又由下图可知,内核的入口地址是0x0010000c,也在此范围中,可能是.text段的内容,因为内核最先加载的就是.text,如下:

3.The Kernel

Exercise 7

VMA(链接地址),LMA(加载地址)。其中加载地址代表的就是这个段被加载到内存中后,它所在的物理地址。链接地址则指的是这个段希望被存放到的逻辑地址。

movl %eax, %cr0指令之后,0x00100000与0xf0100000后4个字完全相同了,我认为是分页后,0x00100000映射到了0xf0100000,完成了分页操作。

先make clean 再注释掉movl %eax, %cr0后,重新编译调试,断点设在0x0010000c处,一步一步运行,在movl $(bootstacktop),%esp指令处出错了。没有分页,不能访问0xf010002c,即访问的逻辑地址超出内存了。

Exercise 8

kern/console.c 中定义了如何把一个字符显示到console上,即我们的显示屏之上,里面包括很多对IO端口的操作。
lib/printfmt.c 中定义的子程序是我们能在编程时直接利用printf函数向屏幕输出信息的关键,是简化的原始printf格式例程?
kern/printf.c 中定义的就是我们在编程中会用到的最顶层的一些格式化输出子程序。

主要联系:
printfmt.c 调用printf.c中的putch函数
console.c调用printf.c中的cprintf函数
printf.c又调用printfmt.c的vprintfmt函数 以及 console.c中的cputchar函数

cputchar函数:打印一个字符到显示屏上
putch函数:调用cputchar函数,并记录一共打印了多少字符
vprintfmt函数:将参数fmt(eg. “%s have %d num”, s,n)打印,并将其中的转义字符(%s,%d)用对应参数(s,n)代替
cprintf函数:类似标准输出。有多个输入参数,调用vcprintf函数,vcprintf函数再调用vprintfmt函数实现打印。

练习补充代码,是仿无符号十六进制写的,感觉putch('0', putdat)可以不要:

果然应该不要这一句,多输出这个0导致在make grade的时候多扣了20分

问答

1.解释printf.c和console.c之间的接口。具体来说,console.c导出什么函数?printf.c是如何使用这个函数的?
答:console.c中没被static修饰的函数都可被其他文件调用,其中,cputchar子函数被printf.c在putch函数中调用了。

2.解释下面代码:

//crt_pos 当前字符将要输入的位置
//CRT_SIZE 能显示的最大字符数if (crt_pos >= CRT_SIZE) {int i;//将当前显示的内容全部往上滚动一行,留出一行空地memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)  crt_buf[i] = 0x0700 | ' ';crt_pos -= CRT_COLS;}

3.在调用cprintf()时,fmt指向什么?ap指向什么?
列出(按执行顺序)对cons_putc、va_arg和vcprintf的每个调用。对于cons_putc,也列出它的参数。对于va_arg,列出调用前后ap指向的内容。对于vcprintf,列出它的两个参数的值。

int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
//在kern/monitor.c的monitor函数中插入这代码,结果如下:


答:才知道断点可以设成 b kern/monitor.c:62真是亏大了。。。fmt指"x %d, y %x, z %d\n",ap就是指的 x, y, z。

4.输出是什么?按照前面的练习一步一步地解释这个输出是如何得到的。这是一个ASCII表,它将字节映射到字符。
输出取决于x86是little-endian这一事实。如果x86是big-endian,为了得到相同的输出,您会将i设置为什么?您是否需要将57616更改为不同的值?

unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);
//大端(big-endian)--高尾端,尾端放在高地址处   如0x11223344 中,44就是尾端
//小端(little-endian)---低尾端, 尾端放在低地址处
//运行结果如下:


答:%x指格式化无符号十六进制数,57616转换成16进制正好是e110。
%s格式化字符串(对应ASII码),0x00646c72在小端格式下对应ASII码查到是rld。
大端格式的话:应该是57616,i=0x726c6400 还是 i=0x00726c64 ???

5.在下面的代码中,'y='后面会打印什么?(注意:答案不是一个特定的值。)为什么会这样?

cprintf("x=%d y=%d", 3);
//结果如下: y的值没有给定,所以输出一个不确定的值


Challenge 改变控制台输出的颜色

int c一共32bit,其中高16位用来表示属性,低16位用来表示字符。因此,与0xff&运算就是去掉属性,只看字符内容。与~0xff&运算就是去掉字符,只看属性。与0x0700作 or运算就是设为默认属性。谢谢又一位仁兄

可以找到控制符号颜色的代码就是这里,可以试着把0x0700改成0x1700,0x0500,0x0200,可以发现,都并木有什么变化。。。然后,然后我就一直没改成功。。。

Exercise 9

确定内核初始化堆栈的位置,以及堆栈在内存中的确切位置。内核如何为堆栈保留空间?堆栈指针初始化为指向该保留区域的哪个“端”?

在entry.S文件可以找到这段代码,用以初始化堆栈的位置。这段完成了boot stack(0xf0108000~0xf0110000),预留大小是KSTKSIZE(32KB)栈顶是$(bootstacktop),存在esp中。esp初始指向高位端 ??? (由于堆栈是向下增长的,所以在运行init函数之前,esp寄存器的值就是0xf0110000,代表堆栈尚未使用)

Exercise 10

要熟悉x86上的C调用约定,请在obj/kern/kernel.asm中找到test_backtrace函数的地址(0xf0100040)。在这里设置一个断点,并检查内核启动后每次调用它时发生了什么。test_backtrace的每个递归嵌套级别在堆栈上推多少个32位字,这些字是什么?

执行test_backtrace(5)前:esp:0xf010ffdc ebp:0xf010fff8

执行完这四条之后,可以发现
test_backtrace(5)的栈帧范围是 esp:0xf010ffc8 ebp:0xf010ffd8,同理
test_backtrace(4)的栈帧范围是 esp:0xf010ffa8 ebp:0xf010ffb8
test_backtrace(3)的栈帧范围是 esp:0xf010ff88 ebp:0xf010ff98
test_backtrace(2)的栈帧范围是 esp:0xf010ff68 ebp:0xf010ff78
test_backtrace(1)的栈帧范围是 esp:0xf010ff48 ebp:0xf010ff58
我不太确定这是什么意思。。。我感觉这应该是表示了栈里面的情况
里面每一行的意义我认为是这样的 ???(以前两行为例):
0xf010ffa8:0x00000000 0xf010ffac:0x00000000 0xf010ffb0:0x00000000 0xf010ffb4:0x00000005
0xf010ffb8:0xf010ffd8 0xf010ffbc:0xf0100068 0xf010ffc0:0x00000004 0xf010ffc4:0x00000005
然后0xf010ffe0、0xf010ffc0、0xf010ffa0、0xf010ff80、0xf010ff60存的是参数x的值

Exercise 11

实现原理:C函数调用时,首先将参数push入栈,然后push返回地址,接着将原来的EBP push入栈,然后将ESP的值赋给EBP,令ESP指向新的栈顶。而函数返回时,会将EBP的值赋予ESP,然后pop出原来的EBP的值赋予EBP指针。 谢谢仁兄

谢谢师兄的指点,让我大致明白了C语言函数调用时栈的变化,并写出了对应函数。师兄说在linux里尽量不用可视化界面,所以后面代码可能会试着用vim编辑。


之前结束条件写错了,少了一行,改过来又加了十分,美滋滋。

Exercise 12

修改堆栈回溯函数以显示每个eip对应的函数名、源文件名和行号。
在debuginfo_eip中,__stab_*来自哪里?(确实不太懂

想要在debuginfo_eip()函数中调用stab_binsearch(),关键得弄懂stab(symbol table)结构体与Eipdebuinfo结构体内参数的意义:

实在不是很懂所以参考了下这位仁兄的介绍

struct StabSymnum是符号索引,换句话说,整个符号表看作一个数组,Symnum是当前符号在数组中的下标n_type是符号类型,FUN指函数名,SLINE指在text段中的行号n_othr目前没被使用,其值固定为0n_desc表示在文件中的行号n_value表示地址。特别要注意的是,这里只有FUN类型的符号的地址是绝对地址,SLINE符号的地址是偏移量,其实际地址为函数入口地址加上偏移量。比如第3行的含义是地址f01000b8(=0xf01000a6+0x00000012)对应文件第34行。

然后stab_binsearch()函数其实看得并不太懂,但是按照hint一步一步下来也知道写,反正有addr,又从stab.h中找到type设为N_SLINE,那直接调用会得到addr对应在stabs表中得Symnum(相当于下标),存在*regin left


emmm,错了,压根没有赋值info->eip_line。。。快改:

然后在mon_backtrace()函数中调用debuginfo_eip()函数。我是这样写的,struct Eipdebuginfo *info;debuginfo_eip(eip,info)老提示没有初始化
只好改成struct Eipdebuginfo info; debuginfo_eip(eip,&info),我好像还没太弄清楚*和&的区别。改完之后变成这样:
line上面已经改对了,任务就只剩下去掉text_backtrace:F(0,20)后面得尾巴以及改对偏移量了。
尾巴好去,提示里使用printf("%.*s", length, string);就可以打印string,长度最多length,恰好,info.eip_fn_namelen都已经给出了length了。只是具体为什么可以做到,不懂
偏移量是输出eip-info.eip_fn_addr,这里也不太理解,绝对地址与相对地址 ???

终于。。。不容易啊。。。

最后就是给kernel模拟器添加backtrace命令,比较简单,仿照kerninfo命令在monitor.c里加一行就行

三、存在问题

  1. C语言中指针还不是很透彻
  2. 内核内存地址不太理解
  3. 对汇编与反汇编不熟悉
  4. 对于GDB的使用不熟练
  5. 阅读英文内容很吃力,过于依靠有道
  6. 实验很难独立完成,参考别人实验过程过多
  7. 有些问题还未找到确定答案,如Exercise 5里修改地址之后运行错误的原因不确定;Exercise 7里分页后的变化不清晰,加载地址与链接地址的对应很模糊;Challeng改变控制台颜色失败;Exercise 8后面问答第3题第6题不会;Exercise 12中__stab_*来自哪里不懂,对于函数名和偏移量的打印也不会

MIT6.828学习之Lab1相关推荐

  1. MIT6.828学习之homework2:shell

    之前同学提起,能区分得开xv6与JOS吗?才发现真不知道,赶紧查了查: 1.extended inspection of xv6, a traditional O/S xv6,一个传统的操作系统的扩展 ...

  2. MIT6.828学习之homework9:Barriers

    在本作业中,我们将探讨如何使用pthread库提供的条件变量来实现barrier.barrier是应用程序中的一个点,在这个点上,所有线程都必须等待,直到所有其他线程也到达该点.条件变量是一种序列协调 ...

  3. MIT6.828——LAB1:Booting a PC

    MIT6.828--LAB1:Booting a PC Part1:PC Bootstrap 练习1: 熟悉X86汇编语言 The PC's Physical Address Space 电脑的物理地 ...

  4. MIT6.828课程学习初步

    MIT6.828课程学习初步 MIT6.828课程是1门比较好的操作系统原理课程,通过动手实践xv6操作系统来熟悉原理. [ 课程网站 ] 原先是想从Linux内核开始看起,但是看了一段时间,由于都是 ...

  5. Mit6.S081学习记录

    Mit6.S081学习记录 前言 一.课程简述 二.课程资源 1,课程主页 2,参考书 3,实验环境 三.学习过程 Mit6.S081-实验环境搭建 Mit6.S081-GDB使用 Mit6.S081 ...

  6. MIT6.828 32位操作系统笔记(3)----系统的启动和初始化

    MIT EDU 6.828 实验源代码 分类 MIT6.828 32位操作系统实验笔记 实验完善代码 LAB2-4下载链接 提取码:79t8 系统的启动过程 物理内存的分布 首先分析PC 开机以后的默 ...

  7. MIT6.828课程JOS在macOS下的环境配置

    本文将介绍如何在macOS下配置MIT6.828 JOS实验的环境. 写JOS之前,在网上搜寻JOS的开发环境,很多博客和文章都提到"不是32位linux就不好配置,会浪费大量时间在配置环境 ...

  8. MIT6.828 异常和中断学习笔记

    csapp分类: Exception(异常),分为同步异常和异步异常,本质都是将控制交给kernel解决的. 异步异常,也称为中断(Interrupt)指由处理器外部的事引起的,计时器中断和I/O中断 ...

  9. MIT6.828 lab1 exercise 23

    relevant source: 1. Brennan's Guide to Inline Assembly http://www.delorie.com/djgpp/doc/brennan/bren ...

最新文章

  1. 在centos服务器安装MySQL数据库详细步骤
  2. JSON.parse使用 之 Unexpected token o in JSON at position 1 报错原因
  3. ArcGIS实验教程——实验八:矢量数据拼接
  4. 你以后会不会有小三?
  5. python连载第七篇~python世界里的注释符号
  6. gtk不是C语言的专属,c++也可以
  7. 智能机器人语音识别技术
  8. Fullpage:基础学习
  9. Luogu3381【模板】最小费用最大流
  10. android定位坑简书,android webview 定位问题
  11. 人工智能--遗传算法求解TSP问题
  12. 在 Shell 脚本中调用另一个 Shell 脚本的三种方式
  13. MuJoCo及mujoco_py安装(以及troubleshooting)
  14. android存储pdf文件怎么打开,android打开pdf文件
  15. Windows下基础免杀技术
  16. TabLayout自定义指示器及样式
  17. Kubernetes IPVS和IPTABLES
  18. 23最新《Android音视频开发进阶指南》,音视频开发者速领
  19. java同一个包是什么意思_java中包到底是什么意思?包访问权限又是什么意思,有C++基础。...
  20. 蜗牛爬树c语言程序函数,弹涂鱼上树吃蜗牛?鄂教版小学二年级课文遭吐槽

热门文章

  1. python怎么解矩阵方程_基于python解线性矩阵方程(numpy中的matrix类)
  2. java培训班值得去吗?
  3. 阿里云ECS服务器常用入门配置命令
  4. How to manually generate ssl certificate for own site in Linux
  5. Android 9.0蓝牙音乐上一首、下一首、暂停和播放
  6. 开源 iOS 项目分类索引大全
  7. html标签的message,Message 消息提示
  8. 抓取淘宝某类商品名称和价格
  9. 汽车技术管理系统c语言,[源码和文档分享]基于C语言实现的汽车牌照的快速查询...
  10. 52单片机定时器0-2实现1ms定时