Lab 1 Part 3: The kernel

  现在我们将开始具体讨论一下JOS内核了。就像boot loader一样,内核开始的时候也是一些汇编语句,用于设置一些东西,来保证C语言的程序能够正确的执行。

使用虚拟内存

  在运行boot loader时,boot loader中的链接地址(虚拟地址)和加载地址(物理地址)是一样的。但是当进入到内核程序后,这两种地址就不再相同了。

  操作系统内核程序在虚拟地址空间通常会被链接到一个非常高的虚拟地址空间处,比如0xf0100000,目的就是能够让处理器的虚拟地址空间的低地址部分能够被用户利用来进行编程。

  但是许多的机器其实并没有能够支持0xf0100000这种地址那么大的物理内存,所以我们不能把内核的0xf0100000虚拟地址映射到物理地址0xf0100000的存储单元处。

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

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

  在这个实验中,首先是采用分页管理的方法来实现上面所讲述的地址映射。但是设计者实现映射的方式并不是通常计算机所采用的分页管理机构,而是自己手写了一个程序lab\kern\entrygdir.c用于进行映射。既然是手写的,所以它的功能就很有限了,只能够把虚拟地址空间的地址范围:0xf0000000~0xf0400000,映射到物理地址范围:0x00000000~0x00400000上面。也可以把虚拟地址范围:0x00000000~0x00400000,同样映射到物理地址范围:0x00000000~0x00400000上面。任何不再这两个虚拟地址范围内的地址都会引起一个硬件异常。虽然只能映射这两块很小的空间,但是已经足够刚启动程序的时候来使用了。


 

  Exercise 7:

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

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

  解答:

  我们可以首先设置断点到0x10000C处,因为我们在之前的练习中已经知道了,0x10000C是内核文件的入口地址。  然后我们从这条指令开始一步步运行,直到碰到movl %eax, %cr0指令。在这条指令运行之前,地址0x00100000和地址0xf0100000两处存储的内容是:

  MIT 6.828 JOS学习笔记10. Lab 1 Part 3: The kernel

  可见当前这两地址处的值是不一样的。

  然后输入stepi命令(其实就是si命令),再查看两个位置:

  MIT 6.828 JOS学习笔记10. Lab 1 Part 3: The kernel

  我们会发现两处存放的值已经一样了! 可见原本存放在0xf0100000处的内容,已经被映射到0x00100000处了。

  

  第二问需要我们把entry.S文件中的%movl %eax, %cr0这句话注释掉,重新编译内核。我们需要先make clean,然后把%movl %eax, %cr0这句话注释掉,重新编译。 再次用qemu仿真,并且设置断点到0x10000C处,开始一步步执行。通过一步步查询发现了出现错误的一句。

  MIT 6.828 JOS学习笔记10. Lab 1 Part 3: The kernel

  其中在0x10002a处的jmp指令,要跳转的位置是0xf010002C,由于没有进行分页管理,此时不会进行虚拟地址到物理地址的转化。所以报出错误,下面是make qemu-gdb这个窗口中出现的信息。

  MIT 6.828 JOS学习笔记10. Lab 1 Part 3: The kernel

  可见你当前访问的逻辑地址超出内存了。


 

格式化输出到控制台(屏幕)

  我们经常会在编程时使用到printf子程序,这个子程序是在操作系统的内核中实现的。这一小部分就是要探究一下这种格式化输出子程序的实现方式。

    通读kern/printf.c,lib/printfmt.c和kern/console.c三个C语言程序(在Exercise 8的解答中有具体的分析),并且确保你能够理解他们之间的关系。在后边的实验中我们会弄清楚为什么printfmt.c子程序会放在lib文件夹下。

  Exercise 8:http://www.cnblogs.com/fatsheep9146/p/5066690.html


 

    回答下试验报告中Exercise 8后面的问题:

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

  答:在Exercise 8的解答中我们已经很具体的分析了两个文件,在console.c中除了被static修饰符修饰的函数之外,都可以被外部所使用,其中被printf所使用的函数就是cputchar子函数。

  2. 解释一下console.c文件中,下面这段代码的含义:

1 if (crt_pos >= CRT_SIZE) {
2        int i;
3        memcpy(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_buf:这是一个字符数组缓冲区,里面存放着要显示到屏幕上的字符

    crt_pos:这个表示当前最后一个字符显示在屏幕上的位置,在介绍这个变量前我们还要知道一些知识,这是我在网上自己查询的。

      早期的计算机如果想显示信息给用户只能通过文字模式,比如当你现在打开电脑时,进入桌面之前,所有的信息都是通过文字显示在屏幕上的。那么这种模式就叫做文字模式,那么这个console.c源程序中考虑的就是一种非常常见的文字模式,80x25文字模式,即整个屏幕上允许显示最多25行字符,每行最多显示80个字符。所以一共代表了80x25个位置。当我们要显示某个特定字符到屏幕某个位置上面时,我们必须要指定显示的位置,和显示字符给屏幕驱动器cga。

    而在console.c文件中,子程序cga_putc(int c)就是完成这项功能,把字符c显示到屏幕当前显示的下一个位置。比如当前屏幕中已经显示了三行数据(0号行,1号行,2号行),并且第三行已经显示了40个字符,此时执行cga_putc(0x65),那么就会把0x65对应的字符'A'显示到2号行第41个字符处。所以cga_putc需要两个变量,crt_buf,这个一个字符数组指针,该字符数组就是当前显示在屏幕上的所有字符。crt_pos则表示下一个要显示的字符存放在数组中的位置,其实通过这个值也可以推导出它显示在屏幕上的位置。比如crt_pos = 85,那么它就应该显示在第2行(即1号行),第6字符(5号字符)处。所以crt_pos的取值范围应该是从0~(80*25-1)。

    上面题目中要分析的这段代码位于cga_putc中,cga_putc的分为三部分,第一部分是根据字符值int c来判断到底要显示成什么样子。而第二部分就是上述代码。第三部分则是把你决定要显示的字符显示到屏幕的指定位置上。咱们具体分析第二部分,

    当crt_pos >= CRT_SIZE,其中CRT_SIZE = 80*25,由于我们知道crt_pos取值范围是0~(80*25-1),那么这个条件如果成立则说明现在在屏幕上输出的内容已经超过了一页。所以此时要把页面向上滚动一行,即把原来的1~79号行放到现在的0~78行上,然后把79号行换成一行空格(当然并非完全都是空格,0号字符上要显示你输入的字符int c)。所以memcpy操作就是把crt_buf字符数组中1~79号行的内容复制到0~78号行的位置上。而紧接着的for循环则是把最后一行,79号行都变成空格。最后还要修改一下crt_pos的值。

 3. 观察下面的一串代码:

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

  回答下列问题:

    * 当调用cprintf时,fmt指向的是什么内容,ap指向的是什么内容。

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

  答:

  观察cprintf函数:

 1 int
 2 cprintf(const char *fmt, ...)
 3 {
 4     va_list ap;
 5     int cnt;
 6 
 7     va_start(ap, fmt);
 8     cnt = vcprintf(fmt, ap);
 9     va_end(ap);
10 
11     return cnt;
12 }
cprintf(const char *fmt, ...)

相关文章: