在分享中學習,才能讓自己跑得更快。
SEH(structured exception handing),中文叫做結構化異常處理。
我們使用SEH,不是意味着可以完全無視代碼之中可能出現的那一些異常,但是可以將程序的功能實現和程序的異常處理區分開來。也就是說我們先把程序的功能都實現起來,到後面再去處理程序在運行的時候可能遇到的一系列情況。
Microsoft把SEH加入Windows的原因之一就是SEH簡化了操作系統的本身開發,微軟使用SEH可以讓操作系統更加地健壯,而我們也能使用SEH讓我們的程序更加健壯。注意一點讓SEH運行起來編譯器的工作遠遠大於操作系統,進入和離開異常處理區域的時候編譯器都必須要生成一系列的特殊代碼與一系列支持SEH的數據結構表,還要提供回調函數給操作系統去調用,一邊操作系統遍歷異常區域。編譯器的工作還不僅僅如此,還要準備進程的stack frame和一些內部信息,這些都是操作系統需要使用或者引用的。
讓編譯器支持SEH並不是一個簡單任務,而不同的編譯器廠商都以不同的方式去實現,但還好,大部分的編譯器廠商都遵循了Microsoft建議的語法。
注意:結構化異常處理SEH和C++的異常處理是不一樣的!C++異常處理就是使用關鍵字catch和throw。
SEH包括了兩個方面的功能:終止處理(termination handing)和異常處理(exception handing)。
1.終止處理
終止處理確保不管the guarded body是怎麼樣子退出的,另外一個終止處理程序都能夠執行。
如下
_try
{
//the guarded body
//也就是被保護的代碼
}
_finally
{
//Termination handing
//也就是終止處理程序
}
首先介紹_try和_finalll是關鍵字,他們的作用是標記了終止處理程序的兩個部分:the guarded body 和Termination handing。在這段代碼中,操作系統和編譯器確保了不管the guarded body代碼是如何退出的,不管是使用了return還是使用了goto等等語句,Termination handing代碼都會被調用。(但是如果使用的是exirprocess,exitthread,terminateprocess,terminatethread函數來終止線程或者進程的話,termination handing代碼就得不到執行。)
接下來我們看代碼來理解SEH終止處理程序。
1.1
DWORD FUNCTION1()
{
//code ①
_try
{
//code ②
}
_finally
{
//code ③
}
return ④
}
代碼中的序號就是代碼的執行順序。這個函數順序沒啥說的。
1.2
DWORD FUNCTION2()
{
//code
DWORD I=0;
_try
{
//code
I=5;
return I;
}
_finally
{
//code
}
I=6;
return I;
}
哈哈哈,這段代碼有點意思呀,try區域的結尾有一個return呀,應該是返回5還是返回6呢?
通過終止處理程序可以防止過早執行return語句,當return語句試圖退出try區域的時候,編譯器會讓finally代碼在return之前執行。finally代碼執行完成之後,函數就可以執行return語句返回了,所以因爲try區域內包含了一個return語句,所以finally區域後的代碼都不會被執行了,所以這個函數返回的是5,而不是6。
編譯器如何保證finally區域可以在try區域退出前被執行?OK,當編譯器檢查代碼時候,會發現try區域裏面居然有一個return語句,哎呀所以編譯器就得做點事情啦,於是編譯器就會生成一些代碼先把返回值(例子中的5)保存在一個由它創建的臨時變量裏面,然後再執行finally區域的代碼。這個過程就叫做局部展開(local unwind)。就是說系統因爲try區域的代碼提前退出而執行finally區域代碼時就會發生局部展開。而一旦finally區域代碼執行完畢,編譯器就會把臨時變量中的值取出來再當做函數返回值。
所以開頭的時候說爲了SEH的運行編譯器做的工作遠遠大於操作系統。當然在不同CPU體系結構上終止處理工作的執行步驟也不同。但是應該注意要避免在try區域裏面使用return語句,因爲這對程序的性能是有影響的!不用return用什麼呢?OK,用_leave關鍵字(下面介紹)。
應該注意,SEH是用來捕獲那一些程序異常的!如果是常見的問題,我們應該改變代碼來解決問題而不是依賴於操作系統和編譯器的SEH來捕捉這些問題。
如果代碼控制流正常地離開try區域(就像FUNCTION1函數那樣子)而進入finally區域,那麼對程序的性能影響是最低的,因爲編譯器只是多加了一條機器指令。
好的我們繼續。
1.3
DWORD FUNCTION3()
{
DWORD I=0;
_try
{
I=5;
goto AreaA;
}
_finally
{
}
I=6;
AreaA:
return I;
}
在這裏,編譯器看到try區域中的goto語句的時候就會發生局部展開而執行finally區域代碼塊,第一次因爲try區域和finally區域都沒有函數返回語句,所以就跳到AreaA標籤去執行下去,而跳過了I=6這一條代碼,所以函數最終返回的是5;因爲goto破壞了try區域到finally區域的正常流程,所以也可能有較大的性能影響,影響程度取決於CPU的體系架構。
1.4
DWORD GetSystemIndex()
{ DWORD I=0;
.......
//運算出錯而導致程序訪問非法內存
return I;
}
DWORD FUNCTION4()
{
DWORD I=0;
_try
{
I= GetSystemIndex();
}
_finally
{
}
return I;
}
這個例子才能看到終止處理程序的價值。
GetSystemIndex函數錯誤而導致程序訪問非法內存,如果沒有SEH的存在,那麼一般情況下最終會導致Windows錯誤報告彈出一個對話框說:Application has stopped working。我們一旦取消這個對話框程序也就ko了。正是因爲有了SEH的存在,發生訪問非法內存的時候,finally區域的代碼得以執行而我們也可以做一些最後該做的事情,比如釋放信號量等內核對象。
1.5
DWORD FUNCTION5()
{
DWORD I=0;
while(I<10)
{
_try
{
if(I==2) continue;
if(I==3) break;
}
_finally
{
I++;
}
I++;
}
I+=10;
return I;
}
這個函數將返回多少呢?答案是I=14;
當if(I==2)爲true的時候,continue會導致finally區域執行所以I++後I變成了3,而後再次循環。這一次if(I==3)爲true了,所以執行break語句,再次進入finally區域,執行finally區域完成後沒有return語句,所以繼續執行finally區域後面的代碼。
1.6
DWIRD FUNCTION6()
{
HANDLE hFile=INVALID_HANDLE_VALUD;
PVOID pBuf=NULL;
DWORD dwByte=0;
BOOK bOk=FALSE;
hFile=CreateFile(TEXT("A.txt"),GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,NULL,NULL);
if(hFile==INVALID_HANDLE_VALUD)
return bOk;
pBuf=VirtualAlloc(NULL,1024,MEM_COMMIT,PAGE_READWRITE);
if(pBuf==NULL)
{
CloseHandle(hFile);
return bOk;
}
bOk=ReadFile(hFile,pBuf,1024,&dwByte,NULL)
if(!bOk||(dwByte==0))
{
CloseHandle(hFile); VirtuaFree(pBuf,MEM_RELEASE|MEM_DECOMMIT);
return bOk;
}
bOk=true;
VirtuaFree(pBuf,MEM_RELEASE|MEM_DECOMMIT);
CloseHandle(hFile);
return bOk;
}
這個函數的錯誤代碼檢查是不是讓你失望?這個函數功能還沒有全部完成,如果在多加那麼幾個函數,一眼看過去真的亂如麻呀!
但是接下來我們用SEH來實現一下。
DWORD FUNCTION7()
{
HANDLE hFile=INVALID_HANDLE_VALUE;
PVOID pBuf=NULL;
BOOL bFunctionOk=FALSE;
_try
{
DWORD dwByte=0;
BOOL bOk=FALSE;
hFile=CreateFile(TEXT("A.BAT"),GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,NULL,NULL);
if(hFile==INVALID_HANDLE_VALUE)
_leave;
pBuf=VirtualAlloc(NULL,1024,MEM_COMMIT,PAGE_READWRITE);
if(pBuf==NULL)
_leave;
bOk=ReadFile(hFile,pBuf,1024,&dwByte,NULL);
if(!bOk||(dwByte==0))
_leave;
//其它代碼......
bFunctionOk=true;
}
_finally
{
if(pBuf!=NULL) VirtualFree(pBuf,MEM_RELEASE|MEM_DECOMMIT);
if(hFile!INVALID_HANDLE_VALUE)
CloseHandle(hFile);
}
return bFunctionOk;
}
怎麼樣?是不是比上面那個函數好多了?關鍵字_leave會導致代碼執行到try區域的結尾而正常地進入finally區域,所以不會產生大開銷。但是我們需要定義一個bool變量來說明函數運行結果成功還是失敗。最後在finally區域裏面檢查那一些變量需要釋放。
但是finally區域需要注意一些事項:
兩種情況下執行finally區域,第一種情況下就是從try區域正常進入finally區域,而第二種就是產生局部展開,就是從try區域提前退出(由於goto,continue,break,return的語句引起)將程序控制流強制進入finally區域。
但還有一種情形下會發生,就是全局展開(以後介紹SEH的異常處理程序會講)。
finally區域執行是由於上面三種情況之一引起的,但是要確定是哪一種情況下引起的,我們需要調用內在函數AbnormalTermination();
函數原型 BOOL AbnormalTermination();
什麼叫內在函數呢?內在函數就是由編譯器識別並處理的特殊函數,編譯器會爲內聯函數生成內聯代碼,而不是生成代碼來調用內在函數,就如memcpy就是內在函數。
我們只能在finally區域裏面調用這個內在函數,內在函數會返回一個bool變量來表明一個與當前finally區域相關的try區域是否提前退出。就是說從try區域正常進入finally,內在函數就會返回false。如果控制流從try區域異常退出(如goto,break等引起局部展開,或者代碼拋出異常而引起全局展開),那麼內在函數的返回值就是true。但是如何進一步區分是全局展開還是局部展開呢?簡單呀,不要在try區域裏面用break,goto,continue,return而是使用leave,那麼內在函數返回值爲true,那就是全局展開了呀!局部展開是我們可以控制的呀。
最後我們總結一下SEH的終止處理程序的使用理由:
因爲清理工作都在finally區域執行,而且保證得到執行,從而簡化了錯誤處理(比如訪問到了非法內存而釋放內核對象)和清理內存。
提高了代碼可讀性。
讓代碼更容易維護。
正常使用下,對程序的性能和體積影響很小。