系統故障解析:Windows異常處理流程(轉)

PigBaby2007發表於2007-08-08

  先來說說異常和中斷的區別。中斷可在任何時候發生,與CPU正在執行什麼指令無關,中斷主要由I/O裝置、處理器時鐘或定時器等硬體引發,可以被允許或取消。而異常是由於CPU執行了某些指令引起的,可以包括儲存器存取違規、除0或者特定除錯指令等,核心也將系統服務視為異常。中斷和異常更底層的區別是當廣義上的中斷(包括異常和硬體中斷)發生時如果沒有設定在服務暫存器(用命令號0xb向8259-1中斷控制器0x20埠讀出在服務暫存器1,用0xb向8259-2中斷控制器的0xa0埠讀出在服務暫存器2)相關的在服務位(每個在服務暫存器有8位,共對應IRQ 0-15)則為CPU的異常,否則為硬體中斷。[@more@]

  

  下面是WINDOWS2000根據INTEL x86處理器的定義,將IDT中的前幾項註冊為對應的異常處理程式(不同的作業系統對此的實現標準是不一樣的,這裡給出的和其它一些資料不一樣是因為這是windows的具體實現):

  

  中斷號  名字    原因

  0x0  除法錯誤    1、DIV和IDIV指令除0

  2、除法結果溢位

  0x1  除錯陷阱    1、EFLAG的TF位置位

  2、執行到除錯暫存器(DR0-DR4)設定的斷點

  3、執行INT 1指令

  0x2  NMI中斷    將CPU的NMI輸入引腳置位(該異常為硬體發生非遮蔽中斷而保留)

  0x3  斷點    執行INT 3指令

  0x4  整數溢位    執行INTO指令且OF位置位

  0x5  BOUND邊界檢查錯誤  BOUND指令比較的值在給定範圍外

  0x6  無效操作碼  指令無法識別

  0x7  協處理器不可用  1、CR0的EM位置位時執行任何協處理器指令

  2、協處理器工作時執行了環境切換

  0x8  雙重異常    處理異常時發生另一個異常

  0x9  協處理器段超限  浮點指令引用記憶體超過段尾

  0xA  無效任務段  任務段包含的描述符無效(windows不

  使用TSS進行環境切換,所以發生該異常說明有其它問題)

  0xB  段不存在    被引用的段被換出記憶體

  0xC  堆疊錯誤    1、被引用記憶體超出堆疊段限制

  2、載入入SS暫存器的描述符的present位置0

  0xD  一般保護性錯誤  所有其它異常處理例程無法處理的異常

  0xE  頁面錯誤    1、訪問的地址未被換入記憶體

  2、訪問操作違反頁保護規則

  0x10  協處理器出錯  CR0的EM位置位時執行WAIT或ESCape指令

  0x11  對齊檢查錯誤  對齊檢查開啟時(EFLAG對齊位置位)訪問未對齊資料

  

  其它異常還包括獲取系統啟動時間服務int 0x2a、使用者回撥int 0x2b、系統服務int 0x2e、除錯服務int 0x2d等系統用來實現自己功能的部分,都是透過異常的機制,觸發方式就是執行相應的int指令。

  

  這裡給出幾個異常處理中重要的結構:

  

  陷阱幀TrapFrame結構(後面提到的異常幀ExceptionFrame結構其實也是一個KTRAP_FRAME結構):

  

  typedef struct _KTRAP_FRAME {

  ULONG  DbgEbp;

  ULONG  DbgEip;

  ULONG  DbgArgMark;

  ULONG  DbgArgPointer;

  ULONG  TempSegCs;

  ULONG  TempEsp;

  ULONG  Dr0;

  ULONG  Dr1;

  ULONG  Dr2;

  ULONG  Dr3;

  ULONG  Dr6;

  ULONG  Dr7;

  ULONG  SegGs;

  ULONG  SegEs;

  ULONG  SegDs;

  ULONG  Edx;

  ULONG  Ecx;

  ULONG  Eax;

  ULONG  PreviousPreviousMode;

  PEXCEPTION_REGISTRATION_RECORD ExceptionList;

  ULONG  SegFs;

  ULONG  Edi;

  ULONG  Esi;

  ULONG  Ebx;

  ULONG  Ebp;

  ULONG  ErrCode;

  ULONG  Eip;

  ULONG  SegCs;

  ULONG  EFlags;

  ULONG  HardwareEsp;

  ULONG  HardwareSegSs;

  ULONG  V86Es;

  ULONG  V86Ds;

  ULONG  V86Fs;

  ULONG  V86Gs;

  } KTRAP_FRAME;

  

  環境Context結構:

  

  typedef struct _CONTEXT {

  ULONG ContextFlags;

  ULONG  Dr0;

  ULONG  Dr1;

  ULONG  Dr2;

  ULONG  Dr3;

  ULONG  Dr6;

  ULONG  Dr7;

  FLOATING_SAVE_AREA FloatSave;

  ULONG  SegGs;

  ULONG  SegFs;

  ULONG  SegEs;

  ULONG  SegDs;

  ULONG  Edi;

  ULONG  Esi;

  ULONG  Ebx;

  ULONG  Edx;

  ULONG  Ecx;

  ULONG  Eax;

  ULONG  Ebp;

  ULONG  Eip;

  ULONG  SegCs;

  ULONG  EFlags;

  ULONG  Esp;

  ULONG  SegSs;

  UCHAR  ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

  } CONTEXT;

  

  異常記錄ExceptionRecord結構:

  

  typedef struct _EXCEPTION_RECORD {

  NTSTATUS ExceptionCode;

  ULONG ExceptionFlags;

  struct _EXCEPTION_RECORD *ExceptionRecord;

  PVOID ExceptionAddress;

  ULONG NumberParameters;

  ULONG_PTR ExceptionInformatio[EXCEPTION_MAXIMUM_PARAMETERS];

  } EXCEPTION_RECORD;

  

  當發生異常後,CPU記錄當前各暫存器狀態並在核心堆疊中建立陷阱幀TrapFrame,然後將控制交給對應異常的陷阱處理程式。當陷阱處理程式能處理異常時,比如缺頁時透過調頁程式MmAccessFault將頁換入實體記憶體後透過iret返回發生異常的地方。但大多數無法處理異常,這時先是呼叫CommonDispatchException在核心堆疊中建立異常記錄ExceptionRecord和異常幀ExceptionFrame。ExceptionRecord很重要,它記錄了異常程式碼、異常地址以及一些其它附加的引數。然後呼叫KiDispatchException進行異常的分派。這個函式是WINDOWS下異常處理的核心函式,負責異常的分派處理。

  

  KiDispatchException的處理流程(每當異常被某個例程處理時處理的例程將返回TRUE到上一個例程,未處理則返回FALSE。當任何一個例程處理了異常返回TRUE時,則KiDispatchException正常返回):

  

  在進行使用者態核心態的異常的分派前,先判斷異常是否來自使用者模式,是的話將Context.ContextFlags(這時候Context結構還剛初始化完,還未賦初值) or上CONEXT_FLOATING_POINT,意味著對來自使用者模式的異常總是嘗試分派浮點狀態,這樣可以允許異常處理程式或偵錯程式檢查和修改協處理器的狀態。然後從陷阱幀中取出暫存器值填入Context結構,並判斷是否是斷點異常(int 0x3和int 0x2d),如果是的話先將Context.Eip減一使它指向int 0x3指令(無論是由int 0x3還是由int 0x2d引起的異常,因為前面的陷阱處理程式裡已經改變過TrapFrame裡面的Eip了)。然後判斷異常是發生於核心模式還是使用者模式,根據不同模式而採取不同處理過程。

  

  如果異常發生於核心模式,會給予核心偵錯程式第一次機會和第二次機會處理異常。當異常被處理後就將設定好陷阱幀並返回到陷阱處理程式,在那裡iret返回發生異常的地方繼續執行。

  

  核心模式異常處理流程為:

  

  (第一次機會)判斷KiDebugRoutine是否為空,不為空就將Context、陷阱幀、異常記錄、異常幀、發生異常的模式等壓入棧並將控制交給KiDebugRoutine。

  

  若KiDebugRoutine為空(正常的系統這裡不為空。正常啟動的系統KiDebugRoutine為KdpStub,在Boot.ini里加上/DEBUG啟動的系統的KiDebugRoutine為KdpTrap。如果這裡為空的話會因為處理不了DbgPrint這類int 0x2d產生的異常而導致系統崩潰)或者KiDebugRoutine未處理異常,則將Context結構和異常記錄ExceptionRecord壓棧並呼叫核心模式的RtlDispatchException在核心堆疊中查詢基於幀的異常處理例程。

  

  RtlDispatchException呼叫RtlpGetRegistrationHead從fs:[0](0xffdff000)處獲取當前執行緒異常處理連結串列指標,並呼叫RtlpGetStackLimits從0xffdff004和0xffdff008取出當前執行緒堆疊底和頂。然後開始由異常處理連結串列指標遍歷連結串列查詢異常處理例程(若在XP和2003下先處理VEH再處理SEH),其實這就是SEH,只是和使用者態有一點不同是既沒有頂層異常處理例程(TOP LEVEL SEH)也沒有預設異常處理例程。然後對每個當前異常處理連結串列指標檢查判斷堆疊是否有效(是否超出了堆疊範圍或者未對齊)及堆疊是否是DPC堆疊。若0xffdff80c處DpcRoutineActive為TRUE且堆疊頂和底在0xffdff81c處取出的DpcStack到DpcStack-0x3000(一個核心堆疊大小),若是則更新堆疊頂和底為DpcStack和DpcStack-0x3000並繼續處理,否則將異常記錄結構裡的異常標誌ExceptionRecord.ExceptionFlags設定EXCEPTION_STACK_INVALID表示為無效堆疊並返回FALSE。

  

  呼叫異常處理連結串列上的異常處理例程之前會在異常處理例程連結串列上插入一個新的節點,對應的異常處理例程是用來處理巢狀異常,也就是在處理異常時發生另一個異常。處理後

  RtlDispatchException判斷異常處理例程的返回值:

  若為ExceptionContinueExecution,若異常標誌ExceptionRecord.ExceptionFlags未設定EXCEPTION_NONCONTINUABLE不可恢復執行,則返回TRUE到上一層,否則在做了一些工作後呼叫RtlRaiseException進入到KiDispatchException的第二次機會處理部分。

  若為ExceptionContinueSearch,則繼續查詢異常處理例程。

  若為ExceptionNestedException,巢狀異常。保留當前異常處理連結串列指標為內層異常處理連結串列並繼續查詢異常處理例程。當發現當前異常處理連結串列地址大於保留的內層異常處理連結串列時,表示當前的異常處理連結串列比保留的更內層(因為堆疊是由高向低擴充套件的,地址越高則入棧越早,表示更內層),則將其值賦予內層異常處理連結串列指標,除了第一次賦初值外發生修改保留的內層

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10144097/viewspace-934656/,如需轉載,請註明出處,否則將追究法律責任。

相關文章