1. 程式人生 > >MIT 操作系統實驗 MIT JOS lab1

MIT 操作系統實驗 MIT JOS lab1

express binary this little 代碼段 quest 漫遊 快捷 ket

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



技術分享


MIT 操作系統實驗 MIT JOS lab1