好了,現在,我們知道了80x86微處理器在硬體級對中斷和異常做了些什麼,接下來,我們繼續關注的是如何初始化中斷描述符表。
核心啟用中斷以前,必須把IDT表的初始地址裝到idtr寄存器,並初始化表中的每一項。這項工作是在初始化系統時完成的。
int指令允許使用者態進程發出一個中斷訊號,其值可以是0-255的任意一個向量。因此,為了防止使用者通過int指令類比非法的中斷和異常,IDT的初始化必須非常小心。這可以通過把中斷或陷阱門描述符的DPL欄位設定成0來實現。如果進程試圖發出其中的一個中斷訊號,控制單元將會發現CPL的值與DPL欄位有衝突,並且產生一個"General protection”異常。
然而,在少數情況下,使用者態進程必須能發出一個編程異常。為此,只要把中斷或陷阱門描述符的DPL欄位設定成3,即特權級儘可能一樣高就足夠了。
現在,讓我們來看一下Linux是如何?這種策略的。
1 中斷門、陷阱門及系統門
我們先回憶一下前一篇博文的“中斷描述符表”,Intel提供了三種類型的中斷描述符:任務門、中斷門及陷阱門描述符。Linux使用與Intel稍有不同的細目分類和術語,把它們如下進行分五類:
中斷門(interrupt gate):使用者態的進程不能訪問Intel中斷門(門的DPL欄位為0)。所有的Linux中斷處理常式都通過中斷門啟用,並全部限制在核心態。
系統門(system gate):使用者態的進程可以訪問Intel陷阱門(門的DPL欄位為3)。通過系統門來啟用三個Linux例外處理常式,它們的向量是4,5及128,因此,在使用者態下,發行就緒into、bound及int $0x80三條組合語言指令。
系統中斷門(system interrupt gate):能夠被使用者態進程訪問的Intel中斷門(門的DPL欄位為3)。與向量3相關的例外處理常式是由系統中斷門啟用的,因此,在使用者態可以使用組合語言指令int3。
陷阱門(trap gate):使用者態的進程不能訪問的一個Intel陷阱門(門的DPL欄位為0)。大部分Linux例外處理常式都通過陷阱門來啟用。
任務門(task gate):不能被使用者態進程訪問的Intel任務門(門的DPL欄位為0)。Linux對“Double fault”異常的處理常式是由任務門啟用的。
下列體繫結構相關的函數用來在IDT中插入門:
set_intr_gate(n,addr)
在IDT的第n個表項插入一個中斷門。門中的段選擇符設定成核心代碼的段選擇符,位移量設定為中斷處理常式的地址addr,DPL欄位設定為0。
set_system_gate(n,addr)
在IDT的第n個表項插入一個陷阱門。門中的段選擇符設定成核心代碼的段選擇符,位移量設定為例外處理常式的地址addr,DPL欄位設定為3。
set_system_intr_gate(n,addr)
在IDT的第n個表項插入一個中斷門。門中的段選擇符設定成核心代碼的段選擇符,位移量設定為例外處理常式的地址addr,DPL欄位設定為3。
set_trap_gate(n,addr)
與前一個函數類似,只不過DPL的欄位設定成0。
set_task_gate(n,gdt)
在IDT的第n個表項中插入一個中斷門。門中的段選擇符中存放一個TSS的通用描述元表的指標,該TSS中包含要被啟用的函數。位移量設定為0,而DPL欄位設定為3。
2 IDT的初步初始化
當電腦還運行在實模式時,IDT就被初始化並由BIOS常式使用。然而,一旦Linux接管,IDT就被移到RAM主存的另一個地區,並進行第二次初始化,因為Linux沒有利用任何BIOS的常式。
在原始碼中,IDT存放在idt_table表中,有256個表項。6位元組的idt_descr變數指定了IDT的大小和它的地址,只有當核心用lidt彙編指令初始化idtr寄存器時才用到這個變數(回憶一下,idtr寄存器存放的是IDT的基址)。
在核心初始化過程中,setup_idt()組合語言函數用同一個中斷門(即指向ignore_int()中斷處理常式)來填充所有這256個idt_table表項:
setup_idt:
lea ignore_int, %edx
movl $(_ _KERNEL_CS << 16), %eax
movw %dx, %ax /* selector = 0x0010 = cs */
movw $0x8e00, %dx /* interrupt gate, dpl=0, present */
lea idt_table, %edi
mov $256, %ecx
rp_sidt:
movl %eax, (%edi)
movl %edx, 4(%edi)
addl $8, %edi
dec %ecx
jne rp_sidt
ret
用組合語言寫成的ignore_int()中斷處理常式,可以看作一個空的處理常式,它執行下列動作:
1. 在棧中儲存一些寄存器的內容。
2. 調用printk()函數列印“Unknown interrupt”系統訊息。
3. 從棧恢複寄存器的內容。
4. 執行iret指令以恢複被中斷的程式。
ignore_int()處理常式應該從不被執行。如果在控制台或記錄檔中出現的“Unknown interrupt”訊息,則標誌著要麼是出現了一個硬體的問題(一個I/O裝置正在產生沒有預料到的中斷),要麼就是出現了一個核心的問題(一個中斷或異常未被適當地處理)。
緊接著這個預初始化,核心將在IDT中進行第二遍初始化,用有意義的陷阱和中斷處理常式替換這個空處理常式。一旦這個過程完成,對控制單元產生的每個不同的異常,IDT都有一個專門的陷阱或系統門,而對於可程式化插斷控制器確認的每一個IRQ,IDT都將包含一個專門的中斷門。
好了,現在有了對中斷硬體環境的瞭解,以及在得到一個空的IDT的表以後,接下來的博文中,將分別針對異常和中斷來詳細地說明這個工作是如何完成的。隨後將分別為中斷和異常舉一個執行個體,一個是進程調度的“心臟”——時鐘中斷,一個是虛擬儲存的核心內容——缺頁異常。敬請期待!