MIT 操作系统实验 MIT JOS lab1

来源:互联网 发布:怎样做淘宝网店 编辑:程序博客网 时间:2024/05/14 23:27

 JOS lab1

首先向MIT还有K&R致敬!

没有很好的开源环境我不可能拿到这么好的东西.


向每一个与我一起交流讨论的programmer致谢!没有道友一起死磕,我也可能会中途放弃.

跟丫死磕到底.(其实这个过程会学到很多东西,很好玩很好玩,不要被panic吓到,等你都能定位panic,并修复触发panic的bug的时候,我相信大家debug的能力会上升一个水平,互勉~)


--------------------------------------------------------------------------------------------------------------------------------------------


安全带系好,开始6.828号星系漫游 : )


首先先看一下MIT的课程实验lab1的安排


这里要求熟悉一下Unix的历史.

Assignment : HW: shell




戳下面的链接吧,具体代码去github看, 本来这贴就比较长了 ....


https://github.com/jasonleaster/MIT_6_828_assignments_2012/blob/master/sh.c


下面是单独开的shell实现分析贴:

http://blog.csdn.net/cinmyheart/article/details/45122619




Part 1: PC Bootstrap


这一部分就是很简单的介绍怎么使用qemu和gdb联调kernel...

打开两个terminal,都进入到lab目录,然后其中一个输入make qemu-gdb 另一个输入make gdb,即可看到下面的画面

                      




注意,这里模拟的是intle 8086. 当系统复位(上电)的时候,

可以发现,开机后第一条指令,当前地址是0xFFFF0. 在此之前,CS == 0xFFFF


更加详细的系统"启动刹那间"分析戳下面的link:

http://blog.csdn.net/cinmyheart/article/details/42064253


有意思的是在读取BIOS信息这个阶段由于系统还有设置堆栈,gdb调试的时候step和next指令都是不能用的(需要堆栈信息),只有单行执行汇编指令的stepi指令可用,并提示一个??()的信息,当前被执行指令不在任何函数内部






第一个 exercise没有什么,只是熟悉汇编就好,都不要很牛的汇编,只要能看懂就行了.自己动手写的也不会多.


早期的intel 16bit的8086 等处理器都是只有1M的地址空间的...


   The first PCs, which were based on the 16­bit Intel 8088 processor, were only capable of addressing 1MB of physical memory. The physical address space of an early PC would therefore start at 0x00000000 but end at 0x000FFFFF instead of 0xFFFFFFFF. 


后面对于内存的需求增大了,才把内存扩展,并为了之前的机器.就把扩展内存从0x100000开始.


The PC architects nevertheless preserved the original layout for the low 1MB of physical address space in order to ensure backward compatibility with existing software. 



熟悉gdb的si指令,没话说...


Part 2: The Boot Loader





当读取完BIOS的信息之后,这个时候就开始执行kernel的代码了

会长跳转到0x7C00地址处


               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. 




从real mode切换到protected model,地址长度从16bits变为32bits!观察gdb的那个【0:7c2d】到0x7c32这种地址的表现形式我们也可以觉察到这一点




而后便是设置protected model下的数据段代码段等信息,然后跳转到bootmain.注意,跳转bootmain之前就设置了堆栈!

movl $start %esp

这是我们看到的最早的内核栈



这部分需要回答一部分问题:

Be able to answer the following questions:


               At what point does the processor start executing 32­bit code? What exactly causes the switch
from 16­ to 32­bit mode?

从real model跳转到protected model的时候开始执行32bit code


            What is the last instruction of the boot loader executed, and what is the first instruction of the
kernel it just loaded?

boot loader最后一行代码:



            Where is the first instruction of the kernel?

首先得定位到上面ELFHDR->e_entry指向的位置,而ELFHDR是指向0x10000(被强制类型转换成struct Elf)


这里通过readseg使得ELFHDR得以初始化.这个初始化的数据来源就是硬盘上的内核镜像.

于是我们从那里去找这个ELFHDR->e_entry指向的位置呢?反汇编kernel镜像!

objdump -x ./obj/kern/kernel


会看到kernel的起始地址是0x10000c


设置断点就会发现这里kernel的第一条语句是 

movw $0x1234, 0x472



我们能够在 kern/entry.S中得到印证,能够找到这句代码



而kernel镜像中的entry 符号就是指向entry.S 这个文件的代码起始地址的

反汇编你会看到一个entry的符号!value是0xf010000c  这就是我们镜像上内核的入口地址了,和上面的0x10000c并不冲突,前者0x10000c是后者0xF010000C转换而来的 


这种转换一开始是手动的,我找了09 年和10年的同样的实验代码。

以前的代码(左边)                                                                 现在的代码(右边)

发现这里是有手动的&转换的,而我现在用的2014年的代码是没有这种强制转换的,为这个问题纠结好久...

                  Many machines don't have any physical memory at address 0xf0100000, so we can't count on being
able to store the kernel there. Instead, we will use the processor's memory management hardware to map virtual address 0xf0100000 (the link address at which the kernel code expects to run) to physical address 0x00100000 (where the boot loader loaded the kernel into physical memory). This way, although the kernel's virtual address is high enough to leave plenty of address space for user processes, it will be loaded in physical memory at the 1MB point in the PC's RAM, just above the BIOS ROM. This approach requires that the PC have at least a few megabytes of physical memory (so that physical address 0x00100000 works), but this is likely to be true of any PC built after about 1990.


因为硬件已经把0xf0100000 映射到0x100000 ,0xf010000c同理映射到0x10000c,...实质上就是手动转换变成硬件直接转换(感觉更晦涩了啊~还是手动转换的好...折腾了我一个小时)

从启动信息我们也可以知道这点(之前这个message被我无视了)


后来有发现自己巨渣...原来objdump的时候也可以看到信息...只怪自己弱,布吉岛啊...

这里的VMA== virtual memory address  LMA == load memory address

So, 0xf0100000是虚拟地址,真正加载的时候使用的LMA,物理地址





            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格式文件储存的信息确定并读取的.

答案是:

看这值得注意的是 VMA 和LMA

  Take particular note of the "VMA" (or link address) and the "LMA" (or load address) of the .text section. The load address of a section is the memory address at which that section should be loaded into memory.


      The link address of a section is the memory address from which the section expects to execute. The linker encodes the link address in the binary in various ways, such as when the code needs the address of a global variable, with the result that a binary usually won't work if it is executing from an address that it is not linked for.

下面是kern/kernel 的 ELF header


下面是 obj/boot/boot.out的 ELF header



会注意到这里有些段有个 LOAD标记(比方说 .text .eh_frame),有些没有比方说 .comment


update:2014.10.10 这里删除了我之前错误的答案 ,这里可能会有疑惑,等到lab2把kernel的内存分布都搞明白就知道

为什么了



               Back in boot/main.c, the  ph->p_pa  field of each program header contains the segment's destination physical address (in this case, it really is a physical address, though the ELF specification is vague on
the actual meaning of this field).

下面这部分就是把各种 ELF header读入到内存中的过程.然后把 ELFHDR->e_entry作为函数入口








这个exercise 4就是提醒来踩坑的娃,童鞋哇,玩不花指针还是不要玩JOS了,好好把K&R看看再来勇敢的踩坑....

下面是这个 pointer.c的测试,如果你不debug,人脑compile然后能判断正确,基本的指针操作就差不多了


http://blog.csdn.net/cinmyheart/article/details/39755621



开始做"邪恶"的事情了.因为之前各种精心准备的链接信息是很重要的(废话).如果链接地址不正确,程序就会出问题.这里我们就尝试改动boot/Makefrag里面的链接地址,然后试试看JOS会不会炸掉哈哈哈

并不是让我们跑去改这个Makefile而是去改链接信息,在kernel.ld里面



会注意到,我把文本段的地址改成了0xF0000,重新编译内核,然后是无法正常启动的,会挂在读取内核的那个地方,无法正常读取内核,于是就重启啊重启.这里就不上图了.



问的查看 BIOS enters the boot loader时候地址 0x00100000开始的八个words是什么东东

和 enter kernel的时候这个地址八个words他们之间有什么不同?

前者实际的enter boot loader地址是 0x7c00 后者的enter kernel 地址是0x10000c

我特意操作了几次,操作的意图在截图里面很明显 : ) 


可以发现前后两次,这个地址里储存的数据是不一样的,前者是空的,后面的有些数据看不懂?没关系,我们把它当做汇编指令来看看,并把它和内核代码做一下比较看看.

一切尽在不言中!!右边是 obj/kern/kernel的汇编代码.左边是是我们enter kernel point断点处查看的0x100000的内容


验证了内核代码(不是bootloader)从0x100000开始,和链接脚本 kern/kernel.ld描述的一致,也和各种 ELF header描述一致 : )



Part 3: The Kernel


                                          

不截图了,si单步调试到 movl %eax, %cr0,记得前后都要查看两个地址的内容,你会发现,在这条指令之前,两个地址的内容是不一样的.之后就变一样了.原因就是之前还没有建立分页机制,高地址内核区域还没有映射到内核的物理地址,而只有低地址有效的.开启分页之后,由于有静态映射表的存在(kern/enterpgdir.c),两块虚拟地址都指向同一块物理地址区域



主要是添加一些代码.

先把下面列出来的代码读一次

Read through  kern/printf.c ,  lib/printfmt.c , and  kern/console.c (反正我是边做边读的...)



               “We have omitted a small fragment of code ­ the code necessary to print octal numbers using patterns of the form "%o". Find and fill in this code fragment.”

找到printfmt.c然后添加如下代码即可:


这里因为很多机制都很健全,只要仿照着16进制输出的做一个8进制输出的初步处理就可以了


Be able to answer the following questions:
1.  Explain the interface between  printf.c  and  console.c . Specifically, what function does  console.c
export? How is this function used by  printf.c ?
 

这里主要是说明所有的printf相关函数(JOS中),实质上都是“一层外壳”,它调用了console.c里面的putch函数.

再者,printf的实现利用到了参数变长的技巧

对于这种技巧的使用,我在这里有详细的说明:http://blog.csdn.net/cinmyheart/article/details/24582895



2.  Explain the following from  console.c :


主要是检测当前屏幕的输出buffer是否满了,这里注意memmove其实就是把第二个参数指向的地址移动n byte到第一个参数指向的地址,这里n byte由第三个参数指定.

如果buffer满了,把屏幕第一行覆盖掉逐行上移,空出最后一行,并由for循环填充以‘ ’(空格),最后把crt_pos置于最后一行的行首!






3.  For the following questions you might wish to consult the notes for Lecture 2. These notes cover GCC's calling convention on the x86.

Trace the execution of the following code step­ by­ step:


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



In the call to  cprintf() , to what does  fmt  point? To what does  ap  point?

fmt指向格式说明符字符串.ap 指向一个va_list 类型变量

不过这个代码在哪儿?我始终没有找到...以后找到update.


List (in order of execution) each call to  cons_putc ,  va_arg , and  vcprintf . For  cons_putc , list its argument as well. For  va_arg , list what  ap  points to before and after the call. For  vcprintf  list the values of its two arguments.




4.  Run the following code.


unsigned int i = 0x00646c72;cprintf("H%x Wo%s", 57616, &i);

What is the output? Explain how this output is arrived at in the step­by­step manner of the previous exercise. Here's an ASCII table that maps bytes to characters.

会输出He110 World

我只想说...呵呵...原理嘛,就是很简单的根据ascii输出就是了

只是注意一下这里的%s部分是打印的i地址处的东东,由于是little endian机器,所以i的值在储存的时候是72 6c 64 00顺序储存的.这样对应的ascii码就是 r l d


                  The output depends on that fact that the x86 is little­endian. If the x86 were instead big­endian
what would you set  i  to in order to yield the same output? Would you need to change  57616  to a different value?
Here's a description of little­ and big­endian and a more whimsical description.

如果是big endian嘛就是i = 0x726c6400,不需要改变57616.


5.  In the following code, what is going to be printed after  'y=' ? (note: the answer is not a specific value.) Why does this happen?
cprintf("x=%d y=%d", 3);

y后会打印垃圾值


6.  Let's say that GCC changed its calling convention so that it pushed arguments on the stack in declaration order, so that the last argument is pushed last. How would you have to change  cprintf or its interface so that it would still be possible to pass it a variable number of arguments?

还是要先看变长参数的实现

#ifndef _STDARG_H#define _STDARG_Htypedef char *va_list;/* Amount of space required in an argument list for an arg of type TYPE.   TYPE may alternatively be an expression whose type is used.  */#define __va_rounded_size(TYPE)  \  (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))#ifndef __sparc__#define va_start(AP, LASTARG) \ (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))#else#define va_start(AP, LASTARG) \ (__builtin_saveregs (),\  AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))#endifvoid va_end (va_list);/* Defined in gnulib */#define va_end(AP)#define va_arg(AP, TYPE)\ (AP += __va_rounded_size (TYPE),\  *((TYPE *) (AP - __va_rounded_size (TYPE))))#endif /* _STDARG_H */

                  从上面可以看到, va arg 每次是以地址往后增长取出下一参数变量的地址的。而这个实现方式就默认假 设了编译器是以从右往左的顺序将参数入栈的. 因为栈是以从高往低的方向增长的。后压栈的参数放在了内存地址的低位置,所以如果要以从左到右的顺序依次取出每个变量,那么 编译器必须以相反的顺序即从右往左将参数压栈。 如果编译器更改了压栈的顺序,那么为了仍然能正确取出所有的参数, 那么需要修改上面代码中的 va_start 和 va_arg 两个宏,将其改成用减法 得到新地址即可。感觉这地方也不少说,具体情况具体分析,不难


对于堆栈的认识最好还是去做CSAPP的lab 2 bomb~ 提前祝炸的开心: )


关于显示器颜色输出的问题:

观察cga_putc函数,



这里会检测c的8bit以上是否为0,如果是,那么黑白显示打印的字符。如果不是,那就是有蹊跷咯...

其实原理很简单

int c这个变量低8位控制显示的ascii码。接着8~15 bits用来控制颜色输出.

仅仅是为了说明原理,这里我没有把功能诠释的很完善, 高手有兴趣折腾的话欢迎交流~

修改./lib/printfmt.c 我对case ‘c’ 有小幅度的修改,增加了一个case ‘C’ ,增添了一个全局变量Color来传递显示何种颜色的信息


测试方法: 修改./kern/monitor.c这个文件




KO~! 囧....其实我本意是想打印绿色的,但是对这里的高8bits的颜色控制不熟悉...So 。。。



实验本还有堆栈部分的练习,但是我觉得去拆炸弹更好,于是我就“节省时间”(偷懒一下)没做了...

http://blog.csdn.net/cinmyheart/article/details/39161471



对于JOS lab1 的实验解答还有诸多不完善的地方,以后再update....



kern/entry.S 这个部分完成了启动时的boot stack.栈顶是$bootstacktop,而这个汇编的全局量在下图中可以看见

非常的明显,在bootloader程序的数据段内,而数据段紧紧跟在文本段之后,启动的boot stack就恰好在数据段开头位置对齐之后开始,然后是KSTKSIZE大小的栈,而后是栈顶.





在/kern/kdebug.c 里面会看到这段代码,关注下面的__STAB_BEGIN__ 那段代码


__STAB_BEGIN__   __STAB_END__相关定义在/kern/kernel.ld里面

下面我给出了kernel.ld的主要内容

ENTRY(_start)SECTIONS{/* Link the kernel at this address: "." means the current address */. = 0xF0100000;/* AT(...) gives the load address of this section, which tells   the boot loader where to load the kernel in physical memory */.text : AT(0x100000) {*(.text .stub .text.* .gnu.linkonce.t.*)}揭示了内核被加载到0x100000线性地址处PROVIDE(etext = .);/* Define the 'etext' symbol to this value */.rodata : {*(.rodata .rodata.* .gnu.linkonce.r.*)}/* Include debugging information in kernel memory */.stab : {PROVIDE(__STAB_BEGIN__ = .);//这里也定义了__STAB_BEGIN__等变量是0xF0100000*(.stab);PROVIDE(__STAB_END__ = .);BYTE(0)/* Force the linker to allocate space   for this section */}.stabstr : {PROVIDE(__STABSTR_BEGIN__ = .);*(.stabstr);PROVIDE(__STABSTR_END__ = .);BYTE(0)/* Force the linker to allocate space   for this section */}/* Adjust the address for the data segment to the next page */. = ALIGN(0x1000); //把数据段和bss段放到下一页/* The data segment */.data : {*(.data)}PROVIDE(edata = .);.bss : {*(.bss)}PROVIDE(end = .); //下一页的起始就是kernel代码段的结束位置/DISCARD/ : {*(.eh_frame .note.GNU-stack)}}




首先要了解struct Stab是用来记录调试信息的结构体

关于struct stab我做了一个简介:http://blog.csdn.net/cinmyheart/article/details/39972701


kdebug.c的注释也讲的很清楚

// stab_binsearch(stabs, region_left, region_right, type, addr)////Some stab types are arranged in increasing order by instruction//address.  For example, N_FUN stabs (stab entries with n_type ==//N_FUN), which mark functions, and N_SO stabs, which mark source files.////Given an instruction address, this function finds the single stab//entry of type 'type' that contains that address.////The search takes place within the range [*region_left, *region_right].//Thus, to search an entire set of N stabs, you might do:////left = 0;//right = N - 1;     /* rightmost stab *///stab_binsearch(stabs, &left, &right, type, addr);////The search modifies *region_left and *region_right to bracket the//'addr'.  *region_left points to the matching stab that contains//'addr', and *region_right points just before the next stab.  If//*region_left > *region_right, then 'addr' is not contained in any//matching stab.////For example, given these N_SO stabs://Index  Type   Address//0      SO     f0100000//13     SO     f0100040//117    SO     f0100176//118    SO     f0100178//555    SO     f0100652//556    SO     f0100654//657    SO     f0100849//this code://left = 0, right = 657;//stab_binsearch(stabs, &left, &right, N_SO, 0xf0100184);//will exit setting left = 118, right = 554.//

这个stab_binsearch被函数debuginfo_eip调用,而这个函数就是为了填充struct Eipdebuginfo结构体而存在的。

始终记住这点,debuginfo_eip是为了填充struct Eipdebuginfo结构体那么在补充debuginfo_eip的时候就不会觉得迷失.




能在i386_init()里面找到这个函数被调用


简单的递归技巧.




这个部分几乎就是去让我们实现一个gdb 调试的时候的trace命令

能够利用栈的结构还有函数调用的特点,一步步的追溯到刚开始调用的函数(有点"反递归"的意思)

https://github.com/jasonleaster/MIT_JOS_2014/blob/lab1/kern/monitor.c




update: 2014.10.13

其实字符颜色控制还是比较简单的. 照着下面的编码来修改之前的COLOR_*** 的值就可以了



update 2015.02.13  添加了qemu的常用快捷键(话说鼠标点在qemu里面出不来了...)

组合键
Ctrl-Alt-f
全屏
Ctrl-Alt-n
切换虚拟终端'n'.标准的终端映射如下:

  • n=1 : 目标系统显示
  • n=2 : 临视器
  • n=3 : 串口
Ctrl-Alt
抓取鼠标和键盘
Ctrl-a h
打印帮助信息
Ctrl-a x
退出模拟
Ctrl-a s
将磁盘信息保存入文件(如果为-snapshot)
Ctrl-a b
发出中断
Ctrl-a c
在控制台与监视器进行切换
Ctrl-a Ctrl-a
发送Ctrl-a

在图形模拟时,我们可以使用下面的这些组合键:

  • 在虚拟控制台中,我们可以使用Ctrl-Up, Ctrl-Down, Ctrl-PageUp 和 Ctrl-PageDown在屏幕中进行移动.

在模拟时,如果我们使用`-nographic'选项,我们可以使用Ctrl-a h来得到终端命令:


update 2015.04.19 

也是羞愧 ... 之前草草贴出了一些 process notes但是有些简陋,

这次更新打算重新把前面的东东强化一下,留个烙印

把没有做的challenges 做了杀一杀 好歹是第二遍了...





1 0