相關學習資料
《深入理解計算機系統(原書第2版)》.pdf http://zh.wikipedia.org/zh/%E4%B8%AD%E6%96%B7 獨辟蹊徑品內核:Linux內核源代碼導讀 李雲華著 中文 PDF版 https://www.kernel.org/ http://blog.csdn.net/orange_os/article/details/7485069 http://blog.csdn.net/sunnybeike/article/details/6958473 http://blog.163.com/di_yang@yeah/blog/static/861184922012124105030284/ http://www.360doc.com/content/13/0829/21/7377734_310810291.shtm
目錄
1. 從異常控制流開始說起 2. 中斷類型 3. 中斷的初始化 4. 門描述符 5. IDT中斷描述符表 6. 異常控制類型
1. 從異常控制流開始說起
0x1: 異常控制流簡介
從給處理器加電開始,知道斷電為止,程序計數器假設一個值的序列:
A0, A1, ...., An-1
其中,每個Ak是某個相應的指令Ik的"地址"。每次從Ak到Ak+1的過渡稱為控制轉移(control transfer)。這樣的控制轉移序列叫作處理器的控制流(flow of control或control flow)
控制流可以大致分為:
1. "平滑的"序列(即順序執行) 其中每個Ik和Ik+1在存儲器中都是相鄰的,CPU按照計數器進行逐條指令的依次執行 2. 非平滑控制流 Ik+1與Ik不相鄰,是由諸如跳轉、調用、和返回這樣一些程序指令造成的。這樣一些指令都是必要的機制,使得程序能夠對由程序變量表示的內部狀態中的變化做出反應。
但是系統也必須能夠對系統狀態變化做出反應,這些系統狀態不是被內部程序變量捕獲的,而且也不一定和程序的執行相關。比如:
但是系統也必須能夠對系統狀態變化做出反應,這些系統狀態不是被內部程序變量捕獲的,而且也不一定和程序的執行相關。比如: 1. 一個硬件定時器定期產生信息,這個事件必須得到處理。 2. 數據包到達網絡適配器后,必須存放在存儲器中。 3. 程序向磁盤請求數據,然后休眠,知道被通知數據已就緒。 4. 當子進程終止時,創造這些子進程的父進程必須得到通知。
現代系統通過使控制流發生突變來對這些情況做出反應(也就是異常控制流)。一般而言,我們把這些突變稱為異常控制流(Exceptional Control Flow ECF)。異常控制流發生在計算機系統的各個層次。比如:
1. 在硬件層,硬件檢測到事件時,會觸發控制,使CPU突然轉移到異常處理程序。 2. 在操作系統層,內核通過上下文切換將控制從一個用戶進程轉移到另一個用戶進程(分時間片執行)。 3. 在應用層,一個進程可以發送信號到另一個進程,而接收者會將控制突然轉移到它的一個信號處理程序。一個程序可以通過回避通常的棧規則,並執行到其他函數中任意位置的非本地跳轉來對錯誤做出反應。
0x2: 異常控制流處理機制和中斷技術的關系
在繼續深入學習之前,我們必須先理清一個基本概念:
異常控制流是操作系統中的一種控制流處理機制,異常控制流處理機制被用於實現操作系統中的CPU處理流程切換的實現,而中斷技術是實現這一技術的方法。即中斷是一個技術,而異常控制流處理是一種機制
2. 中斷類型
中斷是現代操作系統的一項重要技術,利用中斷技術可以極大地提高系統吞吐量。從本質上理解,中斷是CPU提供的一項硬件機制,CPU可以根據中斷號跳轉到相應的中斷處理例程上去
0x1: 中斷硬件實現
在硬件實現上,中斷可以是:
1. 包含控制線路的獨立系統(本文重點學習的) 在IBM個人機上,廣泛使用可編程中斷控制器(Programmable Interrupt Controller,PIC)來負責中斷響應和處理。PIC被連接在若干中斷請求設備(各種外設)和處理器(CPU)的中斷引腳之間,從而實現對處理器中斷請求線路
(多為一針或兩針)的復用 2. 被整合進存儲器子系統中 作為另一種中斷實現的形式,即存儲器子系統實現方式,可以將中斷端口映射到存儲器的地址空間,這樣對特定存儲器地址的訪問實際上是中斷請求
0x2: 中斷分類
從實現機制上來分,中斷可以分為以下2類:
1. 外部中斷(包括可屏蔽、不可屏蔽中斷) 外部中斷是由外部設備引發的中斷,而引發中斷的設備被稱為中斷源,中斷源大致可以分為以下幾種 1) 定時器、計時器 2) 鍵盤 3) 內部實時時鍾 4) 通用接口 5) PS/2鼠標 6) 協處理器 7) IDE/SATE硬盤 8) 串口 9) 並口 10) 軟盤 ... 外部設備通過"可編程中斷控制器(Programmable Interrupt Controller,PIC)"向CPU報告的中斷,大致流程如下: 1) 外部設備通過中斷請求線(IRQ)連接到一個"中斷控制器"上 2) 當一個外部設備需要發出中斷時,會驅動對應的中斷請求線進入有信號狀態 3) 中斷控制器檢測這個中斷是否被屏蔽了(CPU的IF位被置1,則不屏蔽任何外部中斷;CPU的IF位被置0,則屏蔽所有外部中斷),如果沒有被屏蔽就驅動CPU的"INTR中斷請求線"進入信號狀態 4) CPU隨后就能檢測到這個中斷了(在每次CPU周期的下降沿檢測一次中斷) 5) 如果該中斷被屏蔽(CPU中的中斷屏蔽寄存器被選中),中斷控制器中的寄存器中的某一位位將記錄這一請求,等到中斷被開啟時再驅動CPU的"INTR中斷請求線"進入信號狀態 6) 之后CPU通過"中斷應答"從中斷控制器的數據線上讀取中斷號,並通過中斷號獲取中斷向量 7) 如果多個設備(外設中斷源)在同一時刻通過不同的中斷請求線發出中斷請求,中斷控制器也會將這些請求記錄在不同的位中 8) 如果這些中斷都沒有被屏蔽,則中斷控制器根據優先級,依次執行優先級高的中斷(IRQn的數字n越小優先級越大) /* 關於中斷屏蔽,這里需要補充幾點 關閉外部中斷的方式有: 1. 通過cli指令把標志寄存器中的IF位清零,這樣就關閉了"所有的"外部中斷 2. 通過中斷控制器中的中斷屏蔽寄存器,屏蔽某一特定的IRQn,從而屏蔽該中斷(只是屏蔽某個中斷,不影響其他中斷) 3. 在很多外設上,也設有控制寄存器,可以通過外設的中斷控制器從數據源上關閉外設上的某個IRQn,從而屏蔽某個中斷 */ 2. 內部中斷 和外部中斷相對的就是內部中斷。從CPU的角度看,外部中斷是一個異步事件,它可能在任何時候發送,而內部中斷是一個同步事件,它是執行某條指令時產生的。 內部中斷可以大致分為以下幾種 1) 異常(faults) CPU在指令執行時產生的,異常是可以修復的。當異常發生時,壓入堆棧的是產生異常的"那條指令",當CPU執行異常處理程序結束后,將"重新執行那一條指令"。 1.1) 缺頁異常: 14: #PF 1.2) 保護錯誤(內存或其他保護檢查): 13: #GP 1.3) 堆棧段錯誤(堆棧操作或者加載SS): 12: #SS 1.4) 段不存在(加載段寄存器后訪問段): 11: #NP 1.5) 除法錯誤(DIV/IDIV指令): 0: #DE 1.6) 越界: 5: #BR 1.7) 無效操作碼(無效操作指令): 6: #UD 1.8) 對齊校驗(內存訪問): 17: #AC 2) 陷阱(traps) 在CPU執行陷阱指令后,立刻通過中斷描述表執行預定的陷阱處理例程。陷阱處理例程執行結束后,將返回陷阱指令的"下一條指令"繼續執行。 2.1) 系統調用(system call) 系統調用是一種軟中斷,軟中斷是一條CPU指令,用以自陷一個中斷。由於軟中斷指令通常要運行一個切換CPU至內核態(Kernel Mode/Ring 0)的子例程,它常被用作實現系統調用(System call)
這是最長使用到的中斷,我們在編程中使用到的API最終都會通過系統調用這種內部中斷來進行ring3到ring0的切換 2.2) 單步異常(調試異常): 1: #DB 用於單步執行、內存斷點 2.3) INT3(斷點異常): 3: #BP 2.4) 溢出(指令INT0): 4: #OF 3) 終止(aborts) 3.1) 雙重錯誤(所有能產生異常、NMI、或者INTR的指令): 8: #DF /* 關於中斷號(向量號)、中斷向量表、中斷描述符表的區別 1. 中斷號(向量號) 中斷號(向量號)是用來在中斷描述符表中定位中斷描述符的 2. 中斷描述符表 保存中斷描述符的一段連續內存(可以理解為一張表)。中斷描述符是用來獲取中斷向量用的(知道了中斷向量就知道中斷服務程序的入口地址) 3. 中斷向量表 保存中斷向量的一段連續內存(可以理解為一張表)。中斷向量代表着中斷服務程序的入口地址 例如: INT 21H 21就是中斷號 21H就就是一個中斷描述符 21H*4 =84H 得到的就是中斷向量 以84H為首地址(84H 85H 86H 87H)其中存放的就是中斷服務程序的地址
注意,上面說的是在DOS系統中采用的中斷例程尋址方法,隨着計算機體系結構的發展,目前操作系統已經不采用這種尋址方法了,而是采用IDT機制進行中斷例程的尋址。關於IDT,我們下面會講到 */
3. 中斷的初始化
我們知道,中斷向量號是8位的,那么它一共有256項(0-255),也就意味着中斷描述符表也是256項(其中記錄着中斷向量表的表項索引),同時中斷向量表也是256項(其中記錄着中斷處理例程的入口地址)
對於不同的中斷,在中斷初始化和中斷處理過程中,其處理方式是不一樣的
1. 內部中斷(0~31號、0x80作為中斷號) 只要初始化: 1) 相關的中斷向量表 2. 外部中斷(0~255中的除了0~31號、0x80的其他中斷號) 需要初始化: 1) 相關的中斷向量表 2) 以及中斷控制器(控制器負責優先級排隊、屏蔽等工作)
0x1: 內部中斷初始化
內部中斷的初始化需要對0~31號和0x80號系統保留中斷向量的初始化,這部分草走在trap_init()中完成
\linux-3.15.5\arch\x86\kernel\traps
void __init trap_init(void) { int i; #ifdef CONFIG_EISA void __iomem *p = early_ioremap(0x0FFFD9, 4); if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24)) EISA_bus = 1; early_iounmap(p, 4); #endif /* trap_init()主要是調用set_xxx_gate(中斷向量, 中斷處理函數) set_xxx_gate()就是按照中斷門的格式填寫中斷向量表的 Intel x86支持4種"門描述符": 1) 調用門(call gate) 2) 陷阱門(trap gate) 3) 中斷門(iinterrupt gate) 4) 任務門(task gate) */ set_intr_gate(X86_TRAP_DE, divide_error); set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK); /* int4 can be called from all */ set_system_intr_gate(X86_TRAP_OF, &overflow); set_intr_gate(X86_TRAP_BR, bounds); set_intr_gate(X86_TRAP_UD, invalid_op); set_intr_gate(X86_TRAP_NM, device_not_available); #ifdef CONFIG_X86_32 set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS); #else set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK); #endif set_intr_gate(X86_TRAP_OLD_MF, coprocessor_segment_overrun); set_intr_gate(X86_TRAP_TS, invalid_TSS); set_intr_gate(X86_TRAP_NP, segment_not_present); set_intr_gate_ist(X86_TRAP_SS, &stack_segment, STACKFAULT_STACK); set_intr_gate(X86_TRAP_GP, general_protection); set_intr_gate(X86_TRAP_SPURIOUS, spurious_interrupt_bug); set_intr_gate(X86_TRAP_MF, coprocessor_error); set_intr_gate(X86_TRAP_AC, alignment_check); #ifdef CONFIG_X86_MCE set_intr_gate_ist(X86_TRAP_MC, &machine_check, MCE_STACK); #endif set_intr_gate(X86_TRAP_XF, simd_coprocessor_error); /* Reserve all the builtin and the syscall vector: */ for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++) set_bit(i, used_vectors); #ifdef CONFIG_IA32_EMULATION set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall); set_bit(IA32_SYSCALL_VECTOR, used_vectors); #endif //設置系統調用中斷 #ifdef CONFIG_X86_32 set_system_trap_gate(SYSCALL_VECTOR, &system_call); set_bit(SYSCALL_VECTOR, used_vectors); #endif /* * Set the IDT descriptor to a fixed read-only location, so that the * "sidt" instruction will not leak the location of the kernel, and * to defend the IDT against arbitrary memory write vulnerabilities. * It will be reloaded in cpu_init() */ __set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO); idt_descr.address = fix_to_virt(FIX_RO_IDT); /* * Should be a barrier for any external CPU state: */ cpu_init(); x86_init.irqs.trap_init(); #ifdef CONFIG_X86_64 memcpy(&debug_idt_table, &idt_table, IDT_ENTRIES * 16); set_nmi_gate(X86_TRAP_DB, &debug); set_nmi_gate(X86_TRAP_BP, &int3); #endif }
0x2: 外部中斷初始化
外部中斷的初始化需要:
1. 對除了0~31、0x80中斷號之外的其它中斷向量 2. 中斷控制器的初始化(相比內部中斷初始化多了這一步)
這兩步操作都在在init_IRQ()中完成
\linux-3.15.5\arch\x86\kernel\i8259.c
void __init init_IRQ(void) { int i; /* * We probably need a better place for this, but it works for * now ... */ x86_add_irq_domains(); /* * On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15. * If these IRQ's are handled by legacy interrupt-controllers like PIC, * then this configuration will likely be static after the boot. If * these IRQ's are handled by more mordern controllers like IO-APIC, * then this vector space can be freed and re-used dynamically as the * irq's migrate etc. */ for (i = 0; i < legacy_pic->nr_legacy_irqs; i++) //對於單CPU結構, per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i; //x86_init.irqs.intr_init()等價於調用:native_init_IRQ() x86_init.irqs.intr_init(); } void __init native_init_IRQ(void) { int i; /* Execute any quirks before the call gates are initialised: */ x86_init.irqs.pre_vector_init(); //調用 init_ISA_irqs apic_intr_init(); /* * Cover the whole vector space, no vector can escape * us. (some of these will be overridden and become * 'special' SMP interrupts) */ /* interrupt數組,它保存的是每個中斷服務程序的入口地址,它的定義是在\linux-3.15.5\arch\x86\kernel\entry_32.S中 */ for (i = FIRST_EXTERNAL_VECTOR; i < NR_VECTORS; i++) { //設置32~255號中斷 /* IA32_SYSCALL_VECTOR could be used in trap_init already. */ if (!test_bit(i, used_vectors)) { //要除去0x80中斷 set_intr_gate(i, interrupt[i-FIRST_EXTERNAL_VECTOR]); } } if (!acpi_ioapic && !of_ioapic) setup_irq(2, &irq2); #ifdef CONFIG_X86_32 /* * External FPU? Set up irq13 if so, for * original braindamaged IBM FERR coupling. */ if (boot_cpu_data.hard_math && !cpu_has_fpu) setup_irq(FPU_IRQ, &fpu_irq); irq_ctx_init(smp_processor_id()); #endif }
4. 門描述符
在開始學習IDT之前,我們必須先了解一下什么是門描述符。
在I386CPU中,除了"段描述符"(描述某種內存段)之外還有一種描述符叫做"門描述符"(描述控制轉移的入口點,也就是異常控制中斷的入口點),通過這種門可以實現特權級的轉變和任務的切換。門描述符主要由以下幾部分組成:
1. 選擇子 2. 偏移地址 3. DPL
門描述符共有四種
1. 調用門描述符 調用門一般用在特權級的切換,存在於GDT中或者LDT中。調用門的選擇子指向代碼段描述符,偏移地址對應代碼段中的偏移量。當jump和call指令的操作數是調用門的時候,就會跳轉到對應的代碼處,並發生特權級的變化,也就會發生
堆棧的切換 2. 任務門描述符 任務門一般用在任務的切換,可以存放在GDT、LDT或IDT中。任務門的選擇子指向GDT中的TSS選擇符,偏移地址沒有意義。當jmp和Call指令的操作數是任務門的時候,就會發生任務的切換。 3. 中斷門描述符 4. 陷阱門描述符 中斷門描述符、陷阱門描述符用來對中斷服務例程進行尋址,從原理上來理解,中斷例程的尋址本質上也是內存的尋址
看到這里,我們必須靈活、嚴謹地理解操作系統中的關於內存尋址的概念
1. 操作系統一定是使用基於"段選擇子"+"GDT"+"LDT"進行內存尋址 2. 中斷門、陷阱門只是一種數據結構,它包括了"段選擇子"這個字段,同時還有別的字段用於中斷例程尋址的目的,中斷的尋址同樣也是段+段內偏移方式尋址 3. 在windows、linux操作系統中,這種數據結構的疊加、包含很常見,但我們始終要明確操作系統最底層的運行機制 /* 關於"段選擇子"+"GDT"+"LDT"進行內存尋址的相關細節,請參閱另一篇文章 http://www.cnblogs.com/LittleHann/p/3453148.html (搜索"25. PVOID LdtInformation",從那開始就是了) */
我們這里學習一下中斷門、陷阱門描述符的數據結構
/* 1. offset_low: 32位偏移的低16位 2. selector: 選擇子 1) 段索引 2) 指示位 2.1) 0表示在GDT中選擇 2.2) 1表示在LDT中選擇 3) 當前請求特權級(想要訪問什么級別的段) 3. reserved: 保留字段 4. type: 段類型 5. always: 總為1 6. DPL: (描述符特權級 Descriptor Privilege Level) 表示允許訪問此段的最低特權級("段選擇子"中有一個字段(RPL)是標識這個段選擇子也即這個內存訪問請求的特權級),這樣是不是就把對應關系建立起來了,比如DPL為0的段只有當RPL=0時才能訪問,而DPL為3的段,可由任何RPL的
代碼訪問。這樣就解釋了為什么ring3的內存空間ring0的內核代碼可以任意訪問,而ring0的內存空間ring3不能訪問了 7. present 8. offset_high: 2位偏移的高16位 */ typedef struct IDT_GATE_DESCRIPTOR { P2C_U16 offset_low; P2C_U16 selector; P2C_U8 reserved; P2C_U8 type:4; P2C_U8 always:1; P2C_U8 dpl:2; P2C_U8 present:1; P2C_U16 offset_high; } IDT_GATE_DESCRIPTOR, *IDT_GATE_DESCRIPTOR
5. IDT中斷描述符表
linux-2.6.32.63\arch\x86\kernel\traps.c
gate_desc idt_table[NR_VECTORS] __page_aligned_data = { { { { 0, 0 } } }, }; struct desc_struct { unsigned long a,b; }; /* 可以看出 IDT表共256個表項,每一個表項是8個字節的gate_desc結構(這個gate_desc就叫門描述符),在idt_table數組被定義時靜態初始化為0。 */
IDT表可以駐留在線性地址空間的任何地方,處理器使用IDTR寄存器來定位IDT表的位置(LIDT和SIDT指令分別用於加載和保存IDTR寄存器的內容)。這個寄存器中含有IDT表32位的基地址和16位的長度(限長)值,與GDT和LDT表類似,IDT也是由8字節長描述符組成的一個數組(聯想GDT、LDT的結構)
總結一下整個中斷例程的尋址過程
1. 根據IDTR獲取IDT基址(LIDT指令)-> 2. 根據CPU獲取的中斷號得到當前中斷對應於IDT中的某個表項(某個門描述符)-> 3. 解析指定的中斷門、陷阱門(就是上一步獲取的IDT表項)-> 4. 從門描述符中獲得段選擇子等信息-> 5. 根據選擇子+GDT+LDT最終中斷例程虛擬內存尋址-> 6. 獲得指定中斷例程的虛擬內存地址-> 7. 執行中斷程序
6. 異常控制類型
我們已經學習了中斷有內部和外部中斷,外部中斷來自於硬件外設,內部中斷又可以分為異常(faults)、陷阱(traps)、終止(aborts)。我們需要牢記的是,操作系統是不能執行命令的,整個機器中可以執行命令的只有是CPU,CPU通過硬件的方式提供中斷機制,中斷是所有異常處理的根本技術。不管是進程切換、硬件外設、除零異常、虛擬內存中的發生的缺頁異常處理、SEH,歸根結底,全部都要通過CPU的中斷機制來實現。
異常控制流處理就是我們在中斷的基礎上,抽象出的一個理論性概念
異常(exception)就是控制流中的突變,用來響應處理器狀態中的某些變化。在處理器中,狀態被編碼為不同的位和信號。狀態變化稱為事件(event)。
我們在學習異常處理流程的時候,要注意不要把異常和中斷割裂成2個獨立的概念去學習,相反,異常和中斷說的都是一回事。只是一個從操作系統概念層面去闡述,一個從硬件技術原理角度去闡述
在任何情況下,當處理器檢測到有事件(內部異常、外部異常)發生時,它就會通過一張叫做異常表(exception table)的跳轉表(中斷描述符表),進行一個間接過程調用(異常處理例程的調用)(通過中斷向量表),到一個專門用來處理
這類事件的操作系統子程序(異常處理程序 exception handler)。進行相應的異常處理
0x1: 異常的分類
異常可以分為四類:
1. 中斷(interrupt) 來自I/O設備的信號,異步,總是返回到下一條指令 2. 陷阱(trap) 有意的異常,是執行一條指令的結果。陷阱最重要的用途是在用戶程序和內核之間提供一個像過程一樣的接口,叫系統調用 同步, 總是返回到下一條指令 3. 故障(fault) 潛在可恢復的錯誤。如果處理程序能夠修正這個錯誤情況,它就將控制返回到引起故障的指令,從而重新執行它。斗則,處理程序返回到內核中的abort例程,abort例程會終止引起故障的應用程序。 同步,可能返回到當前指令 4. 終止(abort) 不可恢復的錯誤,同步,不會返回
Copyright (c) 2014 LittleHann All rights reserved