相比較其他傳統(tǒng)的語(yǔ)言,C++的一個(gè)變革的特征是支持異常處理。相對(duì)于傳統(tǒng)語(yǔ)言的不清楚容易錯(cuò)誤的錯(cuò)誤處理機(jī)制,C++的異常處理是一個(gè)非常好的替代。在正常的代碼和錯(cuò)誤處理代碼之間清楚的分割使得程序非常整潔和宜于維護(hù)。本文討論編譯器怎么實(shí)現(xiàn)異常處理。假設(shè)讀者熟悉異常處理的語(yǔ)法。本文包含一個(gè)異常處理的VC++的庫(kù)來(lái)替代VC++的異常處理,使用這個(gè)函數(shù):
install_my_handler();
在這以后,程序中發(fā)生的任何異常(包含拋出異常到stackunwinding,調(diào)用catch塊和繼續(xù)執(zhí)行)都使用我自己的異常處理庫(kù)。
譯者注:當(dāng)異常出現(xiàn)時(shí),正常的執(zhí)行流被中斷,異常處理機(jī)制開(kāi)始在當(dāng)前范圍尋找匹配的處理函數(shù)。如果找不到,把當(dāng)前函數(shù)從棧中彈出,在調(diào)用者中繼續(xù)尋找。這個(gè)過(guò)程稱(chēng)為stackunwinding
像其他C++特征一樣,C++的標(biāo)準(zhǔn)并沒(méi)有指定異常處理的實(shí)現(xiàn)機(jī)制。這使得C++實(shí)現(xiàn)者可以使用任何實(shí)現(xiàn)機(jī)制。我將講述VC++怎么實(shí)現(xiàn)的。VC++把異常處理置于SHE(Structuredexceptionhangling)的上面。SHE是windows操作系統(tǒng)提供的結(jié)構(gòu)化的異常處理。
SHE導(dǎo)論
在本討論中,我將考慮那些顯式的異常。例如被0除,空指針訪(fǎng)問(wèn)等。當(dāng)異常出現(xiàn),中斷會(huì)產(chǎn)生,控制被轉(zhuǎn)到OS。OS調(diào)用異常處理,檢查從異常發(fā)出的函數(shù)開(kāi)始的函數(shù)調(diào)用順序,執(zhí)行stackunwinding和控制轉(zhuǎn)移。我們可以開(kāi)發(fā)自己的異常處理函數(shù),在OS中注冊(cè)。OS就會(huì)在異常事件時(shí)調(diào)用它們。
Windows定義了一個(gè)特別的結(jié)構(gòu)用來(lái)注冊(cè):
EXCEPTION_REGISTRATION:
struct EXCEPTION_REGISTRATION
EXCEPTION_REGISTRATION *prev;
DWORD handler;
;
要注冊(cè)自己的異常處理函數(shù),創(chuàng)建這個(gè)結(jié)構(gòu)并將它的地址保存在段(由FS寄存器指向)的0偏移處。如下面的偽匯編指令:
mov FS:[0], exc_regp
結(jié)構(gòu)中的prev字段表示EXCEPTION_REGISTRATION鏈表。當(dāng)我們注冊(cè)了這個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu),我們使用這個(gè)prev字段保存以前注冊(cè)的結(jié)構(gòu)的地址。
關(guān)于異常回調(diào)函數(shù),windows要求異常處理的信號(hào),定義在excp.h文件中:
EXCEPTION_DISPOSITION (*handler)(
_EXCEPTION_RECORD *ExcRecord,
void * EstablisherFrame,
_CONTEXT *ContextRecord,
void * DispatcherContext);
現(xiàn)在你可以忽略所有的參數(shù)和返回類(lèi)型。下面的程序在OS中注冊(cè)了一個(gè)異常處理句柄并將產(chǎn)生一個(gè)被0除的異常。這個(gè)異常被抓到并將打印一個(gè)消息:
#include
#include
using std::cout;
using std::endl;
struct EXCEPTION_REGISTRATION
EXCEPTION_REGISTRATION *prev;
DWORD handler;
;
EXCEPTION_DISPOSITION myHandler(
_EXCEPTION_RECORD *ExcRecord,
void * EstablisherFrame,
_CONTEXT *ContextRecord,
void * DispatcherContext)
cout << "In the exception handler" << endl;
cout << "Just a demo. exiting..." << endl;
exit(0);
return ExceptionContinueExecution; //will not reach here
int g_div = 0;
void bar()
//initialize EXCEPTION_REGISTRATION structure
EXCEPTION_REGISTRATION reg, *preg = ?
reg.handler = (DWORD)myHandler;
//get the current head of the exception handling chain
DWORD prev;
_asm
mov EAX, FS:[0]
mov prev, EAX
reg.prev = (EXCEPTION_REGISTRATION*) prev;
//register it!
_asm
mov EAX, preg
mov FS:[0], EAX
//generate the exception
int j = 10 / g_div; //Exception. Divide by 0.
int main()
bar();
return 0;
/*output
In the exception handler
Just a demo. exiting...
-*/
注意:windows嚴(yán)格地定義了一個(gè)規(guī)則:EXCEPTION_REGISTRATION結(jié)構(gòu)應(yīng)該在棧內(nèi),并且要在以前的代碼的低的內(nèi)存地址。規(guī)則不滿(mǎn)足,windows將中止程序。
函數(shù)和堆棧
堆棧是一塊連續(xù)的內(nèi)存,用來(lái)保存函數(shù)的局部對(duì)象。更明確的說(shuō),每一個(gè)函數(shù)都有關(guān)聯(lián)的棧幀(譯注:stackframe,在調(diào)用函數(shù)時(shí),進(jìn)入函數(shù)以后第一句應(yīng)該是pushebp,然后movebp,esp,所以ebp一般都指向當(dāng)前函數(shù)進(jìn)入時(shí)的棧頂,而且指向的內(nèi)容是上一層調(diào)用函數(shù)進(jìn)入時(shí)棧頂,如此向外,最后找到0,就是系統(tǒng)的入口,這樣一個(gè)函數(shù)使用的那些棧應(yīng)該就是一幀)來(lái)保存所有的函數(shù)局部對(duì)象和函數(shù)表達(dá)式產(chǎn)生的臨時(shí)對(duì)象(譯注:1.C++里對(duì)象的意義很廣泛,不只是class,結(jié)構(gòu),簡(jiǎn)單類(lèi)型也是對(duì)象2.臨時(shí)對(duì)象,學(xué)過(guò)編譯原理的話(huà)應(yīng)該很清楚,舉個(gè)例子,比如有一個(gè)函數(shù)是intmyfun();你在函數(shù)中這樣寫(xiě)intret=myfun();這樣myfun()返回的結(jié)果就放到了ret里,如果你寫(xiě)成myfun();而不理它的返回值,你雖然不理它,但是仍然會(huì)有它返回值存放的地方,這就是一個(gè)臨時(shí)的對(duì)象,在vc的調(diào)試環(huán)境了,看auto變量的頁(yè)面就可以看到臨時(shí)的變量)。請(qǐng)注意上面描述只是很典型的情況。而實(shí)際上,編譯器可能會(huì)儲(chǔ)存所有或部分的變量到寄存器里,以便獲得更快的執(zhí)行速度(譯注:編譯器優(yōu)化)。堆棧是一個(gè)處理器(CPU)級(jí)就支持的概念(譯注:之所以這么說(shuō),因?yàn)閰R編代碼里就有push和pop).處理器提供內(nèi)部的寄存器和特殊的指令來(lái)實(shí)現(xiàn)堆棧處理
圖 2顯示了一個(gè)典型的棧,這是當(dāng)函數(shù)foo調(diào)用函數(shù)bar,然后bar調(diào)用函數(shù)widget以后的棧的內(nèi)容。請(qǐng)注意棧是向下增長(zhǎng)的(譯注:平時(shí)我在紙上畫(huà)棧都是低地址在上,所以作者的這個(gè)圖看起來(lái)感覺(jué)有點(diǎn)怪,但是看懂應(yīng)該沒(méi)有問(wèn)題),這意味著下一個(gè)將要入棧的元素所在的地址將比前一個(gè)元素的地址更小(低)。編譯器用ESP寄存器來(lái)鑒別當(dāng)前的棧幀,在上圖所示的情況下,widget時(shí)正在執(zhí)行鼓票的函數(shù),EBP寄存器指向了widget的棧幀(就是函數(shù)進(jìn)入時(shí),push了ebp以后的棧頂位置)。函數(shù)訪(fǎng)問(wèn)局部變量都是用局部變量所在的位置相對(duì)于幀頂?shù)钠屏俊>幾g器在編譯的時(shí)候就把所有的局部變量從名字變成固定的相對(duì)于幀頂?shù)钠疲纾瑆idget函數(shù)訪(fǎng)問(wèn)它的一個(gè)局部變量就用ebp-24來(lái)指明它的位置
上圖也顯示了ESP寄存器,在圖示的情況下,它指向了堆棧的最后一項(xiàng),也就是處于widget幀的尾部,下一幀將從這個(gè)位置開(kāi)始
處理器支持兩種棧操作:push 和 pop:
pop EAX
意味著從esp所在的位置讀4個(gè)字節(jié)到eax中,然后把esp增加4(32位的處理器下)。同樣的,
push EBP
意味著把esp減4,然后把ebp的內(nèi)容寫(xiě)到esp指向的地方。
當(dāng)編譯器編譯一個(gè)函數(shù), 它在函數(shù)頭部添加一些創(chuàng)建和初始化函數(shù)棧幀的代碼,同樣,在函數(shù)結(jié)尾加上從堆棧里彈出棧幀的代碼。
典型的,編譯器在函數(shù)頭部生成
Push EBP ; save current frame pointer on stack
Mov EBP, ESP ; Activate the new frame
Sub ESP, 10 ; Subtract. Set ESP at the end of the frame
第一句保存當(dāng)前的幀指針ebp到堆棧里,第二句通過(guò)設(shè)置ebp到當(dāng)前的esp來(lái)激活當(dāng)前的函數(shù)幀,.第三句設(shè)置esp寄存器到當(dāng)前幀的尾部,就是把esp減去本函數(shù)內(nèi)的局部對(duì)象的總長(zhǎng)度。編譯器在編譯的時(shí)候就知道有多少的局部對(duì)象和每個(gè)對(duì)象的長(zhǎng)度,所以能夠清楚地知道一個(gè)函數(shù)的幀的確切長(zhǎng)度
在函數(shù)結(jié)束時(shí)把當(dāng)前幀從堆棧中彈出
Mov ESP, EBP
Pop EBP ; activate caller"s frame
Ret ; return to the caller
恢復(fù)ESP和EBP,然后執(zhí)行RET
當(dāng)處理器執(zhí)行RET指令時(shí),實(shí)際上類(lèi)似執(zhí)行了一條popeip,把棧里保存的EIP彈出,然后跳到EIP處開(kāi)始執(zhí)行。相反,call指令執(zhí)行的時(shí)候先把當(dāng)前的EIP推入堆棧,然后jmp到相應(yīng)的地址,
圖 3 顯示了運(yùn)行時(shí)堆棧的更多詳細(xì)信息。如圖所示,函數(shù)參數(shù)也是函數(shù)幀的一部分,調(diào)用函數(shù)者把參數(shù)推入堆棧,然后函數(shù)返回否執(zhí)行
Add ESP, args_size
或者,采用另一種RET指令,如下
Ret 24
相當(dāng)于返回后,執(zhí)行了 ADD ESP , 24
注意沒(méi)有進(jìn)程里的每隔線(xiàn)程都有它自己的茶
www.stockdatas.cn www.stockbests.cn www.stocknewss.cn