Part 2: The Boot Loader
加載核心
為了了解boot/main.c,你需要知道ELF二進制檔案是什麼。當你編譯和連結一個C程式(如JOS核心)時,編譯器将每個C源檔案('. C ')轉換為一個對象檔案('.o'),其中包含以硬體期望的二進制格式編碼的彙編語言指令。連結器接下來将所有已編譯的目标檔案合并為一個二進制映像,如obj/kern/kernel,在這裡是ELF格式的二進制映像,ELF表示“可執行和連結格式”。
關于這種格式的完整資訊可以在我們的參考頁面上的ELF規範中找到,但你不需要在本課程中深入研究這種格式的細節。雖然作為一個整體,這種格式非常強大和複雜,但大部分複雜的部分是為了支援共享庫的動态加載,我們在這個類中不會做這些。維基百科頁面有一個簡短的描述。
對于6.828來說,你可以把ELF可執行檔案看作是一個帶有加載資訊的頭檔案,後面跟着幾個程式段,每個程式段都是一個連續的代碼塊或資料,打算加載到指定的記憶體位址中。啟動加載程式不修改代碼或資料;它将它加載到記憶體中并開始執行。
ELF二進制檔案以一個定長ELF頭檔案開始,接下來是一個變長程式頭檔案,列出了要加載的每個程式段。這些ELF頭檔案的C語言定義在inc/ ELF .h中。我們感興趣的程式部分包括:
.text:程式的可執行指令。
.rodata:隻讀資料,例如C編譯器生成的ASCII字元串常量。(但我們不會設定硬體來禁止寫操作。)
.data: data區域儲存程式的初始化資料,比如用int x = 5這樣的初始化方法聲明的全局變量。
當連結器計算程式的記憶體布局時,它會為未初始化的全局變量(如int x;)在記憶體中緊跟在.data之後的.bss段中預留白間。C語言要求“未初始化”的全局變量的值從0開始。是以,不需要将.bss的内容存儲在ELF二進制檔案中。相反,連結器隻記錄.bss段的位址和大小。加載器或程式本身必須将.bss段置零。
輸入以下指令,可以檢查核心可執行檔案中所有段的名稱、長度和連結位址的完整清單:
athena% objdump -h obj/kern/kernel
您将看到比上面列出的更多的部分,但其他部分對我們的目的不重要。其餘的大部分用于儲存調試資訊,這些資訊通常包含在程式的可執行檔案中,但不會由程式加載器加載到記憶體中。
請特别注意.text部分的“VMA”(或連結位址)和“LMA”(或加載位址)。段的加載位址是該段應該加載到記憶體中的記憶體位址。
段的連結位址是段預期執行的記憶體位址(實際執行不一定是)。連結器以各種方式在二進制檔案中編碼連結位址,例如當代碼需要一個全局變量的位址時,結果是如果從一個沒有連結的位址執行,二進制檔案通常無法工作。(可以生成位置無關的代碼,其中不包含任何絕對位址。它被現代共享庫廣泛使用,但它有性能和複雜性成本,是以我們不會在6.828中使用它。)
連結位址是編譯器給出的,用來計算偏移值,友善程式運作
通常,連結位址和加載位址是相同的。例如,檢視引導加載程式的.text部分:
objdump -h obj/boot/boot.out
引導加載程式使用ELF程式頭來決定如何加載各節。程式頭指定了ELF對象的哪些部分需要加載到記憶體中,以及各個部分應該占用的目标位址。你可以輸入以下指令檢視程式頭:
objdump -x obj/kern/kernel
然後,在objdump輸出的“程式頭”下列出程式頭。ELF對象中需要加載到記憶體中的區域标記為“LOAD”。給出了每個程式頭的其他資訊,如虛拟位址(“vaddr”)、實體位址(“paddr”)、加載區域的大小(“memsz”和“filesz”)。
回到boot/main.c中,每個程式頭的ph->p_pa字段包含該段的目标實體位址(在本例中,它實際上是一個實體位址,盡管ELF規範對該字段的實際含義是模糊的)。
BIOS從位址0x7c00開始将引導扇區加載到記憶體中,是以這是引導扇區的加載位址。這也是啟動扇區執行的地方,是以這也是它的連結位址。我們通過向boot/Makefrag中的連結器傳遞-Ttext 0x7C00來設定連結位址,這樣連結器就會在生成的代碼中生成正确的記憶體位址。
練習5
再次跟蹤引導加載程式的前幾個指令,并識别出第一個指令,如果您錯誤地獲得引導加載程式的連結位址,它将“中斷”或做錯誤的事情。然後将boot/Makefrag中的連結位址更改為錯誤,運作make clean,使用make重新編譯lab,并再次跟蹤到引導加載程式中,看看會發生什麼。别忘了把連結位址改回來,然後再弄幹淨!
回顧核心的加載位址和連結位址。與引導加載程式不同的是,這兩個位址并不相同:核心告訴引導加載程式以低位址(1兆位元組)将其加載到記憶體中,但它期望從高位址執行。我們将在下一節深入探讨如何實作這一功能。
除了節資訊,ELF頭中還有一個字段對我們很重要,名為e_entry。這個字段儲存了程式入口點的連結位址:程式文本部分中應該開始執行的記憶體位址。你可以看到入口點:
objdump -f obj/kern/kernel
現在您應該能夠了解boot/main.c中的最小ELF加載程式。它将核心的每個部分從磁盤讀取到記憶體的加載位址,然後跳轉到核心的入口點。
練習6
我們可以使用GDB的x指令檢查記憶體。GDB手冊提供了完整的細節,但就目前而言,隻要知道指令x/Nx ADDR在ADDR處列印N個記憶體單詞就足夠了。(注意,指令中的兩個` x `都是小寫的。)警告:單詞的大小不是通用标準。在GNU程式集中,一個單詞是兩個位元組(xorw中的“w”代表單詞,意思是兩個位元組)。
重置機器(退出QEMU/GDB并重新啟動它們)。檢查BIOS進入引導加載程式時0x00100000處的8個記憶體字,以及引導加載程式進入核心時的8個記憶體字。它們為什麼不同?第二個斷點處是什麼?(實際上不需要使用QEMU來回答這個問題。隻是覺得)。
産生變化的原因在于boot loader将kernel加載到了記憶體當中。
輸入指令objdump -x obj/kern/kernel,檢視所有header
是以儲存在0x100000中的應該是.text段
Part 3: The Kernel
我們現在開始更詳細地考察最小JOS核心。(你終于可以寫一些代碼了!)與引導加載程式類似,核心以一些彙編語言代碼開始,這些代碼會進行一些設定,使C語言代碼能夠正确執行。
使用虛拟記憶體解決位置依賴問題
當我們檢查上述引導加載程式的連結和加載位址時,它們完全比對,但是核心的連結位址(由objdump列印)與其加載位址之間存在(相當大的)差異。回去檢查一下,確定你能看到我們在說什麼。(連結核心比引導加載程式要複雜得多,是以連結和加載位址位于kern/kernel.ld的頂部。)
作業系統核心通常喜歡連結并在非常高的虛拟位址上運作,例如0xf0100000,以便将處理器虛拟位址空間的較低部分留給使用者程式使用。這種安排的原因在下一個實驗中會更清楚。
許多機器在位址0xf0100000沒有任何實體記憶體,是以我們不能指望能夠在那裡存儲核心。相反,我們将使用處理器的記憶體管理硬體将虛拟位址0xf0100000(核心代碼預期運作的連結位址)映射到實體位址0x00100000(啟動加載程式将核心加載到實體記憶體)。這樣,盡管核心的虛拟位址足夠高,可以為使用者程序留下足夠的位址空間,但它将加載到PC RAM中1MB位置的實體記憶體中,就在BIOS ROM上面。這種方法要求PC至少有幾兆位元組的實體記憶體(這樣實體位址0x00100000才能工作),但這可能适用于1990年左右以後建構的任何PC。
實際上,在下一個實驗中,我們将把PC的整個底層256MB的實體位址空間,從實體位址0x00000000到0x0fffffff(2的28次方),分别映射到虛拟位址0xf0000000到0xffffffff(留下了虛拟底層的256MB)。您現在應該看到為什麼JOS隻能使用實體記憶體的前256MB。
現在,我們隻映射前4MB的實體記憶體,這足以讓我們啟動和運作。我們使用kern/entrypgdir.c中手寫的、靜态初始化的頁目錄和頁表來實作這一點。現在,你不需要了解它如何工作的細節,隻需要了解它實作的效果。直到kern/entry.S設定CR0_PG标志(不啟動分頁),記憶體引用被視為實體位址(嚴格地說,它們是線性位址,但boot/boot.S為我們建立了一個從線性位址到實體位址的身份映射,我們永遠不會改變它)。一旦設定了CR0_PG,記憶體引用就是虛拟位址,由虛拟記憶體硬體(MMU)轉換為實體位址。entry_pgdir将虛拟位址從0xf0000000到0xf0400000轉換為實體位址0x00000000到0x00400000,将虛拟位址0x00000000到0x00400000轉換為實體位址0x00000000到0x00400000。任何不在這兩個範圍内的虛拟位址都将導緻硬體異常,因為我們還沒有設定中斷處理,這将導緻QEMU轉儲機器狀态并退出(或者無限重新開機,如果您沒有使用6.828更新檔版本的QEMU)。
練習7
https://www.cnblogs.com/wuhualong/p/lab01_exercise07_observe_memory_mapping.html
https://blog.csdn.net/weixin_51187533/article/details/123111228
使用QEMU和GDB跟蹤到JOS核心,并在movl %eax, %cr0處停止。檢查0x00100000和0xf0100000的記憶體。現在,使用stepi GDB指令單步執行該指令。同樣,檢查0x00100000和0xf0100000的記憶體。確定你明白剛才發生了什麼。
建立新映射後,如果映射不到位,将無法正常工作的第一條指令是什麼?注釋掉kern/entry.S中的movl %eax, %cr0,追蹤它,看看你是否正确。
根據這個boot.out檢視出其中實體位址和虛拟位址一樣,就是剛開始BIOS和boot loader運作的代碼
檢視核心的虛拟位址和實體位址 發現兩個相差很大
通過這個了解到 0x100000 核心加載的實體位址 0xf0100000 虛拟位址
上面是進入核心後執行movl %eax, %cr0之前時候核心加載的虛拟位址和實體位址中内容的差別
執行這行代碼之後,虛拟位址對應的内容中就存在内容了,而且和實體位址中相同。
movl %eax, %cr0 實作了虛拟位址的啟用。
檢視obj/kern/kernel.asm中的内容
檢視此時寄存器的内容,發現最高位為1,是以啟動分頁實作虛拟位址啟用
在啟動分頁失敗之後,後面的跳轉位址的指令就會失敗,因為之後是按照虛拟位址計算偏移跳轉的
題目第二個問題是判斷記憶體位址失敗後哪些指令會運作失敗,我判斷是下面兩條指令mov $relocated, %eax和jmp %eax就會失敗,我的推理過程:relocated這個位址是由段位址加上偏移位址得到的,段位址是0xf0100008,如果位址映射失敗,那些jmp %eax就會跳到0xf010008加上偏移量的實體位址,導緻出錯。gdb調試結果恰好驗證了我的猜測是正确的。
- 将kern/entry.S的movl %eax, %cr0注釋掉,重新啟動qemu和gdb,在jmp %eax加斷點,使用c指令運作到這裡,使用x/16xw檢視0x00100000和0xf0100000兩個位址往後16個word的内容,發現兩者不同,後者依然是全0(内容與第一節第1步的相同,此處不再提供)。可見位址映射确實失敗了。
- 繼續往下執行一步,發現gdb報錯。應該是因為0xf010002c位址後面的資料全為0,導緻把空指針賦給寄存器而報錯。
(gdb) si
=>0xf010002c <relocated>: add %al,(%eax)
relocated () at kern/entry.S:7474 movl $0x0,%ebp # nuke frame pointer
(gdb)
Remote connection closed
此時QEMU那邊也列印一堆錯誤資訊并終止運作:
qemu: fatal: Trying to execute code outside RAM or ROM at0xf010002cEAX=f010002c EBX=00010094ECX=00000000EDX=000000a4
ESI=00010094EDI=00000000EBP=00007bf8 ESP=00007bec
EIP=f010002c EFL=00000086 [--S--P-] CPL=0 II=0 A20=1 SMM=0HLT=0ES =001000000000 ffffffff 00cf9300 DPL=0DS [-WA]
CS =000800000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =001000000000 ffffffff 00cf9300 DPL=0DS [-WA]
DS =001000000000 ffffffff 00cf9300 DPL=0DS [-WA]
FS =001000000000 ffffffff 00cf9300 DPL=0DS [-WA]
GS =001000000000 ffffffff 00cf9300 DPL=0DS [-WA]
LDT=000000000000 0000ffff 00008200 DPL=0 LDT
TR =000000000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT= 00007c4c 00000017
IDT= 00000000 000003ff
CR0=00000011CR2=00000000CR3=00112000CR4=00000000DR0=00000000DR1=00000000DR2=00000000DR3=00000000
DR6=ffff0ff0 DR7=00000400
CCS=00000084 CCD=80010011 CCO=EFLAGS
EFER=0000000000000000
FCW=037f FSW=0000 [ST=0] FTW=00 MXCSR=00001f80
FPR0=00000000000000000000 FPR1=00000000000000000000
FPR2=00000000000000000000 FPR3=00000000000000000000
FPR4=00000000000000000000 FPR5=00000000000000000000
FPR6=00000000000000000000 FPR7=00000000000000000000
XMM00=00000000000000000000000000000000 XMM01=00000000000000000000000000000000
XMM02=00000000000000000000000000000000 XMM03=00000000000000000000000000000000
XMM04=00000000000000000000000000000000 XMM05=00000000000000000000000000000000
XMM06=00000000000000000000000000000000 XMM07=00000000000000000000000000000000GNUmakefile:165: recipe for target 'qemu-gdb' failed
make: *** [qemu-gdb] Aborted (core dumped)
格式化列印到控制台
大多數人認為像printf()這樣的函數是理所當然的,有時甚至認為它們是C語言的“原語”。但是在作業系統核心中,我們必須自己實作所有的I/O。
請通讀kern/printf.c、lib/printfmt.c和kern/console.c,確定了解它們之間的關系。為什麼printfmt.c位于單獨的lib目錄中,這一點在後面的實驗中會很清楚。
練習8
我們省略了一小段代碼——使用“%o”模式列印八進制數所需的代碼。查找并填充此代碼片段。
能夠回答以下問題:
- 解釋printf.c和console.c之間的接口。具體來說,console.c導出了什麼函數?printf.c如何使用該函數?
解答:printf.c中的putch函數調用了console.c中的cputchar函數,具體調用關系:cprintf -> vcprintf -> putch -> cputchar。
2.請在console.c中解釋以下内容
1 if (crt_pos >= CRT_SIZE) {
2 int i;
3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
5 crt_buf[i] = 0x0700 | ' ';
6 crt_pos -= CRT_COLS;
7 }
解答:聯系代碼上下文,可以了解這段代碼的作用。首先,CRT(cathode ray tube)是陰極射線顯示器。根據console.h檔案中的定義,CRT_COLS是顯示器每行的字長(1個字占2位元組),取值為80;CRT_ROWS是顯示器的行數,取值為25;而#define CRT_SIZE (CRT_ROWS * CRT_COLS)是顯示器螢幕能夠容納的字數,即2000。當crt_pos大于等于CRT_SIZE時,說明顯示器螢幕已寫滿,是以将螢幕的内容上移一行,即将第2行至最後1行(也就是第25行)的内容覆寫第1行至倒數第2行(也就是第24行)。接下來,将最後1行的内容用黑色的空格塞滿。将空格字元、0x0700進行或操作的目的是讓空格的顔色為黑色。最後更新crt_pos的值。總結:這段代碼的作用是當螢幕寫滿内容時将其上移1行,并将最後一行用黑色空格塞滿。
3.對于下列問題,你可能希望查閱第二講的筆記。這些說明涵蓋了GCC在x86上的調用約定。
逐漸跟蹤以下代碼的執行過程:
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
在對cprintf()的調用中,fmt指向什麼?ap指向什麼?
(按執行順序)列出對cons_putc、va_arg和vcprintf的每次調用。對于cons_putc,也列出它的參數。對于va_arg,列出ap在調用之前和之後所指向的内容。對于vcprintf,列出其兩個參數的值。
做這個題首先需要直到C語言中可變參數執行個體
https://www.cnblogs.com/bettercoder/p/3488299.html
https://www.jianshu.com/p/e43f2d3d3216
看這個就基本能懂,小結一下:
對于函數中的可變參數,第一個參數是固定參數,是必須有的,這個題目中的第一個參數就是字元串:"x %d, y %x, z %d\n",在cprintf函數中,使用fmt指向這個字元串。然後後面的x、y、z是ap指向的,ap是一個va_list類型的變量,專門用來儲存可變參數。之後使用va_start函數初始化ap,然後使用va_arg取得取地下一個參數y的位址并儲存在ap中,這樣就不可以周遊通路這些變量的值了。
小結函數:
基本上按照這道題看,就是從cprintf這個函數開始,按照如圖所示的方式調用
vprintfmt函數最關鍵:解析字元串——就是fmt指向的參數,然後根據字元串中數字輸出的方式進行列印,其中putch和cputchar函數似乎就是調用底層的彙編語言進行操作,因為其中好像有gcc内聯彙編的語句。
4.運作下面的代碼。
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);
輸出是什麼?解釋這個輸出是如何按照前一個練習的逐漸方式實作的。下面是一個将位元組映射到字元的ASCII表。
輸出取決于x86是小端序的這一事實。如果x86系統是大端序的,你會将i設定為什麼來産生相同的輸出呢?是否需要将57616更改為其他值?
小端:He110 world
大端:hell0 dlorw
Here's a description of little- and big-endian and a more whimsical description.
5.在下面的代碼中,在’y='的後面将會列印什麼?(注意:答案不是一個明确的數。)為什麼這會發生?
cprintf("x=%d y=%d", 3);
x=3 y=随機 y是随機的,因為這個時候ap這個指針指向3存儲的下一個位址,這個位址當中的值是未知的。
6.假設GCC改變了它的調用約定,按照聲明的順序将參數壓入棧中,這樣最後一個參數就會壓入棧中。如何改變cprintf或它的接口,以便仍然可以向它傳遞可變數量的參數?
(如果将GCC的調用約定改為參數從左到右壓棧,為支援參數數目可變需要怎樣修改cprintf函數?)
原來是這樣的
現在應該是相反了,有兩種方法。一種是程式員調用cprintf函數時按照從右到左的順序來傳遞參數,這種方法不符合我們的閱讀習慣、可讀性較差。第二種方法是在原接口的最後增加一個int型參數,用來記錄所有參數的總長度,這樣我們可以根據棧頂元素找到格式化字元串的位置。這種方法需要計算所有參數的總長度,也比較麻煩
challenge:增強控制台,允許文本以不同的顔色列印。傳統的方法是讓它解釋列印到控制台的文本字元串中嵌入的ANSI轉義序列,但你可以使用任何你喜歡的機制。在6.828參考頁面和其他網站上有大量關于程式設計VGA顯示硬體的資訊。如果你喜歡冒險,可以嘗試将VGA硬體切換到圖形模式,并使控制台在圖形幀緩沖區上繪制文本。
堆棧
在本實驗的最後一個練習中,我們将更詳細地探索C語言在x86上使用棧的方式,并在此過程中編寫一個有用的新核心螢幕函數,用于列印棧的回溯:從嵌套調用指令到目前執行點儲存的指令指針(IP)值的清單。
練習9
MIT 6.828 JOS學習筆記12 Exercise 1.9 - fatsheep9146 - 部落格園 (cnblogs.com) 看這個
判斷一下作業系統核心是從哪條指令開始初始化它的堆棧空間的,以及這個堆棧坐落在記憶體的哪個地方?核心是如何給它的堆棧保留一塊記憶體空間的?堆棧指針又是指向這塊被保留的區域的哪一端的呢?
前面已經分析過boot.S和main.c檔案的運作過程,這個檔案中的代碼是PC啟動後,BIOS運作完成後,首先執行的兩部分代碼。但是它們并不屬于作業系統的核心。當main.c檔案中的bootmain函數運作到最後時,它執行的最後一條指令就是跳轉到entry.S檔案中的entry位址處。此時控制權已經被轉交給了entry.S。
在跳轉到entry之前,并沒有對%esp,%ebp寄存器的内容進行修改,可見在bootmain中并沒有初始化堆棧空間的語句。
下面進入entry.S,在entry.S中我們可以看到它最後一條指令是要調用i386_init()子程式。這個子程式位于init.c檔案之中。在這個程式中已經開始對作業系統進行一些初始化工作,并且自重進入mointor函數。可見到i386_init子程式時,核心的堆棧應該已經設定好了。是以設定核心堆棧的指令就應該是entry.S中位于 call i386_init 指令之前的兩條語句:
movl $0x0,%ebp # nuke frame pointer
movl $(bootstacktop),%esp
x86棧指針(esp寄存器)指向目前正在使用的棧的最低位置。在為棧配置設定的區域中,該位置以下的所有内容都是空閑的。将值壓入棧涉及減小棧指針,然後将值寫入棧指針指向的位置。從棧彈出一個值涉及讀取棧指針指向的值,然後增加棧指針。在32位模式下,棧隻能儲存32位的值,并且esp總是可以被4整除。各種x86指令,比如call,都是“硬連接配接”到堆棧指針寄存器的。
相反,ebp(基本指針)寄存器主要是根據軟體約定與棧相關聯的。在進入C函數時,函數的序言代碼通常會将前一個函數的基指針壓入棧中來儲存它,然後在函數運作期間将目前esp值複制到ebp中。如果程式中的所有函數都遵守這個約定,那麼在程式執行期間的任何給定點,都可以通過跟蹤儲存的ebp指針鍊,并确定導緻程式中特定點到達的嵌套函數調用序列,進而對堆棧進行回溯。這種能力可能特别有用,例如,當某個函數因為傳遞了錯誤的參數而導緻斷言失敗或panic時,但你不确定是誰傳遞了錯誤的參數。棧回溯可以讓你找到有問題的函數。
對于ESP、EBP寄存器的了解 - 狂奔~ - 部落格園 (cnblogs.com)
練習10
MIT 6.828 JOS學習筆記13 Exercise 1.10 - fatsheep9146 - 部落格園 (cnblogs.com)
為熟悉x86平台上的C語言調用約定,請在obj/kern/kernel.asm中找到test_backtrace函數的位址,在那裡設定一個斷點,并檢查在核心啟動後每次調用它時發生了什麼。每個遞歸的test_backtrace嵌套層在棧上壓入多少個32位的單詞,這些單詞是什麼?
注意,為了讓這個練習正常工作,您應該使用工具頁面或Athena上提供的打過更新檔的QEMU版本。否則,你必須手動将所有斷點和記憶體位址轉換為線性位址。
上面的練習應該提供了實作堆棧回溯函數所需的資訊,你應該調用該函數mon_backtrace()。這個函數的原型已經在kern/monitor.c中了。你可以完全用C語言完成,但你可能會發現inc/x86.h中的read_ebp()函數很有用。還必須将這個新函數挂鈎到核心螢幕的指令清單中,以便使用者可以互動式地調用它。
backtrace函數應該以以下格式顯示函數調用幀的清單:
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值表示指向該函數使用的棧的基指針,即棧指針在函數進入和函數序言代碼設定基指針之後的位置。列出的eip值是函數的傳回指令指針:當函數傳回時,控件将傳回到的指令位址。傳回指令指針通常指向調用指令之後的指令(為什麼?)最後,在args之後列出的5個十六進制值是函數的前5個參數,它們會在函數被調用之前被壓入棧。當然,如果調用函數時傳入的參數少于5個,那麼這5個值并不都有用。(為什麼回溯代碼不能檢測實際有多少個參數?如何修複這個限制?)
列印的第一行反映了目前執行的函數,即mon_backtrace本身,第二行反映了調用mon_backtrace的函數,第三行反映了調用該函數的函數,以此類推。你應該列印所有未完成的棧幀。通過學習kern/entry。你會發現有一種簡單的方法告訴你什麼時候停止。
以下是你在K&R第5章中讀到的一些特别的要點,在接下來的練習和以後的實驗中都值得記住。
- 如果int* p = (int*)100,那麼(int)p + 1和(int)(p + 1)是不同的數:第一個是101,第二個是104。在将整數與指針相加時(如第二種情況),整數會隐式地乘以指針所指向對象的大小。
- p[i]定義為與*(p+i)相同,表示p所指向的記憶體中的第i個對象。上述加法規則在對象大于1位元組時适用。
- &p[i]與(p+i)相同,得到的是p所指向的記憶體中第i個對象的位址。
盡管大多數C程式從來不需要轉換指針和整數,但作業系統經常需要轉換。當你看到涉及記憶體位址的加法運算時,問問自己這是整數加法還是指針加法,并確定加法的值是否被正确相乘。
練習11
實作上述backtrace函數。請使用與示例中相同的格式,否則評分腳本将會混淆。當你認為它能正常工作時,運作make grade,看看它的輸出是否符合評分腳本的要求,如果不符合,就修複它。在你送出了你的實驗1代碼之後,歡迎你以任何你喜歡的方式改變回溯函數的輸出格式。
如果您使用read_ebp(),請注意GCC可能會生成“優化”的代碼,在mon_backtrace()的函數序言之前調用read_ebp(),這将導緻不完整的堆棧跟蹤(最近函數調用的堆棧幀丢失)。雖然我們已經嘗試禁用導緻此重排的優化,但您可能希望檢查mon_backtrace()的程式集,并確定對read_ebp()的調用發生在函數序言之後。
在這一點上,你的backtrace函數應該給你導緻mon_backtrace()被執行的堆棧上的函數調用者的位址。但在實踐中,你通常想知道與這些位址對應的函數名。例如,你可能想知道哪些函數可能包含導緻核心崩潰的bug。
為了幫助您實作此功能,我們提供了函數debuginfo_eip(),該函數在符号表中查找eip并傳回該位址的調試資訊。該函數定義在kern/kdebug.c中。
練習12
https://blog.csdn.net/weixin_41761478/article/details/101102354
《MIT 6.828 Lab 1 Exercise 12》實驗報告 - whl1729 - 部落格園 (cnblogs.com)
修改您的堆棧回溯函數,以顯示每個eip對應的函數名、源檔案名和行号。
在debuginfo_eip中,__STAB_ *是從哪裡來的?這個問題的答案很長;為了幫助你找到答案,你可能需要做以下事情:
- look in the file kern/kernel.ld for __STAB_*
- run objdump -h obj/kern/kernel
- run objdump -G obj/kern/kernel
- run gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s.
- 檢視引導加載程式是否在加載核心二進制檔案時加載符号表
通過插入對stab_binsearch的調用來查找位址對應的行号,完成debuginfo_eip的實作。
向核心螢幕添加一個backtrace指令,并擴充mon_backtrace的實作,以調用debuginfo_eip,并為表單的每個堆棧幀列印一行:
K> backtrace
Stack backtrace:
ebp f010ff78 eip f01008ae args 00000001 f010ff8c 00000000 f0110580 00000000
kern/monitor.c:143: monitor+106
ebp f010ffd8 eip f0100193 args 00000000 00001aac 00000660 00000000 00000000
kern/init.c:49: i386_init+59
ebp f010fff8 eip f010003d args 00000000 00000000 0000ffff 10cf9a00 0000ffff
kern/entry.S:70: <unknown>+0
K>
每一行給出了檔案名和棧幀的eip檔案中的行,後面是函數名和eip與函數第一條指令的偏移量(例如,monitor+106表示傳回的eip比monitor的開頭多106位元組)。
確定将檔案和函數名列印在單獨的一行上,以避免混淆評分腳本。
提示:printf格式字元串提供了一種簡單的(雖然不明确)方法來列印像stab表中那樣以非空字元結尾的字元串。printf(" %。*s", length, string)列印字元串中最多長度的字元看看printf手冊頁,以了解為什麼這樣做可行。
你可能會發現回溯過程中遺漏了一些函數。例如,您可能會看到對monitor()的調用,但不會看到對runcmd()的調用。這是因為編譯器内聯了一些函數調用。其他優化可能會讓你看到意想不到的行号。如果從GNUMakefile中去掉-O2,則回溯可能會更有意義(但核心将運作得更慢)。
實驗提供了int debuginfo_eip(uintptr_t addr, struct Eipdebuginfo *info)函數,功能是輸入一個指令位址addr,和一個Eipdebuginfo結構指針,該函數會查找addr處指令有關的資訊,若查找成功則傳回0,并把資訊填充到該結構中,比如指令所在檔案、行号、函數名、函數第一條指令位址等。
要了解并利用這個函數,要先了解stab表。
stab表
stab表是什麼?
GCC把C語言源檔案( ‘.c’ )編譯成彙編語言檔案( ‘.s’ ), 彙編器把彙編語言檔案翻譯成目标檔案( ‘.o’ )。在目标檔案中, 調試資訊用 ‘.stab’ 打頭的一類彙編指導指令表示, 這種調試資訊格式叫’Stab’, 即符号表(Symbol table)。這些調試資訊包括行号、變量的類型和作用域、函數名字、函數參數和函數的作用域等源檔案的特性。
由此我們知道要求輸出的資訊就是調試資訊。
那麼是怎麼把調試資訊填入目标檔案的呢?
在GCC編譯源檔案時, 如要生成Stab調試資訊, 打開編譯選項 ‘- gstabs’ 。彙編器處理 ‘.stab’ 打頭指導指令, 把Stab中的調試資訊填入 ‘.o’ 檔案的符号表和串表(string table)中,最後由連結器連結所有的目标檔案和有關的庫生成可執行檔案( ‘a.out’ ),這個可執行檔案含有一個符号表和一個串表。