C語(yǔ)言的反人類函數(shù):setjmp和longjmp的詳細(xì)剖析
我希望看這篇文章的你對(duì)C++的傳統(tǒng)異常處理,即try...catch...throw有了解(不是Windows SEH),這樣才能方便你最深入的理解這2個(gè)C語(yǔ)言的反人類函數(shù)。
當(dāng)然如果不了解就先看下面的“C++式的異常處理”,如果感覺自己了解了,可以直接skip看到“C語(yǔ)言中的模擬”。
【C++式的異常處理】
首先,我們寫一個(gè)類,請(qǐng)不要想這個(gè)類有什么特別的地方,其只是為了打印出來(lái)構(gòu)造和析構(gòu)。
class?CFoo { public: ????CFoo() ????{ ????????printf("Create?CFoo.n"); ????} ????~CFoo() ????{ ????????printf("~Destroy?CFoo.n"); ????} };
然后我們寫一個(gè)函數(shù),這個(gè)函數(shù)foo是為了根據(jù)情況拋出異常:
void?foo(int?exp) { ????if?(exp?==?'a') ????????throw?std::exception("a"); ????printf("foo?ok?%d.n",exp); }
我們來(lái)寫第一個(gè)main:
int?main() { ????int?val?=?getchar(); ????foo(val); ????return?0; }
此時(shí)我們輸入b,其輸出的肯定是:
foo ok 98.
這里98是b的ascii值。
而我們輸入a,則會(huì)出情況了:
因?yàn)閒oo拋出了一個(gè)異常,但是沒例程去處理他,所以程序崩潰。
所以我們現(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; }
好了,我們?cè)俅屋斎隺,則會(huì)出現(xiàn):
skip ex:a.
foo在throw下正常的printf則不會(huì)執(zhí)行,流程被改變。
所以我們可以簡(jiǎn)單理解為throw是一個(gè)“帶有異常信息的”return,當(dāng)然實(shí)際情況比這個(gè)復(fù)雜的多,我這樣說(shuō)只是為了讓你有一種C語(yǔ)言的感覺。
還記得上面那個(gè)CFoo嘛,我一直沒使用它,現(xiàn)在我們把foo函數(shù)改一下:
void?foo(int?exp) { ????CFoo?cfoo; ????if?(exp?==?'a') ????????throw?std::exception("a"); ????printf("foo?ok?%d.n",exp); }
可以看到我只加了一行代碼,在堆棧上開了一個(gè)cfoo的實(shí)例,我們main不動(dòng),輸入一個(gè)p試試:
Create CFoo.
foo ok 112.
~Destroy CFoo.
可以看到,其輸出了CFoo的構(gòu)造和析構(gòu),這個(gè)是正常的情況,因?yàn)槲覀兛吹絧rintf執(zhí)行了。
那我們輸入a呢,我們來(lái)嘗試:
Create CFoo.
~Destroy CFoo.
skip ex:a.
我們可以看到,雖然throw下面的printf沒有被執(zhí)行,但是CFoo被構(gòu)造和析構(gòu)了,這就是C++異常會(huì)遵循C++的棧上展開的特點(diǎn),也就是即便發(fā)生異常了,throw前的棧上對(duì)象,都需要被析構(gòu),如果他們有“真正的”析構(gòu)代碼的話。
在執(zhí)行析構(gòu)的時(shí)候情況也是十分復(fù)雜,這里不扯那么多,因?yàn)檫@文章不是介紹C++異常處理的。。。
不過(guò)為了讓你看得更清除點(diǎn),我們?cè)賮?lái)把CFoo函數(shù)改一下,也是一行代碼:
void?foo(int?exp) { ????CFoo?cfoo; ????if?(exp?==?'a') ????????throw?std::exception("a"); ????CFoo?cfoo2; ????printf("foo?ok?%d.n",exp); }
我們?cè)俅屋斎雙:
Create CFoo.
Create CFoo.
foo ok 112.
~Destroy CFoo.
~Destroy CFoo.
可以看到這是輸出,好,我們輸入a:
Create CFoo.
~Destroy CFoo.
skip ex:a.
可以很明顯的看到,因?yàn)閏foo2構(gòu)造在throw下面,所以它在異常導(dǎo)致foo進(jìn)行return的時(shí)候,并不需要被析構(gòu),因?yàn)樗]有生成一個(gè)真正的實(shí)例。
好了到這里你就算不懂C++異常處理可能也可以入門了(如果你有興趣的話)。
【C語(yǔ)言中的模擬】
這里我們開始正式說(shuō)一下setjmp和longjmp。
如果上面那個(gè)foo函數(shù):
void?foo(int?exp) { ????if?(exp?==?'a') ????????throw?std::exception("a"); ????printf("foo?ok?%d.n",exp); }
因?yàn)樵贑語(yǔ)言中沒有C++異常,foo一般使用一個(gè)返回值來(lái)交出結(jié)果判斷失敗,然后調(diào)用者根據(jù)返回值進(jìn)行流程控制,比如foo我們可以寫成:
bool?foo(int?exp) { ????if?(exp?==?'a') ????????return?false; ????printf("foo?ok?%d.n",exp); ????return?true; }
我們用bool來(lái)給出返回值,當(dāng)然更多是使用int,char。
如果我們有特殊的情懷,或者我們有一些批量的任務(wù),希望用一個(gè)統(tǒng)一的例程處理他們的錯(cuò)誤。。。
我們想在C語(yǔ)言中,使用C++類似的東西,在foo中拋出一個(gè)異常,在main中catch呢?
這里需要用到setjmp和longjmp,我先給你一些概念:
setjmp=try;
longjmp=throw。
可以看到try和throw都有了,那catch在哪里?
要知道C語(yǔ)言是流程式的語(yǔ)言,那catch在C語(yǔ)言中肯定得遵循某一個(gè)流程表達(dá)式,沒錯(cuò)。。。就是if。。。
所以你可以看到:
setjmp=try,longjmp=throw,if=catch。
好像所有條件都具備了,到底怎么玩?來(lái)我們繼續(xù)。
我們還是上面那個(gè)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; }
按照上面的路子來(lái),我們輸入b:
foo ok 98.
其輸入也是一樣的,那我們輸入a呢:
skip ex:97.
這里97是a的ascii碼,也就是其是跟上面的異常流程處理是一樣的,是不是感覺很奇葩。
你肯定在想,為什么,按照理論上來(lái)說(shuō),setjmp后==0,foo才會(huì)執(zhí)行,按照我們的傳統(tǒng)流程,既然foo被執(zhí)行了,那else應(yīng)該永遠(yuǎn)得不到執(zhí)行,那longjmp又是如何從foo里面跑回去了main?
我們來(lái)設(shè)想一下,else要如何才能被執(zhí)行?
對(duì)了,肯定是jmp_ref != 0嘛,沒錯(cuò),longjmp做的就是這個(gè)工作。
我們先不要在意jmp_buf,我們先看下longjmp的第二個(gè)參數(shù),他是一個(gè)值類型,這個(gè)參數(shù)我指定的是'a',也就是97,你看到了,我在prntf里面打印了jmp_ret的值,也就是,我們?cè)趌ongjmp時(shí)指定某一個(gè)值后,longjmp會(huì)把當(dāng)前函數(shù)的流程做一個(gè)大轉(zhuǎn)彎,直接跳回到這里:
if (jmp_ret == 0) //try
而此時(shí),jmp_ret已經(jīng)是我們指定的值,就是97了,那if的==不會(huì)被成立,則去執(zhí)行else了。
此時(shí)可能你想,如果我這樣:
longjmp(jb,0);
那不是jmp_ret還是==0,還又去執(zhí)行foo,又被longjmp,不是死循環(huán)了么?
這個(gè)情況在CRT已經(jīng)考慮過(guò)了,如果你給longjmp使用0值,其會(huì)自動(dòng)修改為1,也就是0值是永遠(yuǎn)不會(huì)被出現(xiàn)的。
好,我們來(lái)總結(jié):
1、首先setjmp需要==0才執(zhí)行foo。
2、foo發(fā)現(xiàn)錯(cuò)誤,把setjmp的==0給改了。
3、if表達(dá)式的else被執(zhí)行。
可能你現(xiàn)在頭還有點(diǎn)暈,不過(guò)我們先說(shuō)這個(gè)到這里,我們來(lái)看setjmp的第一個(gè)參數(shù):jmp_buf。
這個(gè)jmp_buf是什么呢,首先我們來(lái)再寫一個(gè)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,則會(huì)輸出:
skip ex:97.
hello.
你肯定想這是當(dāng)然的,因?yàn)閟z變量在main范圍內(nèi)嘛。
但是別忘了,我們?cè)L問(wèn)sz可是在else里,也就是我們?cè)L問(wèn)的時(shí)候,是被longjmp跳過(guò)去的。。。
要知道,執(zhí)行foo的時(shí)候,可能整個(gè)堆棧環(huán)境已經(jīng)變得離譜了,如果你知曉匯編,肯定知道,執(zhí)行foo的時(shí)候,main使用堆棧指針EBP(當(dāng)然也可以直接ESP,不過(guò)這里做一個(gè)比方)會(huì)被保存起來(lái),要等f(wàn)oo進(jìn)行return的時(shí)候,才會(huì)恢復(fù)EBP,然后main的局部變量才能通過(guò)EBP訪問(wèn)到,但是我們的foo可是直接longjmp的,我們沒有任何代碼用于恢復(fù)EBP的值,那如何保證飛過(guò)去else的時(shí)候,訪問(wèn)sz變量的地址是正確的?
對(duì)了,在setjmp的時(shí)候,CRT會(huì)把EBP等變量的值保存在jmp_buf里面,然后在longjmp里面,把EBP的值從jmp_buf里面取出來(lái),進(jìn)行恢復(fù)。
這樣在執(zhí)行l(wèi)ongjmp的時(shí)候,EBP會(huì)被恢復(fù)到setjmp時(shí)的情況,也就保證了sz變量的地址在執(zhí)行else的時(shí)候也是正確的。
如果你只會(huì)C語(yǔ)言,那看到這里,你應(yīng)該大概理解了,如果你還了解過(guò)匯編,那可以繼續(xù)看下去,我會(huì)為你揭示setjmp、longjmp背后的一些東西。
【深入探索】
我們把剛才那個(gè)exe進(jìn)行動(dòng)態(tài)反匯編,以便我們整體的了解setjmp和longjmp的所有情況。
首先在調(diào)試器里面,main是這樣的:
可以看到,關(guān)鍵就是在TEST EAX,EAX這里有一個(gè)JNZ跳,如果不是0則跳到下面的catch。
我們來(lái)看setjmp的匯編:
可以看到其保存了幾個(gè)windows關(guān)鍵的寄存器。
注意,在win32下,eax、edx、ecx被定義為易失寄存器,比如我們調(diào)用foo的時(shí)候,如果foo需要用到ebx,esi,它也需要保存,退出時(shí)恢復(fù),但是使用edx則不需要保存。
setjmp也是遵循這個(gè)原則。
可以看到setjmp的返回是XOR EAX,EAX,就是返回0。
好我們來(lái)看longjmp的反匯編:
可以看到其檢測(cè)了一下jmp_buf的正確性,然后就進(jìn)行寄存器的恢復(fù),最終把call自身的堆棧平衡了后,就使用JMP指令直接JMP到setjmp后的那個(gè)指令地址,而此時(shí)其把EAX改成了longjmp的第二個(gè)參數(shù):
那接下來(lái)的TEST EAX,EAX肯定不會(huì)成功,就會(huì)跑去執(zhí)行catch了。
【與C++的結(jié)合】
文章寫到這里,應(yīng)該快結(jié)束了,可還有一個(gè)點(diǎn),可能你沒注意到,我們還是回到我們第一個(gè)代碼——CFoo這個(gè)類來(lái)。
在上面的C++異常里面,我們看到了這樣的代碼:
void?foo(int?exp) { ????CFoo?cfoo; ????if?(exp?==?'a') ????????throw?std::exception("a"); ????printf("foo?ok?%d.n",exp); }
按照C++的規(guī)范,異常發(fā)生的時(shí)候,cfoo也會(huì)被析構(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); }
你肯定會(huì)想,cfoo應(yīng)該只會(huì)被構(gòu)造,而不會(huì)被析構(gòu),因?yàn)閘ongjmp可是CRT的函數(shù)。
其實(shí)原來(lái)我也是這樣想的,但是我不懂是不是VC spec,我在跟蹤longjmp的時(shí)候發(fā)現(xiàn)了堆棧展開的代碼。。。
也就是,其實(shí)cfoo在longjmp的時(shí)候,也是會(huì)被析構(gòu)的:
Create CFoo.
~Destroy CFoo.
skip ex:97.
hello.
這個(gè)要注意一下。
如果你想看匯編,在下面。
這個(gè)是foo函數(shù)的匯編:
SEH處理器在這里:
然后會(huì)展開到析構(gòu)函數(shù):
【完結(jié)】
為這2個(gè)狗血的東西寫了那么多,也說(shuō)的差不多了。
其實(shí)這2個(gè)東西,因?yàn)槠浞慈祟惖奶匦?,在?xiàng)目開發(fā)中,不應(yīng)該被使用上,在這里只是告訴大家,如果遇到有setjmp、longjmp的情況的時(shí)候,可以判斷出來(lái)代碼的執(zhí)行流程。