MIT 操作系统实验 MIT JOS lab1

时间:2024-03-12 13:14:26

 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


这一部分就是非常easy的介绍怎么使用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

我仅仅想说...呵呵...原理嘛。就是非常easy的依据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_H

typedef 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)))
#endif

void 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,假设是,那么黑白显示打印的字符。假设不是。那就是有蹊跷咯...

事实上原理非常easy

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 做了杀一杀 好歹是第二遍了...