天天看點

Linux在Windows下實作結構化的異常處理

    盲目性的習慣編寫程式,畢竟是很不好的心态;顯而已知在這些過程中便形成了程式設計就變得異常了。之是以還是安下心來認真,然而認真還是不夠的,今天我便認真閱讀和思考了這樣一些關于Linux在Windows下實作結構化的異常處理問題;往往這就是我們所突劣的重要所在,比如我們在Windows平台下編寫程式,經常會遇到代碼因為種種原因而發生異常的情況;而Linux下是沒有類似的機制的,異常處理例程是是靜态包含在核心代碼中的;而且必須精确定位出可能産生異常的代碼的位址,這樣便給異常處理帶來很大的麻煩。通常用Linux是開放式的,既然核心自己提供類似的機制,那就讓我們自己來實作它。     首先我們要先認識SEH總名稱(Structure Exception Handling) 技術的出現為程式員供了一個标準解決方案,程式員隻需要注冊好異常處理函數(由編譯器負責,通常不是一個完整的函數,而是和可能發生異常的代碼在同一個函數中),如果在目前線程的代碼引發異常時系統便會自動調用異常處理函數,完成善後工作。要完成SEH工作,必須能夠攔截異常,攔截之後便轉移到相關的代碼,最後在可能産生異常的代碼之前注冊異常代碼處理例程。     要做到異常攔截,程式員必須直接修改IDT表,用自己的GP(general-Protection)異常處理服務代碼替換掉核心的異常處理代碼就行啦,這個工能很容易實作。以下代碼如:

<a href="http://huangyouliang10.blog.51cto.com/attachment/201010/27/1562091_1288151424lSAy.png"></a>

  注意:在程式推出時必須恢複原來的GP異常入口。      要做到異常處理時,必須在自己的異常處理服務程式區檢測到相應的異常處理程式是否注冊,如果是,那麼則恢複現場,然後跳轉到注冊異常處理程式中運作;如果沒有注冊就把控制權轉交給核心的異常處理程式。       我們在異常處理時把新的異常服務程式分為兩個部分,第一部分用彙編語言實作,通常用于保護現場及完成一些對于C代碼無法實作的操作,代碼如下:

<a href="http://huangyouliang10.blog.51cto.com/attachment/201010/27/1562091_1288151429IhWh.png"></a>

     首先,這一部分代碼要使用的是機器碼而不是彙編代碼,是因為其中部分代碼需要在初始化時動态修改,以機器碼的形式寫出來便便于計算偏移量。另外也友善在注釋中使用了Intel彙編語言格式,畢竟是AT&amp;T彙編的格式太麻煩。現在我們來解釋一下這些代碼的作用:       第1、2行,起保護現場,儲存所有有可能會破壞到的寄存器。       第3行,EAX指向堆找棧中的異常錯誤代碼。       第4、5行,以指向異常錯誤代碼的指針為參數調第二部分異常處理程式,因為彙彙譯之前無法确定第二部分異常處理程式的位址,是以初始化時需要将第二部分異常處理程式的位址填入0xe8之後的雙字中。       第6 行,第二部分異常處理程式執行完畢之後恢複調用前的堆棧。       第7、8 行,确認該異常是否已經處理,如果沒有處理則跳轉到_call_system_handler(見第15行)       第9、10行,将異常發生時的标志寄存的值複制到第1行儲存的标志寄存器的位置。       第11、12行,恢複異常發生前的現場。       第13行,清除堆棧中的錯誤代碼,注意這裡絕對不能用add esp,4因為此時标志寄存器已經恢複到發生異常的狀态,add指令會改變标志寄存器的值,而用pop指令清除堆棧的話沒,有地方存放取出的資料,是以隻能用lea指令。       第14行,以遠的調用方式傳回被中斷代碼,這一句非常重要,因為是異常處理程式傳回被中斷代碼,是以本來應該使用IRET來傳回,但異常可能發生在某個子程式中,是以在傳回時需要調整堆棧指針,是以這裡帶調整堆棧的RETF指令來完成這一任務,這裡RETF附帶的參數也需要按每次異常發生是的狀态來修正。       第15、16行,如果目前異常不能處理,恢複現場,準備轉到系統的異常處理程式。       第17行,跳轉到系統的異常處理程式。這裡是跳轉的位址也是在初始化是設定。       異常處理程式的第二部分,用于搜尋注冊的異常處理程式鍊以檢查目前異常是否需要由我們處理。如果是,設定傳回資訊,傳回非0值;如果不是,傳回0,這樣由系統進行異常程式處理。        第二部代碼如下:

    第二部分代碼的主要功能是确認發生異常的線程是否已經注冊過異常處理例程(異常處理例程可以嵌套,即在某段代碼注冊了異常處理之後,它的子程式依舊可以注冊異常處理例程,在發生異常時,将會執行最近注冊的那個異常處理例程),如果沒有注冊,表示該異常由作業系統處理;而如果該線程注冊過異常處理程式,則恢複現場并轉到注冊的異常程式入口執行。      同時在"_enum_exception_handler()函數搜尋目前能夠用目前線程是否已經注冊過異常處理例程;如果有則傳回在最後注冊的一個異常處理例程的入口以及異常處理例程注冊時的堆棧指針。      pStackTop參數有異常服務程式的第一部分傳入,指向異常發生後的棧頂。在IA32架構裡,如果要發生異常時堆棧便産生有4種不同的變化狀态,下例是異常發生時堆棧狀态如圖所示:

<a href="http://huangyouliang10.blog.51cto.com/attachment/201010/27/1562091_1288151440lBMg.png"></a>

    驅動時是在核心層運作,發生運作異常時不會有特權級轉換,之是以(c)、(d)兩種情況不用考慮,而對于GP而言,是肯定有錯誤碼産生 ,是以隻需要處理狀态(b)格式的堆棧資訊,把異常處理程式的入口位址寫入堆棧中儲存EIP的位置,然後清除錯誤碼,再用IRETD或REIF指令傳回就能轉移到異常處理程式運作了。是以在第二段異常服務程式中修改傳回位址是用:

第一宏用于注冊異常處理程式,首先取得異常處理程式的入口位址(第4行:asm volatile("jmp 1f\n");), 标号1定義在第二個宏中,其後就是異常處理程式,第二個宏的第5行(asm volatile ("1:\t call 2b\n");) 調用第一個宏中的标點号2,然後彈出堆棧第一個雙字就得到了異常處理程式入口位址,将這個異常位址和目前堆棧指針入棧作為參數調用_except_register_handler函數一定要指定asmlinkage字首以指明參數用堆棧傳遞,不然編譯器肯定會用寄存器傳遞參數。注冊完之後就進入到危險代碼區了,然後在危險代碼結束時,就來到第二個宏的第一句:asm volatile("jmp 3f\n");跳轉到說尾部分來取消目前注冊的異常處理程式。這裡我們必須注意的是三個宏之間多于出來的那兩組花括号,這兩組花括号絕對不能省略,它們會把危險的代碼和異常處理程式标記為函數中相對獨立的部分,以避免編譯器在優化時将這兩部分的代碼和其他部分又化成一個整體(直接後果就是堆棧混亂)。(以上是Linux在Windows下實作結構化的異常處理的全部過程)

本文轉自huangyouliang10 51CTO部落格,原文連結:http://blog.51cto.com/1572091hyl10/400746

繼續閱讀