環(huán)境
ubuntu 20.04 64系統(tǒng)
本文大部分內(nèi)容翻譯mit6.828 Lab1 part3的內(nèi)容,原地址點(diǎn)擊這里
課程主頁:MIT6.828-2018 Fall
正文
本篇是MIT6.828 Lab1 的part3的內(nèi)容。經(jīng)過了part2對boot loader的深入研究以及知道了如何去加載一個elf文件,現(xiàn)在我們終于可以來看看JOS的kernel了。像boot loader一樣,kernel也是由一些匯編代碼開始的,并且匯編代碼做好了準(zhǔn)備工作,這樣才使得C語言代碼能夠正常的工作。
當(dāng)你觀察boot loader的link address(虛擬地址)和loaded address(物理地址)的時候,會發(fā)現(xiàn)他們倆匹配的很好
ps:
這里稍微提一下,可能我們會產(chǎn)生疑問為什么有虛擬地址和物理地址這樣的東西。首先一個最直觀的好處在這樣的機(jī)制下,每一個程序都有自己的地址空間,我們只需要將不同的虛擬地址映射(mapping)到不同的物理地址,這樣當(dāng)某些惡心程序想修改某些屬于內(nèi)核地址的時候,經(jīng)過映射的轉(zhuǎn)換,那些地址并不是真正的內(nèi)存地址,這樣就使得每一個程序在運(yùn)行的時候相互獨(dú)立,不會影響到其他程序或者是操作系統(tǒng)。我們根據(jù)elf文件提供的虛擬地址和實(shí)際地址,將程序加載到elf文件中制定的物理地址,然后那些虛擬地址要映射(mapping)到這些物理地址,就可以正常的運(yùn)行了。
回歸正題,我們先來看下boot loader的地址映射,如下圖:

我們可以看到,在boot loader當(dāng)中,虛擬地址和物理地址剛好是相等的。也就是part3里面說的matched perfectly?;叵胍韵?,我們在對boot loader鏈接的時候,-Ttext指定了0x7c00為鏈接地址,當(dāng)bios把boot loader加載到0x7c00后,因?yàn)樘摂M地址是從0x7c00(可以聯(lián)想下 org這條匯編語句,他們所達(dá)到的效果是一樣的)開始的,恰好就一一對應(yīng),所以可以正常的運(yùn)行。
回歸part3,但是在kernel的link address和physical address之間由很大的間隔(相當(dāng)?shù)拇?。好我們來看看效果如何,如下圖所示:

果然如此,我們看到link address和physical address之間有很大的間隔,關(guān)于VirtAddr和PhyAddr的指定,看一下./lab/kernel/kernel.ld文件,里面指定了link address和映射到的Physical address(重申一邊,我對linker了解的不多,感興趣的老鐵請看《linker and loader》)。
操作系統(tǒng)長常常將地址連接到內(nèi)存的高半部分,比如說0xf0100000(就是上圖的例子),剩下的很大一塊虛擬地址都留給用戶程序,至于這樣做的結(jié)果在下一個lab會很清楚。
很多機(jī)器并沒有真正的這么多內(nèi)存,所以我們不能將內(nèi)核放到這里。因此,我們使用了處理的內(nèi)存管理硬件來將虛擬地址0xf0100000映射到物理內(nèi)存的0x100000。這樣一來,內(nèi)核的虛擬內(nèi)存就留下了很大一塊空間來留給用戶程序。這樣映射的要求我們內(nèi)存地址要大于1MB,不過在1990年后生產(chǎn)的電腦,都滿足這個要求。
實(shí)際上,在下一個實(shí)驗(yàn)當(dāng)中,我們將會將低端的256MB的物理地址,也就是0x0到256MB,映射到虛擬地址的0xf000_0000到0xffff_ffff。所以你現(xiàn)在就知道了JOS只需要256MB的內(nèi)存。
到目前為止,我們只需要其中的4MB,這么點(diǎn)大已經(jīng)足夠了。這些內(nèi)存的頁(在kernel.asm當(dāng)中通過幾行代碼已經(jīng)開啟了頁式內(nèi)存管理), 采用了一種比較笨的方法在./kernel/entrypgdir.c當(dāng)中初始化好了。到目前位置我們不需要理解這些初始化的含義,只要知道它已經(jīng)是頁式內(nèi)存管理了就行了。在./kernel/entrypgdir.c的entry_pgdir將虛擬地址翻譯到物理地址.entry_pgdir將虛擬地址從0xf000_0000到0xf040_0000映射到了物理地址的0x0到0x0040_0000,并且將虛擬地址的0x0到0x0040_0000也映射到了物理地址的0x0到0x0040_0000。這說明,在頁內(nèi)存管理模式下,我們看到的虛擬地址0xf000_0000到0xf040_0000和0x0到0x0040_0000兩者的內(nèi)容應(yīng)該相同,因?yàn)樗麄冇成涞搅讼嗤奈锢淼刂?,這一點(diǎn)很重要,下面的實(shí)驗(yàn)部分將會展示這個效果。
接下來是虛擬地址的一個實(shí)驗(yàn):
Exercise 7
使用QEMU和GDB來最終JOS的內(nèi)核(主要是調(diào)試obj/kern/kernel.asm),停在
movl %eax, %cr0這條語句,查看內(nèi)存0x0010_0000和0xf010_0000處的內(nèi)容,接著使用si命令單步調(diào)試,查看mov命令執(zhí)行后的效果。
實(shí)驗(yàn):
執(zhí)行前

可以看到在執(zhí)行前,0x0010_0000處的數(shù)據(jù)就是我們的內(nèi)核代碼,第一個字的內(nèi)容為:0x1bad_b002,來對照一下obj/kern/kernel.asm的第一條語句,如下圖所示(注意這里因?yàn)閘ittle-endian的緣故,看到的數(shù)據(jù)和實(shí)際上的數(shù)據(jù)的內(nèi)容是相反的):

可以看到,在0x0010_0000處正式我們的第一條語句的十六進(jìn)制碼,說明我們已經(jīng)正確地將內(nèi)核加載到了內(nèi)存。另外此時還沒有執(zhí)行mov語句,所以內(nèi)存0xf010_0000的內(nèi)容都是空的。接下來,我們單步調(diào)試,執(zhí)行mov語句,結(jié)果如下:

好了大功告成了,我們正確的開啟了頁表功能,可以看到此時0xf010_0000和0x0010_0000處的內(nèi)容相同。說明此時虛擬地址已經(jīng)映射正確。此時要想起,我們已經(jīng)開啟了頁式內(nèi)存管理,所以原來的0x0010_0000也不是原來的的物理地址,這里之所以能看到原來的內(nèi)容,原因就是上面那一行黑體字了,因?yàn)槲覀冇成涞搅讼嗤奈锢淼刂贰?p>
閱讀kern/printf.c,lib/printfmt.c和kern/console.c,弄清楚他們之間的關(guān)系。
Exercise 8
我們省略了一小段代碼--使用%o來格式化八進(jìn)制的數(shù),請在代碼中找到并且寫上代碼實(shí)現(xiàn)
并且回答下列問題
- 解釋printf.c和console.c它們的interface(這里說的是接口,我沒有完全理解,我的理解應(yīng)該是讓我們解釋每個函數(shù)的作用)。console.c對外提供了什么函數(shù)?,這個函數(shù)在printf.c當(dāng)中如何被使用的?
- 解釋下面代碼的意思
if (crt_pos >= CRT_SIZE) {
int i;
memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
crt_buf[i] = 0x0700 | ' ';
crt_pos -= CRT_COLS;
}
- 接下來的問題要看Lecture2 notes才能徹底理解,這些notes描述了關(guān)于x86平臺上的C calling convention
逐步執(zhí)行以下的代碼
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
回答兩個小問題:
- 在調(diào)用cprintf()的時候,fmt指向了哪里?
- 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.
- 運(yùn)行下面的代碼
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);
輸出結(jié)果是什么? 逐步調(diào)試來解釋它的輸出結(jié)果(這個問題和endian相關(guān))
- 下面代碼的輸出結(jié)果是什么?
cprintf("x=%d y=%d", 3);
不得不說part3的題目多多了,而且做起來也不容易,不過學(xué)習(xí)路上不能偷懶,開始干活吧~
解釋下printf.c和console.c當(dāng)中各個函數(shù)是干嘛的?
說在前面,console.c當(dāng)中不少和硬件相關(guān)的,我沒有完全理解,比如說串口(serial port)和并口(parallel port)的初始化以及相關(guān)數(shù)據(jù)的寫入,我很多都沒有了解,我認(rèn)為,學(xué)習(xí)操作系統(tǒng)更應(yīng)該是關(guān)注軟件層面的東西。遇到相關(guān)的,知道的我說一下,不懂的省略,希望有時間來補(bǔ)充這里的空白吧。
當(dāng)我們使用cprintf()這個函數(shù)的時候,調(diào)用順序是如下的:
cprintf () -> vcprintf() -> vprintfmt() -> vprintfmt() (定義在printfmt.c中)->putch()(定義在printf.c中) -> cputchar() (定義在console.c當(dāng)中) -> ....(console.c還調(diào)用了別的函數(shù))。
下面著重講幾個比較重要的函數(shù)
cprint
int
cprintf(const char *fmt, ...)
{
va_list ap;
int cnt;
va_start(ap, fmt);
cnt = vcprintf(fmt, ap);
va_end(ap);
return cnt;
}
這里使用了C語言中的可變參數(shù),這里使用的幾個va開頭的都是GCC compiler builtin的宏(macros)。這里介紹了這幾個宏的意思,在lab1中的這幾個宏定義在stdarg.h當(dāng)中,在這里我們只需要記住一點(diǎn),va_start,va_arg,va_end都是和處理可變參數(shù)有關(guān),而cprintf用到了可變參數(shù),所以這里出現(xiàn)了他們的身影。根據(jù)文章描述,va_start的第一個參數(shù)是va_list類型的并且指向可選參數(shù)的第一個參數(shù),va_start的第二個參數(shù)是指向必須參數(shù)(required parameters)(并且必要參數(shù)要在可選參數(shù)之前),可以看到代碼確實(shí)按照這樣的思路來實(shí)現(xiàn)的。
After all arguments have been retrieved, va_end resets the pointer to NULL. va_end must be called on each argument list that's initialized with va_start or va_copy before the function returns.
va_end必須要在函數(shù)返回前被調(diào)用,所以在return cnt前面我們調(diào)用了va_end。
vcprintf
int
vcprintf(const char *fmt, va_list ap)
{
int cnt = 0;
vprintfmt((void*)putch, &cnt, fmt, ap);
return cnt;
}
vcprintf是針對格式化符號處理的,putch()是一個函數(shù)指針,在vcprintf內(nèi)調(diào)用vprintfmt()格式化字符串結(jié)束后,調(diào)用putch()來改變屏幕上的光標(biāo)位置(雖然改變光標(biāo)位置的真正的函數(shù)并不是putch,putch調(diào)用了其他函數(shù)來實(shí)現(xiàn)這個功能)。vprintfmt()中完成了對屏幕內(nèi)容的輸出。
接下來看一下vprintfmt()
原來函數(shù)的實(shí)現(xiàn)有點(diǎn)長,而且有些代碼也不好懂,我選了一點(diǎn)我們最熟悉的。比如說%d打印數(shù)字
可以看到case 'd'就是處理打印數(shù)字的代碼。通過這樣就可以知道,vprintfmt()完成了格式化字符串的任務(wù)。另外作業(yè)也做好了,實(shí)現(xiàn)打印八進(jìn)制的數(shù)字,基本思路和十進(jìn)制的一樣,只需要改以下base就行.
case 'd':
num = getint(&ap, lflag);
if ((long long) num < 0) {
putch('-', putdat);
num = -(long long) num;
}
base = 10;
goto number;
// unsigned decimal
case 'u':
num = getuint(&ap, lflag);
base = 10;
goto number;
// (unsigned) octal
case 'o':
// Replace this with your code.
//打印8進(jìn)制數(shù)
num = getuint(&ap, lflag);
base = 8;
goto number;
下面解釋下一下putch()函數(shù)
static void putch(int ch, int *cnt)
{
cputchar(ch);
*cnt++;
}
//在console.c中
void cputchar(int c)
{
cons_putc(c);
}
//這個在console.c當(dāng)中
static void cons_putc(int c)
{
//輸出到串口(serial port)
serial_putc(c);
//輸出到并口(parallel port)
lpt_putc(c);
//輸出到屏幕
cga_putc(c);
}
putch每次向屏幕會輸出一個字符,cnt用于記錄輸出的字符個數(shù),putch()調(diào)用了cputchar(),雖然參數(shù)是一個int類型,實(shí)際上每次傳入的都是ascii,cputchar()調(diào)用cons_putc(),cons_putc()中的cga_putc()才是真正的輸出內(nèi)容到屏幕的函數(shù)。
好了現(xiàn)在就可以回答第一個問題了,雖然我們沒有查看console.c中所有的函數(shù)。但是已經(jīng)大概知道了如何向屏幕輸出內(nèi)容。console.c中向printf.c提供了cputchar()函數(shù),Printf.c中的putch函數(shù)來使用cputchar()完成了輸出
第二到問題,解釋代碼意思
if (crt_pos >= CRT_SIZE) {
int i;
memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
crt_buf[i] = 0x0700 | ' ';
crt_pos -= CRT_COLS;
}
這段代碼的意思是,當(dāng)屏幕滿的時候,滾屏。屏幕可以顯示的是25*80字節(jié)的內(nèi)容。所以if (crt_pos >= CRT_SIZE)判斷當(dāng)前光標(biāo)位置是否超過了屏幕。很自然的下面的代碼就是為滾屏服務(wù)的,memove()函數(shù)就是為了在復(fù)制內(nèi)容。新空出來的一行,以黑底白字,空格填充,最后光標(biāo)位置減80。這個函數(shù)當(dāng)中的其他部主要處理的都是改變光表位置,比如\n,光標(biāo)位置+80。
第三問題,逐步調(diào)代碼

從上面結(jié)果可以看到,可以看到把可變參數(shù)都壓入到棧了,所以我們可以得到結(jié)論,ap肯定是指向棧頂?shù)?,這樣才可以壓入?yún)?shù)。那么fmt自然指向的就是前面的字符串了。
第四道題,調(diào)試cprintf語句
這道題目非常有意思.廢話少說,先看實(shí)驗(yàn)結(jié)果:


竟然打印出來了Hello world。不過仔細(xì)觀察以下這里,兩個不是字母L,而是數(shù)字1。57616=0xe110。
在來看一下World是怎么出現(xiàn)的。這里涉及到little-endian這個問題,在這里我暫時先不仔細(xì)的說endian的問題。先記住一點(diǎn),這個問題會出現(xiàn)在多字節(jié)數(shù)據(jù)或者字(word)存儲的時候,上面的無符號數(shù)就是一個4字節(jié)的數(shù)據(jù),當(dāng)他通過總線寫入到內(nèi)存的時候,是低字節(jié)在前,高字節(jié)在后,是反著的。然后我們使用了%s來讀取,讀出來的數(shù)據(jù)自然也是反著的。在將他轉(zhuǎn)為對應(yīng)的ascii,r = 114 = 0x72,l = 108 = 0x6c, d = 100 = 0x64。這就產(chǎn)生了He110 World,注意,這個He110是假冒偽劣
第五個問題, In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?
實(shí)話說,第五個問題為不知道他的用意是什么,逐步調(diào)試到這里也十分麻煩。我說一下為觀察到的結(jié)果,
在我的電腦上輸出結(jié)果為:x=3,y=1600,我想當(dāng)然猜測為什么會出現(xiàn)這樣的結(jié)果,是因?yàn)?,y沒有壓入到棧,所有棧得到的數(shù)據(jù)是內(nèi)存中的其他值。
上面一個實(shí)驗(yàn),我們知道了JOS中那些函數(shù)完成了向屏幕輸出內(nèi)容,如何格式化輸出的內(nèi)容,并且是如何改變光標(biāo)位置的,并且草草地知道了幾個GCC內(nèi)置的用于處理可變參數(shù)的宏。接下來的內(nèi)容是和棧相關(guān)的,又是比較麻煩的一個part。
棧(Stack)
本個lab最后一個實(shí)驗(yàn)就是,我們將會更加詳細(xì)地探索以下C語言是如何使用x86的棧的,并且我們要寫一個非常有用的內(nèi)核監(jiān)控函數(shù)(kernel monitor function),它會打印目前所執(zhí)行函數(shù)之前的函數(shù)的EIP。
Exercise 9
看一看在哪里初始化了內(nèi)核的棧,并且內(nèi)核棧初始化在哪個內(nèi)存地址?內(nèi)核是如何為它的內(nèi)核保留棧的?這塊區(qū)域的哪一端是esp指向的呢?
這個Exercise相對來說還是比較容易的,一個一個回答。
首先棧的初始化是在./lab/kern/entry.S中初始化的,代碼如下:

其中,KSTKSIZE在inc/memlayout.h當(dāng)中,它的大小是40968。在這里,.space*應(yīng)個和intel語法下的db 用法差不多,內(nèi)核預(yù)留內(nèi)存空間給棧使用。bootstacktop這個標(biāo)號就是棧的初始地址,由于在x86當(dāng)中棧是由高地址向低地址延伸的,所以esp最開始指向bootstacktop,代碼實(shí)現(xiàn)如下所示:

Exercise 10
為了使得我們對x86 calling convention更加熟悉,找到test_backtrace的地址,并且打一個斷點(diǎn)在那里,每次在調(diào)用它后發(fā)生了什么?棧當(dāng)中壓入了多少數(shù)據(jù)? 我們推薦使用qemu pachted,MIT推薦使用這個,但是我好像在lab1 沒有發(fā)現(xiàn)什么問題。
首先來跟蹤以下test_backtrace這個函數(shù),這個函數(shù)是遞歸調(diào)用的,初始的參數(shù)是5,接著4.3.2.1。在這里我代碼就不貼了,我就跟蹤了test_backtrace(5)到test_backtrace(4)的情況,下面先給反匯編的代碼:
void
test_backtrace(int x)
{
f0100040: f3 0f 1e fb endbr32
f0100044: 55 push %ebp ;將原來的ebp亞入,esp = init-4
f0100045: 89 e5 mov %esp,%ebp ; ebp = init-4
f0100047: 56 push %esi ;esp = init-8
f0100048: 53 push %ebx ;esp = init-12
f0100049: e8 8f 01 00 00 call f01001dd <__x86.get_pc_thunk.bx>
f010004e: 81 c3 ba 12 01 00 add $0x112ba,%ebx ;dont konw
f0100054: 8b 75 08 mov 0x8(%ebp),%esi ;ebp+8 = agrs
cprintf("entering test_backtrace %d\n", x);
f0100057: 83 ec 08 sub $0x8,%esp ;空出8字節(jié)
f010005a: 56 push %esi ;args壓棧
f010005b: 8d 83 18 08 ff ff lea -0xf7e8(%ebx),%eax ;不知道lea命令是干嘛,猜測是獲得字符串的地址
f0100061: 50 push %eax ;字符串地址壓棧,總共esp變化了16字節(jié)
f0100062: e8 37 0a 00 00 call f0100a9e <cprintf>
if (x > 0)
f0100067: 83 c4 10 add $0x10,%esp ;esp+16,恢復(fù)了上面cprintf的調(diào)用
f010006a: 85 f6 test %esi,%esi
f010006c: 7e 29 jle f0100097 <test_backtrace+0x57> ;條件不成立,跳轉(zhuǎn)mon_backtrace
test_backtrace(x-1);
f010006e: 83 ec 0c sub $0xc,%esp ;空出12字節(jié)
f0100071: 8d 46 ff lea -0x1(%esi),%eax ;猜測,原來args在esi,這里做了esi=esi-1,eax=esi
f0100074: 50 push %eax ;eax=4壓棧
f0100075: e8 c6 ff ff ff call f0100040 <test_backtrace> ;遞歸執(zhí)行
f010007a: 83 c4 10 add $0x10,%esp
else
mon_backtrace(0, 0, 0);
cprintf("leaving test_backtrace %d\n", x);
f010007d: 83 ec 08 sub $0x8,%esp ;空出兩個字節(jié)
f0100080: 56 push %esi ;壓入esi=5
f0100081: 8d 83 34 08 ff ff lea -0xf7cc(%ebx),%eax
f0100087: 50 push %eax ;壓入字符串
f0100088: e8 11 0a 00 00 call f0100a9e <cprintf> ;打印leaving
}
f010008d: 83 c4 10 add $0x10,%esp ;總共壓入了16字節(jié),恢復(fù)棧,所以add0x10
f0100090: 8d 65 f8 lea -0x8(%ebp),%esp
f0100093: 5b pop %ebx ;test_backtrace執(zhí)行結(jié)束
f0100094: 5e pop %esi ;恢復(fù)寄存器內(nèi)容,這幾個寄存器都是callee register
f0100095: 5d pop %ebp ;也是callee register
f0100096: c3 ret ;遞歸已經(jīng)結(jié)束,從這里返回
mon_backtrace(0, 0, 0);
f0100097: 83 ec 04 sub $0x4,%esp
f010009a: 6a 00 push $0x0 ;mon_backtrace三個參數(shù)壓棧
f010009c: 6a 00 push $0x0
f010009e: 6a 00 push $0x0
f01000a0: e8 1d 08 00 00 call f01008c2 <mon_backtrace>
f01000a5: 83 c4 10 add $0x10,%esp ;因?yàn)樯厦鎠ub 0x04,加上上面三個0x00的亞棧,因此這里add 0x10
f01000a8: eb d3 jmp f010007d <test_backtrace+0x3d> ;跳轉(zhuǎn)到打印leaving那一段函數(shù)
我的實(shí)驗(yàn)?zāi)繕?biāo),看一下test_backtrace(5)到test_backtrace(4) call之前的棧的情況。下面是上面代碼的一部分:

在push ebx這條指令結(jié)束后。應(yīng)該是(從高到低):參數(shù)(就是數(shù)字5),下一條執(zhí)行的命令的地址(ret需要用),ebp(0xf010_0040的代碼),esi,ebx。
在我的實(shí)驗(yàn)環(huán)境下,此時ebp = 0xf010_fff8,eip=0xf010010d,esi=0x00010094,ebx=0xf0111308。如下圖

看一下實(shí)際內(nèi)存當(dāng)中的信息,這里稍微有點(diǎn)不一樣,因?yàn)槲掖藭r的內(nèi)存是執(zhí)行完sub $0x8,%esp后的結(jié)果,但是沒關(guān)系??梢钥吹角皫讉€內(nèi)存的內(nèi)容和我的表格是一一對應(yīng)的:

下面稍微偷懶,我只記錄下對棧操作的時候(例如push)的時候棧的截圖:
sub $0x8,%esp
這個不贅述,就在上面。,它這里實(shí)際上是讓棧直接空了8字節(jié)的內(nèi)容,在上圖,一個是0xf010_004e,另外一個是0x0。0xf010_004e是怎么產(chǎn)生的,我沒有去逐步調(diào)試。
push %esi
認(rèn)真看上面的代碼,并且結(jié)合自己的編譯出來的boot.asm,我們知道esi存放的是agrs,就是0x05,所以此時棧如下所示:

可以看到0x05已經(jīng)被壓入了。
push %eax
這里eax是字符串的地址,和上面的x一起當(dāng)作參數(shù)給cprintf使用,此時棧內(nèi)如下,其中0xf010_1b20和0x05都是cprintf的參數(shù):

回想以下,到目前棧減少了4個字節(jié)的數(shù)據(jù),分別是sub指令,兩個push指令,所以cprintf結(jié)束后
f0100067: 83 c4 10 add $0x10,%esp ;esp+16,恢復(fù)了上面cprintf的調(diào)用
f010006a: 85 f6 test %esi,%esi
f010006c: 7e 29 jle f0100097 <test_backtrace+0x57> ;條件不成立,跳轉(zhuǎn)mon_backtrace
調(diào)用者恢復(fù)棧(C calling convention),此時的?;謴?fù)到了最初情況,如下,仔細(xì)對照以下上面點(diǎn)的那個表格:

好繼續(xù)前進(jìn),在f0100075停下,查看一下棧
test_backtrace(x-1);
f010006e: 83 ec 0c sub $0xc,%esp ;空出12字節(jié)
f0100071: 8d 46 ff lea -0x1(%esi),%eax ;猜測,原來args在esi,這里做了esi=esi-1,eax=esi
f0100074: 50 push %eax ;eax=4壓棧
f0100075: e8 c6 ff ff ff call f0100040 <test_backtrace> ;遞歸執(zhí)行
棧的情況如下所示:

看上面代碼,sub $0xc,%esp空出12字節(jié),接著push %eax ;壓入?yún)?shù)4,看上圖,我們的4被壓入了。成功了,如果你此時在按下si,就去執(zhí)行test_backtrace(4)了。
下面對test_traceback(5)的寄存器做一個總結(jié),這個對于待會的作業(yè)非常重要:

如上圖所示,ebp持有的是之前ebp的地址,esp是本次test_backtrace()的參數(shù)底。上面一共8個int的長度,所以是32字節(jié)。現(xiàn)在我們可以回答問題了,每次調(diào)用test_backtrace()壓入了32字節(jié)的數(shù)據(jù)。不得不說,這一串真的挺麻煩的,花了不少時間在研究。
回到正文,下面繼續(xù)介紹part3的內(nèi)容(我要開始繼續(xù)翻譯了哈哈哈)。
以上的練習(xí),他應(yīng)該給了你一些信息,并且你需要根據(jù)這些信息實(shí)現(xiàn)一個mon_backtrace()。函數(shù)的聲明已經(jīng)在kern/monitor.c當(dāng)中了。
mon_backtrace()打印出來的信息應(yīng)該如下所示:
Stack backtrace:
ebp f0109e58 eip f0100a62 args 00000001 f0109e80 f0109e98 f0100ed2 00000031
ebp f0109ed8 eip f01000d6 args 00000000 00000000 f0100058 f0109f28 00000061
...
每一行都包括了ebp,eip,和args。ebp應(yīng)該是在進(jìn)入后函數(shù)后的ebp寄存器的值,就是棧指針的在進(jìn)入函數(shù)以及完成開場白后的位置。eip的值是返回指針的值(也就是被ret所使用的地址)。返回指針通常指向call之后的下一條語句(這個很好理解吧,只有指向下一條指令才可以繼續(xù)運(yùn)行)。最后,5個十六進(jìn)制數(shù)就是5個需要傳遞的參數(shù)。如果函數(shù)所需要的參數(shù)少于5個,當(dāng)然并不是這5個所有的參數(shù)都有用(通過上面的觀察,確實(shí)好幾個參數(shù)沒用),遺留問題:為什么不能夠從代碼中獲得到底有多少個參數(shù)呢?如何解決這個問題呢?
Exercise 11
根據(jù)上面的輸出例子實(shí)現(xiàn)mon_backtrace()函數(shù)。請使用上面的輸出格式,否這grade script無法通過,如果你覺得你做好了就用make grade來測試下你是不是作對了。
代碼實(shí)現(xiàn):
int mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// 獲取寄存器ebp本身的位置
int regebp = read_ebp(); //獲得ebp寄存器的內(nèi)容
int* ebp = (int*)regebp; //將內(nèi)容轉(zhuǎn)為指針類型,然后就可以獲得需要的參數(shù)了
while(*ebp != 0) {
cprintf("ebp:%08x ",*ebp);
cprintf("eip:%08x ",*(ebp+1));
cprintf("args:%08x ",*(ebp+2));
cprintf("%08x ",*(ebp+3));
cprintf("%08x ",*(ebp+4));
cprintf("%08x ",*(ebp+5));
cprintf("%08x \n",*(ebp+6));
ebp = (int *)(*ebp);
}
return 0;
}
注意看之前的那個寄存器總結(jié)圖,ebp內(nèi)的內(nèi)容就是一個地址,因?yàn)槲覀儓?zhí)行了mov esp,ebp命令。所以思路很簡單,先獲得ebp的值,就得到了棧單元的地址。然后在根據(jù)上面的圖,就可以計算處各個參數(shù)的值了。效果如下:

唉,就這么一點(diǎn)簡單的代碼,花了我差不多一天的時間在調(diào)試,最后把它寫出來,屬實(shí)不容易。在最后,我還有一個Exercise 12沒有實(shí)現(xiàn)。先暫時這樣吧,草草看了一下Exercise 12,Exercise 12是在Exercise 11的基礎(chǔ)上增加一點(diǎn)功能,我暫時沒做,留到有時間在做吧,我想Exercise 11已經(jīng)讓我們收獲很多了,我們知道了C calling convention,并且根據(jù)此知道了如何獲得之前的eip寄存器內(nèi)容。