當前位置:首頁 > 芯聞號 > 充電吧
[導讀]我希望看這篇文章的你對C++的傳統(tǒng)異常處理,即try...catch...throw有了解(不是Windows SEH),這樣才能方便你最深入的理解這2個C語言的反人類函數(shù)。當然如果不了解就先看下面的

我希望看這篇文章的你對C++的傳統(tǒng)異常處理,即try...catch...throw有了解(不是Windows SEH),這樣才能方便你最深入的理解這2個C語言的反人類函數(shù)。
當然如果不了解就先看下面的“C++式的異常處理”,如果感覺自己了解了,可以直接skip看到“C語言中的模擬”。

【C++式的異常處理】
首先,我們寫一個類,請不要想這個類有什么特別的地方,其只是為了打印出來構(gòu)造和析構(gòu)。

class?CFoo
{
public:
????CFoo()
????{
????????printf("Create?CFoo.n");
????}
????~CFoo()
????{
????????printf("~Destroy?CFoo.n");
????}
};

然后我們寫一個函數(shù),這個函數(shù)foo是為了根據(jù)情況拋出異常:

void?foo(int?exp)
{
????if?(exp?==?'a')
????????throw?std::exception("a");

????printf("foo?ok?%d.n",exp);
}

我們來寫第一個main:

int?main()
{
????int?val?=?getchar();
????foo(val);

????return?0;
}

此時我們輸入b,其輸出的肯定是:
foo ok 98.
這里98是b的ascii值。

而我們輸入a,則會出情況了:

因為foo拋出了一個異常,但是沒例程去處理他,所以程序崩潰。
所以我們現(xiàn)在在main上加上處理foo異常的代碼:

int?main()
{
????int?val?=?getchar();
????try{
????????foo(val);
????}catch?(std::exception&?ex)
????{
????????printf("skip?ex:%s.n",ex.what());
????}

????return?0;
}

好了,我們再次輸入a,則會出現(xiàn):
skip ex:a.
foo在throw下正常的printf則不會執(zhí)行,流程被改變。
所以我們可以簡單理解為throw是一個“帶有異常信息的”return,當然實際情況比這個復雜的多,我這樣說只是為了讓你有一種C語言的感覺。

還記得上面那個CFoo嘛,我一直沒使用它,現(xiàn)在我們把foo函數(shù)改一下:

void?foo(int?exp)
{
????CFoo?cfoo;
????if?(exp?==?'a')
????????throw?std::exception("a");

????printf("foo?ok?%d.n",exp);
}

可以看到我只加了一行代碼,在堆棧上開了一個cfoo的實例,我們main不動,輸入一個p試試:
Create CFoo.
foo ok 112.
~Destroy CFoo.

可以看到,其輸出了CFoo的構(gòu)造和析構(gòu),這個是正常的情況,因為我們看到printf執(zhí)行了。
那我們輸入a呢,我們來嘗試:
Create CFoo.
~Destroy CFoo.

skip ex:a.

我們可以看到,雖然throw下面的printf沒有被執(zhí)行,但是CFoo被構(gòu)造和析構(gòu)了,這就是C++異常會遵循C++的棧上展開的特點,也就是即便發(fā)生異常了,throw前的棧上對象,都需要被析構(gòu),如果他們有“真正的”析構(gòu)代碼的話。
在執(zhí)行析構(gòu)的時候情況也是十分復雜,這里不扯那么多,因為這文章不是介紹C++異常處理的。。。
不過為了讓你看得更清除點,我們再來把CFoo函數(shù)改一下,也是一行代碼:

void?foo(int?exp)
{
????CFoo?cfoo;
????if?(exp?==?'a')
????????throw?std::exception("a");

????CFoo?cfoo2;
????printf("foo?ok?%d.n",exp);
}

我們再次輸入p:
Create CFoo.
Create CFoo.
foo ok 112.
~Destroy CFoo.
~Destroy CFoo.
可以看到這是輸出,好,我們輸入a:
Create CFoo.
~Destroy CFoo.
skip ex:a.

可以很明顯的看到,因為cfoo2構(gòu)造在throw下面,所以它在異常導致foo進行return的時候,并不需要被析構(gòu),因為它并沒有生成一個真正的實例。
好了到這里你就算不懂C++異常處理可能也可以入門了(如果你有興趣的話)。

【C語言中的模擬】
這里我們開始正式說一下setjmplongjmp
如果上面那個foo函數(shù):

void?foo(int?exp)
{
????if?(exp?==?'a')
????????throw?std::exception("a");

????printf("foo?ok?%d.n",exp);
}

因為在C語言中沒有C++異常,foo一般使用一個返回值來交出結(jié)果判斷失敗,然后調(diào)用者根據(jù)返回值進行流程控制,比如foo我們可以寫成:

bool?foo(int?exp)
{
????if?(exp?==?'a')
????????return?false;

????printf("foo?ok?%d.n",exp);
????return?true;
}

我們用bool來給出返回值,當然更多是使用int,char。
如果我們有特殊的情懷,或者我們有一些批量的任務,希望用一個統(tǒng)一的例程處理他們的錯誤。。。
我們想在C語言中,使用C++類似的東西,在foo中拋出一個異常,在main中catch呢?
這里需要用到setjmp和longjmp,我先給你一些概念:
setjmp=try;
longjmp=throw。

可以看到try和throw都有了,那catch在哪里?
要知道C語言是流程式的語言,那catch在C語言中肯定得遵循某一個流程表達式,沒錯。。。就是if。。。
所以你可以看到:
setjmp=try,longjmp=throw,if=catch。

好像所有條件都具備了,到底怎么玩?來我們繼續(xù)。
我們還是上面那個foo函數(shù):
(首先我們使用setjmp和longjmp需要include setjmp.h)

void?foo(int?exp,jmp_buf&?jb)
{
????if?(exp?==?'a')
????????longjmp(jb,'a');?//throw?std::exception("a");

????printf("foo?ok?%d.n",exp);
}

然后我們寫main:

int?main()
{
????jmp_buf?jb;
????int?jmp_ret?=?setjmp(jb);
????if?(jmp_ret?==?0)?//try
????{
????????int?val?=?getchar();
????????foo(val,jb);
????}else{?//catch
????????printf("skip?ex:%d.n",jmp_ret);
????}

????return?0;
}

按照上面的路子來,我們輸入b:
foo ok 98.
其輸入也是一樣的,那我們輸入a呢:
skip ex:97.
這里97是a的ascii碼,也就是其是跟上面的異常流程處理是一樣的,是不是感覺很奇葩。

你肯定在想,為什么,按照理論上來說,setjmp后==0,foo才會執(zhí)行,按照我們的傳統(tǒng)流程,既然foo被執(zhí)行了,那else應該永遠得不到執(zhí)行,那longjmp又是如何從foo里面跑回去了main?
我們來設想一下,else要如何才能被執(zhí)行?
對了,肯定是jmp_ref != 0嘛,沒錯,longjmp做的就是這個工作。

我們先不要在意jmp_buf,我們先看下longjmp的第二個參數(shù),他是一個值類型,這個參數(shù)我指定的是'a',也就是97,你看到了,我在prntf里面打印了jmp_ret的值,也就是,我們在longjmp時指定某一個值后,longjmp會把當前函數(shù)的流程做一個大轉(zhuǎn)彎,直接跳回到這里:
if (jmp_ret == 0) //try
而此時,jmp_ret已經(jīng)是我們指定的值,就是97了,那if的==不會被成立,則去執(zhí)行else了。

此時可能你想,如果我這樣:
longjmp(jb,0);
那不是jmp_ret還是==0,還又去執(zhí)行foo,又被longjmp,不是死循環(huán)了么?
這個情況在CRT已經(jīng)考慮過了,如果你給longjmp使用0值,其會自動修改為1,也就是0值是永遠不會被出現(xiàn)的。
好,我們來總結(jié):
1、首先setjmp需要==0才執(zhí)行foo。
2、foo發(fā)現(xiàn)錯誤,把setjmp的==0給改了。
3、if表達式的else被執(zhí)行。

可能你現(xiàn)在頭還有點暈,不過我們先說這個到這里,我們來看setjmp的第一個參數(shù):jmp_buf。
這個jmp_buf是什么呢,首先我們來再寫一個main:

int?main()
{
????char?sz[128]?=?"hello.n";

????jmp_buf?jb;
????int?jmp_ret?=?setjmp(jb);
????if?(jmp_ret?==?0)?//try
????{
????????int?val?=?getchar();
????????foo(val,jb);
????}else{?//catch
????????printf("skip?ex:%d.n",jmp_ret);
????????printf(sz);
????}

????return?0;
}

輸入a,則會輸出:
skip ex:97.
hello.

你肯定想這是當然的,因為sz變量在main范圍內(nèi)嘛。
但是別忘了,我們訪問sz可是在else里,也就是我們訪問的時候,是被longjmp跳過去的。。。
要知道,執(zhí)行foo的時候,可能整個堆棧環(huán)境已經(jīng)變得離譜了,如果你知曉匯編,肯定知道,執(zhí)行foo的時候,main使用堆棧指針EBP(當然也可以直接ESP,不過這里做一個比方)會被保存起來,要等foo進行return的時候,才會恢復EBP,然后main的局部變量才能通過EBP訪問到,但是我們的foo可是直接longjmp的,我們沒有任何代碼用于恢復EBP的值,那如何保證飛過去else的時候,訪問sz變量的地址是正確的?

對了,在setjmp的時候,CRT會把EBP等變量的值保存在jmp_buf里面,然后在longjmp里面,把EBP的值從jmp_buf里面取出來,進行恢復。
這樣在執(zhí)行l(wèi)ongjmp的時候,EBP會被恢復到setjmp時的情況,也就保證了sz變量的地址在執(zhí)行else的時候也是正確的。

如果你只會C語言,那看到這里,你應該大概理解了,如果你還了解過匯編,那可以繼續(xù)看下去,我會為你揭示setjmp、longjmp背后的一些東西。

【深入探索】
我們把剛才那個exe進行動態(tài)反匯編,以便我們整體的了解setjmp和longjmp的所有情況。
首先在調(diào)試器里面,main是這樣的:

可以看到,關(guān)鍵就是在TEST EAX,EAX這里有一個JNZ跳,如果不是0則跳到下面的catch。
我們來看setjmp的匯編:

可以看到其保存了幾個windows關(guān)鍵的寄存器。
注意,在win32下,eax、edx、ecx被定義為易失寄存器,比如我們調(diào)用foo的時候,如果foo需要用到ebx,esi,它也需要保存,退出時恢復,但是使用edx則不需要保存。
setjmp也是遵循這個原則。
可以看到setjmp的返回是XOR EAX,EAX,就是返回0。

好我們來看longjmp的反匯編:

可以看到其檢測了一下jmp_buf的正確性,然后就進行寄存器的恢復,最終把call自身的堆棧平衡了后,就使用JMP指令直接JMP到setjmp后的那個指令地址,而此時其把EAX改成了longjmp的第二個參數(shù):

那接下來的TEST EAX,EAX肯定不會成功,就會跑去執(zhí)行catch了。

【與C++的結(jié)合】
文章寫到這里,應該快結(jié)束了,可還有一個點,可能你沒注意到,我們還是回到我們第一個代碼——CFoo這個類來。
在上面的C++異常里面,我們看到了這樣的代碼:

void?foo(int?exp)
{
????CFoo?cfoo;
????if?(exp?==?'a')
????????throw?std::exception("a");

????printf("foo?ok?%d.n",exp);
}

按照C++的規(guī)范,異常發(fā)生的時候,cfoo也會被析構(gòu),如果我們使用longjmp呢,就像下面:

void?foo(int?exp,jmp_buf&?jb)
{
????CFoo?cfoo;
????if?(exp?==?'a')
????????longjmp(jb,'a');?//throw?std::exception("a");

????printf("foo?ok?%d.n",exp);
}

你肯定會想,cfoo應該只會被構(gòu)造,而不會被析構(gòu),因為longjmp可是CRT的函數(shù)。
其實原來我也是這樣想的,但是我不懂是不是VC spec,我在跟蹤longjmp的時候發(fā)現(xiàn)了堆棧展開的代碼。。。
也就是,其實cfoo在longjmp的時候,也是會被析構(gòu)的:
Create CFoo.
~Destroy CFoo.
skip ex:97.
hello.

這個要注意一下。
如果你想看匯編,在下面。
這個是foo函數(shù)的匯編:

SEH處理器在這里:

然后會展開到析構(gòu)函數(shù):


【完結(jié)】
為這2個狗血的東西寫了那么多,也說的差不多了。
其實這2個東西,因為其反人類的特性,在項目開發(fā)中,不應該被使用上,在這里只是告訴大家,如果遇到有setjmp、longjmp的情況的時候,可以判斷出來代碼的執(zhí)行流程。

本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉