目錄
一、中斷與異常處理
1.1 中斷與異常
1.2 IDT
1.3 異常的概念
1.4 異常分類
二、windows異常處理方式
2.1 概述
2.2 結構化異常處理
2.3 向量化異常處理之VEH
2.4 向量化異常處理之VCH
2.5 默認的異常處理函數
2.6 如何手動安裝 SEH 節點
2.7?異常處理的優先級
一、中斷與異常處理
1.1 中斷與異常
????????計算機其實就是執行指令的機器,只要給它設定一個起始位置,它就會一條接一條地按順序執行指令。但這種只知道按順序執行的方式會有問題,就像人在路上走路不躲開來往的車,或者心臟病發作了還非要把飯吃完一樣,可能會讓自己出問題。
????????為了解決這個問題,就有了中斷和異常這兩種機制。有了它們,計算機變得更靈活。中斷和異常能讓處理器去執行正常控制流程之外的代碼。
????????中斷一般是由外部硬件引發的,它的作用是告訴操作系統有一些操作發生了。比如鍵盤、鼠標、時鐘這些設備,它們引發的中斷屬于異步事件。而異常通常是程序在執行的時候出了問題,需要馬上處理,它和處理器當前正在執行的任務有關,是同步事件。
????????CPU為了能高效地和外部硬件互動,采用了中斷機制。就拿鼠標來說,它是接在CPU外面的硬件。要是CPU一直不停地去監測鼠標有沒有移動、有沒有點擊,會占用大量的CPU資源。但如果鼠標在有移動、點擊等動作時主動通知CPU,情況就好多了。這樣一來,CPU就不用一直盯著每個外部硬件,等外部硬件需要CPU響應時,硬件自己會去通知CPU。在主板上,CPU有一根引腳和外部硬件相連,通過這根引腳,它們就能傳遞信息,這為中斷機制提供了硬件上的支持。
????????實際上,外部中斷是由PIC(可編程中斷控制器)控制的。
????????當外部硬件產生中斷,并且CPU能處理這個中斷(也就是IF標志位沒有被設置成屏蔽所有中斷),CPU會從PIC那里讀取兩個字節的數據,一般是“0xcd 0x??”。這兩個字節的數據會被CPU當成指令來執行,其中0xCD是INT XX中斷指令的操作碼,另一個字節就是中斷編號。INT XX中斷指令為CPU響應外部硬件提供了軟件方面的支持。
????????中斷不只是來自外部硬件,CPU內部也可能產生中斷。比如CPU執行除以0的操作時,會主動產生一個中斷,讓處理除數為0的代碼開始運行,防止CPU死機。因為除數是0時,CPU算不出結果,卻會一直不停地算,導致下一條指令沒辦法執行,所以CPU得有個功能能強制結束當前這條無效的指令。
????????為了能更清楚地區分這兩種不同來源的中斷,一般把外部硬件產生的中斷就叫做“中斷”,把CPU內部產生的中斷叫做“異常”。不管是外部中斷還是內部異常,都會有一個中斷號,這個中斷號就是IDT表(中斷描述符表)的索引號,通過它能找到處理中斷的函數的地址。
1.2 中斷描述符表(IDT)
????????系統會統一管理中斷和異常,針對每一種中斷或者異常,系統都配備了一個處理函數,這個函數就是陷阱處理器。當中斷或者異常出現時,原本正在執行的指令就會暫停,轉而讓特定的陷阱處理器來處理中斷或異常。
????????在保護模式的環境下,如果產生了中斷或者異常,CPU會借助中斷描述符表(IDT)來查找對應的處理函數。所以,IDT表就像是CPU(硬件)和操作系統(軟件)之間交接中斷和異常的重要“關卡”。操作系統在剛開始啟動的時候,有一項重要的工作,就是設置好IDT表,并且準備好處理各種異常和中斷的函數。
????????IDT是存放在物理內存里的一個線性表,一共有256項。在64位模式下,IDT表的每個元素長度是16個字節,整個表的長度是4096字節(也就是4Kb);在32位模式下,每個元素長度是8個字節,IDT表總長度為2048字節(即2Kb)。IDT表是在操作系統啟動的初期就被初始化的,所以IDT表以及里面的中斷處理函數都是由操作系統提供的。
(1)查看IDT
1. 先用Windbg和VirtualKD把雙機調試環境配置好,然后把符號加載完成。
2. 輸入命令“!idt/a” 。
這里不同索引區間有不同作用:
??? - 0x00到0x14這個區間,用來存放各種異常對應的陷阱處理器。
??? - 0x14到0x20這個區間,是保留著的,暫時沒其他用途。
??? - 0x20到0x29這個區間,目前是空著的,系統和程序員可以自行分配和注冊使用。
??? - 0x2A到0x2E這個區間,是Windows系統自己使用的5個中斷號。
??? - 0x30到0xFF這個區間,是給硬件中斷用的。
(2)使用PC-Hunter查看IDT
????????要查看IDT表及對應陷阱處理器的內容,可以打開PC - Hunter軟件,找到“內核鉤子”選項并點擊,然后在展開的子項中進行相關操作,即可查看IDT表中的內容以及與之對應的陷阱處理器中的內容。
1.3 異常的概念
????????異常其實就是CPU內部主動搞出來的中斷。那CPU到底在啥情況下會產生中斷呢?為啥又要產生中斷呢?要弄清楚這些問題,就得了解一些底層的運作機制。
(1)CPU為啥要主動產生異常
????????咱們用高級語言寫好的程序,會被編譯成可執行程序。這個可執行程序里存的就是機器碼,也就是機器指令(Machine Instruction)。很多人以為到機器碼這就算到底層了,其實不是。當機器碼被讀取到CPU內部后,指令還會被進一步細分成一個個獨立的小段,這些小段就叫微指令。
????????微指令是這么回事:把一條指令拆成好多條微指令,按順序執行這些微指令,就能實現這條指令原本的功能。好多條微指令組合起來就成了一個微程序,而一個微程序對應著一條機器指令。說白了,一條機器指令的功能得靠若干條微指令組成的序列來實現,一條機器指令要完成的操作也是被拆成若干條微指令去完成,并且由微指令來解釋和執行。
????????要是一條指令里的某條微指令出問題了,那這條指令就執行不下去了,后面的指令也就沒法接著執行,CPU就卡在那了。比如說,我們申請了一塊內存,準備往里面寫數據。要是申請內存這一步失敗了,那就不該再往內存里寫數據了,而應該跳到其他代碼接著執行。在CPU里,就是靠內部中斷把代碼的執行流程轉移到別的地方。
(2)CPU在哪些情況下會產生異常
????????最常見的情況就是除0異常,因為除數是0的時候,CPU根本沒法計算。還有內存缺頁異常。在保護模式下,CPU開啟了分頁機制,程序用的都是線性地址,但CPU實際操作得用物理地址。所以,當執行像mov eax, [401000]這種帶有線性地址的指令時,得先把線性地址轉換成物理地址。Windows系統還有個換頁機制,當物理內存不夠用了,就允許把部分物理內存里的分頁數據交換到磁盤文件里存著。要是CPU去訪問一個線性地址,結果發現這個地址的數據不在物理內存里,那就會產生缺頁異常。缺頁異常特別常見,在Windows系統上,一秒鐘可能就產生好幾百次。產生缺頁異常,主要是為了讓操作系統有機會把之前交換到硬盤的物理內存數據再換回到物理內存里。
(3)CPU產生異常后是怎么處理的
????????不同類型的異常,處理方式也不一樣。就拿缺頁異常來說,異常一觸發,IDT表里對應的函數就會被調用,然后操作系統把對應的物理內存分頁數據從硬盤換回到物理內存。等換完了,操作系統會用iret指令回到觸發異常的那條指令,這時候再執行這條指令,就不會再出現缺頁異常了,程序就能正常接著跑了。
????????對于其他異常,Windows會調用一個函數來好好處理。這個處理過程包括給異常找到合適的異常處理函數,比如把異常交給調試器處理,或者交給程序自己的結構化異常處理機制(VEH、SEH)來處理。這個過程就叫做異常的分發,調用的這個函數就叫異常分發函數。在這個過程中,有一些細節挺值得琢磨的,比如異常處理函數是怎么知道到底是哪個地址發生了缺頁異常的。
1.4 異常分類
按照CPU報告異常的方式,以及導致異常的指令能不能安全地重新執行,異常能分成三類,分別是錯誤、陷阱、終止。
(1)錯誤
????????錯誤類異常一般是能被糾正的。當出現錯誤類異常時,會先把當前線程的環境保存下來,然后從IDT里找到專門處理這個錯誤的陷阱處理器。等處理好了,再恢復線程環境,接著回到產生錯誤的地方繼續執行。常見的錯誤類異常就是內存頁錯誤。要注意,線程環境里保存的EIP是導致異常的那條指令的地址,不是下一條指令的地址。
(2)陷阱
????????和錯誤類異常不一樣,陷阱類異常產生的時候,出錯的指令已經執行完了。所以線程環境里保存的是產生異常的指令的下一條指令的地址。一般來說,陷阱類異常也能恢復執行。常見的陷阱類異常有int 3異常,調試器實現軟件斷點就靠它。
(3)中止
????????要是產生了中止類異常,那就說明出了非常嚴重的錯誤,程序通常沒辦法恢復執行了。比如說程序一開始就有問題,但運行了一段時間才表現出來,或者正在處理一個異常的時候,又出現了新的異常。
(4)異常分類表
二、windows異常處理方式
2.1 概述
????????異常機制的作用就是讓計算機能把程序運行時出現的錯誤處理得更好。從編程的角度來講,它能把錯誤處理和程序原本的邏輯分開,這樣開發者就能專心去開發程序的關鍵功能,還能統一管理程序可能出現的各種異常情況。Windows系統提供了好幾種異常處理機制,主要有下面這幾種:
- SEH - 結構化異常處理
- VEH - 向量化異常處理
- VCH - 向量化異常處理
2.2 結構化異常處理
????????Structed Exception Handler(結構化異常處理),大家一般簡稱它為SEH,這是微軟給出的一種處理異常的辦法。在VC++編程環境里,程序員可以借助try、finally、except、leave這四個微軟提供的關鍵字,有效地運用這個機制。下面簡單講講它的用法。
(1)try與finally
????????try和finally是組合起來用的,代碼大概像下面這樣:
try {//這里面是被保護的代碼塊
}
finally {//這里是終結處理塊
}
????????try里面的代碼就是被保護的部分,finally里面的是終結處理器。不管try里的代碼是怎么離開這個被保護區域的,最后都會去執行終結處理器里的內容。離開被保護代碼塊的情況有兩種:
1. 正常情況:代碼順順利利地執行完了,或者執行了_leave語句。
2. 非正常情況:代碼運行過程中產生了異常,又或者因為遇到 return、goto、break、continue 這些控制程序流程的語句,從而離開了保護代碼塊。在終結處理塊里,可以用int cdecl AbnormalTermination(void);
這個函數,來判斷代碼是正常還是非正常離開的。根據判斷結果,開發者就能決定后續操作,比如繼續運行程序、重啟程序、釋放資源,或者嘗試解決錯誤。需要注意的是,在被保護的代碼區域里,最好用_leave 語句,而不是 return 語句。
(2)try與except
代碼結構如下:
try {//被保護的代碼塊在這兒
}
except(/*過濾表達式*/) {//這里是異常處理塊
}
????????這里面重要的是過濾表達式的取值,有3種可能:
????????- EXCEPTION_EXECUTE_HANDLER(1):表示執行緊跟在_except后面的異常處理塊(可以理解成“我能處理,交給我”)。
????????- EXCEPTION_CONTINUE_SEARCH(0):意思是去找下一個能處理異常的地方(也就是“我處理不了,找別人幫忙”)。
????????- EXCEPTION_CONTINUE_EXECUTION(-1):就是接著執行產生異常的那條指令(類似于“不相信會出錯,再執行一次看看”)。只要被保護的代碼塊產生了異常,程序就會執行except部分。過濾表達式的形式多種多樣,它可以是一個數值、一個函數調用,或者是一個運算符組成的表達式,只要這個表達式算出來的值是上面說的這三種情況之一就行。
????????在異常處理過程中,有兩個常用的函數。第一個是`unsigned long GetExceptionCode(void)`,這個函數返回的值代表了異常的類型。下面是相關的示例代碼:
int a = 0;
DWORD Filter(DWORD Code, _EXCEPTION_POINTERS* pexception_Point) {if (EXCEPTION_ACCESS_VIOLATION == Code) {//大家可以F12看一下都有哪些異常//return EXCEPTION_EXECUTE_HANDLER; //請執行異常處理塊吧pexception_Point->ContextRecord->Eax = (DWORD)&a;return EXCEPTION_CONTINUE_EXECUTION; //修復了異常,從產生異常處執行。}else if (EXCEPTION_BREAKPOINT == Code) {return EXCEPTION_EXECUTE_HANDLER; //請執行異常處理塊吧}else if (EXCEPTION_STACK_OVERFLOW == Code) {return EXCEPTION_CONTINUE_SEARCH; //修復不了,讓別人修復吧}
}
int _tmain(int argc, _TCHAR* argv[]) {try {_asm mov eax, 0;_asm mov [eax], 100;printf("安全渡過異常");}except(Filter(GetExceptionCode(), GetExceptionInformation())) {printf("進入了異常處理塊");}system("pause");return 0;
事例代碼二:結構化異常處理的嵌套
#include <windows.h>
static unsigned int nStep = 1;
void Fun_B() {int x, y = 0;__try {x = 5 / y; //引發異常}finally {printf("Step %d:執行Fun_B的finally塊的內容\n", nStep++);}
}
void Fun_A() {__try {Fun_B();}finally {printf("Step %d:執行Fun_A的finally塊的內容\n", nStep++);}
}
long MyExcepteFilter() {printf("Step %d:執行main的異常過濾器\n", nStep++);return EXCEPTION_EXECUTE_HANDLER;
}
int main() {__try {Fun_A();}__except(MyExcepteFilter()) {printf("Step %d:執行main的except塊的內容\n", nStep++);}system("pause");return 0;
}
2.3 向量化異常處理之VEH
????????結構化異常處理作用范圍相對有限,而向量化異常處理就厲害多了。它是全局性的,只要完成注冊,對整個進程都起作用,而且使用起來特別方便。要使用向量化異常處理,只需要提供一個回調函數,然后通過一個API完成注冊操作。從那之后,不管進程里出現什么樣的異常,都會去調用這個回調函數。常見的向量化異常處理形式有VEH和VCH這兩種。
????????說起VEH,我們首先得知道一個相關的API :
PVOID WINAPI AddVectoredExceptionHandler(_In_ULONG First, //調那順序_In_PVECTORED_EXCEPTION_HANDLER Handler //回調函數
);
參數說明:
- 參數1:異常處理函數被調用的順序。
- 參數2:異常處理回調函數。
回調函數定義如下:
typedef LONG(NTAPI *PVECTORED_EXCEPTION_HANDLER)(struct _EXCEPTION_POINTERS *ExceptionInfo
);
回調函數的返回值只有兩種情況:
- EXCEPTION_CONTINUE_EXECUTION(-1) :繼續執行。
- EXCEPTION_CONTINUE_SEARCH(0) :繼續搜索。
2.4 向量化異常處理之VCH
????????VCH和VEH的運作方式差不多,都得借助一個API以及一個回調函數來實現相應功能。只不過在具體使用時,VCH所用到的API和回調函數,其名字與VEH所對應的有所不同 。?
PVOID WINAPI AddVectoredContinueHandler(VLH_In_ULONG First, //調風順序_In_PVECTORED_EXCEPTION_HANDLER Handler);
typedef LONG(NTAPI *PVECTORED_EXCEPTION_struct _EXCEPTION_POINTERS *ExceptionInfo);
????????我們能夠通過編寫程序代碼,來探究VEH(向量化異常處理)、VCH(另一種向量化異常處理方式)以及SEH(結構化異常處理)這三者之間是如何相互作用和影響的 。在代碼中設置不同的異常場景,觀察這幾種異常處理機制在面對異常時,以怎樣的順序被調用、各自如何處理異常,以及它們之間的處理結果是怎樣相互影響的。
VEH及SEH:
#include "stdafx.h"
#include <windows.h>LONG WINAPI veh(EXCEPTION_POINTERS* pExce)
{printf("veh\n");// 繼續執行, 說明異常已被處理,產生異常的指令將會// 被繼續執行EXCEPTION_CONTINUE_EXECUTION;// 讓下一個veh節點處理異常.return EXCEPTION_CONTINUE_SEARCH;
}LONG WINAPI seh(EXCEPTION_POINTERS* pExce){printf("seh\n");// 讓下一個veh節點處理異常.return EXCEPTION_CONTINUE_SEARCH;
}int _tmain(int argc, _TCHAR* argv[])
{//1. 將異常處理函數注冊到系統AddVectoredExceptionHandler(TRUE, veh);__try{*(int*)0 = 0;}__except (seh(GetExceptionInformation())){}return 0;
}
VCH及SEH:
#include "stdafx.h"
#include <windows.h>// VCH回調函數
LONG WINAPI vch(EXCEPTION_POINTERS* pExce) {printf("vch\n");// 讓下一個異常處理節點處理異常return EXCEPTION_CONTINUE_SEARCH;
}// SEH異常處理函數
LONG WINAPI seh(EXCEPTION_POINTERS* pExce) {printf("seh\n");// 讓下一個異常處理節點處理異常return EXCEPTION_CONTINUE_SEARCH;
}int _tmain(int argc, _TCHAR* argv[]) {// 注冊VCH回調函數AddVectoredContinueHandler(TRUE, vch);__try {// 觸發一個除零異常int a = 1 / 0;}__except (seh(GetExceptionInformation())) {// SEH異常處理塊}return 0;
}
2.5 默認的異常處理函數
????????除了前面提到的那些異常處理辦法,還有一種方式。從本質上講,它屬于SEH的范疇。這種方式能夠添加一個默認的SEH異常處理程序。和其他一些異常處理機制類似,它也是由一個API函數以及一個回調函數構成 。
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(_In_opt_LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter );
回調函數原型:
typedef LONG(WINAPI *PTOP_LEVEL_EXCEPTION_FILTER)(_In_struct _EXCEPTION_POINTERS *ExceptionInfo );
????????要留意啦,這個回調函數不是隨便就會運行的。只有當SEH已經在起作用了,而且所有SEH相關的處理方法都沒辦法搞定異常的情況下,這個回調函數才會開始運行。
2.6 如何手動安裝 SEH 節點
手動安裝 SEH 節點的作用:
????????自定義異常處理邏輯:在 Windows 系統里,SEH 機制能夠讓程序在碰到異常時,調用特定的異常處理函數。通過手動安裝 SEH 節點,你能夠自定義異常處理邏輯,按照自身需求對異常進行處理,而不局限于系統默認的異常處理方式。
????????精準控制異常處理流程:手動安裝 SEH 節點可以讓你精確控制異常處理函數的調用順序與執行流程。你能夠在代碼里靈活地添加、移除或者調整 SEH 節點,從而實現對異常處理流程的精細控制。
????????增強程序的健壯性:在程序里手動安裝 SEH 節點,可以捕獲并處理各種異常情況,防止程序因未處理的異常而崩潰,進而增強程序的健壯性和穩定性。
代碼示例:
#include "stdafx.h"
#include <windows.h>EXCEPTION_DISPOSITION NTAPI seh(struct _EXCEPTION_RECORD *ExceptionRecord,PVOID EstablisherFrame,struct _CONTEXT *ContextRecord,PVOID DispatcherContext)
{printf("seh\n");// 繼續執行return ExceptionContinueExecution;
}int _tmain(int argc, _TCHAR* argv[])
{
// EXCEPTION_REGISTRATION_RECORD node;/** 產生異常后 , 操作系統使用fs段寄存器找到TEB, * 通過TEB.ExceptionList 找到SEH鏈表的頭節點, * 通過節點中記錄的異常處理函數的地址調用該函數.*/
// node.Handler = seh;
// node.Next = NULL;_asm{push seh; // 將SEH異常處理函數的地址入棧push fs:[0];//將SEH頭節點的地址入棧;// esp + 0 -- > [fs:0]; node.Next;;// esp + 4 -- > [seh]; node.handler;mov fs:[0], esp;// fs:[0] = &node;}*(int*)0 = 0;// 平衡棧空間// 還原FS:[0]原始的頭節點_asm{pop fs : [0]; // 將棧頂的數據(原異常頭節點的地址)恢復到FS:[0],然后再平衡4個字節的棧add esp, 4; // 平衡剩下的4字節的棧.}return 0;
}
????????該程序的主要目的是展示如何手動安裝和移除 SEH 節點,以及自定義異常處理邏輯。通過手動安裝 SEH 節點,程序能夠捕獲并處理異常,避免因未處理的異常而崩潰。
2.7?異常處理的優先級
????????下面演示 Windows 系統中不同異常處理機制(VEH、SEH、VCH、UEH)的優先級和調用順序。
#include "stdafx.h"
#include <windows.h>LONG WINAPI vch(EXCEPTION_POINTERS* pExcept){printf("vch\n");return EXCEPTION_CONTINUE_SEARCH;
}LONG WINAPI veh(EXCEPTION_POINTERS* pExcept){printf("veh\n");return EXCEPTION_CONTINUE_SEARCH;
}LONG WINAPI seh(EXCEPTION_POINTERS* pExcept){printf("seh\n");return EXCEPTION_CONTINUE_SEARCH;
}LONG WINAPI ueh(EXCEPTION_POINTERS* pExcept){printf("ueh\n");return EXCEPTION_CONTINUE_SEARCH;
}int _tmain(int argc, _TCHAR* argv[])
{AddVectoredContinueHandler(TRUE, vch);//vchAddVectoredExceptionHandler(TRUE, veh);//veh// 在64位系統下, 當程序被調試時,UEH不會被調用// 不被調試才會被調用.// 在32位系統下,被調試時也會被調用.SetUnhandledExceptionFilter(ueh);__try{*(int*)0 = 0;}__except (seh(GetExceptionInformation())){}return 0;
}
????????通過觸發一個訪問違規異常,來展示不同異常處理機制的調用順序。正常情況下,當異常發生時,系統會按照 VEH → SEH → (可能的 VCH)→ UEH
的順序依次調用異常處理函數。在這個程序中,每個異常處理函數都返回 EXCEPTION_CONTINUE_SEARCH
,表示無法處理該異常,從而讓系統繼續尋找下一個異常處理函數。通過輸出的信息,你可以觀察到不同異常處理函數的調用順序,進而了解它們之間的優先級關系。
通過上面的試驗代碼,總結出下面這些異常處理的規律:
(1)要是異常要交給用戶來處理,會按照VEH、SEH、VCH這個順序調用異常處理方式。
(2)如果VEH說它把異常處理好了,就不會把異常再傳給SEH,但還是會傳給VCH。
(3)如果VEH沒處理好異常,就會把異常傳給SEH。
(4)如果SEH里所有的異常處理函數都沒辦法處理異常,就會調用默認的SEH處理函數。
(5)如果SEH處理好了異常,從except那里開始接著執行,就不會再把異常傳給VCH。
(6)如果SEH要回到異常產生的地方接著執行,在回去之前會調用VCH。
注意事項:
(1)執行順序不可變:Windows嚴格保持VEH→SEH→VCH→UEH的調用順序
(2)調試器優先原則:當存在調試器時,所有異常會首先發送給調試器
(3)返回值影響:
??????? EXCEPTION_CONTINUE_EXECUTION:嘗試恢復執行
??????? EXCEPTION_CONTINUE_SEARCH:傳遞給下一處理程序
??????? EXCEPTION_EXECUTE_HANDLER:執行異常處理代碼
(4)64位系統差異:在x64架構下,SEH的實現機制與x86有所不同,但優先級順序保持不變
?