中斷或異常發生之前
當 CPU 執行了當前指令之后,CS 和 EIP 這對寄存器中所包含的內容就是下一條將要執行 指令的邏輯地址。在對下一條指令執行前,CPU 先要判斷在執行當前指令的過程中是否發生 了中斷或異常。
如果發生了一個中斷或異常
那么 CPU 將做以下事情
• 確定所發生中斷或異常的向量i(在 0~255 之間)。
• 通過 IDTR 寄存器找到 IDT 表,讀取 IDT 表第i項(或叫第i個門)。
• 分兩步進行有效性檢查:首先是“段”級檢查,將 CPU 的當前特權級 CPL(存放在 CS 寄存器的最低兩位)與 IDT 中第i項段選擇符中的 DPL 相比較,如果 DPL(3)大於 CPL(0), 就產生一個“通用保護”異常(中斷向量 13),因為中斷處理程序的特權級不能低於引起中 斷的程序的特權級。這種情況發生的可能性不大,因為中斷處理程序一般運行在內核態,其 特權級為 0。然后是“門”級檢查,把 CPL 與 IDT 中第 i個門的 DPL 相比較,如果 CPL 大於 DPL,也就是當前特權級(3)小於這個門的特權級(0),CPU 就不能“穿過”這個門,於是 產生一個“通用保護”異常,這是為了避免用戶應用程序訪問特殊的陷阱門或中斷門。但是 請注意,這種“門”級檢查是針對一般的用戶程序,而不包括外部 I/O 產生的中斷或因 CPU 內部異常而產生的異常,也就是說,如果產生了中斷或異常,就免去了“門”級檢查。
(這里的免去沒大明白,是不進行有效性檢查了?直接跳過這一步?)
• 檢查是否發生了特權級的變化。當中斷發生在用戶態(特權級為 3),而中斷處理程 序運行在內核態(特權級為0),特權級發生了變化,所以會引起堆棧的更換。也就是說,從 用戶堆棧切換到內核堆棧。而當中斷發生在內核態時,即 CPU 在內核中運行時,則不會更換堆棧。
找到對應的門
異常處理沒說怎么在idt_table中找到對應的門的,中斷倒是因為有中斷號,可以根據這個中斷號去idt_table中找到門,反正cpu硬件干的
堆棧變化
如果堆棧變化則將當前的(SS,ESP)壓入棧中,此時的棧為內核棧,因為一旦出現中斷或異常,堆棧就切換到了內核堆棧,上面這些操作是由硬件完成的,至於具體怎么操作的也沒大明白,壓內核棧不是得先更新成內核的SS和ESP嗎?ESP更新了,那壓的ESP不就是內核的?看見書中有提到此時的內核堆棧是空的,也許壓的固定位置吧。看到后面搞懂了再更新。
更新:有看到說從TSS中取到的內核堆棧,難道是用movl實現的?
10/29/2015更新:看到后面有些理解了,因為每個進程有task_struct記錄其所有信息,也就是常說的pcb,而這個task_struct與該進程的內核堆棧共用8KB的存儲空間,所以中斷或異常發生的時候完全可以從tss_struct取出內核esp指針,再得到內核堆棧,因為內核堆棧開始與頁首部,而且以8KB為單位,所以esp&~8191UL就可以取到內核堆棧起始地址了,壓棧操作的目的地就是這兒,壓完了再進行esp切換。tss_struct是任務狀態段,是intel設計的cpu任務切換硬件支持,linux為了保證靈活性和對出錯恢復的可操作性以及性能考慮,未完全使用這種切換方式。
處理程序格式對於異常和中斷是不同的
異常處理
handler_name: |
處理程序名稱如:debug,nmi,int3,overflow,bounds等異常名,與異常類型相對應 |
pushl $0 /* only for some exceptions */ |
沒有錯誤碼的需要壓一個0,使內核堆棧保持一致性 |
pushl $do_handler_name |
將真正的處理函數地址壓棧 |
jmp error_code |
跳至公共異常處理 |
此時堆棧狀態
error_code: |
公共異常處理 |
pushl %ds |
ds入棧 |
pushl %eax |
eax入棧 |
xorl %eax,%eax |
eax清零 |
pushl %ebp |
ebp入棧 |
pushl %edi |
edi入棧 |
pushl %esi |
esi入棧 |
pushl %edx |
edx入棧 |
decl %eax |
# eax = -1 |
pushl %ecx |
ecx入棧 |
pushl %ebx |
ebx入棧 |
cld |
清eflag中的DF標志,使EIP朝向增長方向 |
movl %es,%ecx |
es移入ecx |
movl ORIG_EAX(%esp), %esi |
# get the error code,移入esi |
movl ES(%esp), %edi |
# get the function address,上面壓棧的do_handler_name函數的地址,移入edi |
movl %eax, ORIG_EAX(%esp) |
-1移入esp+ORIG_EAX的位置,對應原來的錯誤碼的位置 |
movl %ecx, ES(%esp) |
ecx中的值(es)存入esp+ES的位置,即是ES本該存的地方 |
movl %esp,%edx |
將當前的堆棧地址存入edx |
pushl %esi |
# push the error code,esi中錯誤碼入棧 |
pushl %edx |
# push the pt_regs pointer,將現場信息的起始地址壓棧,類似於pt_reg的作用,使異常處理程序可以按照統一規則去訪問出錯現場的數據 |
movl $(__KERNEL_DS),%edx |
讀取內核數據段 |
movl %edx,%ds |
加載內核數據段 |
movl %edx,%es |
加載內核ES段 |
GET_CURRENT(%ebx) |
將當前進程的task_struct存入ebx,中斷返回進程調度和信號處理需要task_struct中的信息,而且只能在內核態操作,因為task_struct存在內核底部 |
call *%edi |
call執行真正的異常處理程序 |
addl $8,%esp |
真正的異常處理程序返回后丟棄錯誤碼和異常處理程序地址 |
jmp ret_from_exception |
跳轉異常返回,還需對進程調度,信號處理,vm模式和是否返回用戶態進行處理,恢復現場 |
中斷處理
預處理后的結果
IRQn_interrupt: |
中斷號為n的中斷處理程序 |
pushl $n-256 |
將中斷號入棧,與異常處理的硬件自動壓棧錯誤碼或者手動壓0相對應 |
jmp common_interrupt |
跳至公共中斷處理 |
預處理后的結果(因為加了asmlinkage標識,所以do_IRQ會在棧中尋找參數,即pt_regs就是ESP)
common_interrupt: |
公共異常處理 |
SAVE_ALL |
保存現場到棧中,作為pt_regs參數,與公共異常處理前半截壓棧操作相對應,異常處理程序還需將錯誤碼和真正的異常處理程序地址取出來,將es存入正確位置,然后才調用真正的異常處理程序進行處理,同樣的采用棧傳參數的方式,傳入的是*pt_regs和錯誤碼兩個參數,只是第一個參數是ESP地址,而中斷的第一個參數取到的就是ESP |
call do_IRQ |
call調用中斷處理程序 |
jmp ret_from_intr |
跳至從中斷返回 |