mit6.828的JOS系統(tǒng)啟動(dòng)分為兩部分他炊,Boot Loader和kern江兢。BIOS加載Boot Loader程序符相,在完成它的一系列初始化后便把控制權(quán)交給Boot Loader程序丑搔,在 我們的 JOS 實(shí)驗(yàn)中,我們的 Boot Loader 程序會(huì)在編譯成可執(zhí)行代碼后被放在模擬硬盤的第一個(gè)扇區(qū)。實(shí)驗(yàn)代碼中有Kern和Boot兩個(gè)可執(zhí)行文件,其中 Kernel 是即將被 Boot Loader 程序裝入內(nèi)存的內(nèi)核程序,而 Boot 便是 Boot Loader 本身 的可執(zhí)行程序括蝠。
lab1的exercize代碼見 這里。
1 系統(tǒng)啟動(dòng)流程
實(shí)驗(yàn)環(huán)境用的qemu饭聚,模擬了BIOS加載引導(dǎo)程序(Boot Loader)到內(nèi)存中忌警,然后啟動(dòng)系統(tǒng)。現(xiàn)代PC的BIOS程序通常都固化在EPROM芯片中秒梳,這是一種可擦除可編程的非易失性芯片法绵。CPU硬件邏輯設(shè)計(jì)為在加電瞬間強(qiáng)行將CS值置為0XF000,IP為0XFFF0(模擬器里面的CPU初始指令設(shè)置是這么設(shè)置的端幼,最終的地址是0xFFFF0礼烈,而在80386之后的CPU中這個(gè)地址是0xFFFFFFF0)弧满,這樣實(shí)模式下CS:IP就指向0XFFFF0這個(gè)位置(段地址 << 4 + 偏移地址)婆跑,這個(gè)位置正是BIOS程序的入口地址。BIOS 的作用是完成機(jī)器自檢庭呜,對(duì)系統(tǒng)進(jìn)行初始化滑进,比如像激活 顯卡犀忱、檢查內(nèi)存的總量,設(shè)置中斷向量等扶关。在進(jìn)行完這些初始化后阴汇,BIOS 便將Boot Loader從一個(gè)合適的位置裝載到內(nèi)存0x7C00處,這些位置可以是軟盤节槐、硬盤搀庶、CD-ROM 或者是網(wǎng)絡(luò),在這之后铜异,BIOS 便會(huì)將控制權(quán)交給操作系統(tǒng)哥倔。
有一個(gè)問題就是,BIOS程序是固化到它自己的芯片中的揍庄,那CPU是如何在初始的情況下就能運(yùn)行BIOS程序的咆蒿。這是CPU自身決定的,CPU的reset vector設(shè)置為了前面提到的0XFFFF0蚂子,每次PC復(fù)位沃测,則CPU就會(huì)從該位置執(zhí)行,這正好是BIOS程序的起始位置食茎。注意蒂破,此時(shí)內(nèi)核還沒有進(jìn)行內(nèi)存初始化,CPU是怎么讀取BIOS ROM的别渔?x86對(duì)BIOS ROM進(jìn)行統(tǒng)一編址寞蚌,此時(shí)CPU使用通用的讀指令從BIOS ROM(不是內(nèi)存)里面讀取指令執(zhí)行(CPU讀取指令通常先發(fā)送到北橋芯片,而北橋芯片根據(jù)內(nèi)存地址映射來決定該指令地址是發(fā)往哪里钠糊。BIOS ROM內(nèi)存映射在低端地址處挟秤,于是CPU訪問0XFFFF0這個(gè)地址時(shí)北橋芯片會(huì)將讀取指令請(qǐng)求發(fā)送到BIOS ROM,QEMU有自己的BIOS程序抄伍,它會(huì)被加載到模擬的地址空間0xf0000到0xfffff這段物理內(nèi)存艘刚,從而可以讓虛擬機(jī)的CPU執(zhí)行對(duì)應(yīng)位置的BIOS指令)〗卣洌可以看到BIOS程序首先執(zhí)行了一個(gè)跳轉(zhuǎn)指令 ljmp $0xf000,$0xe05b
攀甚,這是因?yàn)锽IOS在內(nèi)存中的結(jié)束范圍為 0x100000,而 0xFFFF0 到 0x100000 只有16個(gè)字節(jié)岗喉,想想這么一點(diǎn)內(nèi)存也存放不了幾條指令秋度,因此先跳轉(zhuǎn)到一個(gè)第一點(diǎn)的地方執(zhí)行。
另外一個(gè)問題是钱床,BIOS將Boot Loader加載到內(nèi)存0x7C00處荚斯,加載完成后將控制權(quán)交給Boot Loader,Boot Loader然后加載操作系統(tǒng)的內(nèi)核到內(nèi)存中,那BIOS是如何判斷從哪里加載Boot Loader呢事期?BIOS將所檢查磁盤的第一個(gè)扇區(qū)(512B)載入內(nèi)存滥壕,放在 0x0000:0x7c00 處, 如果該扇區(qū)的最后兩個(gè)字節(jié)是“55 AA”兽泣,那么這就是一個(gè)引導(dǎo)扇區(qū)绎橘,這個(gè)磁盤也就是一塊可引導(dǎo)盤。通常這個(gè)大小為 512B 的程序就稱為引導(dǎo)程序(bootloader)唠倦。如果最后兩個(gè)字節(jié)不是“55 aa”称鳞,那么 BIOS 就檢查下一個(gè)磁盤驅(qū)動(dòng)器,這個(gè)檢查順序也是可以在BIOS中設(shè)置的稠鼻,BIOS設(shè)置存儲(chǔ)在CMOS中胡岔。
PC的物理地址空間的布局如下所示,其中低1M空間是安排好了的枷餐,包括BIOS中斷向量靶瘸,BIOS數(shù)據(jù),VGA顯存等毛肋。注意怨咪,BIOS,集成顯卡的顯存等都是統(tǒng)一編址的润匙,而磁盤和獨(dú)立顯卡(如jos實(shí)驗(yàn)中qemu模擬的是獨(dú)立顯卡)等則是獨(dú)立編址的诗眨,需要通過特殊的指令(如in和out)進(jìn)行操作。此時(shí)孕讳,操作系統(tǒng)還沒有加載匠楚,那么BIOS加載Boot Loader是通過什么方式呢?通過BIOS中斷以及讀取磁盤命令來實(shí)現(xiàn)厂财。BIOS在完成post自檢芋簿、設(shè)置好中斷向量表后,觸發(fā) 0x19 中斷璃饱,然后cpu執(zhí)行bios對(duì)應(yīng)中斷處理程序來加載bootloader与斤。因?yàn)槲覀儗?shí)驗(yàn)中qemu指定了是通過磁盤啟動(dòng),0x19中斷處理程序在設(shè)置好讀取磁盤的參數(shù)后會(huì)觸發(fā)0x13中斷荚恶,然后0x13中斷處理程序會(huì)首先讀取引導(dǎo)磁盤的第一個(gè)扇區(qū)(即bootloader)到 0x7c00的位置撩穿,然后檢查扇區(qū)最后兩個(gè)字節(jié)是否是0x55aa
,如果不是谒撼,則引導(dǎo)報(bào)錯(cuò)食寡,嘗試其他設(shè)備如floppy和cd/dvd找bootloader。如果驗(yàn)證沒問題廓潜,則跳轉(zhuǎn)到0x7c00處執(zhí)行抵皱。
+------------------+ <- 0xFFFFFFFF (4GB)
| 32-bit |
| memory mapped |
| devices |
| |
/\/\/\/\/\/\/\/\/\/\
/\/\/\/\/\/\/\/\/\/\
| |
| Unused |
| |
+------------------+ <- depends on amount of RAM
| |
| |
| Extended Memory |
| |
| |
+------------------+ <- 0x00100000 (1MB)
| BIOS ROM |
+------------------+ <- 0x000F0000 (960KB)
| 16-bit devices, |
| expansion ROMs |
+------------------+ <- 0x000C0000 (768KB)
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
| |
+------------------+ <- 0x00000000
而加載的地址0x7C00是歷史原因善榛,IBM最早的個(gè)人電腦IBM PC 5150用的是Intel最早的個(gè)人電腦芯片8088,當(dāng)時(shí)叨叙,搭配的操作系統(tǒng)是86-DOS。這個(gè)操作系統(tǒng)需要的內(nèi)存最少是32KB堪澎。內(nèi)存地址從0x0000開始編號(hào)擂错,32KB的內(nèi)存就是0x0000~0x7FFF。
8088芯片本身預(yù)留了地址空間0x0000~0x03FF樱蛤,用來保存各種中斷向量的儲(chǔ)存位置钮呀。所以,內(nèi)存只剩下0x0400~0x7FFF可以使用昨凡。為了把盡量多的連續(xù)內(nèi)存留給操作系統(tǒng)爽醋,Boot Loader就被放到了內(nèi)存地址的尾部。由于Boot Loader所在的這個(gè)扇區(qū)是512字節(jié)便脊,另外Boot Loader數(shù)據(jù)和棧需要預(yù)留512字節(jié)蚂四。所以,Boot Loader加載位置是0x7c00哪痰,而且因?yàn)椴僮飨到y(tǒng)加載完成后Boot Loader不需要再使用遂赠,這部分內(nèi)存之后操作系統(tǒng)是可以重復(fù)利用的。
0x7FFF - 512 - 512 + 1 = 0x7C00
2 引導(dǎo)程序
引導(dǎo)程序Boot Loader負(fù)責(zé)加載操作系統(tǒng)內(nèi)核Kern晌杰,它被BIOS加載到0x7c00-0x7dff中跷睦。“boot block is 406 bytes (max 510)”這句話表示存放在第一個(gè)扇區(qū)的Boot Loader 可執(zhí)行程序的大小不能超過 510 個(gè)字節(jié)肋演,由于磁盤的一個(gè)扇區(qū)的大小為 512 字節(jié)抑诸, 這樣便保證了 bootloader 僅僅只占據(jù)磁盤的第一個(gè)扇區(qū)。
實(shí)驗(yàn)中編譯好的Boot Loader位于obj/boot/boot
爹殊,大小剛好是512字節(jié)蜕乡。我們可以確認(rèn)下最后兩個(gè)字節(jié)確實(shí)是55 aa
。
# hexdump obj/boot/boot
0000000 fa fc 31 c0 8e d8 8e c0 8e d0 e4 64 a8 02 75 fa
0000010 b0 d1 e6 64 e4 64 a8 02 75 fa b0 df e6 60 0f 01
.......
00001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa
JOS 的引導(dǎo)由boot/boot.S
的匯編程序和boot/main.c
的C程序兩個(gè)程序完成梗夸。boot.S 主要是將處理器從實(shí)模式轉(zhuǎn)換到 32 位的保護(hù)模式异希,因?yàn)橹挥性诒Wo(hù)模式中我們才能訪問到物理內(nèi)存高于 1MB 的空間。main.c 的主要作用是將內(nèi)核的可執(zhí)行代碼從硬盤鏡像中讀入到內(nèi)存中绒瘦, 具體的方式是運(yùn)用 x86 專門的 I/O 指令讀取称簿。
boot.S 會(huì)初始化寄存器,設(shè)置代碼段選擇子和數(shù)據(jù)段選擇子的值(保護(hù)模式會(huì)用到)惰帽,打開A20地址線憨降。接著lgdt gdtdesc
加載全局描述符表(這里一共兩個(gè)描述符表,分別用于代碼段和數(shù)據(jù)段選擇子)该酗,全局描述符表就是一個(gè)類似數(shù)組的結(jié)構(gòu)而已授药。設(shè)置寄存器CR0的最低位值為1打開保護(hù)模式士嚎,然后跳轉(zhuǎn)到 PROT_MODE_CSEG: protcseg
執(zhí)行。從全局描述符表中可以得知悔叽,代碼段選擇子 PROT_MODE_CSEG 的段基址為 0莱衩,其中偏移地址是 protcseg 實(shí)際上代表 的是接下來指令的鏈接地址娇澎,也就是可執(zhí)行程序在內(nèi)存中的虛擬地址(VMA)笨蚁,只是剛好在這里編譯生成的可執(zhí)行程序 boot 的加載地址(LMA)與鏈接地址是一致的,于是 $protcseg 就相當(dāng)于指令在內(nèi)存中實(shí)際存放位置的物理地址趟庄,所以這個(gè)長(zhǎng)跳轉(zhuǎn)可以成功的跳轉(zhuǎn)到下一條指令的位置括细。
進(jìn)入保護(hù)模式后,程序在重新對(duì)段寄存器進(jìn)行了初始化并且賦值了堆棧指針后便調(diào)用 bootmain 函數(shù)戚啥,在“call bootmain”之后便是一個(gè)無限循環(huán)的跳轉(zhuǎn)指令奋单,這個(gè)無限循環(huán)沒有太大意義,只是為了讓整個(gè)代碼更有完整性猫十。之后的代碼則是定義了 GDT 表览濒,一共3個(gè)表項(xiàng),第一個(gè)為NULL,接著為代碼段選擇子和數(shù)據(jù)段選擇子拖云,其中代碼中的 STA_X(0x8) 表示可執(zhí)行(僅限于可執(zhí)行段)匾七,STA_R(0x2) 表示可讀(僅限于可執(zhí)行段),STA_W(0x2) 為可寫(僅限于非可執(zhí)行段)江兢。
3 實(shí)模式和保護(hù)模式
實(shí)模式采用 16 位尋址模式昨忆,在該模式中,最大尋址空間為 1MB杉允,最大分段為 64KB邑贴。
由于處理器的設(shè)計(jì)需要考慮到兼容問題,8086 處理器地址總線擴(kuò)展到 20 位叔磷,但CPU的ALU寬度(數(shù)據(jù)總線)卻只有 16 位拢驾,也就是說直接參與運(yùn)算的數(shù)值都是 16 位的。為支持 1MB 尋址空間改基,8086 處理器在實(shí)模式下引入了分段方法繁疤。在處理器中設(shè)置了四個(gè) 16 位的段寄存器:CS、DS秕狰、SS稠腊、ES,對(duì) 應(yīng)于地址總線中的高 16 位鸣哀。尋址時(shí)架忌,采用以下公式計(jì)算實(shí)際訪問的物理內(nèi)存地址,這樣我衬,便實(shí)現(xiàn)了 16 位內(nèi)存地址到 20 位物理地址的轉(zhuǎn)換叹放。
實(shí)際物理地址 = (段寄存器 << 4) + 偏移地址
在保護(hù)模式下饰恕,段式尋址可用 xxxx:yyyyyyyy 表示。其中 xxxx 表示索引井仰,也就是段選
擇子埋嵌,是 16 位的。yyyyyyyy 是偏移量俱恶,是 32 位的分段機(jī)制是利用一個(gè)稱作段選擇子的偏移量到全局描述符表中找到需要的段描述符雹嗦,而這個(gè)段描述符中就存放著真正的段的物理首地址,然后再加上偏移地址量便得到了最后的物理地址速那。需要指出的是俐银,在 32 位平臺(tái)上尿背,段物理首地址和偏移址都是 32 位的端仰,實(shí)際物理地址的計(jì)算不再需要將段首地址左移 4 位了,直接相加即可田藐,如果發(fā)生溢出的情況荔烧,則將溢出位舍棄。
保護(hù)模式下會(huì)有一個(gè)寄存器GDTR(Global Descriptor Table Register)用于存儲(chǔ)全局描述符表的物理地址和長(zhǎng)度汽久,這樣可以由段選擇子 xxxx 查詢?nèi)置枋龇?全局描述符表存儲(chǔ)類似數(shù)組)鹤竭,得到對(duì)應(yīng)段描述符,一個(gè) 64 位的段描述符包含了段的物理首地址景醇、段的界限以及段的屬性臀稚。在描述符中,段基址占 32 位三痰,段限長(zhǎng)占 20 位吧寺,屬性占 12 位,詳細(xì)字段說明參見 GDT散劫。段描述符的基地址加上偏移 yyyyyyyy 即可得到物理地址(嚴(yán)格來講稚机,此時(shí)還是將邏輯地址轉(zhuǎn)換為了線性地址,如果設(shè)置了寄存器CR3的最低位為1開啟了分頁(yè)后获搏,會(huì)再將這個(gè)地址再次通過MMU轉(zhuǎn)換為真正的物理地址)赖条。
段描述符的定義如下:
BYTE7 BYTE6 BYTE5 BYTE4 BYTE3 BYTE2 BYTE1 BYTE0
段基址31...24 屬性 段基址 23...0 段限長(zhǎng) 15...0
4 ELF 文件結(jié)構(gòu)
在Boot Loader那節(jié)中我們看到了一些如 gdtdesc, progcseg 這樣的地址標(biāo)識(shí)符,那么這些地址是什么地址呢常熙?它們與內(nèi)存中的物理地址區(qū)別是什么呢纬乍?這一節(jié)來探討下這個(gè)問題。
在說明鏈接地址和加載地址區(qū)別之前裸卫,我們先來看下 ELF 文件格式蕾额。ELF 文件可以分為這樣幾個(gè)部分: ELF 文件頭、程序頭表(program header table)彼城、節(jié)頭表(section header table)和文件內(nèi)容诅蝶。而其中文件內(nèi)容部分又可以分為這樣的幾個(gè)節(jié):.text 節(jié)退个、.rodata 節(jié)、.stab 節(jié)调炬、.stabstr 節(jié)语盈、.data 節(jié)、.bss 節(jié)缰泡、.comment 節(jié)刀荒。
+------------------+
| ELF 文件頭 |
+------------------+
| 程序頭表 |
+------------------+
| .text 節(jié) |
+------------------+
| .rodata 節(jié) |
+------------------+
| .stab 節(jié) |
+------------------+
| .stabstr 節(jié) |
+------------------+
| .data 節(jié) |
+------------------+
| .bss 節(jié) |
+------------------+
| .comment 節(jié) |
+------------------+
| 節(jié)頭表 |
+------------------+
ELF 文件頭結(jié)構(gòu)如下,其中e_entry 是可執(zhí)行程序的入口地址棘钞,即從內(nèi)存的這個(gè)位置開始執(zhí)行缠借,在這里入口地址是虛擬地址 VMA ,也就是鏈接地址; e_phoff 和 e_phnum 可以用來找到所有的程序頭表項(xiàng)宜猜,e_phoff 是程序頭表的第一項(xiàng)相對(duì)于 ELF 文件的開始位置的偏移泼返,而 e_phnum 則是表項(xiàng)的個(gè)數(shù);同理 e_ shoff 和 e_ shnum 可以用來找到所有的節(jié)頭表項(xiàng)。
// ELF 文件頭
struct Elf {
uint32_t e_magic; // 標(biāo)識(shí)是否是ELF文件
uint8_t e_elf[12]; // 魔數(shù)和相關(guān)信息
uint16_t e_type; // 文件類型
uint16_t e_machine;
uint16_t e_version; // 版本信息
uint32_t e_entry; // 程序入口點(diǎn)
uint32_t e_phoff; // 程序頭表偏移值
uint32_t e_shoff; // 節(jié)頭表偏移值
uint32_t e_flags;
uint16_t e_ehsize; // 文件頭長(zhǎng)度
uint16_t e_phentsize; // 程序頭部長(zhǎng)度
uint16_t e_phnum; // 程序頭部個(gè)數(shù)
uint16_t e_shentsize; // 節(jié)頭部長(zhǎng)度
uint16_t e_shnum; // 節(jié)頭部個(gè)數(shù)
uint16_t e_shstrndx; // 節(jié)頭部字符索引
};
程序頭表中每個(gè)表項(xiàng)就代表一個(gè)段姨拥,這里的段是不同于之前節(jié)的概念绅喉,幾個(gè)節(jié)可能會(huì)包含在同一個(gè)段里。程序頭表項(xiàng)的數(shù)據(jù)結(jié)構(gòu)如下所示:
struct Proghdr {
uint32_t p_type; // 段類型
uint32_t p_align; // 段在內(nèi)存中的對(duì)齊標(biāo)志
uint32_t p_offset; // 段位置相對(duì)于文件開始處的偏移量
uint32_t p_va; // 段的虛擬地址
uint32_t p_pa; // 段的物理地址
uint32_t p_filesz; // 段在文件中長(zhǎng)度
uint32_t p_memsz; // 段在內(nèi)存中的長(zhǎng)度
uint32_t p_flags; // 段標(biāo)志
}
這里比較重要的幾個(gè)成員是 p_offset叫乌、p_va柴罐、p_filesz 和 p_memsz。其中通過 p_offset 可以找到該段在磁盤中的位置憨奸,通過 p_va 可以知道應(yīng)該把這個(gè)段放到內(nèi)存的哪個(gè)位置革屠,而之所以需要 p_filesz 和 p_memsz 這兩個(gè)長(zhǎng)度是因?yàn)?.bss 這種節(jié)在硬盤沒有存儲(chǔ)空間而在內(nèi)存中需要為其分配空間。
通過 ELF 文件頭與程序頭表項(xiàng)找到文件的第 i 段地址的方法如下:
第 i 段程序頭表表項(xiàng)位置 = 文件起始位置 + 程序頭表偏移e_phoff + i * 程序頭表項(xiàng)字節(jié)數(shù)
第 i 段地址就是第i個(gè)程序頭表表項(xiàng)的 p_offset 值排宰。
而 ELF 文件還有節(jié)點(diǎn)表似芝,通過 ELF 文件頭和節(jié)頭表可以找到對(duì)應(yīng)節(jié)的位置,方式與找到第i段的位置類似额各。ELF 的文件內(nèi)容主要的幾個(gè)節(jié)的說明如下(一些特殊節(jié)如eh_frame用于展示棧的調(diào)用幀信息国觉,這里不做說明):
// 節(jié)頭信息
struct Secthdr {
uint32_t sh_name;// 節(jié)名稱
uint32_t sh_type; // 節(jié)類型
uint32_t sh_flags; // 節(jié)標(biāo)志
uint32_t sh_addr; // 內(nèi)存中的虛擬地址
uint32_t sh_offset; // 相對(duì)于文件首部的偏移
uint32_t sh_size; // 節(jié)大小
uint32_t sh_link; // 與其他節(jié)關(guān)系
uint32_t sh_info; // 其他信息
uint32_t sh_addralign; // 字節(jié)對(duì)齊標(biāo)志
uint32_t sh_entsize; // 表項(xiàng)大小
};
// 各個(gè)節(jié)說明
.text 節(jié): 可執(zhí)行指令的部分。
.rodata 節(jié): 只讀全局變量部分虾啦。
.stab 節(jié): 符號(hào)表部分麻诀,在程序報(bào)錯(cuò)時(shí)可以提供錯(cuò)誤信息。
.stabstr 節(jié): 符號(hào)表字符串部分傲醉。
.data 節(jié): 可讀可寫的全局變量部分蝇闭。
.bss 節(jié): 未初始化全局變量部分,這一部分不會(huì)在磁盤有存儲(chǔ)空間硬毕,但在內(nèi)存中會(huì)分配空間呻引。
.comment 節(jié):注釋部分,這一部分不會(huì)被加載到內(nèi)存吐咳。
可以使用 objdump 命令查看 ELF 文件的節(jié)信息逻悠,如boot文件有下面5個(gè)節(jié)元践,而kernel文件則有上面提到的7個(gè)節(jié),而且可以看到kernel中.bss和.comment的在文件的偏移 File off
是一樣的童谒,說明.bss不占用磁盤空間单旁,僅僅記錄了它的長(zhǎng)度。
# objdump -h obj/boot/boot.out
Idx Name Size VMA LMA File off Algn
0 .text 0000017e 00007c00 00007c00 00000054 2**2
CONTENTS, ALLOC, LOAD, CODE
1 .eh_frame 000000cc 00007d80 00007d80 000001d4 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 000006d8 00000000 00000000 000002a0 2**2
CONTENTS, READONLY, DEBUGGING
3 .stabstr 000007df 00000000 00000000 00000978 2**0
CONTENTS, READONLY, DEBUGGING
4 .comment 00000011 00000000 00000000 00001157 2**0
CONTENTS, READONLY
# objdump -h obj/kern/kernel
Idx Name Size VMA LMA File off Algn
0 .text 00001917 f0100000 00100000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000714 f0101920 00101920 00002920 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 00003889 f0102034 00102034 00003034 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .stabstr 000018af f01058bd 001058bd 000068bd 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data 0000a300 f0108000 00108000 00009000 2**12
CONTENTS, ALLOC, LOAD, DATA
5 .bss 00000644 f0112300 00112300 00013300 2**5
ALLOC
6 .comment 0000002b 00000000 00000000 00013300 2**0
CONTENTS, READONLY
5 鏈接地址和加載地址
好了饥伊,是時(shí)候來說下鏈接地址和加載地址的區(qū)別了象浑。ELF文件的節(jié)的鏈接地址實(shí)際上就是它期望開始執(zhí)行的內(nèi)存地址,鏈接器可用多種方式編碼二進(jìn)制可執(zhí)行文件中的鏈接地址琅豆,編譯器在編譯的時(shí)候會(huì)認(rèn)定程序?qū)?huì)連續(xù)的存放在從鏈接地址起始處開始的內(nèi)存空間愉豺。程序的鏈接地址實(shí)際上就是鏈接器對(duì)代碼中的變量、函數(shù)等符號(hào)進(jìn)行一個(gè)地址編排茫因,賦予這些抽象的符號(hào)一個(gè)地址蚪拦,然后在程序中通過地址訪問相應(yīng)變量和函數(shù)。要清楚的一點(diǎn)是节腐,在ELF文件中的匯編代碼或者機(jī)器指令中外盯,符號(hào)已經(jīng)不復(fù)存在摘盆,一切引用都是地址翼雀!使用ld等鏈接程序時(shí)通過-Ttext xxxx
和 -Tdata yyyy
指定代碼段/數(shù)據(jù)段的鏈接地址。運(yùn)行期間孩擂,代碼指令和數(shù)據(jù)變量的地址都在相對(duì)-T指定的基址的某個(gè)偏移量處狼渊。這個(gè)地址實(shí)際上就是鏈接地址(VMA)。
而加載地址則是可執(zhí)行程序在物理內(nèi)存中真正存放的位置类垦,而在 JOS 中狈邑,Boot Loader 是被 BIOS 裝載到內(nèi)存的,而這里 BIOS 實(shí)際上規(guī)定 Boot Loader 是要存放在物理內(nèi)存的 0x7c00 處蚤认,于是不論程序的鏈接地址(VMA)怎么改變米苹,它的加載地址(LMA)都不會(huì)改變。
在JOS中砰琢,Boot Loader的鏈接地址在 boot/Makefrag
里面定義的蘸嘶,為0x7C00。該文件的另外幾個(gè)命令是生成 obj/boot/
目錄下面的幾個(gè)文件的陪汽,該目錄下 boot.out
是由 boot/boot.S
和 boot/main.c
編譯鏈接后生成的 ELF 可執(zhí)行文件训唱,而 boot.asm
是從可執(zhí)行文件 boot.out
反編譯的包含源碼的匯編文件,而最后通過 objcopy 拷貝 boot.out中的 .text 代碼節(jié)生成最終的二進(jìn)制引導(dǎo)文件 boot (380個(gè)字節(jié))挚冤,最后通過 sign.pl這個(gè)perl腳本填充 boot 文件到512字節(jié)(最后兩個(gè)字節(jié)設(shè)置為 55 aa况增,代表這是一個(gè)引導(dǎo)扇區(qū))。最終生成的鏡像文件在 obj/kern/kernel.img
训挡,它大小為5120000字節(jié)澳骤,即10000個(gè)扇區(qū)大小歧强。第一個(gè)扇區(qū)寫入的是 obj/boot/boot
,第二個(gè)扇區(qū)開始寫入的是 obj/kern/kernel
为肮。
# boot 相關(guān)文件生成代碼
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o $@.out $^
$(V)$(OBJDUMP) -S $@.out >$@.asm
$(V)$(OBJCOPY) -S -O binary -j .text $@.out $@
$(V)perl boot/sign.pl $(OBJDIR)/boot/boot
# kernel.img 創(chuàng)建代碼
$(V)dd if=/dev/zero of=$(OBJDIR)/kern/kernel.img~ count=10000 2>/dev/null
$(V)dd if=$(OBJDIR)/boot/boot of=$(OBJDIR)/kern/kernel.img~ conv=notrunc 2>/dev/null
$(V)dd if=$(OBJDIR)/kern/kernel of=$(OBJDIR)/kern/kernel.img~ seek=1 conv=notrunc 2>/dev/null
$(V)mv $(OBJDIR)/kern/kernel.img~ $(OBJDIR)/kern/kernel.img
Boot Loader的鏈接地址和加載地址是一樣的誊锭,都是0x7C00。而Kernel的鏈接地址和加載地址卻是不一樣的弥锄。鏈接地址是 0xF0100000
丧靡,加載地址是0x00100000
,也就是說Kernel加載到了內(nèi)存中的 0x00100000 這個(gè)低地址處籽暇,但是卻期望在一個(gè)高地址 0xF0100000 執(zhí)行温治,為什么要這么做呢?這是因?yàn)槲覀兊膬?nèi)核通常期望鏈接和運(yùn)行在一個(gè)高的虛擬地址戒悠,以便把低位的虛擬地址空間讓給用戶程序使用熬荆。但是,以前的機(jī)器通常沒有 0xF0100000 這么大的物理內(nèi)存绸狐,因此需要通過處理器的內(nèi)存管理硬件來將 0xF0100000 映射到 0x00100000卤恳,我們?cè)谙乱还?jié)會(huì)看到這個(gè)機(jī)制是怎么實(shí)現(xiàn)的。
查看obj/boot/boot.asm
可以看到確實(shí)最終的二進(jìn)制代碼文件 boot 的鏈接地址為 0x7C00寒矿,代碼從這個(gè)地址開始依次存放突琳。而執(zhí)行時(shí),我們前面提到過是BIOS將Boot Loader代碼加載到內(nèi)存中 0x7C00 的位置符相,這里恰好 VMA 和 LMA是一樣的拆融。如果我們把Makefrag
中的 0x7C00 改成 0x7C10,會(huì)在哪條指令報(bào)錯(cuò)呢啊终?答案是在 jmp 0008:7c42
后報(bào)錯(cuò)镜豹。這是因?yàn)槲覀冩溄拥刂犯某闪?0x7C10 ,而實(shí)際加載地址還是 0x7C00蓝牲,這樣在執(zhí)行完 ljmp 0008:7c42
這條指令時(shí)趟脂,此時(shí)要去尋找 0x7c42 處的指令movw ax, 10
運(yùn)行,而實(shí)際上這個(gè)地址處并不是合法的指令例衍,因?yàn)榧虞d地址并沒有變昔期,movw ax, 10
還是在地址 0x7c32 處,因此此時(shí)會(huì)報(bào)錯(cuò)肄渗。
6 加載內(nèi)核(Kernel)
BIOS負(fù)責(zé)加載Boot Loader镇眷,而Boot Loader 負(fù)責(zé)加載內(nèi)核。編譯好的內(nèi)核位于 obj/kern/kernel
翎嫡,當(dāng)然最終要寫入到鏡像文件 obj/kern/kernel.img
中欠动。從 kern/kernel.ld
中可以看到內(nèi)核的鏈接地址設(shè)置的是 0xF0100000
,而加載地址設(shè)置的是 0x00100000
。 boot/main.c
中的 bootmain()
函數(shù)負(fù)責(zé)加載內(nèi)核具伍,采用的是分段加載方法翅雏。使用命令 readelf -l obj/kern/kernel
可以看到內(nèi)核分段信息,kernel需要加載的有2段(LOAD標(biāo)識(shí))人芽,以 4K (0x1000) 對(duì)齊望几。我們可以看到加載kernel時(shí),是以扇區(qū)為單位讀取并加載的萤厅。首先會(huì)將文件的前 4K 數(shù)據(jù)即ELF頭和程序頭表(注意是從扇區(qū)1開始讀取橄抹,因?yàn)樯葏^(qū)0是bootloader)加載到物理地址 0x10000 處(注意這個(gè)不是kernel加載的地址 0x00100000,少個(gè)0)惕味,這樣ELF文件頭和程序頭表都能讀取了楼誓,接著就可以根據(jù)程序頭表加載程序段到對(duì)應(yīng)物理地址了。
# readelf -l obj/kern/kernel
Elf file type is EXEC (Executable file)
Entry point 0x10000c
There are 3 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0xf0100000 0x00100000 0x0716c 0x0716c R E 0x1000
LOAD 0x009000 0xf0108000 0x00108000 0x0a300 0x0a944 RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
Section to Segment mapping:
Segment Sections...
00 .text .rodata .stab .stabstr
01 .data .bss
02
可以看到 kernel 可執(zhí)行文件的第一段包含了 ELF 文件的.text 節(jié)名挥、.rodata 節(jié)疟羹、.stab
.data節(jié),而文件的第二段包含了.data 節(jié)以及在硬盤上不占用空間但在內(nèi)存中占據(jù) 0x644 字節(jié)的.bss 節(jié)禀倔,這樣 Boot Loader 便會(huì)在從硬盤讀入第二段的同時(shí)為 .bss 節(jié)在內(nèi)存中分配空間榄融。另外,.comment 節(jié)沒有被包含在任意一段救湖,這表明它沒有被裝入內(nèi)存愧杯。
另外需要注意的是,硬盤中的每一扇區(qū)加載到內(nèi)存的時(shí)候都需要按 512 字節(jié)對(duì)齊捎谨。readsec() 函數(shù)用于讀取一個(gè)扇區(qū)的數(shù)據(jù)民效,而waitdisk() 函數(shù)的作用是等待直到硬盤準(zhǔn)備好可以讓程序讀取數(shù)據(jù)憔维。
void
bootmain(void)
{
struct Proghdr *ph, *eph;
// 讀取前4K數(shù)據(jù)涛救,包括ELF頭部和程序頭表到物理地址0處
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
// 判斷是不是ELF文件
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
// 加載程序段
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
// p_pa 是加載地址,也就是物理地址业扒,p_memsz是段在內(nèi)存中長(zhǎng)度检吆,p_offset段在文件中的偏移。
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
// 轉(zhuǎn)到kernel入口點(diǎn)執(zhí)行程储,并且不再返回蹭沛。
((void (*)(void)) (ELFHDR->e_entry))();
}
bootmain() 加載kernel完成后,跳轉(zhuǎn)到kernel的入口 e_entry(物理地址0x10000c處)執(zhí)行章鲤。需要牢記的是摊灭,在代碼中的地址我們稱之為虛擬地址或鏈接地址,在開啟了保護(hù)模式后(設(shè)置cr0的低位為1)败徊,會(huì)經(jīng)過全局描述符表項(xiàng)來生成線性地址帚呼,如果開啟了分頁(yè)(設(shè)置cr3的低位為1),則會(huì)將線性地址經(jīng)過分頁(yè)轉(zhuǎn)換為物理地址,否則煤杀,線性地址就等于物理地址眷蜈。
因此,bootmain() 函數(shù)里面最后跳轉(zhuǎn)的是 e_entry 的物理地址值 0x0010000c沈自,而不是鏈接地址值 0xf010000c酌儒。因?yàn)?kernel 代碼本身就是加載在 0x0010000c,而我們的描述符表項(xiàng)里面存的代碼段基地址是0枯途,所以只有在 0x0010000c 處才能找到可執(zhí)行指令忌怎,此時(shí)在 0xf010000c處我們還沒有做好映射。
注意到kernel的 e_entry 的值其實(shí)就是匯編文件 kern/entry.S
中的 _start
值酪夷,所以這個(gè)值必須是指向物理地址才行呆躲,因?yàn)槲覀冞€沒有做好高地址的映射。設(shè)置好了入口地址 _start 后捶索,kernel/entry.S
設(shè)置好cr3寄存器的值為頁(yè)目錄的物理地址插掂,然后設(shè)置cr0開啟分頁(yè),最后跳轉(zhuǎn)到 relocated 執(zhí)行腥例。relocated 是類似0xf01xxxxx之類的連接地址辅甥,為什么可以執(zhí)行了呢?這是因?yàn)槲覀円呀?jīng)開啟分頁(yè)燎竖,而且在 entrypgdir.c
中已經(jīng)設(shè)置好高位地址的頁(yè)目錄項(xiàng)璃弄。在 relocated 中,先初始化 棧幀指針寄存器 ebp构回,用于追溯函數(shù)調(diào)用流程夏块,然后設(shè)置 堆棧寄存器 esp 的值為內(nèi)核棧棧頂,堆棧大小為32KB纤掸。(留個(gè)問題脐供,如果將kern/entry.S中的 jmp *%eax 改成 call *%eax,會(huì)報(bào)triple fault借跪,為什么呢政己?想想call和jmp的區(qū)別以及entry_pgdir在低地址和高地址的頁(yè)表的映射權(quán)限有什么區(qū)別,報(bào)錯(cuò)的地址是 0x7be8掏愁,此時(shí)的esp是0x7bec歇由,我想你應(yīng)該知道原因了)。
.globl _start
_start = RELOC(entry)
.globl entry
entry:
movw $0x1234,0x472 # warm boot
# entry_pgdir在entrypgdir.c中定義果港,這段代碼作用
# 是加載entry_pgdir的物理地址到cr3寄存器中沦泌。
movl $(RELOC(entry_pgdir)), %eax
movl %eax, %cr3
# 開啟分頁(yè)
movl %cr0, %eax
orl $(CR0_PE|CR0_PG|CR0_WP), %eax
movl %eax, %cr0
# 注意,第一句mov $relocated, %eax 是將 relocated的鏈接地址加載到eax寄存器中辛掠,
# 后一句 jmp *%eax 則是跳轉(zhuǎn)到relocated的地址處執(zhí)行谢谦。
mov $relocated, %eax
jmp *%eax
relocated:
# 清空棧指針寄存器 ebp
movl $0x0,%ebp
# 設(shè)置棧寄存器
movl $(bootstacktop),%esp
# 轉(zhuǎn)向C語(yǔ)言函數(shù)
call i386_init
初始化頁(yè)目錄和頁(yè)表項(xiàng)代碼如下,將 [0, 4MB) 和 [KERNBASE, KERNBASE+4MB)都映射到了物理地址 [0, 4MB)。頁(yè)目錄為 1024 個(gè)他宛,我們只初始化了2個(gè)船侧,且頁(yè)目錄項(xiàng)的值為頁(yè)表數(shù)組的起始物理地址。頁(yè)表初始化了 1024 個(gè)厅各,每個(gè)指向大小為4KB的頁(yè)镜撩,頁(yè)表項(xiàng)的結(jié)構(gòu)下一節(jié)會(huì)詳細(xì)分析。開啟分頁(yè)后队塘,CPU訪問的地址會(huì)經(jīng)由 虛擬地址 -> 線性地址 -> 物理地址 轉(zhuǎn)換袁梗,這個(gè)轉(zhuǎn)換由CPU內(nèi)部的MMU部件來完成。MMU除了做地址轉(zhuǎn)換之外憔古,還提供內(nèi)存保護(hù)機(jī)制遮怜。各種體系結(jié)構(gòu)都有用戶模和特權(quán)模式之分,操作系統(tǒng)可以在頁(yè)表中設(shè)置每個(gè)內(nèi)存頁(yè)面的訪問權(quán)限鸿市,有些頁(yè)面不允許訪問锯梁,有些頁(yè)面只有在CPU處于特權(quán)模式時(shí)才允許訪問,有些頁(yè)面在兩種模式下都可以訪問焰情。訪問權(quán)限又分為可讀陌凳、可寫和可執(zhí)行三種。當(dāng)CPU要訪問一個(gè)線性地址時(shí)内舟,MMU會(huì)檢查CPU當(dāng)前處于用戶模式還是特權(quán)模式合敦,訪問內(nèi)存的目的是讀數(shù)據(jù)、寫數(shù)據(jù)還是取指令验游,如果和操作系統(tǒng)設(shè)定的頁(yè)面權(quán)限相符充岛,就允許訪問,把它轉(zhuǎn)換成物理地址耕蝉,否則不允許訪問崔梗,產(chǎn)生一個(gè)異常。
__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {
// 映射虛擬地址 [0, 4MB) 到物理地址 [0, 4MB)
[0]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
// 映射虛擬地址 [KERNBASE, KERNBASE+4MB) 到物理地址 [0, 4MB)
// PDXSHIFT是22赔硫,即取地址最高10位作為頁(yè)目錄的索引炒俱。
[KERNBASE>>PDXSHIFT]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};
// 第0項(xiàng)指向第0個(gè)物理頁(yè)起始地址,第1項(xiàng)指向第1個(gè)物理頁(yè)爪膊,按4KB對(duì)齊,以此類推砸王。
__attribute__((__aligned__(PGSIZE)))
pte_t entry_pgtable[NPTENTRIES] = {
0x000000 | PTE_P | PTE_W,
0x001000 | PTE_P | PTE_W,
0x002000 | PTE_P | PTE_W,
......
0x3ff000 | PTE_P | PTE_W,
}
而函數(shù) i386_init
則是初始化了bss段內(nèi)容為0(其中 edata 表示的是 bss 節(jié)在內(nèi)存中開始的位置推盛,而 end 則是表示內(nèi)核可執(zhí)行程序在內(nèi)存中結(jié)束的位置),調(diào)用調(diào) cons_init 函數(shù)完成一系列的系統(tǒng)初始化谦铃,包括顯存的初始化耘成、鍵盤的初始化等。接著調(diào)用 cprintf 函數(shù)用 8 進(jìn)制的形式打印一個(gè) 10 進(jìn)制的數(shù),但是此時(shí) vprintfmt 函數(shù)中的關(guān)于打印 8 進(jìn)制數(shù)的部分還沒有實(shí)現(xiàn)瘪菌,這是lab1的一個(gè)作業(yè)撒会,所以這個(gè)時(shí)候會(huì)打印出如下的結(jié)果: 6828 decimal is XXX octal!
. test_backtrace
函數(shù)是通過堆棧來對(duì)函數(shù)調(diào)用進(jìn)行回溯,后面再細(xì)說师妙。最后程序無限循環(huán)的調(diào)用了 monitor 函數(shù)诵肛,用于提示用戶輸入命令與操作系統(tǒng)進(jìn)行交互。
void
i386_init(void)
{
extern char edata[], end[];
// 初始化BSS段默穴,保證靜態(tài)和全局變量默認(rèn)值為0
memset(edata, 0, end - edata);
// 初始化控制臺(tái)怔檩,初始化之前不能調(diào)用cprintf
cons_init();
cprintf("6828 decimal is %o octal!\n", 6828);
// 測(cè)試堆棧的函數(shù)
test_backtrace(5);
// monitor提示用戶輸入命令并與操作系統(tǒng)交互
while (1)
monitor(NULL);
}
7 顯示輸出
這里要完成 cprintf 函數(shù)的實(shí)現(xiàn),注意可變參數(shù)的幾個(gè)宏的實(shí)現(xiàn)蓄诽,如 va_start
薛训,va_end
。其中 va_list ap 其實(shí)是一個(gè)指針仑氛,va_start(ap, fmt)使ap指向fmt參數(shù)的下一個(gè)參數(shù)唾戚。然后我們就可以用 va_arg 宏依次讀取之后的可變參數(shù)。在對(duì)參數(shù)指針進(jìn)行了初始化后游盲,程序接著調(diào)用了 vcprintf 函數(shù)入客,在得到 vcprintf 函數(shù)的返回值后,最后便使用 va_end 宏結(jié)束了對(duì)可變參數(shù)的獲取嚎莉,C標(biāo)準(zhǔn)要求在函數(shù)返回前調(diào)用va_end米酬。
int
cprintf(const char *fmt, ...)
{
va_list ap;
int cnt;
va_start(ap, fmt);
cnt = vcprintf(fmt, ap);
va_end(ap);
return cnt;
}
cprintf 函數(shù)與指針 ap 的關(guān)系如下所示,在vcprintf中趋箩,調(diào)用了vprintfmt((void*)putch, &cnt, fmt, ap);
赃额,其中putch是輸出函數(shù),它調(diào)用了cputchar函數(shù)叫确,最終cputchar調(diào)用了cga_putc函數(shù)來完成顯示功能跳芳,在cga_putc函數(shù)中的crt_buf 是一個(gè)指向 16 位無符號(hào)整形數(shù)的靜態(tài)指針,它實(shí)際上指向的是內(nèi)存中物理地址為 0xb8000 的位置竹勉,在前面章節(jié)我們已經(jīng)知道物理內(nèi)存的 0xa0000 到 0xc0000 這 128KB 的空間是留給 VGA 顯示緩存飞盆。在JOS中,顯示屏規(guī)定為 25 行次乓,每行可以輸出 80 個(gè)字符吓歇,由于每個(gè)字符實(shí)際上占顯存中的兩個(gè)字節(jié)(字符ASCII碼和字符屬性),于是物理內(nèi)存中從 0xb8000 到 0xb8fa0 之間的內(nèi)容都可以用字符的形式在屏幕上顯示出來票腰。
顯示輸出中參數(shù)的低 8 位是字符的 ASCII 碼城看,而 8 到 15 位則是字符屬性,字符屬性高4位是背景色(IRGB)杏慰,低4位是前景色(IRGB)测柠,其中RGB就是經(jīng)典的紅綠藍(lán)三色炼鞠,I表示字符是否高亮。當(dāng)然cga_putc函數(shù)還考慮了顯存溢出的問題轰胁,即在物理地址超過 0xb8fa0 的內(nèi)存部分中存儲(chǔ)字符數(shù)據(jù)谒主,此時(shí)實(shí)際上顯示屏就無法顯示超出部分,此時(shí)顯示屏?xí)L屏(將2~N+1行數(shù)據(jù)memmove拷貝到原來的 1~N 行)好讓最新輸出的字符能夠顯示出來赃阀。
高地址-> +-----------------+
| 可變參數(shù)n |
+-----------------+
| ....... |
+-----------------+
| 可變參數(shù)2 |
+-----------------+
| 可變參數(shù)1 |
+-----------------+ <- ap
| 格式參數(shù)fmt |
低地址-> +-----------------+
8 函數(shù)調(diào)用堆棧
在JOS實(shí)驗(yàn)1中會(huì)有一個(gè)遞歸的調(diào)用霎肯,函數(shù)調(diào)用的回溯需要對(duì)調(diào)用棧有了解,先看一個(gè)例子:
// stack.c
int bar(int c, int d) {
int e = c + d;
return e;
}
int foo(int a, int b) {
return bar(a, b);
}
int main() {
foo(1, 2);
return 0;
}
編譯為匯編文件: gcc -S -O0 -m32 -o stack.s stack.c
// stack.s
bar:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 12(%ebp), %eax
movl 8(%ebp), %edx
addl %edx, %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
foo:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
movl 12(%ebp), %eax
movl %eax, 4(%esp)
movl 8(%ebp), %eax
movl %eax, (%esp)
call bar
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
movl $2, 4(%esp)
movl $1, (%esp)
call foo
movl $0, %eax
leave
ret
通過匯編文件 stack.s凹耙,我們可以看到函數(shù)調(diào)用時(shí)棧的變化姿现。前面提到過,堆棧是由高地址向低地址擴(kuò)展的肖抱,函數(shù)參數(shù)是從右往左壓棧的备典。這里的call,leave和ret指令要說明下:
- call:將call的下一條指令壓棧意述,然后修改eip寄存器的值并跳轉(zhuǎn)到指定函數(shù)執(zhí)行提佣。
- leave:這個(gè)指令是函數(shù)開頭的
pushl %ebp
和movl %esp, %ebp
的逆操作,即將 ebp的值賦給 esp荤崇,這樣esp指向的棧頂保存著上一個(gè)函數(shù)的 ebp 的值拌屏。然后執(zhí)行 popl %ebp,將棧頂元素彈出到 ebp 寄存器术荤,同時(shí)esp的值加4指向上一個(gè)函數(shù)的下一條指令地址倚喂。 - ret:彈出棧頂元素并將eip設(shè)置為該值,跳轉(zhuǎn)到該地址執(zhí)行瓣戚。
而ret則是將棧頂端圈。我們可以看到調(diào)用函數(shù)時(shí)堆棧如下:
+-----------------+
| b: 2 |
+-----------------+
| a: 1 |
+-----------------+
| ret(main) |
+-----------------+
| ebp(main) |
+-----------------+ <- ebp(foo)
| d: 2 |
+-----------------+
| c: 1 |
+-----------------+
| ret(foo) |
+-----------------+
| ebp(foo) |
+-----------------+ <- ebp(bar)
| e: 3 |
+-----------------+
由棧的分布可知,因?yàn)?esp 寄存器的值會(huì)隨著pushl和popl操作而不斷變化子库,為了追溯函數(shù)調(diào)用舱权,需要用 ebp 寄存器來保存棧指針以串聯(lián)各個(gè)函數(shù)之間關(guān)系。如在 bar 函數(shù)中可以通過ebp來找到自己的參數(shù)和局部變量仑嗅,也可以找到 foo 函數(shù)中保存在棧上的值宴倍;而有了 foo 函數(shù)的ebp,foo函數(shù)則可以找到自己的參數(shù)和局部變量仓技,也可以找到 main 保存在棧上的值鸵贬。
在JOS中有個(gè) test_backtrace()函數(shù)來跟蹤函數(shù)調(diào)用過程,稍有不同的是用了遞歸(即同一個(gè)函數(shù)調(diào)用多次)浑彰,其實(shí)原理是一樣的恭理。
lab1的作業(yè)代碼地址在 這里。
參考資料
- https://pdos.csail.mit.edu/6.828/2017/labs/lab1/
- 邵志遠(yuǎn)老師 《多核操作系統(tǒng)設(shè)計(jì)》講義
- 《深入理解計(jì)算機(jī)系統(tǒng)》
- 宋勁松 《Linux一站式編程》