環(huán)境
ubuntu 20.04 64位系統(tǒng)
之前有些實驗是在xv6的源碼上操作的抹恳,發(fā)現(xiàn)20.04無法運行。后來發(fā)現(xiàn)lab還是可以在20.04正常做的,就把環(huán)境切換到本機(jī)上了椎组。
lab地址:mit 6.828 lab3
廢話
這次實驗感覺難做了不少,參考了別人不少的代碼历恐。畢竟代碼是別人寫的寸癌,要花心思去理解別人的思路。注釋里面的提示我也覺得有點模棱兩可弱贼。做起來比較困難蒸苇,還存在一些問題都沒有很好理解。完成本lab最好閱讀一下xv6 book的trap和page table這兩節(jié)哮洽。
正文
先說在前面填渠,本次lab里面提到的environment就是進(jìn)程(process),不知道為什么要起這樣奇奇怪怪的名字。
新引入的文件inc/env.h包括了JOS中envrionment的基本定義(當(dāng)成進(jìn)程來就完事兒了)氛什。kernel使用Env這個struct來操作每一個用戶進(jìn)程莺葫。在本次lab當(dāng)中你只需要創(chuàng)建一個進(jìn)程,但是你需要讓JOS能夠支持多進(jìn)程;lab4將會是用這個特性通過使用fork來創(chuàng)建一個新的用戶進(jìn)程枪眉。
幾個關(guān)鍵的變量(kern/env.c):
struct Env *envs = NULL; // All environments
struct Env *curenv = NULL; // The current env
static struct Env *env_free_list; // Free environment list
注釋里面寫的很明白了捺檬,envs
指向所有的進(jìn)程,curenv
指向當(dāng)前正在運行的進(jìn)程贸铜,env_free_list
是一個空閑進(jìn)程的鏈表堡纬。當(dāng)JOS啟動的時候,envs指向了系統(tǒng)所有的進(jìn)程蒿秦。在JOS中烤镐,所有的同時可運行的進(jìn)程數(shù)量為NENV
個((NENV在inc/env.h當(dāng)中)),1<<LOG2NENV
=1024,這也就是說JOS最多可以同時運行1024個進(jìn)程棍鳖。
當(dāng)我們需要創(chuàng)建一個新的進(jìn)程的時候炮叶,只需要從env_free_list
中拿一個出來,然后放到envs
當(dāng)中就行渡处。都知道鏈表對于添加和刪除是非常方便的镜悉。
下面是進(jìn)程的定義:
struct Env {
struct Trapframe env_tf; // Saved registers
struct Env *env_link; // Next free Env
envid_t env_id; // Unique environment identifier
envid_t env_parent_id; // env_id of this env's parent
enum EnvType env_type; // Indicates special system environments
unsigned env_status; // Status of the environment
uint32_t env_runs; // Number of times environment has run
// Address space
pde_t *env_pgdir; // Kernel virtual address of page dir
};
- env_tf: 這個進(jìn)程所屬的寄存器的值,當(dāng)發(fā)生進(jìn)程切換到時候医瘫,我們需要保存當(dāng)前正在運行的進(jìn)程的寄存器的值侣肄。
- env_link:這個就是相當(dāng)于鏈表中的next,指向下一個節(jié)點醇份,這樣才能夠把所有env串起來
- env_id:進(jìn)程的ID
- env_parent_id:附進(jìn)程的ID
- env_type: 進(jìn)程的類型稼锅,雖然大部分都進(jìn)程都是
ENV_TYPE_USER.
()用戶進(jìn)程 - env_status:
ENV_FREE
表示這個Env結(jié)構(gòu)是空閑的,也就說它還在env_free_list當(dāng)中僚纷,可以被分配使用缰贝。ENV_RUNNABLE
表示這個進(jìn)程可運行。比如說我們從硬盤中加載這個進(jìn)程的代碼到內(nèi)存當(dāng)中畔濒,初始化它的寄存器后剩晴,那么此時這個進(jìn)程就是runnable了。ENV_RUNNING
表示這個進(jìn)程正在占用CPU侵状。ENV_NOT_RUNNABLE
這個就是進(jìn)程被掛起了赞弥,這個進(jìn)程此時激活的(inactive)的,但是還沒準(zhǔn)備好運行趣兄,比如說等待硬盤或者網(wǎng)絡(luò)的IO結(jié)束绽左。ENV_DYING
zombie進(jìn)程,它此時還在內(nèi)存中沒有被釋放艇潭,當(dāng)下一次時鐘中斷發(fā)生的時候且輪到它執(zhí)行拼窥,kernel發(fā)現(xiàn)他是zombie就會將他釋放了(本次lab沒有涉及到這個) - env_pgdir:這個進(jìn)程的page directory的虛擬地址,注意看是虛擬地址戏蔑。
JOS中的environment和xv6中的process是差不多一個東西,就是JOS中的environemnt沒有進(jìn)程自己的棧鲁纠,所以JOS使用的棧是內(nèi)核棧
Allocating the Environments Array
在lab2當(dāng)中总棵,我們?yōu)閜ages[]這個數(shù)組分配了內(nèi)存。現(xiàn)在我們需要為envs
這個數(shù)組分配內(nèi)存改含。envs應(yīng)該放在UENVS
開始的地方并且要讓用戶程序也可以訪問情龄。
這個和lab2當(dāng)中對pages的操作差不多,在 kern/pmap.c 中加入下面的代碼:
//為envs分配內(nèi)存
envs = (struct Env*) boot_alloc(NENV * sizeof(struct Env));
memset(envs,0, NENV* sizeof (struct Env));
//將envs映射到UENVS開始的地方,注意PTE_U,因為用戶程序也要訪問
boot_map_region(kern_pgdir,UENVS,PTSIZE,PADDR(envs),PTE_U | PTE_P);
Creating and Running Environments
接下來我們要寫一些代碼來運行進(jìn)程捍壤,因為我們此時還沒有文件系統(tǒng)骤视,所以我們在內(nèi)核當(dāng)中嵌入了一些進(jìn)程鏡像,這些鏡像也是ELF格式的鹃觉。
Exercise 2
env_init():初始化所有的Env結(jié)構(gòu)专酗,并且放到env_free_list當(dāng)中
env_setup_vm():為進(jìn)程分配page directory,并且在頁表中分配kernel所屬的內(nèi)存。
region_alloc():為進(jìn)程分配物理內(nèi)存
load_icode():將ELF文件加載為進(jìn)程(畢竟ELF文件是無法直接運行的)
env_create():調(diào)用env_alloc()盗扇,env_alloc()初始化好進(jìn)程所需要的東西笼裳,然后還需要使用load_icode()為這個進(jìn)程加載ELF鏡像
env_run():運行用戶進(jìn)程
一上來就直接實現(xiàn)這幾個函數(shù)可能十分的難,所以首先先來整理下這些函數(shù)的調(diào)用關(guān)系粱玲,如下圖所示:
env_init():
在前面我們已經(jīng)位envs
分配好了內(nèi)存空間。env_init()的目的就是可用的進(jìn)程串到env_free_list當(dāng)中拜轨。根據(jù)注釋中的提示信息抽减,它要求我們數(shù)組中進(jìn)程的順序和鏈表中的順序要相同。所以for從數(shù)組的后面開始橄碾,然后使用頭插法插入到鏈表當(dāng)中卵沉,從而使得鏈表中的第一個元素就是數(shù)組的envs[0]。知道了鏈表的頭插法插入數(shù)據(jù)后法牲,代碼應(yīng)該很好理解
void env_init(void)
{
// Set up envs array
// LAB 3: Your code here.
for(int i = NENV-1; i >= 0; i--) {
//頭插法
envs[i].env_id = 0;
envs[i].env_status = ENV_FREE;
envs[i].env_link = env_free_list;
env_free_list = &envs[i];
}
// Per-CPU part of the initialization
env_init_percpu();
}
env_setup_vm():
新的進(jìn)程要有自己的page directory史汗。閱讀下xv6 book page table這一節(jié)(有點忘了,應(yīng)該是在這節(jié))拒垃,我們知道每一個進(jìn)程的地址空間都分為kernel portion和user space portion停撞。這樣一來就理解了注釋中說的初始化kernel portion。page_alloc()用于分配一個物理頁悼瓮,返回可用的物理頁對應(yīng)的PageInfo結(jié)構(gòu)戈毒。前面說了env_pgdir是虛擬地址,所以我們使用page2kva()將PageInfo結(jié)構(gòu)轉(zhuǎn)為虛擬地址横堡。然后根據(jù)注釋埋市,我們不需要初始化地址空間中屬于用戶程序的那部分,所以e->env_pgdir[i] = 0;
命贴。但是需要初始化地址空間中屬于內(nèi)核的那部分道宅,所以這一部分只需要將內(nèi)核的page directory中page directory復(fù)制過來就行食听,e->env_pgdir[i] = kern_pgdir[i];
最后在進(jìn)程自己的page directory將page directory對應(yīng)的page entry寫入就行。
int i = 0;
struct PageInfo *p = NULL;
// Allocate a page for the page directory
if (!(p = page_alloc(ALLOC_ZERO)))
return -E_NO_MEM;
e->env_pgdir = (pde_t *)page2kva(p);
p->pp_ref++;
for(; i<PDX(UTOP); i++){
e->env_pgdir[i] = 0;
}
for(; i<NPDENTRIES; i++){
e->env_pgdir[i] = kern_pgdir[i];
}
//進(jìn)程的頁目錄表
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;
region_alloc():
這個函數(shù)主要完成的是:將進(jìn)程的虛擬地址va和物理地址映射起來污茵,長度是len樱报。在前面的函數(shù)中,我們可以使用env_setup_vm()來獲得page directory省咨。想象一下肃弟,建立新的映射關(guān)系,相當(dāng)于往page table(頁表)中加入page table(當(dāng)然有時候page table可能在page directory中還不存在零蓉,所以需要現(xiàn)在page directory存放page table的地址笤受,這個工作室pgdir_walk()完成的,忘了的老鐵可以看一下pgdir_walk()的實現(xiàn))敌蜂。另外注釋也說了箩兽,注意起始地址和結(jié)束地址的對齊,所以我們還需要調(diào)用一下ROUNDUP和ROUNDDOWN章喉。
uintptr_t va_start =(uintptr_t) ROUNDDOWN(va,PGSIZE);
uintptr_t va_end = (uintptr_t)ROUNDUP(va+len,PGSIZE);
struct PageInfo *p;
int result;
for(uintptr_t i = va_start; i < va_end; i += PGSIZE) {
p = page_alloc(ALLOC_ZERO); //申請一個頁
result = page_insert(e->env_pgdir,p,(void *)i,PTE_W | PTE_U); //插入新申請的頁到page table 當(dāng)中
if( p == NULL || result != 0) {
cprintf("region_alloc:allocate memory failed");
}
}
load_icode():
這個代碼有一個點比較有意思汗贫。這個函數(shù)要實現(xiàn)的就是將elf文件加載到它指定的內(nèi)存地址當(dāng)中去。這一過程和之前在boot階段加載內(nèi)核比較相似秸脱。為了將進(jìn)程放到用戶程序所屬的地址空間去落包,肯定需要切換cr3寄存器,然后加載完了之后在切換到內(nèi)核的page directory摊唇。因此這里引出一個問題咐蝇,變換了cr3寄存器的內(nèi)容,為什么代碼還可以執(zhí)行巷查? 因為有序,我們在之前在用戶進(jìn)程中的page directory中把屬于內(nèi)核的那部分從內(nèi)核的page directory中復(fù)制過來了,所以可以繼續(xù)執(zhí)行岛请。 接下來要做的就是旭寿,加載用戶程序。根據(jù)注釋崇败,我們只加載ph->p_type == ELF_PROG_LOAD
的program header盅称。每一個program header要被加載到的虛擬地址是ph->p_va。進(jìn)程所需要的空間為ph->p_filesz 字節(jié)后室,這些字節(jié)是從binary + ph->p_offset開始的微渠,應(yīng)該復(fù)制到ph->p_va去。它還提到咧擂,通常來說ph->p_filesz <= ph->p_memsz,這里關(guān)于為什么直接去google搜下就行逞盆。還有一點,e->env_pgdir是虛擬地址在,所以我們在放入cr3寄存器的時候松申,需要用PADDR()將他轉(zhuǎn)為物理地址云芦。
struct Elf *ELF_Header = (struct Elf*)binary;
if (ELF_Header->e_magic != ELF_MAGIC)
panic("The binary is not a ELF magic!\n");
e->env_tf.tf_eip = ELF_Header->e_entry;
lcr3(PADDR(e->env_pgdir)); //切換cr3寄存器呐籽,這樣才能操作用戶程序的地址空間
struct Proghdr *ph, *eph;
ph = (struct Proghdr *) ((uint8_t *) ELF_Header + ELF_Header->e_phoff); // 獲得program header的地址
eph = ph + ELF_Header->e_phnum;
for (; ph < eph; ph++)
if(ph->p_type == ELF_PROG_LOAD){
if(ph->p_memsz < ph->p_filesz)
panic("segment out of memory!\n");
region_alloc(e, (void *)ph->p_va, ph->p_memsz); //為進(jìn)程申請內(nèi)存
memset((void *)ph->p_va, 0, ph->p_memsz);//這里注釋解釋了事镣,先初始化進(jìn)程空間的內(nèi)容
memcpy((void *)ph->p_va, binary+ph->p_offset, ph->p_filesz);//復(fù)制進(jìn)程的內(nèi)容
}
region_alloc(e, (void *)(USTACKTOP-PGSIZE), PGSIZE); //申請棧空間
lcr3(PADDR(kern_pgdir)); //切換到內(nèi)核的cr3
env_create():
創(chuàng)建一個新的進(jìn)程,這個沒啥好說的狂男,比較簡單属拾,直接貼代碼吧
void
env_create(uint8_t *binary, enum EnvType type)
{
// LAB 3: Your code here.
struct Env *e;
if(env_alloc(&e, 0)<0)
panic("fail to create a env!\n");
load_icode(e, binary);
e->env_type = type;
}
env_run():
這個也不怎么難舞萄,根據(jù)注釋應(yīng)該可以完成见转,直接貼代碼吧,注意要把panic給注釋了
void
env_run(struct Env *e)
{
if (curenv && curenv->env_status == ENV_RUNNING) {
curenv->env_status = ENV_RUNNABLE;
}
curenv = e;
curenv->env_status = ENV_RUNNING;
curenv->env_runs++;
lcr3(PADDR(curenv->env_pgdir));
env_pop_tf(&(curenv->env_tf));
}
當(dāng)我們完成這些函數(shù)時候,如果此時我們直接運行make qemu,會發(fā)現(xiàn)qemu會一直重復(fù)輸出一些內(nèi)容,如下圖
這就是官網(wǎng)說的會連續(xù)不斷的重啟的結(jié)果旗笔,如果我們用的事MIT官網(wǎng)給的那個定制版qemu,會觸發(fā)triple fault彪置。之所以會發(fā)生triple fault,是因為我們此時還沒有設(shè)置中斷處理函數(shù)蝇恶,當(dāng)lib/entry.S中調(diào)用int 0x30的時候拳魁,這時候就出錯了。這里應(yīng)該做成gif合適點撮弧。不過應(yīng)該可以看到上面結(jié)果潘懊,很多內(nèi)容重復(fù)出現(xiàn)的。不過我們接下來就要先去判斷下我們是否成功進(jìn)入了用戶程序贿衍。在一個窗口運行make qemu-gdb 授舟,另外一個運行make gdb。然后設(shè)置一個斷點b env_pop_tf贸辈。如果我們寫的程序是對的話释树,執(zhí)行了env_pop_tf之后應(yīng)該由iret語句跳轉(zhuǎn)到用戶程序中去執(zhí)行。
對照一下env_pop_tf的源代碼裙椭,目前來說都是對的,接著iret跳轉(zhuǎn)到了lib/entry.S署浩,第一條語句是cmp語句.如下圖:
對照下entry的源代碼揉燃,我們成功了。
Handling Interrupts and Exceptions
官網(wǎng)中關(guān)于中斷和異常的文字比較多筋栋,翻譯比較花時間炊汤,就不翻譯了,應(yīng)該也不是特別難懂(如果你已經(jīng)知道一些關(guān)于x86的異常和中斷處理的過程)弊攘。簡單的來說,x86處理器有一個IDT用于存放所有的中斷或者異常的處理函數(shù)抢腐。比如說int 1就是調(diào)用1號中斷,CPU會從IDT中拿出這個中斷處理函數(shù)的CS和EIP襟交。然后跳轉(zhuǎn)到中斷處理函數(shù)去執(zhí)行迈倍。當(dāng)內(nèi)核處理完中斷后,還需要返回到用戶程序捣域,所以內(nèi)核需要將用戶程序的所屬的各個寄存器的值保存起來啼染。在JOS中宴合,inc/trap.h定義了Trapframe這個結(jié)構(gòu),用于保存用戶程序的各個寄存器迹鹅。另外卦洽,當(dāng)用戶程序發(fā)生異常的時候(比如說除0),往往需要發(fā)生特權(quán)級的轉(zhuǎn)移(由CPL=3轉(zhuǎn)移到CPL=0)斜棚。內(nèi)核是最重要的阀蒂,所以不能枉然的使用用戶程序的棧,也因此我們需要由內(nèi)核棧切換到用戶棧弟蚀。為了獲得內(nèi)核棧的地址蚤霞,有一個結(jié)構(gòu)叫做TSS(task state segment),我們事先在里面寫入了內(nèi)核棧的地址。當(dāng)發(fā)生了特權(quán)級轉(zhuǎn)換的時候粗梭,處理器會從TSS中取得內(nèi)核的棧地址争便,并且將用戶程序的ss,esp,eflags,cs,eip壓入到內(nèi)核棧當(dāng)中,這些是處理去做的断医。所以我們還需要手動的壓入那些通用寄存器(待會要做的_alltraps要做的事情)滞乙。當(dāng)然有時候內(nèi)核也會發(fā)生異常,當(dāng)在內(nèi)核的中斷處理函數(shù)中發(fā)生異常的時候鉴嗤,此時的就不需要壓入棧了斩启。只需要壓入cs,eip,eflags就行。這種在中斷中發(fā)生中斷的情況叫做nested exceptions and interrupts(嵌套的中斷和異常).
Setting Up the IDT
閱讀完官網(wǎng)的那些文字醉锅,接下來要做的就是設(shè)置好IDT(interrupt descriptor table)和以及對應(yīng)的中斷處理函數(shù)。到現(xiàn)在為止垄琐,我們只需要設(shè)置0-31這些中斷狸窘。下面這幅圖描述了基本的流程:
每一個中斷或者異常應(yīng)該在trapentry.S中有一個他們自己的handler翻擒,然后再trap_init()初始化牛哺。每一個handler應(yīng)該構(gòu)建一個Trapframe然后將指向trapframe的指針作為參數(shù)傳給trap函數(shù)引润。然后trap()把這些中斷分發(fā)到對應(yīng)的中斷處理函數(shù)去淳附。
Exercise
修改 trapentry.S和trap.c文件并且實現(xiàn)上面說的功能凰荚。兩個宏函數(shù)TRAPHANDLER和TRAPHANDER_NOEC還有定義在inc/trap.h中的T_*(一開始沒看懂便瑟,原來就是說一些宏定義)對你很有幫助到涂。你需要在trapentry.S中為每一個中斷實現(xiàn)他們的entry point践啄。還要實現(xiàn)一個_alltraps屿讽。你還需要修改trap_init()函數(shù)來初始化idt吠裆。
_alltraps:
- 往棧里面壓入值试疙,使得棧看起來像一個trapframe
- 加載GD_KD到ds和es寄存器
- 壓入esp作為參數(shù)傳給trap()
- 調(diào)用 trap
實現(xiàn)代碼的前戲:
這道題的代碼一開始我是做不來的履澳,參考了別人的代碼做完的距贷。首先我們要做的是實現(xiàn)中斷的entry point忠蝗,題目要求說了在trapentry.S中寫好entry point滨溉,注意這里要使用那兩個宏函數(shù)來完成晦攒。首先先來講解一下這兩個宏函數(shù),簡書的markdown太垃圾了脯颜,竟然把宏函數(shù)給注釋掉了贩据,認(rèn)真觀察闸餐。
TRAPHANDLER:
#define TRAPHANDLER(name, num) \
.globl name; /* define global symbol for 'name' */ \
.type name, @function; /* symbol type is function */ \
.align 2; /* align function definition */ \
name: /* function starts here */ \
pushl $(num); \
jmp _alltraps
TRAPHANDLER_NOEC:
#define TRAPHANDLER_NOEC(name, num) \
.globl name; \
.type name, @function; \
.align 2; \
name: \
pushl $0; \
pushl $(num); \
jmp _alltraps
如果你仔細(xì)閱讀了上面的文字,知道在中斷中有一些中斷會使得處理器壓入error code,有些不會壓入error code剔宪。看一下這兩個宏定義的代碼以及注釋感帅,可以看到這倆的區(qū)別就是:有些中斷不會壓入error code,所以TRAPHANDLER_NOEC有一條語句push 0用來替代沒有error code的情況失球。通過這兩個宏定義实苞,我們把中斷處理函數(shù)的entry point變得更加通用了硬梁。
此時我們大概了解了要借助于這兩個宏函數(shù)來完成entry point的過程胞得,另外一個關(guān)鍵點就是在idt中初始化一下阶剑。了解過C語言編譯的過程,我們知道宏函數(shù)在編譯的時候是被直接替換的素邪,所以上面提到的TRAPHEANDLER和TRAPHANDLER_NOEC會被直接替換為對應(yīng)的標(biāo)號兔朦。我們只要把這些標(biāo)號放入到每一個IDT成員中就行了沽甥,IDT就初始化完成了乏奥。
還有一個問題就是完成_alltraps。前面提到過媳瞪,由用戶程序切換到內(nèi)核蛇受,我們需要保存用戶程序的各個寄存器信息龙巨,這些信息都被保存到用戶程序的Trapframe里面熊响。_alltraps要做的事情就是構(gòu)建一個trapframe接著代碼跳轉(zhuǎn)到trap()當(dāng)中去執(zhí)行。幫助理解秸弛,下面是trapframe的圖示:
PS:error code并非任何時候都有递览,對于沒有error code的中斷绞铃,TRAPHANDLER_NOEC宏函數(shù)壓入了0儿捧,宏函數(shù)中的push $(num)指令壓入了trapno挑宠。
trap_init():
首先先做的就是初始化IDT,我們要填充kern/trap.c
的trap_init()的代碼懒鉴。我們需要初始化的中斷定義在inc/trap.h
當(dāng)中临谱。與之對應(yīng)我們聲明一些中斷處理函數(shù),比如說除0悉默,就叫divide_handler()
下面是所有的中斷處理函數(shù)麦牺。注意剖膳,只需要聲明岭辣,具體的函數(shù)是在trapentry.S當(dāng)中的沦童。
void divide_handler();
void debug_handler();
void nmi_handler();
void brkpt_handler();
void oflow_handler();
void bound_handler();
void device_handler();
void illop_handler();
void tss_handler();
void segnp_handler();
void stack_handler();
void gpflt_handler();
void pgflt_handler();
void fperr_handler();
void align_handler();
void mchk_handler();
void simderr_handler();
void syscall_handler();
void dblflt_handler();
void trap_init(void)
{
extern struct Segdesc gdt[];
// LAB 3: Your code here.
//#define SETGATE(gate, istrap, sel, off, dpl)
// 把SETGATE這個宏放在這里偷遗,幫助理解
SETGATE(idt[T_DIVIDE], 0, GD_KT, divide_handler, 0);
SETGATE(idt[T_DEBUG], 0, GD_KT, debug_handler, 0);
SETGATE(idt[T_NMI], 0, GD_KT, nmi_handler, 0);
SETGATE(idt[T_BRKPT], 0, GD_KT, brkpt_handler, 3);
SETGATE(idt[T_OFLOW], 0, GD_KT, oflow_handler, 0);
SETGATE(idt[T_BOUND], 0, GD_KT, bound_handler, 0);
SETGATE(idt[T_DEVICE], 0, GD_KT, device_handler, 0);
SETGATE(idt[T_ILLOP], 0, GD_KT, illop_handler, 0);
SETGATE(idt[T_DBLFLT], 0, GD_KT, dblflt_handler, 0);
SETGATE(idt[T_TSS], 0, GD_KT, tss_handler, 0);
SETGATE(idt[T_SEGNP], 0, GD_KT, segnp_handler, 0);
SETGATE(idt[T_STACK], 0, GD_KT, stack_handler, 0);
SETGATE(idt[T_GPFLT], 0, GD_KT, gpflt_handler, 0);
SETGATE(idt[T_PGFLT], 0, GD_KT, pgflt_handler, 0);
SETGATE(idt[T_FPERR], 0, GD_KT, fperr_handler, 0);
SETGATE(idt[T_ALIGN], 0, GD_KT, align_handler, 0);
SETGATE(idt[T_MCHK], 0, GD_KT, mchk_handler, 0);
SETGATE(idt[T_SIMDERR], 0, GD_KT, simderr_handler, 0);
SETGATE(idt[T_SYSCALL], 0, GD_KT, syscall_handler, 3);
// Per-CPU setup
trap_init_percpu();
}
trapentry.S
如果了解過C語言的編譯過程喉酌,我們知道宏函數(shù)在編譯的時候會被直接替換掉泪电。如果我們調(diào)用TRAPHANDER_NOEC(divide_handler,T_DIVIDE)就相當(dāng)于下面的代碼:
divide_handler:
push $0
push T_DIVIDE
這樣一來就和我們在trap_init()中的代碼對應(yīng)起來了相速,正式開始實現(xiàn)代碼之前鲜锚,先看一下x86中哪些中斷有error code芜繁,osDev這里總結(jié)的比mit那個好浆洗,Error Code 伏社。接下來只要調(diào)用兩個宏函數(shù)就行了:
/*
* Lab 3: Your code here for generating handler points for the different traps.
*/
TRAPHANDLER_NOEC(divide_handler, T_DIVIDE);
TRAPHANDLER_NOEC(debug_handler, T_DEBUG);
TRAPHANDLER_NOEC(nmi_handler, T_NMI);
TRAPHANDLER_NOEC(brkpt_handler, T_BRKPT);
TRAPHANDLER_NOEC(oflow_handler, T_OFLOW);
TRAPHANDLER_NOEC(bound_handler, T_BOUND);
TRAPHANDLER_NOEC(illop_handler, T_ILLOP);
TRAPHANDLER_NOEC(device_handler, T_DEVICE);
TRAPHANDLER(dblflt_handler, T_DBLFLT);
TRAPHANDLER(tss_handler, T_TSS);
TRAPHANDLER(segnp_handler, T_SEGNP);
TRAPHANDLER(stack_handler, T_STACK);
TRAPHANDLER(gpflt_handler, T_GPFLT);
TRAPHANDLER(pgflt_handler, T_PGFLT);
TRAPHANDLER_NOEC(fperr_handler, T_FPERR);
TRAPHANDLER(align_handler, T_ALIGN);
TRAPHANDLER_NOEC(mchk_handler, T_MCHK);
TRAPHANDLER_NOEC(simderr_handler, T_SIMDERR);
TRAPHANDLER_NOEC(syscall_handler, T_SYSCALL);
_alltraps:
代碼如下摘昌,要記得一點就是_alltraps是要構(gòu)建一個trapframe的聪黎。不過有一點我沒有明白,為什么要push esp锦秒,這個是參考別人代碼的旅择,我覺得和x86 c calling convention可能有關(guān)。另外再說一下沉噩,當(dāng)處理器在執(zhí)行下面的代碼的時候川蒙,已經(jīng)是在CPL=0的內(nèi)核中執(zhí)行了长已。雖然已經(jīng)發(fā)生了特權(quán)級的轉(zhuǎn)換痰哨,但是中斷發(fā)生的時候處理器只是替換了CS和EIP寄存器斤斧。所以此時的ds撬讽,es游昼,eax,ebx寄存去都還是用戶程序的寄存器载庭。所以構(gòu)建一個trapframe是可行的囚聚。
_alltraps:
pushl %ds
pushl %es
pushal
movl $GD_KD, %eax
movl %eax, %ds
movl %eax, %es
push %esp //我沒有理解
call trap
實驗結(jié)果:
通過運行make grade,如果divezero顽铸,softint,badsegment都正確了的話說明你成功了谓松。下面是我的實驗結(jié)果鬼譬;
question
- 為什么每一個中斷或者異常都有自己的處理函數(shù)(也就是說如果所有的中斷和異常都使用相同的處理函數(shù)會怎么樣)
答:這個題目感覺有點無厘頭,有點廢話男窟,不同的中斷當(dāng)然需要不同的處理函數(shù)歉眷,難道除0和page fault做相同的事情嗎?淑际。春缕。 不知道是不是我對題目理解有問題
- 怎么樣才能使softint正常運行艘蹋?評分腳本希望他產(chǎn)生13號中斷女阀,但是softint的代碼是調(diào)用了14號中斷,怎么做冯键?
答: 只要讓14號中斷的DPL=0就行了,softint是用戶程序庸汗,它運行在DPL=3。當(dāng)14號中斷的DPL=0的時候蚯舱,用戶程序因為他的DPL比較低(DPL=0)所以沒有權(quán)限去調(diào)用這個中斷雕薪,因此會觸發(fā)general protection fault。