在上一篇文章ucore操作系統(tǒng)實(shí)驗(yàn)筆記 - Lab1中,我已經(jīng)比較詳細(xì)地記錄了中斷的使用叮阅。那篇文章關(guān)于中斷的重點(diǎn)是如何使用IDT躺苦、中斷描述符和中斷向量表等。這篇文章我將把重點(diǎn)放到另外一個(gè)地方能庆,也就是中斷的過程中如何保存和恢復(fù)現(xiàn)場(chǎng)九杂。
CPU接收到中斷信號(hào)后會(huì)做什么
- CPU在執(zhí)行完當(dāng)前程序的每一條指令后颁湖,都會(huì)去確認(rèn)在執(zhí)行剛才的指令過程中中斷控制器(如:8259A)是否發(fā)送中斷請(qǐng)求過來,如果有那么CPU就會(huì)在相應(yīng)的時(shí)鐘脈沖到來時(shí)從總線上讀取中斷請(qǐng)求對(duì)應(yīng)的中斷向量例隆;
- CPU根據(jù)得到的中斷向量(以此為索引)到IDT中找到該向量對(duì)應(yīng)的中斷描述符甥捺,中斷描述符里保存著中斷服務(wù)例程的段選擇子;
- CPU使用IDT查到的中斷服務(wù)例程的段選擇子從GDT中取得相應(yīng)的段描述符镀层,段描述符里保存了中斷服務(wù)例程的段基址和屬性信息镰禾,此時(shí)CPU就得到了中斷服務(wù)例程的起始地址,并跳轉(zhuǎn)到該地址唱逢;
- CPU會(huì)根據(jù)CPL和中斷服務(wù)例程的段描述符的DPL信息確認(rèn)是否發(fā)生了特權(quán)級(jí)的轉(zhuǎn)換吴侦。比如當(dāng)前程序正運(yùn)行在用戶態(tài),而中斷程序是運(yùn)行在內(nèi)核態(tài)的惶我,則意味著發(fā)生了特權(quán)級(jí)的轉(zhuǎn)換妈倔,這時(shí)CPU會(huì)從當(dāng)前程序的TSS信息(該信息在內(nèi)存中的起始地址存在TR寄存器中)里取得該程序的內(nèi)核棧地址博投,即包括內(nèi)核態(tài)的ss和esp的值绸贡,并立即將系統(tǒng)當(dāng)前使用的棧切換成新的內(nèi)核棧。這個(gè)棧就是即將運(yùn)行的中斷服務(wù)程序要使用的棧毅哗。緊接著就將當(dāng)前程序使用的用戶態(tài)的ss和esp壓到新的內(nèi)核棧中保存起來听怕;
- CPU需要開始保存當(dāng)前被打斷的程序的現(xiàn)場(chǎng)(即一些寄存器的值),以便于將來恢復(fù)被打斷的程序繼續(xù)執(zhí)行虑绵。這需要利用內(nèi)核棧來保存相關(guān)現(xiàn)場(chǎng)信息尿瞭,即依次壓入當(dāng)前被打斷程序使用的eflags,cs翅睛,eip声搁,errorCode(如果是有錯(cuò)誤碼的異常)信息;
- CPU利用中斷服務(wù)例程的段描述符將其第一條指令的地址加載到cs和eip寄存器中捕发,開始執(zhí)行中斷服務(wù)例程疏旨。這意味著先前的程序被暫停執(zhí)行,中斷服務(wù)程序正式開始工作扎酷。
上面這些內(nèi)容是我從ucore實(shí)驗(yàn)指導(dǎo)書上直接摘抄下來的檐涝,在之前那篇文章中,我主要關(guān)注前3步和最后一步,這篇文章谁榜,我將關(guān)注第4幅聘、5步。
特權(quán)級(jí)轉(zhuǎn)換的檢測(cè)
我個(gè)人覺得第4窃植、5步應(yīng)該是發(fā)生在CPU跳轉(zhuǎn)到ISR(中斷服務(wù)例程)之前帝蒿,所以把第3步放在第5步的后面更合適,之后我會(huì)解釋為什么我這么覺得巷怜。當(dāng)CPU獲取到IDT中的中斷描述符后陵叽,會(huì)對(duì)特權(quán)級(jí)的轉(zhuǎn)換進(jìn)行一次檢測(cè),具體檢測(cè)如下圖所示:
當(dāng)CPU獲取了中斷描述符后丛版,CPU會(huì)用中斷描述符的DPL和當(dāng)前段選擇子的CPL進(jìn)行比較巩掺,從而判斷是否需要進(jìn)行特權(quán)級(jí)的轉(zhuǎn)換。同時(shí)页畦,它還會(huì)做一些列的檢測(cè)工作胖替,比如對(duì)于硬中斷而言,CPL一定要大于等于DPL豫缨,因?yàn)樘貦?quán)級(jí)是向著更高特權(quán)級(jí)或者平級(jí)轉(zhuǎn)換的独令。而對(duì)于軟中斷而言,轉(zhuǎn)換后的特權(quán)級(jí)不能超過轉(zhuǎn)換前的特權(quán)級(jí)好芭,這是為了防止用戶代碼隨意觸發(fā)中斷燃箭。對(duì)于CPL和DPL不同的情況,我們需要使用TSS來對(duì)內(nèi)核棧進(jìn)行切換舍败,關(guān)于TSS的內(nèi)容我之后會(huì)單獨(dú)開篇文章招狸。
內(nèi)核棧的變化
第4、5步一個(gè)重要的功能就是向內(nèi)核棧中壓入各種寄存器邻薯。壓入這些寄存器既可以起到保存現(xiàn)場(chǎng)的作用裙戏,又能讓ISR知道中斷的各種信息,所以這兩步是很重要的厕诡。我們來看看哪些寄存器是CPU必須壓入內(nèi)核棧的:
這是發(fā)生中斷并且特權(quán)級(jí)轉(zhuǎn)換后椑郯瘢空間變化的示意圖,對(duì)于不發(fā)生特權(quán)級(jí)轉(zhuǎn)換的中斷灵嫌,有兩個(gè)地方不同壹罚,第一,它只用到一個(gè)棧寿羞,也就是說Procedure和Handler用的是同一個(gè)棧猖凛;第二,CPU不需要壓入SS和ESP稠曼。除此之外形病,這兩種情況都需要壓入CS客年,EIP和Error Code(如果有的話)。之所以我說第3步應(yīng)該在第5步后漠吻,原因就在這里量瓜,如果先跳到了ISR,那么壓入的EIP就是ISR中的EIP了途乃,并不是中斷前的EIP绍傲,因此我們應(yīng)該在第3步前完成步驟4和5。
Trapframe和ISR
除了CPU要壓入的各種寄存器耍共,我們還需要壓入其他一些寄存器用于保存現(xiàn)場(chǎng)和提供給ISR中斷信息烫饼。在ucore中,我們使用結(jié)構(gòu)體trapframe來將保存的寄存器傳給ISR试读。下面就先來看看trapframe:
/* registers as pushed by pushal */
struct pushregs {
uint32_t reg_edi;
uint32_t reg_esi;
uint32_t reg_ebp;
uint32_t reg_oesp; /* Useless */
uint32_t reg_ebx;
uint32_t reg_edx;
uint32_t reg_ecx;
uint32_t reg_eax;
};
struct trapframe {
struct pushregs tf_regs;
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
其中pushregs中的寄存器都是pushal中需要壓入棧的所有寄存器杠纵。有了這個(gè)數(shù)據(jù)結(jié)構(gòu)后,我們就可以在中斷后獲取中斷的信息钩骇,并將它傳給ISR比藻,ISR會(huì)根據(jù)傳入的trapframe來進(jìn)行相應(yīng)的操作。
下面我們來看看如何給trapframe賦值倘屹,如何將trapframe傳給ISR:
.globl vector2
vector2:
pushl $0
pushl $2
jmp __alltraps
上面這段代碼是中斷向量2银亲,在第6步時(shí)CPU會(huì)執(zhí)行這里的指令。它首先壓入0和2纽匙,0是error code(對(duì)于沒有error code的中斷务蝠,ISR會(huì)壓入0作為error code;如果中斷有error code烛缔,這里就不會(huì)壓入0)馏段,2是中斷向量號(hào)。注意力穗,在這之前毅弧,CPU已經(jīng)壓入了EFLAGS,CS当窗,EIP和Error Code(如果有的話)。在壓入error code和中斷向量號(hào)后寸宵,CPU跳到__alltraps崖面,__alltraps會(huì)將所有中斷需要保存的寄存器存到內(nèi)核棧,然后將此時(shí)棧頂?shù)牡刂?$esp)作為參數(shù)傳給trap()梯影,trap()會(huì)將此時(shí)棧中壓入的各種寄存器整體當(dāng)成trapframe來處理巫员。trap()會(huì)會(huì)根據(jù)trapframe中的內(nèi)容,對(duì)中斷進(jìn)行相應(yīng)的處理甲棍。
.text
.globl __alltraps
__alltraps:
# push registers to build a trap frame
# therefore make the stack look like a struct trapframe
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
這段代碼將所有中斷需要保存的寄存器壓入內(nèi)核棧简识。
# load GD_KDATA into %ds and %es to set up data segments for kernel
movl $GD_KDATA, %eax
movw %ax, %ds
movw %ax, %es
這段代碼將此時(shí)的數(shù)據(jù)段和附加段設(shè)置為內(nèi)核的數(shù)據(jù)段(ISR是位于kernel的)。
# push %esp to pass a pointer to the trapframe as an argument to trap()
pushl %esp
# call trap(tf), where tf=%esp
call trap
這段代碼先將%esp的值壓入內(nèi)核棧,%esp的值將作為函數(shù)trap()的參數(shù)七扰,然后我們?cè)賑all trap奢赂。通過向棧中壓入各種寄存器的信息并且將棧頂?shù)牡刂纷鳛閠rapframe的地址,我們完成了對(duì)trapframe的賦值颈走。trap()函數(shù)接收到trapframe后就可以根據(jù)中斷類型做出相應(yīng)處理了膳灶。我們來看看此時(shí)棧中的情況:
因?yàn)闂J菑母叩刂废虻偷刂飞L(zhǎng)的,因此立由,棧中藍(lán)色部分EFLAGS地址最高轧钓,EDI地址最低。這個(gè)和trapframe中的元素也是吻合的锐膜,tf_eflags地址最高(如何不考慮tf_esp, tf_ss)毕箍,而reg_edi地址最低。因此我們可以通過Old ESP這個(gè)地址道盏,把棧中藍(lán)色部分當(dāng)成trapframe來處理霉晕。
# pop the pushed stack pointer
popl %esp
# return falls through to trapret...
.globl __trapret
__trapret:
# restore registers from stack
popal
# restore %ds, %es, %fs and %gs
popl %gs
popl %fs
popl %es
popl %ds
# get rid of the trap number and error code
addl $0x8, %esp
iret
當(dāng)trap()運(yùn)行結(jié)束后,我們需要將寄存器恢復(fù)到中斷前的狀態(tài)捞奕。在這里牺堰,我們只需要將內(nèi)核棧中的內(nèi)容分別彈出,并保存到相應(yīng)的寄存器即可颅围。最后伟葫,通過調(diào)用iret指令來恢復(fù)EIP,CS和EFLAGS院促。如果還存在特權(quán)級(jí)的轉(zhuǎn)化筏养,我們還需要彈出之前保存的SS和ESP。到此為止常拓,整個(gè)中斷的過程就結(jié)束了渐溶。