Windows異常處理流程

來源:互聯網
上載者:User
  先來說說異常和中斷的區別。中斷可在任何時候發生,與CPU正在執行什麼指令無關,中斷主要由I/O裝置、處理器時鐘或定時器等硬體引發,可以被允許或取消。而異常是由於CPU執行了某些指令引起的,可以包括儲存空間存取違規、除0或者特定調試指令等,核心也將系統服務視為異常。中斷和異常更底層的區別是當廣義上的中斷(包括異常和硬體中斷)發生時如果沒有設定在服務寄存器(用命令號0xb向8259-1中斷控制器0x20連接埠讀出在服務寄存器1,用0xb向8259-2中斷控制器的0xa0連接埠讀出在服務寄存器2)相關的在服務位(每個在服務寄存器有8位,共對應IRQ 0-15)則為CPU的異常,否則為硬體中斷。
    下面是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,嵌套異常。保留當前異常處理鏈表指標為內層異常處理鏈表並繼續尋找異常處理常式。當發現當前異常處理鏈表地址大於保留的內層異常處理鏈表時,表示當前的異常處理鏈表比保留的更內層(因為堆棧是由高向低擴充的,地址越高則入棧越早,表示更內層),則將其值賦予內層異常處理鏈表指標,除了第一次賦初值外發生修改保留的內層異常處理鏈表指標這種情況表示嵌套了不止一次的異常。當搜尋到新的異常處理鏈表指標和保留的內層異常處理鏈表指標相同,則清除ExceptionRecord.ExceptionFlags的嵌套異常位(&(~EXCEPTION_NESTED_CALL)),表示嵌套的異常已經處理完。
    其它的傳回值都視為無效,調用RtlRaiseException回到iDispatchException的第二次機會處理部分。
    當異常處理鏈表指標為0xffffffff時,異常處理常式鏈表已到頭。
    若RtlDispatchException無法處理異常,(第二次機會)判斷KiDebugRoutine是否為空白,不為空白則交給KiDebugRoutine處理。
    當所有常式都無法處理異常時,調用KeBugCheckEx藍屏,錯誤碼為KMODE_EXCEPTION_NOT_HANDLED,表示誰也沒有處理異常,系統視這個異常為無法恢複的致命異常。至此核心模式下異常處理完畢。
    若異常發生在使用者模式,同樣給予調試器第一次機會和第二次機會調試。只不過因為調試器是使用者態調試器,所以通過LPC發送訊息給會話管理器smss.exe,再由會話管理器將訊息轉寄給調試器。
    使用者模式例外處理流程:
    若KiDebugRoutine不為空白,則不為空白就將Context、陷阱幀、異常記錄、異常幀、發生異常的模式等壓入棧並將控制交給KiDebugRoutine。當處理完畢用Context設定陷阱幀並返回到上一級常式。(第一次機會)否則把異常記錄壓棧並調用DbgkForwardException,在DbgkForwardException裡判斷當前線程ETHREAD結構的HideFromDebugger成員如果為FALSE(為TRUE表示該異常對使用者調試器不可見)則向當前進程的調試連接埠(DebugPort)發送LPC訊息。
    當上一步無法處理異常時將Context結構拷貝到使用者堆棧,在堆棧中設定一個陷阱幀,陷阱幀的Eip為Ke(i)UserExceptionDisptcher((i)表示這個函數的Ke和Ki打頭的符號其實是一回事),接著返回設陷處理常式,由設陷處理常式iret返回使用者態執行Ke(i)UserExceptionDispatcher(這個函數雖然是Ke(Ki)打頭,卻不是核心裡的函數。同樣性質特殊的還有Ke(i)RaiseUserExceptionDispatcher、Ke(i)UserCallbackDispatcher、Ke(i)UserApcDispatcher。它們的共同特點就是不是被調用的,而是由核心常式設定了陷阱幀TrapFrame.Eip為該函數後iret執行到這裡的)。Ke(i)UserExceptionDisptcher調用RtlDispatchException(使用者態下的)尋找堆棧中基於幀的異常處理常式(若在XP和2003下先處理VEH再處理SEH),這個流程大家應該很熟了,就是搜尋SEH鏈表,若都不處理就調用頂層異常處理(TOP LEVEL SEH)常式。當再無法處理時就調用預設異常處理常式終止進程(有VC時這裡就換成了VC)。有點不同的是使用者態下的RtlDispatchException只判斷傳回值是ExceptionContinueExecution還是ExceptionContinueSearch。若RtlDispatchException找到異常處理常式能夠處理異常,則調用ZwContinue按照設定好的Context結構繼續執行,否則調用ZwRaiseException,並且把第三個布爾參數設為FALSE,表示進入第二次機會處理。        
    ZwRaiseException經過一系列調用最後直接調用KiDispatchException,由於把布爾值FirstChance設定為FALSE,在KiDispatchException裡直接進入第二次機會處理。
    (第二次機會)向進程的DebugPort發訊息,若無法處理,則改向進程的ExceptionPort發訊息(這裡同樣如果該異常對使用者調試器不可見,則只會發送到ExceptionPort)。DebugPort和ExceptionPort的區別在於,若向ExceptionPort發訊息,先停止目標進程所有線程的執行,直到收到回應訊息後線程才恢複執行,而向DebugPort發訊息則不需要停止線程運行。還有DebugPort是向會話管理器發訊息,而ExceptionPort是向Win32子系統發訊息,當向ExceptionPort發訊息時,已經不給使用者態調試器任何機會了:)。
當異常還是無法處理,則終止當前線程,並調用KeBugCheckEx藍屏,錯誤碼為KMODE_EXCEPTION_NOT_HANDLED。至此使用者模式下異常處理完畢。
    有一點要說明的是,這裡只是列出了作業系統的流程。如果現在去看比方說驅動或者一個應用程式裡,加入了__try/__except代碼,卻沒發現SEH上的節點對應的異常處理常式和自己寫的有啥關係。其中的原因就是因為M$的編譯器(比方說VC++、DDK)在系統內部封裝了SEH的機制。在異常處理常式鏈表節點上的異常處理常式實際上是_except_handler3,這個函數自己在內部又實現了一套類似SEH的機制,就是通過對每個函數建立一個表,包括了該函數中所有__try塊對應的過濾異常常式指標(__try()括弧裡的對應函數,如果是括弧裡是EXCEPTION_EXECUTE_HANDLER之類的則該過濾異常常式很簡單地把eax賦為相應的1、0或-1然後返回,對應了EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_SEARCH、EXCEPTION_CONTINUE_EXECUTE)和處理常式指標(__except{}和__finally{}裡面代碼的地址)。所以對應每個被調用到的函數都在SEH鏈表上註冊一個節點,並建立一張表。象諸如EXCEPTION_EXECUTE_HANDLER、XCEPTION_CONTINUE_SEARCH、EXCEPTION_CONTINUE_EXECUTE實際上是返回給_except_handler3的傳回值,而不是返回給RtlDispatchException的。
    看得有點暈吧,呵呵。總結一下流程(指異常未被處理的完整流程,若異常在任一個環節被處理都從該環節上退出繼續原來正常程式碼的執行):
    核心模式下:
    KiDispatchException->(第一次機會)KiDebugRoutine->tlDispatchException在核心堆棧尋找SEH(VEH)->(第二次機會)KiDebugRoutine->KeBugCheckEx
    使用者模式下:
    KiDispatchException->KiDebugRoutine->(第一次機會)發送訊息到進程調試連接埠->RtlDispatchException在使用者堆棧尋找SEH(VEH)->ZwRaiseException回到KiDispatchException->(第二次機會)發送訊息到進程調試或異常連接埠->KeBugCheckEx。
    就這麼簡單,呵呵。舉個例子來說明異常的處理。   
    我們程式中通過加入int 0x3來對程式進行調試,那麼int 0x3產生的異常是怎麼處理的呢?
    若int 0x3發生在核心模式下(驅動程式裡),又分為核心調試器是否載入。核心調試器未載入時,KiDebugRoutine(KdpStub)不處理int 0x3異常,則在堆棧中搜尋由驅動程式編寫者註冊的SEH,若也沒有對int 0x3異常的處理,則只好調用KeBugCheckEx藍屏,錯誤碼是KMODE_EXCEPTION_NOT_HANDLED(誰都不幹活,系統只好悲憤地自我了結了)。若有核心調試器載入,則KiDebugRoutine(KdpTrap)可以處理int 0x3異常,異常處理到這裡被正常返回。處理方法是將當前的處理器狀態(比如各寄存器等重要訊息)發送給通過串口相連的主機上的核心調試器,並一直在等待核心調試器的回應,系統這時在KdpTrap裡調用一個Kd函數KdpSendWaitContinue迴圈等待來自串口的資料(核心調試器和被調試系統通過串口聯絡),直到核心調試器下達繼續執行的命令,系統可以正常從int 0x3後面一條指令執行。
    若int 0x3發生在使用者模式下,也分為使用者調試器是否載入。使用者調試器未載入時,KiDebugRoutine不處理int 0x3異常,且使用者進程無DebugPort,將記錄異常的相關結構拷入使用者堆棧交由使用者模式的RtlDispatchException搜尋使用者堆棧中的SEH。若也沒有對int 0x3異常的處理,則調用預設異常處理常式,預設情況下是將發生異常的進程終止。若有使用者調試器載入,則向進程的DebugPort或ExceptionPort發送有關異常的LPC訊息並判斷髮送函數的返回狀態,若使用者調試器繼續執行,則返回STATUS_SUCCESS,核心視為異常已解決繼續執行。
    這裡也說明了一點,就是相同的錯誤,發生在內核態下比發生在使用者態下致命得多。其它異常的處理流程基本也一樣。少數不一樣的異常象DebugPrint、DebugPrompt、載入和卸載symbol等是通過調用DebugService的(這實際上是通過產生一個異常int 0x2d)。可以在KdpStub中就被處理(處理很簡單,只是把ontext結構的Eip加一略過當前int 0x2d後那條int 0x3指令),若在KdpTrap中被處理則是和核心調試器更進一步的互動。關於KdpStub、KdpTrap和DebugService更詳細的介紹參見我的另一篇文章《windows核心調試器原理淺析》。
Exception Handling Best Practices in .NET
By Daniel Turini
Rule 1 Don/'t throw new Exception()
Exception is a too broad class, And it/'s hard to catch without side-effects. Derive your own exception class, but derive it from ApplicationException. This way you could set a specialized exception handler for exceptions thrown by the framework And another for exceptions thrown by yourself.
要有自己的錯誤處理類,這樣可以對framework的異常和應用程式自己拋出的異常分別處理--其實錯誤類應該有一些基本的自訂屬性,譬如ID,錯誤類別,錯誤層級...等這些資訊吧
Rule 2 Don/'t put important exception information on the Message field
Exceptions are classes. When you return the exception information, create fields to store data. If you fail on doing it, people will need to parse the Message field to get the information they need. Now, think what will happen to the calling code if you need to localize Or even just correct a spelling error in error messages. You may never know how much code you/'ll break by doing it.
不要把一些錯誤的關鍵字都放到Message中,以免後面又要解析Message--這個原則一般都會遵守的吧,譬如錯誤的種類和錯誤的ID這些資訊就不應該放到Message中去吧。
Rule 3 Put a single catch (Exception ex) per thread
Generic exception handling should be done in a central point in your application. Each thread needs a separate try/catch block, Or you/'ll lose exceptions And you/'ll have problems hard to understand. When an application starts several threads to do some background processing, often you create a class for storing processing results. Don/'t forget to add a field for storing an exception that could happen Or you won/'t be able to communicate it to the main thread. In /"fire And forget/" situations, you probably will need to duplicate the main application exception handler on the thread handler.
每個線程都有自己的異常捕捉
Rule 4 Generic Exceptions caught should be published
It really doesn/'t matter what you use for logging - log4net, EIF, Event Log, TraceListeners, text files, etc. What/'s really important is: if you caught a generic Exception, log it somewhere. But log it only once - often code is ridden with catch blocks that log exceptions And you end up with a huge log, with too much repeated information to be useful.
錯誤資訊應該有記錄--這也是為了以後維護的方便,日誌還是很有必要的
Rule  5 Log Exception.ToString(); never log only Exception.Message!
As we/'re talking about logging, don/'t forget that you should always log Exception.ToString(), And never Exception.Message. Exception.ToString() will give you a stack trace, the inner exception And the message. Often, this information is priceless And if you only log Exception.Message, you/'ll only have something like /"Object reference not set to an instance of an object/".
日誌應該記載全部的錯誤資訊,而不是Message。
Rule 6 Don/'t ever swallow exceptions
The worst thing you can do is catch (Exception) And put an empty code block on it. Never do this.
不要在catch中什麼都不作
Rule 7 Use exceptions for errors that should not be ignored
I/'ll use a real world example for this. When developing an API so people could access Crivo (my product), the first thing that you should do is calling the Login method. If Login fails, Or is not called, every other method call will fail. I chose to throw an exception from the Login method if it fails, instead of simply returning false, so the calling program cannot ignore it.
在函數調用中不要僅僅返回成功還是失敗,應該返回錯誤
Rule 8 Don/'t clear the stack trace when re-throwing an exception
The stack trace is one of the most useful information that an exception carries. Often, we need to put some exception handling on catch blocks (e.g., to rollback a transaction) And re-throw the exception. See the right (and the wrong) way of doing it: The wrong way: try
{
    // Some code that throws an exception
}
catch (Exception ex)
{
    // some code that handles the exception
    throw ex;
}
Why is this wrong? Because, when you examine the stack trace, the point of the exception will be the line of the /"throw ex;/", hiding the real error location. Try it.
try
{
    // Some code that throws an exception
}
catch (Exception ex)
{
    // some code that handles the exception
    throw;
}
What has changed? Instead of /"throw ex;/", which will throw a new exception And clear the stack trace, we have simply /"throw;/". If you don/'t specify the exception, the throw statement will simply rethrow the very same exception the catch statement caught. This will keep your stack trace intact, but still allows you to put code in your catch blocks.
在嵌套throw中不要throw老的exception!!!
Rule 9 Avoid changing exceptions without adding semantic value
Only change an exception if you need to add some semantic value to it - e.g., you/'re doing a DBMS connection driver, so the user doesn/'t care about the specific socket error And wants only to know that the connection failed.
If you ever need to do it, please, keep the original exception on the InnerException member. Don/'t forget that your exception handling code may have a bug too, And if you have InnerException, you may be able to find it easier.
盡量不要增加自己的語義到錯誤資訊裡去,但是如果需要,則最好儲存innerexception,而不是exception,這樣有利於你最快的發現錯誤所在
 
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.