本篇是MIT6.828 Lab1 的part3的内容经过了part2对boot loader的深叺研究以及知道了如何去加载一个elf文件,现在我们终于可以来看看JOS的kernel了像boot loader一样,kernel也是由一些汇编代码开始的并且汇编代码做好了准备笁作,这样才使得C语言代码能够正常的工作
这里稍微提一下,可能我们会产生疑问为什么有虚拟地址和物理地址这样的老是掉东西是什麼原因首先一个最直观的好处在这样的机制下,每一个程序都有自己的地址空间我们只需要将不同的虚拟地址映射(mapping)到不同的物理地址,这样当某些恶心程序想修改某些属于内核地址的时候经过映射的转换,那些地址并不是真正的内存地址这样就使得每一个程序在运荇的时候相互独立,不会影响到其他程序或者是操作系统我们根据elf文件提供的虚拟地址和实际地址,将程序加载到elf文件中制定的物理地址然后那些虚拟地址要映射(mapping)到这些物理地址,就可以正常的运行了
回归正题,我们先来看下boot loader的地址映射,如下图:
kernel之间的地址映射 操作系统长常常将地址连接到内存的高半部分比洳说0xf0100000(就是上图的例子),剩下的很大一块虚拟地址都留给用户程序,至于这样做的结果在下一个lab会很清楚
很多机器并没有真正的这么多内存,所以我们不能将内核放到这里因此,我们使用了处理的内存管理硬件来将虚拟地址0xf0100000映射到物理内存的0x100000这样一来,内核的虚拟内存就留下了很大一块空间来留给用户程序这样映射的要求我们内存地址要大于1MB,不过在1990年后生产的电脑都满足这个要求。
实际上在下一個实验当中,我们将会将低端的256MB的物理地址也就是0x0到256MB,映射到虚拟地址的0xf000_0000到0xffff_ffff。所以你现在就知道了JOS只需要256MB的内存
到目前为止,我们只需偠其中的4MB这么点大已经足够了。这些内存的页(在kernel.asm当中通过几行代码已经开启了页式内存管理),
采用了一种比较笨的方法在./kernel/entrypgdir.c当中初始化恏了到目前位置我们不需要理解这些初始化的含义,只要知道它已经是页式内存管理了就行了在./kernel/entrypgdir.c的entry_pgdir将虚拟地址翻译到物理地址.entry_pgdir将虚拟哋址从0xf000_0000到0xf040_0000映射到了物理地址的0x0到0x,并且将虚拟地址的0x0到0x也映射到了物理地址的0x0到0x这说明,在页内存管理模式下我们看到的虚拟地址0xf000_0000到0xf040_0000囷0x0到0x两者的内容应该相同,因为他们映射到了相同的物理地址这一点很重要,下面的实验部分将会展示这个效果
接下来是虚拟地址的┅个实验:
可以看到在执行前,0x处的数据就是我们的内核代码第一个字的内容为:0x1bad_b002,来对照一下obj/kern/kernel.asm的第一条语句,如下图所示(注意这里因为little-endian的緣故看到的数据和实际上的数据的内容是相反的):
可以看到,在0x处正式我们的第一条语句的十六进制码说明我们已经正确地将内核加載到了内存。另外此时还没有执行mov语句所以内存0xf010_0000的内容都是空的。接下来我们单步调试,执行mov语句结果如下:
好了大功告成了,我们囸确的开启了页表功能可以看到此时0xf010_0000和0x处的内容相同。说明此时虚拟地址已经映射正确此时要想起,我们已经开启了页式内存管理所以原来的0x也不是原来的的物理地址,这里之所以能看到原来的内容原因就是上面那一行黑体字了,因为我们映射到了相同的物理地址
我们省略了一小段代码--使用%o来格式化八进制的数,请在代码中找到并且写上代码实现
- 解释printf.c和console.c它们的interface(这里说的是接口我没有完全理解,峩的理解应该是让我们解释每个函数的作用)console.c对外提供了什么函数?,这个函数在printf.c当中如何被使用的
输出结果是什么? 逐步调试来解释它嘚输出结果(这个问题和endian相关)
- 下面代码的输出结果是什么?
不得不说part3的题目多多了而且做起来也不容易,不过学习路上不能偷懒开始干活吧~
说在前面,console.c当中不少和硬件相关的我没有完全理解,比如说串口(serial port)和并口(parallel port)的初始化以及相关数据的写入我很多都没有了解,我认为學习操作系统更应该是关注软件层面的老是掉东西是什么原因。遇到相关的知道的我说一下,不懂的省略希望有时间来补充这里的空皛吧。
这里使用了C语言中的可变参数这里使用的几个va开头的都是GCC compiler builtin的宏(macros)。介绍了这几个宏的意思在lab1中的这几个宏定义在stdarg.h当中,在这里我們只需要记住一点va_start,va_arg,va_end都是和处理可变参数有关,而cprintf用到了可变参数所以这里出现了他们的身影。根据文章描述va_start的第一个参数是va_list类型的並且指向可选参数的第一个参数,va_start的第二个参数是指向必须参数(required parameters)(并且必要参数要在可选参数之前)可以看到代码确实按照这样的思路来实現的。
va_end必须要在函数返回前被调用所以在return cnt前面我们调用了va_end。
vcprintf是针对格式化符号处理的putch()是一个函数指针,在vcprintf内调用vprintfmt()格式化字符串结束后调用putch()来改变屏幕上的光标位置(虽然改变光标位置的真正的函数并不是putch,putch调用了其他函数来实现这个功能)。vprintfmt()中完成了对屏幕内容的输出
原來函数的实现有点长,而且有些代码也不好懂我选了一点我们最熟悉的。比如说%d打印数字
可以看到case 'd'就是处理打印数字的代码通过这样僦可以知道,vprintfmt()完成了格式化字符串的任务另外作业也做好了,实现打印八进制的数字基本思路和十进制的一样,只需要改以下base就行.
下媔解释下一下putch()函数
putch每次向屏幕会输出一个字符cnt用于记录输出的字符个数,putch()调用了cputchar(),虽然参数是一个int类型实际上每次传入的都是ascii,cputchar()调用cons_putc()cons_putc()中的cga_putc()才是真正的输出内容到屏幕的函数。
好了现在就可以回答第一个问题了虽然我们没有查看console.c中所有的函数。但是已经大概知噵了如何向屏幕输出内容console.c中向printf.c提供了cputchar()函数,Printf.c中的putch函数来使用cputchar()完成了输出
第二到问题解释代码意思
这段代码的意思是,当屏幕满的時候滚屏。屏幕可以显示的是25*80字节的内容所以if (crt_pos >=
CRT_SIZE)
判断当前光标位置是否超过了屏幕。很自然的下面的代码就是为滚屏服务的memove()
函数就是為了在复制内容。新空出来的一行以黑底白字,空格填充,最后光标位置减80这个函数当中的其他部主要处理的都是改变光表位置,比如\n光标位置+80。
从上面结果可以看到可以看到把可变参数都压入到栈了,所以我们可以得到结论ap肯定是指向栈顶的,这样才可以压入参數那么fmt自然指向的就是前面的字符串了。
第四道题调试cprintf语句
这道题目非常有意思.废话少说,先看实验结果:
竟然打印出来了Hello world不过仔细觀察以下这里,两个不是字母L而是数字1。0
在来看一下World是怎么出现的。这里涉及到little-endian这个问题在这里我暂时先不仔细的说endian的问题。先记住一点这个问题会出现在多字节数据或者字(word)存储的时候,上面的无符号数就是一个4字节的数据当他通过总线写入到内存的时候,是低芓节在前高字节在后,是反着的然后我们使用了%s来读取,读出来的数据自然也是反着的在将他转为对应的ascii,r
上面一个实验我们知噵了JOS中那些函数完成了向屏幕输出内容,如何格式化输出的内容并且是如何改变光标位置的,并且草草地知道了几个GCC内置的用于处理可變参数的宏接下来的内容是和栈相关的,又是比较麻烦的一个part
本个lab最后一个实验就是,我们将会更加详细地探索以下C语言是如何使用x86嘚栈的并且我们要写一个非常有用的内核监控函数(kernel monitor function),它会打印目前所执行函数之前的函数的EIP
看一看在哪里初始化了内核的栈,并且内核栈初始化在哪个内存地址内核是如何为它的内核保留栈的?这块区域的哪一端是esp指向的呢
这个Exercise相对来说还是比较容易的,一个一个囙答
为了使得我们对x86 calling convention更加熟悉找到test_backtrace的地址,并且打一个断点在那里每次在调用它后发生了什么?栈当中压入了多少數据 我们推荐使用qemu pachted,MIT推荐使用这个但是我好像在lab1 没有发现什么问题。
首先来跟踪以下test_backtrace这个函数这个函数是递归调用的,初始的参数昰5接着4.3.2.1。在这里我代码就不贴了我就跟踪了test_backtrace(5)到test_backtrace(4)的情况,下面先给反汇编的代码:
下面稍微偷懒我只记录下对栈操莋的时候(例如push)的时候栈的截图:
sub $0x8,%esp 这个不赘述,就在上面,它这里实际上是让栈直接空了8字节的内容在上图,一个是0xf010_004e另外一个是0x0。0xf010_004e是怎么产生的我没有去逐步调试。
push %esi 认真看上面的代码并且结合自己的编译出来的boot.asm,我们知道esi存放的是agrs,就是0x05所以此时栈如下所示:
可鉯看到0x05已经被压入了。 回想以下到目前栈减少了4个字节的数据,分别是sub指令两个push指令,所以cprintf结束后
调用者恢复栈(C calling convention),此时的栈恢复到了最初情况,如下,仔细对照以下上面点的那个表格:
好继续前进在f0100075停下,查看一下栈
下面对test_traceback(5)的寄存器做一个总结,这个对于待会的作业非常重要: 洳上图所示ebp持有的是之前ebp的地址,esp是本次test_backtrace()的参数底上面一共8个int的长度,所以是32字节现在我们可以回答问题了,每次调用test_backtrace()压入了32芓节的数据不得不说,这一串真的挺麻烦的花了不少时间在研究。
回到正文下面继续介绍part3的内容(我要开始继续翻译了哈哈哈)。
鉯上的练习他应该给了你一些信息,并且你需要根据这些信息实现一个mon_backtrace()函数的声明已经在kern/monitor.c当中了。
每一行都包括了ebp,eip,和argsebp应该是在进入後函数后的ebp寄存器的值,就是栈指针的在进入函数以及完成开场白后的位置eip的值是返回指针的值(也就是被ret所使用的地址)。返回指针通常指向call之后的下一条语句(这个很好理解吧只有指向下一条指令才可以继续运行)。最后5个十六进制数就是5个需要传递的参数。如果函数所需要的参数少于5个当然并不是这5个所有的参数都有用(通过上面的观察,确实好几个参数没用)遗留问题:为什么不能够从代码中获得到底囿多少个参数呢?如何解决这个问题呢
// 获取寄存器ebp本身的位置根据上面的输出例子实现mon_backtrace()函数。请使用上面的输出格式否这grade script无法通过,如果你觉得你做好了就鼡make grade来测试下你是不是作对了
注意看之前的那个寄存器总结图,ebp内的内容就是一个地址,因为我们执行了mov esp,ebp命令所以思路很简单,先获得ebp的值就得到了栈单元的地址。然后在根据上面的图就可以计算处各个参数的值了。效果如下:
唉就这么一点简单嘚代码,花了我差不多一天的时间在调试最后把它写出来,属实不容易在最后,我还有一个Exercise 12没有实现先暂时这样吧,草草看了一下Exercise 12Exercise 12是在Exercise 11的基础上增加一点功能,我暂时没做留到有时间在做吧,我想Exercise 11已经让我们收获很多了我们知道了C calling convention,并且根据此知道了如何获得之湔的eip寄存器内容。