MIT6.828 lab1地址:http://pdos.csail.mit.edu/6.828/2014/labs/lab1/
第一个练习,主要是让我们熟悉汇编,嗯,没什么好说的。
Part 1: PC Bootstrap
首先,整个实验使用qemu这款模拟软件来,来对代码进行调试,相当于我们在qemu这个模拟的计算机平台上,运行自己的程序。可以再qemu这个软件上进行gdb的调试,比较方便。
首先看下整个内核在qemu上的模拟的结果:
整个内核现在能实现的就两个功能,一个kerninfo,显示内核的一些简单信息,还有个help,暂时什么都没有。这里先来看下kerninfo里面的东西,主要是内核的一些入口地址,数据,程序地址什么的。这里可以看到,虚拟内存和物理内存的映射就是简单的线性映射,比较直观。
接下来看一下系统的具体的启动过程是怎么样的:
这张图是整个系统的物理内存的分布。主要看一下地址在0x100000(1MB)以下的内存分布情况,系统启动的时候主要是在1M以下的地方活动。对于很多PC来说,1M一下的存储布局都是差不多的,这个主要是因为最开始的Intel的的8088一开始内存只有1M,所以后面的计算机为了向下兼容,在1M一下的内存布局都是这样的。
这里主要是看下BIOS,BIOS是Basic Input/Output System,主要的工作是做各种初始化,比如检查一下内存的大小,还有显卡等一些外设的初始化,然后会载入OS,运行内核。
接下来看一下BIOS的具体的引导步骤:
首先,打开两个终端,分别输入命令:make qemu-gdb和gdb
打开gdb,就可以看到下面的内容:
程序是从地址0xffff0开始的,而且一开始就来一个长跳转。开始地址是0xffff0的原因是,以前的PC因为只有1M的内存,采用的是实地址模式,为了在开机和重启的时候能够保证跳到BIOS里面,就把开始的地址设定为0xffff0,因为那个地址已经非常的接近1M地址的顶部,在那里不会有其他程序。
可以看到,在左边,有一个[f000:fff0],程序地址就是通过这个来计算的。这两个分别是寄存器cs和eip的值。
通过gdb也可以看到,eip是0xfff0,cs是0xf000。其中,cs是段地址。程序地址的寻址是通过下面的方法来计算的:
addr=cs*16+eip
通过计算,就可以得到现在程序指向的地址:0xffff0
程序地址这么计算的原因是因为,在以前的8088上,地址线有20根,而数据线只有16根,这样就没有办法用16根数据线来表示20位的地址。所以后面就用这个方法,采用段地址,来进行数据线的扩展,来进行寻址操作。后面的计算机数据线和地址线就没有这个问题了,但为了向下兼容以前的程序,在BIOS里面,还是采用这种方式来进行寻址。
练习二:熟悉gdb的si指令。
接下来的代码,感觉没什么特别的,然后下面的就全是BIOS的一些常规设置。
Part 2: The Boot Loader
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.
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.
直接在gdb中打断点,到0x7c00
其中,进行一系列的设置,并且载入了GDT,来进入保护模式。最后一句:
mov %eax,%cr0
eax当时的值是0x11,cr0被赋值为0x11.
来看一下cr0寄存器:
可以看到,上面的指令是将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位。
接下来可以看到,程序设置了各种段寄存器,并且最后设置了栈指针为0x7c00,因为引导程序是在0x7c00上的,而栈是向下增长的,所以,在引导程序里面,就把栈地址0x7bff(0x7c00-1,push会先减4(一个字的大小),在存入数据)和下面的空间设置为程序的栈空间,这一段地址空间是属于上面提到的1M地址空间里面的low memory的区域。
接下来,程序进入了bootmain(0x7d9b),程序的主要作用是去引导ELF kernel image的。
bootmain程序里面的最后一句:
从这里可以看到,程序进入了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的地址,只要反汇编内核程序就可以
在反汇编得到的信息里面查找entry,可以得到entry的地址:
entry的地址就在0xf010000c里,不过那个是虚拟地址,实际的载入地址是0x10000c,这里后来看了其他文章,知道0xf010000c被硬件转换为0x10000c,在以前的6.828课程里面,这种转换是手动转化,现在成了硬件自动转换:
09年和10年的课程代码:(还是以前的代码清晰,现在的代码没有手动转换,一开始在0xf010000c位置打断点,等了半天)
在那个地址打断点,得到的指令是:
接下来第三个问题也可以回答了:
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)得到的文件头,应该是载入这些:
Loading the Kernel
objdump -h obj/kern/kernel
第五个练习主要是要改变链接器的链接地址,这里一开始按照题目要求盖面boot/Makefrag里面的-text的地址没有用……
后来知道,是要改变kernel.ld里面的内核载入地址的值:
这里吧AT(0x10000)里面的地址改变,就会出错,qemu一直在booting from hard disk界面跳,没有办法载入内核。
bootload的程序的入口是0x7c00,打断点,查看内存地址:
在0x10000开始的地方,全部都是0.接下来是内存入口的地址0x10000c打断点
可以看到,在进入内核后,在0x10000地址的物理内存的地方,已经有了程序了。这个很好解释,因为内核的程序载入是在bootload的程序里面完成的,在刚刚进入bootload程序的时候,0x10000地址里面当然是空的。在bootload执行完成之后,内核程序已经载入了内存。
Part 3: The Kernel
这个主要讲保护模式下,虚拟内存的一些东西。
可以看到两句指令:
or $0x80010001,%eax
mov %eax,%cr0
这两条指令是把cr0的PE,PG,WP三个控制开关置1.
PE:置位是进入保护模式。启用分段管理模式。当PE=1,PG=0,此时只进入分段模式,所有的线性地址等于物理地址。
PG: 置位是进入了分页模式,线性地址需要经过一定的转化才是物理地址
WP: 当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作。
此时查看0x100000和0xf0100000,地址都是实际的物理地址,没有经过转化,所以0xf0100000和0x100000是没有关系的,不存在映射:
当设置了cr0寄存器之后,启动了分页机制,0x10000和0xf010000是对应的,具有映射关系,其实0xf0100000就是物理内存上0x100000上的内容。
如果分页机制没有准确执行的话,那通过指令:
mov $0xf01002f,%eax
jmp *%eax
当执行到jmp的时候,就会报错,因为没有启动分页模式,那0xf010002f就是实际的物理地址,那个地址里面是0,跳转过去无法执行指令。
Formatted Printing to the Console
第八个实验主要是看print(),这个我会在另外一篇详细分析一下print()这个函数。
The Stack
后面的实验主要是将栈,这个玩过csapp的实验,还是可以接受的。
首先是内核栈的初始化:
可以看到,设置完了cr0寄存器,马上就设置了esp寄存器,esp设置为0xf0110000.
最后一个练习主要是写一个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;
}
结果:
下面的是在gdb里面bt命令给出的结果:
可以看到,eip都是对的,还有调用的函数也是对的。但是位置有点问题,这个主要是通过它给的程序,程序通过stba来给出调试信息,stba这方面的东西也不是太懂,就不深究了。下次看看有机会碰到这方面的内容再说吧。
版权声明:本文为博主原创文章,未经博主允许不得转载。