6.828 lab1 bootload

MIT6.828 lab1地址:http://pdos.csail.mit.edu/6.828/2014/labs/lab1/

6.828 lab1 bootload6.828 lab1 bootload

第一个练习,主要是让我们熟悉汇编,嗯,没什么好说的。

Part 1: PC Bootstrap

首先,整个实验使用qemu这款模拟软件来,来对代码进行调试,相当于我们在qemu这个模拟的计算机平台上,运行自己的程序。可以再qemu这个软件上进行gdb的调试,比较方便。

首先看下整个内核在qemu上的模拟的结果:

6.828 lab1 bootload

6.828 lab1 bootload

整个内核现在能实现的就两个功能,一个kerninfo,显示内核的一些简单信息,还有个help,暂时什么都没有。这里先来看下kerninfo里面的东西,主要是内核的一些入口地址,数据,程序地址什么的。这里可以看到,虚拟内存和物理内存的映射就是简单的线性映射,比较直观。

接下来看一下系统的具体的启动过程是怎么样的:

6.828 lab1 bootload

6.828 lab1 bootload

这张图是整个系统的物理内存的分布。主要看一下地址在0x100000(1MB)以下的内存分布情况,系统启动的时候主要是在1M以下的地方活动。对于很多PC来说,1M一下的存储布局都是差不多的,这个主要是因为最开始的Intel的的8088一开始内存只有1M,所以后面的计算机为了向下兼容,在1M一下的内存布局都是这样的。

这里主要是看下BIOS,BIOS是Basic Input/Output System,主要的工作是做各种初始化,比如检查一下内存的大小,还有显卡等一些外设的初始化,然后会载入OS,运行内核。

接下来看一下BIOS的具体的引导步骤:

首先,打开两个终端,分别输入命令:make qemu-gdb和gdb

打开gdb,就可以看到下面的内容:

6.828 lab1 bootload

6.828 lab1 bootload

程序是从地址0xffff0开始的,而且一开始就来一个长跳转。开始地址是0xffff0的原因是,以前的PC因为只有1M的内存,采用的是实地址模式,为了在开机和重启的时候能够保证跳到BIOS里面,就把开始的地址设定为0xffff0,因为那个地址已经非常的接近1M地址的顶部,在那里不会有其他程序。

可以看到,在左边,有一个[f000:fff0],程序地址就是通过这个来计算的。这两个分别是寄存器cs和eip的值。

6.828 lab1 bootload

通过gdb也可以看到,eip是0xfff0,cs是0xf6.828 lab1 bootload000。其中,cs是段地址。程序地址的寻址是通过下面的方法来计算的:

addr=cs*16+eip

通过计算,就可以得到现在程序指向的地址:0xffff0

程序地址这么计算的原因是因为,在以前的8088上,地址线有20根,而数据线只有16根,这样就没有办法用16根数据线来表示20位的地址。所以后面就用这个方法,采用段地址,来进行数据线的扩展,来进行寻址操作。后面的计算机数据线和地址线就没有这个问题了,但为了向下兼容以前的程序,在BIOS里面,还是采用这种方式来进行寻址。

6.828 lab1 bootload

6.828 lab1 bootload

练习二:熟悉gdb的si指令。

6.828 lab1 bootload

接下来的代码,感觉没什么特别的,然后下面的就全是BIOS的一些常规设置。

Part 2: The Boot Loader

等BIOS执行完之后,接下来就会跳转到引导扇区了,引导扇区中的内容被再到内存地址为0x7c00---0x7dff上,然后执行引导程序。
If the disk is bootable, the first sector is called the boot sector, since this is where the boot loader code resides. When the BIOS finds a bootable floppy
or hard disk, it loads the 512-byte boot sector into memory at physical addresses 0x7c00 through 0x7dff, and then uses a jmp instruction to set the CS:IP to 0000:7c00, passing control to the boot loader.
引导程序主要做两件事:1.将系统从实模式转化为32位保护模式,在保护模式下,才能访问全部的地址空间(>1M的地址)2.载入内核文件。
关于保护模式和实地址模式:

In the protected mode, selector values are interpreted completely differently than in real mode. In real mode, a selector value is a paragraph number of physical memory. In protected mode,
a selector value is an index into a descriptor table. In both modes, programs are divided into segments. In real mode, these segments are at fixed positions in physical memory and the selector value denotes the paragraph number of the beginning of the segment.
In protected mode, the segments are not at fixed positions in physical memory. In fact, they do not have to be in memory at all!

In protected mode, each segment is assigned an entry in a descriptor table. This entry hasall the information that the system needs to know about the segment. Thisinformation includes: is
it currently in memory; if in memory, where is it;access permissions (e.g., read-only). The index of the entry of the segment is the selector value that is stored in segment registers.

6.828 lab1 bootload6.828 lab1 bootload

 

直接在gdb中打断点,到0x7c00

6.828 lab1 bootload

其中,进行一系列的设置,并且载入了GDT,来进入保护模式。最后一句:

mov %eax,%cr0

eax当时的值是0x11,cr0被赋值为0x11.

来看一下cr0寄存器:

6.828 lab1 bootload

6.828 lab1 bootload

可以看到,上面的指令是将cr0的第0位置1,PE=1时,系统进入保护模式。

所以系统从0x7c2a之后,就进入了保护模式。

这里就可以回答第一个问题:

  • At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?
  • 系统从0x7c2a之后就开始实行了32位编码。从实模式转换到保护模式,使系统从16位转到了32位。
 
6.828 lab1 bootload
6.828 lab1 bootload

接下来可以看到,程序设置了各种段寄存器,并且最后设置了栈指针为0x7c00,因为引导程序是在0x7c00上的,而栈是向下增长的,所以,在引导程序里面,就把栈地址0x7bff(0x7c00-1,push会先减4(一个字的大小),在存入数据)和下面的空间设置为程序的栈空间,这一段地址空间是属于上面提到的1M地址空间里面的low memory的区域。

接下来,程序进入了bootmain(0x7d9b),程序的主要作用是去引导ELF kernel image的。

bootmain程序里面的最后一句:

6.828 lab1 bootload6.828 lab1 bootload

从这里可以看到,程序进入了entry,即内核的开始入口地址,而这也是整个boot程序的最后一句。

  • What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?
  • 最后一句的就是上面的那句转入entry的代码。内核的第一句指令通过查看entry地址可以看到。要找entry的地址,只要反汇编内核程序就可以
  • 6.828 lab1 bootload

在反汇编得到的信息里面查找entry,可以得到entry的地址:

6.828 lab1 bootload

6.828 lab1 bootload

entry的地址就在0xf010000c里,不过那个是虚拟地址,实际的载入地址是0x10000c,这里后来看了其他文章,知道0xf010000c被硬件转换为0x10000c,在以前的6.828课程里面,这种转换是手动转化,现在成了硬件自动转换:

09年和10年的课程代码:(还是以前的代码清晰,现在的代码没有手动转换,一开始在0xf010000c位置打断点,等了半天)

6.828 lab1 bootload

在那个地址打断点,得到的指令是:

6.828 lab1 bootload

接下来第三个问题也可以回答了:

3.Where is the first instruction of the kernel?

0x10000c第一条指令位置。

4.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文件知道需要的信息。下面是通过反汇编命令(objdump -h kernel)得到的文件头,应该是载入这些:

6.828 lab1 bootload

Loading the Kernel

6.828 lab1 bootload
ex4主要是先来一遍C指针的坑,里面的东西比较简单,都是基本的C指针,就不展开了。
接下来主要讲的是一些关于ELF的东西,ELF里面有一个固定大小的ELF header,里面主要要注意的是以下部分:
.text:程序的代码部分
.rodate:程序的只读数据部分。
.date:程序中被初始化的全局变量部分。
.bss:程序未初始化全局变量的保存地址。程序不给.bss段分配额外空间,只是记录其地址空间的大小,因为未初始化全局变量默认初始化为0;的未初始化全局变量默认为0;
通过反汇编命令:
objdump -h obj/kern/kernel
 
就可以查看,上面有图,EFL好像主要是给链接器用的,主要作用可以当成是程序载入物理内存的一个指引。
6.828 lab1 bootload
这里主要注意VMA是虚拟地址,LMA是载入地址,也就是这段内容需要被载入的物理内存的地址。在最后会有个LOAD的标志,表示需要被载入,想.bss和.comment里面的内容就不用被载入内存。
 
6.828 lab1 bootload

第五个练习主要是要改变链接器的链接地址,这里一开始按照题目要求盖面boot/Makefrag里面的-text的地址没有用……

后来知道,是要改变kernel.ld里面的内核载入地址的值:

6.828 lab1 bootload

这里吧AT(0x10000)里面的地址改变,就会出错,qemu一直在booting from hard disk界面跳,没有办法载入内核。

6.828 lab1 bootload

bootload的程序的入口是0x7c00,打断点,查看内存地址:

6.828 lab1 bootload

在0x10000开始的地方,全部都是0.接下来是内存入口的地址0x10000c打断点

6.828 lab1 bootload

可以看到,在进入内核后,在0x10000地址的物理内存的地方,已经有了程序了。这个很好解释,因为内核的程序载入是在bootload的程序里面完成的,在刚刚进入bootload程序的时候,0x10000地址里面当然是空的。在bootload执行完成之后,内核程序已经载入了内存。

Part 3: The Kernel

6.828 lab1 bootload

这个主要讲保护模式下,虚拟内存的一些东西。

6.828 lab1 bootload

可以看到两句指令:

or $0x80010001,%eax
mov %eax,%cr0

这两条指令是把cr0的PE,PG,WP三个控制开关置1.

PE:置位是进入保护模式。启用分段管理模式。当PE=1,PG=0,此时只进入分段模式,所有的线性地址等于物理地址。

PG: 置位是进入了分页模式,线性地址需要经过一定的转化才是物理地址

WP: 当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作。

此时查看0x100000和0xf0100000,地址都是实际的物理地址,没有经过转化,所以0xf0100000和0x100000是没有关系的,不存在映射:

6.828 lab1 bootload

当设置了cr0寄存器之后,启动了分页机制,0x10000和0xf010000是对应的,具有映射关系,其实0xf0100000就是物理内存上0x100000上的内容。

6.828 lab1 bootload

如果分页机制没有准确执行的话,那通过指令:

mov $0xf01002f,%eax
jmp *%eax

当执行到jmp的时候,就会报错,因为没有启动分页模式,那0xf010002f就是实际的物理地址,那个地址里面是0,跳转过去无法执行指令。

Formatted Printing to the Console

6.828 lab1 bootload

第八个实验主要是看print(),这个我会在另外一篇详细分析一下print()这个函数。

The Stack

6.828 lab1 bootload

后面的实验主要是将栈,这个玩过csapp的实验,还是可以接受的。

首先是内核栈的初始化:

6.828 lab1 bootload

可以看到,设置完了cr0寄存器,马上就设置了esp寄存器,esp设置为0xf0110000.

6.828 lab1 bootload

最后一个练习主要是写一个backtrace函数,这个主要是根据现在已知的ebp和esp,采用回溯的方法,来得到程序的frame信息。有点类似于gdb里面的bt命令。这个只要了解在程序调用和程序返回的过程中,esp,eip,ebp三个指针的变化过程就可以了。在另一篇文章里面详细讲述了这方面的东西,这里就不展开了。

最后写的代码:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{ struct Eipdebuginfo info;
unsigned int *ebp=(unsigned int *)read_ebp();
unsigned int *esp=(unsigned int *)read_esp();
unsigned int *eip=0;
unsigned int arg[5];
int i=0;
while(ebp)
{
for(i=0;i<5;i++)
arg[i]=*(ebp+i+2);
eip=ebp+1;
debuginfo_eip(*eip,&info);
cprintf(" ebp %08x eip %08x args ",(unsigned int)ebp,*eip );
for(i=0;i<5;++i)
cprintf("%08x ", arg[i]);
cprintf("\n"); cprintf("\t\t%s:%u:%.*s+%u\n",
info.eip_file,
info.eip_line,
info.eip_fn_namelen,
info.eip_fn_name,
*eip-info.eip_fn_addr);
esp=ebp+2;
ebp=(unsigned int *)*ebp;
}
return 0;
}

结果:

6.828 lab1 bootload

下面的是在gdb里面bt命令给出的结果:

6.828 lab1 bootload

可以看到,eip都是对的,还有调用的函数也是对的。但是位置有点问题,这个主要是通过它给的程序,程序通过stba来给出调试信息,stba这方面的东西也不是太懂,就不深究了。下次看看有机会碰到这方面的内容再说吧。

版权声明:本文为博主原创文章,未经博主允许不得转载。

上一篇:SpringBoot的重要特性


下一篇:python 操作PostgreSQL