MIT6.828 LAB1 PART3 The Kernel

Exercise 7

实验内容

一、使用Qemu和GDB去追踪JOS内核文件,并且停止在movl %eax, %cr0指令前。此时看一下内存地址0x00100000以及0xf0100000处分别存放着什么。然后使用stepi命令执行完这条命令,再次检查这两个地址处的内容。确保你真的理解了发生了什么。

二、如果这条指令movl %eax, %cr0并没有执行,而是被跳过,那么第一个会出现问题的指令是什么?我们可以通过把entry.S的这条语句加上注释来验证一下。

实验过程

一、

在0x0010000c处设置断点,这是内核入口地址处。在碰到movl %eax, %cr0指令之前0x00100000和0xf0100000的地址处存放的内容如下图所示。

 然后输入si命令,使得执行到movl %eax, %cr0指令,再查看这两个位置的内存内容。我们发现两处存放的值已经一样了,原本存放在0xf0100000处的内容映射到了0x00100000处。

原因是操作系统内核程序在虚拟地址空间中通常被链接到非常高的虚拟地址空间处,如0xf0100000,使得低地址空间能够被用于用户编程。但是很多机器并不支持0xf0100000这么大的物理内存,因此无法把这个虚拟地址映射到相同的物理地址。所以在实际操作中我们把内核程序放到低地址空间如0x0010000,然后再通过某个机构把0xf0100000虚拟地址空间映射到0x00100000物理地址空间。

 二、

没有进行分页管理,所以不会进行虚拟地址到物理地址的转化。逻辑地址的访问超出了内存。

================================================================================================

Exercise 8

在这个练习中,要阅读以下三个源文件的代码:kern/printf.c,kern/console.c,lib/printfmt.c。

首先打开kern/printf.c文件,其中的vcprintf和cprintf子程序均调用了vprintmt函数,putch子程序调用了cputchar函数

lib/printfmt.c中的某些程序也调用了cputchar子程序。

cputchar函数在kern/console.c中定义,所以可以得出结论,kern/printf.clib/printfmt.c两个文件的功能依赖于kern/console.c。所以我们就首先研究kern/console.c。

1.kern/console.c

cpuchar函数:打印一个字符到显示屏上。它是最高层的console的IO控制程序。

vprintfmt函数:将参数fmt(eg. “%s have %d num”, s,n)打印,并将其中的转义字符(%s,%d)用对应参数(s,n)代替

2.lib/printfmt.c

这个文件包含的是打印各种样式的子程序,经常被其他打印函数调用。因此这个文件中定义的子程序是能够利用printf函数向屏幕输出的关键。

cprintf函数:类似标准输出。有多个输入参数,调用vcprintf函数,vcprintf函数再调用vprintfmt函数实现打印。

问答

可见答案于https://www.cnblogs.com/fatsheep9146/p/5070145.html

================================================================================================

Exercise 9

1. 判断操作系统内核是从哪条指令开始初始化它的堆栈空间的

A:%esp和%ebp两个寄存器和堆栈息息相关。在进入内核之前,也就是执行entry指令之前,并没有对%esp,%ebp的内容进行修改,所以在bootmain中未对堆栈空间进行初始化。

进入entry.S,最后一条指令是调用i386_init()子程序,这个子程序位于init.c文件中,对操作系统进行一些初始化工作。可见在进入i386_init()子程序之前应该已经把内核的堆栈空间设置好了。找到call i386_init指令之前的语句,与%esp和%ebp寄存器相关。是这两条语句初始化堆栈空间。

movl    $0x0,%ebp            # nuke frame pointer
movl    $(bootstacktop),%esp

 2.这个堆栈坐落在内存的什么地方?

在gdb中设置一个断点:b *0x7d6b,是进入entry的最后一条指令。通过si命令进行追踪。

(gdb) si
=> 0x10000c:	movw   $0x1234,0x472

追踪到两条关键指令

75		movl	$0x0,%ebp			# nuke frame pointer
(gdb) si
=> 0xf0100034 <relocated+5>:	mov    $0xf011a000,%esp
relocated () at kern/entry.S:78
78		movl	$(bootstacktop),%esp

这两条指令分别设置了两个与堆栈相关的寄存器的值。其中%ebp被修改为0,%esp被修改被bootstacktop的值。这个值为0xf0110000.

在数据段定义栈顶bootstacktop之前,首先分配了KSTKSIZE=32KB的存储空间,又因为栈顶指针指向0xf01100000,所以堆栈的内存空间为0xf0108000-0xf0110000。

3.内核是如何给它的堆栈保持一块内存空间的?

在entry.S中的数据段里面声明一块大小为32KB的空间作为堆栈使用。

4.堆栈指针是指向被保留区域的哪一端?

堆栈是向下生长的,所以堆栈指针指向最高地址,也就是bootstacktop的值,把这个值赋给堆栈指针寄存器。若esp寄存器的值是0xf0110000,代表堆栈尚未使用。

================================================================================================

Exercise 10

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

test_backtrace子程序的地址是f0100040.

在kern/init.c文件中找到了test_backtrace子程序的C语言代码。可以看到test_backtrace程序是一个递归调用的程序。

// Test the stack backtrace function (lab 1 only)
void
test_backtrace(int x)
{
        cprintf("entering test_backtrace %d\n", x);
        if (x > 0)
                test_backtrace(x-1);
        else
                mon_backtrace(0, 0, 0);
        cprintf("leaving test_backtrace %d\n", x);
}

 在i386_init子程序中调用test_backtrace(5),地址为f01000e8.

 

因此,在gdb中设置断点b *0xf0100e8,并且通过info r指令获取esp,ebp寄存器的内容。

(gdb) b *0xf01000e8
Breakpoint 1 at 0xf01000e8: file kern/init.c, line 41.
(gdb) c
Continuing.
=> 0xf01000e8 <i386_init+66>:	movl   $0x5,(%esp)

Breakpoint 1, i386_init () at kern/init.c:41
41		test_backtrace(5);  //Lab1

(gdb) info r
esp            0xf0119fe0	0xf0119fe0
ebp            0xf0119ff8	0xf0119ff8

在进入test_backtrace函数之前,栈顶位置是0xf0119fe0(为啥超出了内核堆栈使用空间???,查看到i386_init函数的堆栈空间,也超出了内核堆栈使用空间。可能代码有什么问题,回头再解决,先做下一个exercise)

可能是因为我使用的代码是已经编辑过的代码吧,所以地址有问题。于是我换了官方的代码框架,重新进行实验。实验结果截图如下(上面的实验无参考价值,做一个错误的记录):

调用test_backtrace(5)的地址为f010012e。

       // Test the stack backtrace function (lab 1 only)
        test_backtrace(5);
f0100127:       c7 04 24 05 00 00 00    movl   $0x5,(%esp)
f010012e:       e8 0d ff ff ff          call   f0100040 <test_backtrace>
f0100133:       83 c4 10                add    $0x10,%esp

 所以在进入test_backtrace程序之前堆栈寄存器的内容如下:

(gdb) b *0xf010012e
Breakpoint 1 at 0xf010012e: file kern/init.c, line 46.
(gdb) info r
esp            0xf010ffd0	0xf010ffd0
ebp            0xf010fff8	0xf010fff8

函数执行mon_backtrace的地址是f0100073

                mon_backtrace(0, 0, 0);
f010006a:       83 ec 04                sub    $0x4,%esp
f010006d:       6a 00                   push   $0x0
f010006f:       6a 00                   push   $0x0
f0100071:       6a 00                   push   $0x0
f0100073:       e8 4a 08 00 00          call   f01008c2 <mon_backtrace>

在这之前test_backtrace已经递归结束,因此将断点设置在0xf0100073,并且查看在这之前esp堆栈寄存器的内容。

(gdb) b *0xf0100073
Breakpoint 1 at 0xf0100073: file kern/init.c, line 18.
(gdb) x /52xw $esp
0xf010ff10:	0x00000000	0x00000000	0x00000000	0xf010004a
0xf010ff20:	0xf0111308	0x00000001	0xf010ff48	0xf01000a1
0xf010ff30:	0x00000000	0x00000001	0xf010ff68	0xf010004a
0xf010ff40:	0xf0111308	0x00000002	0xf010ff68	0xf01000a1
0xf010ff50:	0x00000001	0x00000002	0xf010ff88	0xf010004a
0xf010ff60:	0xf0111308	0x00000003	0xf010ff88	0xf01000a1
0xf010ff70:	0x00000002	0x00000003	0xf010ffa8	0xf010004a
0xf010ff80:	0xf0111308	0x00000004	0xf010ffa8	0xf01000a1
0xf010ff90:	0x00000003	0x00000004	0xf010ffb8	0xf010004a
0xf010ffa0:	0xf0111308	0x00000005	0xf010ffc8	0xf01000a1
0xf010ffb0:	0x00000004	0x00000005	0xf010fff8	0xf010004a
0xf010ffc0:	0xf0111308	0x00010094	0xf010fff8	0xf0100133
0xf010ffd0:	0x00000005	0x00001aac	0xf010ffec	0x00000000

 栈顶向下生长。所以从上往下为执行顺序。

test_backtrace在被调用时:

在执行call test_backtrace时有一个副作用就是压入这条指令下一条指令的地址,压入4字节返回地址

push %ebp,将上一个栈帧的地址压入,增加4字节

push %ebx,保存ebx寄存器的值,增加4字节

sub $0x14, %esp,开辟20字节的栈空间,后面的函数调用传参直接操作这个栈空间中的数,而不是用pu sh的方式压入栈中

加起来一共是32字节,也就是8个int。因此上面打印出来的栈内容,每两行表示一个栈帧,看起来还算清晰。

栈中的信息简单表示如图:

参考文章:https://github.com/clpsz/mit-jos-2014/tree/master/Lab1/Exercise10

================================================================================================

Exercise 11

实现一个堆栈回溯函数,mon_backtrace。在kern/monitor.c中已经为你声明了这个函数。你可以用C语言实现它。不仅如此,你还要把它加入到kernel moniter的命令集合中,这样用户就可以通过moniter的命令行调用它了。

   这个函数应该能够展示出下面这种格式的信息:

   Stack backtrace:

     ebp f0109358 eip f0100a62 args 00000001 f0109e80  f0109e98 f0100ed2 00000031

     ebp f0109ed8 eip f01000d6 args 00000000 00000000 f0100058 f0109f28 00000061

         ...

  其中第一行 "Stack backtrace"表明现在正在执行的就是mon_backtrace子程序。第二行展示的就是调用mon_backtrace的程序a,第三行展示的就是调用程序a的程序b,依次类推,直到最外层。

  在每一行中,ebp后面的值代表的是被这个函数所使用的ebp寄存器的值,这个值也是这个函数的栈帧的最高地址。而eip后面的值代表的是函数的返回地址。最后的五个列在args之后的16进制的值,是传递给这个函数的头五个输入参数,当然,有可能输入参数不到五个。

解答:

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


代码实现:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
        // Your code here.
        cprintf("Stack backtrace:\n");
        unsigned int esp, ebp, eip;
        unsigned int args[5];
        int i=5;
        ebp = read_ebp();
        while(ebp){
                eip =*((unsigned int*)(ebp+4));
                for(i=0;i<5;i++){
                    args[i] =*((unsigned int*)(ebp+8+4*i));
                }
                cprintf("    ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",ebp,eip,args[0],args[1],args[2],args[3],args[4]);
                ebp = *(unsigned int*)ebp;

        }
        return 0;
}

================================================================================================

Exercise 12

exercise12内容好多 重开一篇博客。

猜你喜欢

转载自blog.csdn.net/qq_43012789/article/details/107364879