




版權(quán)說(shuō)明:本文檔由用戶(hù)提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
1、C+編譯器如何實(shí)現(xiàn)異常處理譯者注:本文在網(wǎng)上已經(jīng)有幾個(gè)譯本,但都不完整,所以我決定自己把它翻譯過(guò)來(lái)。雖然力求信、雅、達(dá),但鑒于這是我的第一次翻譯經(jīng)歷,不足之處敬請(qǐng)諒解并指出。與傳統(tǒng)語(yǔ)言相比,C+的一項(xiàng)革命性創(chuàng)新就是它支持異常處理。傳統(tǒng)的錯(cuò)誤處理方式經(jīng)常滿(mǎn)足不了要求,而異常處理則是一個(gè)極好的替代解決方案。它將正常代碼和錯(cuò)誤處理代碼清晰的劃分開(kāi)來(lái),程序變得非常干凈并且容易維護(hù)。本文討論了編譯器如何實(shí)現(xiàn)異常處理。我將假定你已經(jīng)熟悉異常處理的語(yǔ)法和機(jī)制。本文還提供了一個(gè)用于VC+的異常處理庫(kù),要用庫(kù)中的處理程序替換掉VC+提供的那個(gè),你只需要調(diào)用下面這個(gè)函數(shù): install_my_handler(;
2、之后,程序中的所有異常,從它們被拋出到堆棧展開(kāi)(stack unwinding),再到調(diào)用catch塊,最后到程序恢復(fù)正常運(yùn)行,都將由我的異常處理庫(kù)來(lái)管理。與其它C+特性一樣,C+標(biāo)準(zhǔn)并沒(méi)有規(guī)定編譯器應(yīng)該如何來(lái)實(shí)現(xiàn)異常處理。這意味著每一個(gè)編譯器的提供商都可以用它們認(rèn)為恰當(dāng)?shù)姆绞絹?lái)實(shí)現(xiàn)它。下面我會(huì)描述一下VC+是怎么做的,但即使你使用其它的編譯器或操作系統(tǒng),本文也應(yīng)該會(huì)是一篇很好的學(xué)習(xí)材料。VC+的實(shí)現(xiàn)方式是以windows系統(tǒng)的結(jié)構(gòu)化異常處理(SEH)為基礎(chǔ)的。 結(jié)構(gòu)化異常處理概述在本文的討論中,我認(rèn)為異?;蛘呤潜幻鞔_的拋出的,或者是由于除零溢出、空指針訪(fǎng)問(wèn)等引起的。當(dāng)它發(fā)生時(shí)會(huì)產(chǎn)生一個(gè)中斷,
3、接下來(lái)控制權(quán)就會(huì)傳遞到操作系統(tǒng)的手中。操作系統(tǒng)將調(diào)用異常處理程序,檢查從異常發(fā)生位置開(kāi)始的函數(shù)調(diào)用序列,進(jìn)行堆棧展開(kāi)和控制權(quán)轉(zhuǎn)移。Windows定義了結(jié)構(gòu)“EXCEPTION_REGISTRATION”,使我們能夠向操作系統(tǒng)注冊(cè)自己的異常處理程序。 struct EXCEPTION_REGISTRATIONEXCEPTION_REGISTRATION* prev;DWORD handler; 注冊(cè)時(shí),只需要?jiǎng)?chuàng)建這樣一個(gè)結(jié)構(gòu),然后把它的地址放到FS段偏移0的位置上去就行了。下面這句匯編代碼演示了這一操作:mov FS:0, exc_regpprev字段用于建立一個(gè)EXCEPTION_REGIST
4、RATION結(jié)構(gòu)的鏈表,每次注冊(cè)新的EXCEPTION_REGISTRATION時(shí),我們都要把原來(lái)注冊(cè)的那個(gè)的地址存到prev中。那么,那個(gè)異常回調(diào)函數(shù)長(zhǎng)什么樣呢?在excpt.h中,windows定義了它的原形:EXCEPTION_DISPOSITION (*handler( _EXCEPTION_RECORD *ExcRecord, void* EstablisherFrame, _CONTEXT *ContextRecord, void* DispatcherContext; 不要管它的參數(shù)和返回值,我們先來(lái)看一個(gè)簡(jiǎn)單的例子。下面的程序注冊(cè)了一個(gè)異常處理程序,然后通過(guò)除以零產(chǎn)生了一個(gè)異常
5、。異常處理程序捕獲了它,打印了一條消息就完事大吉并退出了。#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 &
6、lt;< "In the exception handler" << endl; cout << "Just a demo. exiting." << endl; exit(0; return ExceptionContinueExecution; /不會(huì)運(yùn)行到這 int g_div = 0; void bar( /初始化一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu) EXCEPTION_REGISTRATION reg, *preg = ® reg.handler = (DWORDmyHandl
7、er; /取得當(dāng)前異常處理鏈的“頭” DWORD prev; _asm mov EAX, FS:0 mov prev, EAX reg.prev = (EXCEPTION_REGISTRATION* prev; /注冊(cè)! _asm mov EAX, preg mov FS:0, EAX /產(chǎn)生一個(gè)異常 int j = 10 / g_div; /異常,除零溢出 int main( bar(; return 0; /*-輸出- In the exception handler Just a demo. exiting. -*/注意EXCEPTION_REGISTRATION必須定義在棧上,并且必須
8、位于比上一個(gè)結(jié)點(diǎn)更低的內(nèi)存地址上,Windows對(duì)此有嚴(yán)格要求,達(dá)不到的話(huà),它就會(huì)立刻終止進(jìn)程。函數(shù)和堆棧堆棧是用來(lái)保存局部對(duì)象的連續(xù)內(nèi)存區(qū)。更明確的說(shuō),每個(gè)函數(shù)都有一個(gè)相關(guān)的棧楨(stack frame)來(lái)保存它所有的局部對(duì)象和表達(dá)式計(jì)算過(guò)程中用到的臨時(shí)對(duì)象,至少理論上是這樣的。但現(xiàn)實(shí)中,編譯器經(jīng)常會(huì)把一些對(duì)象放到寄存器中以便能以更快的速度訪(fǎng)問(wèn)。堆棧是一個(gè)處理器(CPU)層次的概念,為了操縱它,處理器提供了一些專(zhuān)用的寄存器和指令。 圖1是一個(gè)典型的堆棧,它示出了函數(shù)foo調(diào)用bar,bar又調(diào)用widget時(shí)的情景。請(qǐng)注意堆棧是向下增長(zhǎng)的,這意味著新壓入的項(xiàng)的地址低于原有項(xiàng)的地址。通常編譯器
9、使用EBP寄存器來(lái)指示當(dāng)前活動(dòng)的棧楨。本例中,CPU正在運(yùn)行widget,所以圖中的EBP指向了widget的棧楨。編譯器在編譯時(shí)將所有局部對(duì)象解析成相對(duì)于棧楨指針(EBP)的固定偏移,函數(shù)則通過(guò)棧楨指針來(lái)間接訪(fǎng)問(wèn)局部對(duì)象。舉個(gè)例子,典型的,widget訪(fǎng)問(wèn)它的局部變量時(shí)就是通過(guò)訪(fǎng)問(wèn)棧楨指針以下的、有著確定位置的幾個(gè)字節(jié)來(lái)實(shí)現(xiàn)的,比如說(shuō)EBP-24。上圖中也畫(huà)出了ESP寄存器,它叫棧指針,指向棧的最后一項(xiàng)。在本例中,ESP指著widget的棧楨的末尾,這也是下一個(gè)棧楨(如果它被創(chuàng)建的話(huà))的開(kāi)始位置。處理器支持兩種類(lèi)型的棧操作:壓棧(push)和彈棧(pop)。比如,pop EAX的作用是從ES
10、P所指的位置讀出4字節(jié)放到EAX寄存器中,并把ESP加上(記住,棧是向下增長(zhǎng)的)4(在32位處理器上);類(lèi)似的,push EBP的作用是把ESP減去4,然后將EBP的值放到ESP指向的位置中去。 編譯器編譯一個(gè)函數(shù)時(shí),會(huì)在它的開(kāi)頭添加一些代碼來(lái)為其創(chuàng)建并初始化棧楨,這些代碼被稱(chēng)為序言(prologue);同樣,它也會(huì)在函數(shù)的結(jié)尾處放上代碼來(lái)清除棧楨,這些代碼叫做尾聲(epilogue)。 一般情況下,序言是這樣的:Push EBP ; 把原來(lái)的棧楨指針保存到棧上 Mov EBP, ESP ; 激活新的棧楨 Sub ESP, 10 ; 減去一個(gè)數(shù)字,讓ESP指向棧楨的末尾第一條指令把原來(lái)的棧楨指
11、針EBP保存到棧上;第二條指令通過(guò)讓EBP指向主調(diào)函數(shù)的EBP的保存位置來(lái)激活被調(diào)函數(shù)的棧楨;第三條指令把ESP減去了一個(gè)數(shù)字,這樣ESP就指向了當(dāng)前棧楨的末尾,而這個(gè)數(shù)字是函數(shù)要用到的所有局部對(duì)象和臨時(shí)對(duì)象的大小。編譯時(shí),編譯器知道函數(shù)的所有局部對(duì)象的類(lèi)型和“體積”,所以,它能很容易的計(jì)算出棧楨的大小。 尾聲所做的正好和序言相反,它必須把當(dāng)前棧楨從棧上清除掉:Mov ESP, EBP Pop EBP ; 激活主調(diào)函數(shù)的棧楨 Ret ; 返回主調(diào)函數(shù)它讓ESP指向主調(diào)函數(shù)的棧楨指針的保存位置(也就是被調(diào)函數(shù)的棧楨指針指向的位置),彈出EBP從而激活主調(diào)函數(shù)的棧楨,然后返回主調(diào)函數(shù)。一旦CPU遇
12、到返回指令,它就要做以下兩件事:把返回地址從棧中彈出,然后跳轉(zhuǎn)到那個(gè)地址去。返回地址是主調(diào)函數(shù)執(zhí)行call指令調(diào)用被調(diào)函數(shù)時(shí)自動(dòng)壓棧的。Call指令執(zhí)行時(shí),會(huì)先把緊隨在它后面的那條指令的地址(被調(diào)函數(shù)的返回地址)壓入棧中,然后跳轉(zhuǎn)到被調(diào)函數(shù)的開(kāi)始位置。圖2更詳細(xì)的描繪了運(yùn)行時(shí)的堆棧。如圖所示,主調(diào)函數(shù)把被調(diào)函數(shù)的參數(shù)也壓進(jìn)了堆棧,所以參數(shù)也是棧楨的一部分。函數(shù)返回后,主調(diào)函數(shù)需要移除這些參數(shù),它通過(guò)把所有參數(shù)的總體積加到ESP上來(lái)達(dá)到目的,而這個(gè)體積可以在編譯時(shí)知道: Add ESP, args_size當(dāng)然,也可以把參數(shù)的總體積寫(xiě)在被調(diào)函數(shù)的返回指令的后面,讓被調(diào)函數(shù)去移除參數(shù),下面的指令就
13、在返回主調(diào)函數(shù)前從棧中移去了24個(gè)字節(jié):Ret 24取決于被調(diào)函數(shù)的調(diào)用約定(call convention),這兩種方式每次只能用一個(gè)。你還要注意的是每個(gè)線(xiàn)程都有自己獨(dú)立的堆棧。C+和異常 回憶一下我在第一節(jié)中介紹的EXCEPTION_REGISTRATION結(jié)構(gòu),我們?cè)盟虿僮飨到y(tǒng)注冊(cè)了發(fā)生異常時(shí)要被調(diào)用的回調(diào)函數(shù)。VC+也是這么做的,不過(guò)它擴(kuò)展了這個(gè)結(jié)構(gòu)的語(yǔ)義,在它的后面添加了兩個(gè)新字段:struct EXCEPTION_REGISTRATION EXCEPTION_REGISTRATION* prev; DWORD handler; int id; DWORD ebp; ; VC+會(huì)
14、為絕大部分函數(shù)添加一個(gè)EXCEPTION_REGISTRATION類(lèi)型的局部變量,它的最后一個(gè)字段(ebp)與棧楨指針指向的位置重疊。函數(shù)的序言創(chuàng)建這個(gè)結(jié)構(gòu)并把它注冊(cè)給操作系統(tǒng),尾聲則恢復(fù)主調(diào)函數(shù)的EXCEPTION_REGISTRATION。id字段的意義我將在下一節(jié)介紹。VC+編譯函數(shù)時(shí)會(huì)為它生成兩部分?jǐn)?shù)據(jù):a)異?;卣{(diào)函數(shù) b)一個(gè)包含函數(shù)重要信息的數(shù)據(jù)結(jié)構(gòu),這些信息包括catch塊、這些塊的地址和這些塊所關(guān)心的異常的類(lèi)型等等。我把這個(gè)結(jié)構(gòu)稱(chēng)為funcinfo,有關(guān)它的詳細(xì)討論也在下一節(jié)。 圖3是考慮了異常處理之后的運(yùn)行時(shí)堆棧。widget的異常回調(diào)函數(shù)位于由FS:0指向的異常處理鏈的開(kāi)
15、始位置(這是由widget的序言設(shè)置的)。異常處理程序把widget的funcinfo結(jié)構(gòu)的地址交給函數(shù)_CxxFrameHandler,_CxxFrameHandler會(huì)檢查這個(gè)結(jié)構(gòu)看函數(shù)中有沒(méi)有catch塊對(duì)當(dāng)前的異常感興趣。如果沒(méi)有的話(huà),它就返回ExceptionContinueSearch給操作系統(tǒng),于是操作系統(tǒng)會(huì)從異常處理鏈表中取得下一個(gè)結(jié)點(diǎn),并調(diào)用它的異常處理程序(也就是調(diào)用當(dāng)前函數(shù)的那個(gè)函數(shù)的異常處理程序)。 這一過(guò)程將一直進(jìn)行下去直到處理程序找到一個(gè)能處理當(dāng)前異常的catch塊為止,這時(shí)它就不再返回操作系統(tǒng)了。但是在調(diào)用catch塊之前(由于有funcinfo結(jié)構(gòu),所以知道ca
16、tch塊的入口,參見(jiàn)圖3),必須進(jìn)行堆棧展開(kāi),也就是清理掉當(dāng)前函數(shù)的棧楨下面的所有其他的棧楨。這個(gè)操作稍微有點(diǎn)復(fù)雜,因?yàn)椋寒惓L幚沓绦虮仨氄业疆惓0l(fā)生時(shí)生存在這些棧楨上的所有局部對(duì)象,并依次調(diào)用它們的析構(gòu)函數(shù)。后面我將對(duì)此進(jìn)行詳細(xì)介紹。異常處理程序把這項(xiàng)工作委托給了各個(gè)棧楨自己的異常處理程序。從FS:0指向的異常處理鏈的第一個(gè)結(jié)點(diǎn)開(kāi)始,它依次調(diào)用每個(gè)結(jié)點(diǎn)的處理程序,告訴它堆棧正在展開(kāi)。與之相呼應(yīng),這些處理程序會(huì)調(diào)用每個(gè)局部對(duì)象的析構(gòu)函數(shù),然后返回。此過(guò)程一直進(jìn)行到與異常處理程序自身相對(duì)應(yīng)的那個(gè)結(jié)點(diǎn)為止。 由于catch塊是函數(shù)的一部分,所以它使用的也是函數(shù)的棧楨。因此,在調(diào)用catch塊之前,
17、異常處理程序必須激活它所隸屬的函數(shù)的棧楨。其次,每個(gè)catch塊都只接受一個(gè)參數(shù),其類(lèi)型是它希望捕獲的異常的類(lèi)型。異常處理程序必須把異常對(duì)象本身或者是異常對(duì)象的引用拷貝到catch塊的棧楨上,編譯器在funcinfo中記錄了相關(guān)信息,處理程序根據(jù)這些信息就能知道到哪去拷貝異常對(duì)象了??截愅戤惓2⒓せ顥E后,處理程序?qū)⒄{(diào)用catch塊。而catch塊將把控制權(quán)下一步要轉(zhuǎn)移到的地址返回來(lái)。請(qǐng)注意:雖然這時(shí)堆棧已經(jīng)展開(kāi),棧楨也都被清除了,但它們占據(jù)的內(nèi)存空間并沒(méi)有被覆蓋,所有的數(shù)據(jù)都還好好的待在棧上。這是因?yàn)楫惓L幚沓绦蛉栽趫?zhí)行,象其他函數(shù)一樣,它也需要棧來(lái)存放自己的局部對(duì)象,而其棧楨就位于發(fā)生異常
18、的那個(gè)函數(shù)的棧楨的下面。catch塊返回以后,異常處理程序需要“殺掉”異常對(duì)象。此后,它讓ESP指向目標(biāo)函數(shù)(控制權(quán)要轉(zhuǎn)移到的那個(gè)函數(shù))的棧楨的末尾這樣就把(包括它自己的在內(nèi)的)所有棧楨都刪除了,然后再跳轉(zhuǎn)到catch塊返回的那個(gè)地址去,就勝利的完成整個(gè)異常處理任務(wù)了。但它怎么知道目標(biāo)函數(shù)的棧楨末尾在哪呢?事實(shí)上它沒(méi)法知道,所以編譯器把這個(gè)地址保存到了棧楨上(由前言來(lái)完成),如圖3所示,棧楨指針EBP下面第16個(gè)字節(jié)就是。當(dāng)然,catch塊也可能拋出新異常,或者是將原來(lái)的異常重新拋出。處理程序必須對(duì)此有所準(zhǔn)備。如果是拋出新異常,它必須殺掉原來(lái)的那個(gè);而如果是重新拋出原來(lái)的異常,它必須能繼續(xù)傳播
19、(propagate)這個(gè)異常。這里我要特別強(qiáng)調(diào)一點(diǎn):由于每個(gè)線(xiàn)程有自己獨(dú)立的堆棧,所以每個(gè)線(xiàn)程也都有自己獨(dú)立的、由FS:0指向的EXCEPTION_REGISTRATION鏈。C+和異常2圖4是funcinfo的布局,注意這里的字段名可能與VC+編譯器實(shí)際使用的不完全一致,而且我也只給出了和我們的討論相關(guān)的字段。堆棧展開(kāi)表(unwind table)的結(jié)構(gòu)留到下節(jié)再討論。 異常處理程序在函數(shù)中查找catch塊時(shí),它首先要判斷異常發(fā)生的位置是否在當(dāng)前函數(shù)(發(fā)生異常的那個(gè)函數(shù))的一個(gè)try塊中。是則查找與此try塊相關(guān)的catch塊表,否則直接返回。 先來(lái)看看它怎樣找try塊。編譯時(shí),編譯器給每
20、個(gè)try塊都分配了start id和end id。通過(guò)funcinfo結(jié)構(gòu),異常處理程序可以訪(fǎng)問(wèn)這兩個(gè)id,見(jiàn)圖4。編譯器為函數(shù)中的每個(gè)try塊都生成了相關(guān)的數(shù)據(jù)結(jié)構(gòu)。 上一節(jié)中,我說(shuō)過(guò)VC+給EXCEPTION_REGISTRATION結(jié)構(gòu)加上了一個(gè)id字段?;貞浺幌聢D3,這個(gè)結(jié)構(gòu)位于函數(shù)的棧楨上。異常發(fā)生時(shí),處理程序讀出這個(gè)值,看它是否在try塊的兩個(gè)id確定的區(qū)間start id,end id中。是的話(huà),異常就發(fā)生在這個(gè)try塊中;否則繼續(xù)查看try塊表中的下一個(gè)try塊。誰(shuí)負(fù)責(zé)更新id的值,它的值又應(yīng)該是什么呢?原來(lái),編譯器會(huì)在函數(shù)的多個(gè)位置安插代碼來(lái)更新id的值,以反應(yīng)程序的實(shí)時(shí)運(yùn)行
21、狀態(tài)。比如說(shuō),編譯器會(huì)在進(jìn)入try塊的地方加上一條語(yǔ)句,把try塊的start id寫(xiě)到棧楨上。找到try塊后,處理程序就遍歷與其關(guān)聯(lián)的catch塊表,看是否有對(duì)當(dāng)前異常感興趣的catch塊。在try塊發(fā)生嵌套時(shí),異常將既源于內(nèi)層try塊,也源于外層try塊。這種情況下,處理程序應(yīng)該按先內(nèi)后外的順序查找catch塊。但它其實(shí)沒(méi)必要關(guān)心這些,因?yàn)?,在try塊表中,VC+總是把內(nèi)層try塊放在外層try塊的前面。異常處理程序還有一個(gè)難題就是“如何根據(jù)catch塊的相關(guān)數(shù)據(jù)結(jié)構(gòu)判斷這個(gè)catch塊是否愿意處理當(dāng)前異?!?。這是通過(guò)比較異常的類(lèi)型和catch塊的參數(shù)的類(lèi)型來(lái)完成的。例如下面這個(gè)程序:vo
22、id foo( try throw E(; catch(H /. 如果H和E的類(lèi)型完全相同的話(huà),catch塊就要捕獲這個(gè)異常。這意味著處理程序必須在運(yùn)行時(shí)進(jìn)行類(lèi)型比較,對(duì)C等語(yǔ)言來(lái)說(shuō),這是不可能的,因?yàn)樗鼈儫o(wú)法在運(yùn)行時(shí)得到對(duì)象的類(lèi)型。C+則不同,它有了運(yùn)行時(shí)類(lèi)型識(shí)別(runtime type identification,RTTI),并提供了運(yùn)行時(shí)類(lèi)型比較的標(biāo)準(zhǔn)方法。C+在標(biāo)準(zhǔn)頭文件中定義了type_info類(lèi),它能在運(yùn)行時(shí)代表一個(gè)類(lèi)型。catch塊數(shù)據(jù)結(jié)構(gòu)的第二個(gè)字段(ptype_info,見(jiàn)圖4)是一個(gè)指向type_info結(jié)構(gòu)的指針,它在運(yùn)行時(shí)就代表catch塊的參數(shù)類(lèi)型。type_in
23、fo也重載了=運(yùn)算符,能夠指出兩種類(lèi)型是否完全相同。這樣,異常處理程序只要比較(調(diào)用=運(yùn)算符)catch塊參數(shù)的type_info(可以通過(guò)catch塊的相關(guān)數(shù)據(jù)結(jié)構(gòu)來(lái)訪(fǎng)問(wèn))和異常的type_info是否相同,就能知道catch塊是不是愿意捕獲當(dāng)前異常了。catch塊的參數(shù)類(lèi)型可以通過(guò)funcinfo結(jié)構(gòu)得到,但異常的type_info從哪來(lái)呢?當(dāng)編譯器碰到 throw E(;這條語(yǔ)句時(shí),它會(huì)為異常生成一個(gè)excpt_info結(jié)構(gòu),如圖5所示。還是要提醒你注意這里用的名字可能與VC+使用的不一致,而且仍然只有與我們的討論相關(guān)的字段。從圖中可以看出,異常的type_info可以通過(guò)excpt_i
24、nfo結(jié)構(gòu)得到。由于異常處理程序需要拷貝異常對(duì)象(在調(diào)用catch塊之前),也需要消除掉它(在調(diào)用catch塊之后),所以編譯器在這個(gè)結(jié)構(gòu)中同時(shí)提供了異常的拷貝構(gòu)造函數(shù)、大小和析構(gòu)函數(shù)的信息。 在catch塊的參數(shù)是基類(lèi),而異常是派生類(lèi)時(shí),異常處理程序也應(yīng)該調(diào)用catch塊。然而,這種情況下,比較它們的type_info絕對(duì)是不相等,因?yàn)樗鼈儽緛?lái)就不是相同的類(lèi)型。而且,type_info類(lèi)也沒(méi)有提供任何其他函數(shù)或運(yùn)算符來(lái)指出一個(gè)類(lèi)是另一個(gè)類(lèi)的基類(lèi)。但異常處理程序還必須得去調(diào)用catch塊!為了解決這個(gè)問(wèn)題,編譯器只能為處理程序提供更多的信息:如果異常是派生類(lèi),那么etypeinfo_table
25、(通過(guò)excpt_info訪(fǎng)問(wèn))將包含多個(gè)指向etype_info(擴(kuò)展了type_info,這個(gè)名字是我啟的)的指針,它們分別指向了各個(gè)基類(lèi)的etype_info。這樣,處理程序就可以把catch塊的參數(shù)和所有這些type_info比較,只要有一個(gè)相同,就調(diào)用catch塊。在結(jié)束這一部分之前,還有最后一個(gè)問(wèn)題:異常處理程序是怎么知道異常和excpt_info結(jié)構(gòu)的?下面我就要回答這個(gè)問(wèn)題。 VC+會(huì)把throw語(yǔ)句翻譯成下面的樣子: /throw E(; /編譯器會(huì)為E生成excpt_info結(jié)構(gòu) E e = E(; /在棧上創(chuàng)建異常 _CxxThrowException(&e, E
26、_EXCPT_INFO_ADDR;_CxxThrowException會(huì)把控制權(quán)連帶它的兩個(gè)參數(shù)都交給操作系統(tǒng)(控制權(quán)轉(zhuǎn)移是通過(guò)軟件中斷實(shí)現(xiàn)的,請(qǐng)參見(jiàn)RaiseException)。而操作系統(tǒng),在為調(diào)用異?;卣{(diào)函數(shù)做準(zhǔn)備時(shí),會(huì)把這兩個(gè)參數(shù)打包到一個(gè)_EXCEPTION_RECORD結(jié)構(gòu)中。接著,它從EXCEPTION_REGISTRATION鏈表的頭結(jié)點(diǎn)(由FS:0指向)開(kāi)始,依次調(diào)用各節(jié)點(diǎn)的異常處理程序。而且,指向當(dāng)前EXCEPTION_REGISTRATION結(jié)構(gòu)的指針也會(huì)作為異常處理程序的第二個(gè)參數(shù)出現(xiàn)。前面已經(jīng)說(shuō)過(guò),VC+中的每個(gè)函數(shù)都在棧上創(chuàng)建并注冊(cè)了EXCEPTION_REGIST
27、RATION結(jié)構(gòu)。所以傳遞這個(gè)參數(shù)可以讓處理程序知道很多重要信息,比如說(shuō):EXCEPTION_REGISTRATION的id字段(用于查找catch塊)、函數(shù)的棧楨(用于清理?xiàng)E)和EXCEPTION_REGISTRATION結(jié)點(diǎn)在異常鏈表中的位置(用于堆棧展開(kāi))等。第一個(gè)參數(shù)是指向_EXCEPTION_RECORD結(jié)構(gòu)的指針,通過(guò)它可以找到異常和它的excpt_info結(jié)構(gòu)。下面是excpt.h中定義的異?;卣{(diào)函數(shù)的原型: EXCEPTION_DISPOSITION (*handler( _EXCEPTION_RECORD* ExcRecord, void* EstablisherFrame
28、, _CONTEXT *ContextRecord, void* DispatcherContext;后兩個(gè)參數(shù)和我們的討論關(guān)系不大。函數(shù)的返回值是一個(gè)枚舉類(lèi)型(也在excpt.h中定義),我前面已經(jīng)說(shuō)過(guò),如果處理程序找不到catch塊,它就會(huì)向系統(tǒng)返回ExceptionContinueSearch,對(duì)本文而言,我們只要知道這一個(gè)返回值就行了。_EXCEPTION_RECORD結(jié)構(gòu)是在winnt.h中定義的: struct _EXCEPTION_RECORD DWORD ExceptionCode; DWORD ExceptionFlags; _EXCEPTION_RECORD* ExcRec
29、ord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation15; EXCEPTION_RECORD; ExceptionInformation數(shù)組中元素的個(gè)數(shù)和類(lèi)型取決于ExceptionCode字段。如果是C+異常(異常代碼是0xe06d7363,源于throw語(yǔ)句),那么數(shù)組中將包含指向異常和excpt_info結(jié)構(gòu)的指針;如果是其他異常,那數(shù)組中基本上就不會(huì)有什么內(nèi)容,這些異常包括除零溢出、訪(fǎng)問(wèn)違例等,你可以在winnt.h中找到它們的異常代碼。ExceptionFlags字段用于告訴異常
30、處理程序應(yīng)該采取什么操作。如果它是EH_UNWINDING(見(jiàn)Except.inc),那是說(shuō)堆棧正在展開(kāi),這時(shí),處理程序要清理?xiàng)E,然后返回。否則處理程序應(yīng)該在函數(shù)中查找catch塊并調(diào)用它。清理?xiàng)E意味著必須找到異常發(fā)生時(shí)生存在棧楨上的所有局部對(duì)象,并調(diào)用其析構(gòu)函數(shù),下一節(jié)我們將就此進(jìn)行詳細(xì)討論。清理?xiàng)EC+標(biāo)準(zhǔn)明確指出:堆棧展開(kāi)工作必須調(diào)用異常發(fā)生時(shí)所有生存的局部對(duì)象的析構(gòu)函數(shù)。如下面的代碼: int g_i = 0; void foo( T o1, o2; T o3; 10/g_i; /這里會(huì)發(fā)生異常 T o4; /. foo有o1、o2、o3、o4四個(gè)局部對(duì)象,但異常發(fā)生時(shí),o3已經(jīng)“
31、死亡”,o4還未“出生”,所以異常處理程序應(yīng)該只調(diào)用o1和o2的析構(gòu)函數(shù)。前面已經(jīng)說(shuō)過(guò),編譯器會(huì)在函數(shù)的很多地方安插代碼來(lái)記錄當(dāng)前的運(yùn)行狀態(tài)。實(shí)際上,編譯器在函數(shù)中設(shè)置了一些關(guān)鍵區(qū)域,并為它們分配了id,進(jìn)入關(guān)鍵區(qū)域時(shí)要記錄它的id,退出時(shí)恢復(fù)前一個(gè)id。try塊就是一個(gè)例子,其id就是start id。所以,在try塊的入口,編譯器會(huì)把它的start id記到棧楨上去。局部對(duì)象從創(chuàng)建到銷(xiāo)毀也確定了一個(gè)關(guān)鍵區(qū)域,或者,換句話(huà)說(shuō),編譯器給每個(gè)局部對(duì)象分配了唯一的id,例如下面的程序: void foo( T t1; /. 編譯器會(huì)在t1的定義后面(也就是t1創(chuàng)建以后),把它的id寫(xiě)到棧楨上: v
32、oid foo( T t1; _id = t1_id; /編譯器插入的語(yǔ)句 /. 上面的_id是編譯器偷偷創(chuàng)建的局部變量,它的位置與EXCEPTION_REGISTRATION的id字段重疊。類(lèi)似的,在調(diào)用對(duì)象的析構(gòu)函數(shù)前,編譯器會(huì)恢復(fù)前一個(gè)關(guān)鍵區(qū)域的id。清理?xiàng)E時(shí),異常處理程序讀出id的值(通過(guò)EXCEPTION_REGISTRATION結(jié)構(gòu)的id字段或棧楨指針EBP下面的4個(gè)字節(jié)來(lái)訪(fǎng)問(wèn))。這個(gè)id可以表明,函數(shù)在運(yùn)行到與它相關(guān)聯(lián)的那個(gè)點(diǎn)之前沒(méi)有發(fā)生異常。所有在這一點(diǎn)之前定義的對(duì)象都已初始化,應(yīng)該調(diào)用這些對(duì)象中的一部分或全部對(duì)象的析構(gòu)函數(shù)。請(qǐng)注意某些對(duì)象是屬于子塊(如前面代碼中的o3)的,
33、發(fā)生異常時(shí)可能已經(jīng)銷(xiāo)毀了,不應(yīng)該調(diào)用它們的析構(gòu)函數(shù)。編譯器還為函數(shù)生成了另一個(gè)數(shù)據(jù)結(jié)構(gòu)堆棧展開(kāi)表(unwindtable,我啟的名字),它是一個(gè)unwind結(jié)構(gòu)的數(shù)組,可通過(guò)funcinfo來(lái)訪(fǎng)問(wèn),如圖4所示。函數(shù)的每個(gè)關(guān)鍵區(qū)域都有一個(gè)unwind結(jié)構(gòu),這些結(jié)構(gòu)在展開(kāi)表中出現(xiàn)的次序和它們所對(duì)應(yīng)的區(qū)域在函數(shù)中的出現(xiàn)次序完全相同。一般unwind結(jié)構(gòu)也會(huì)關(guān)聯(lián)一個(gè)對(duì)象(別忘了,每個(gè)對(duì)象的定義都開(kāi)辟了關(guān)鍵區(qū)域,并有id與其對(duì)應(yīng)),它里面有如何銷(xiāo)毀這個(gè)對(duì)象的信息。每當(dāng)編譯器碰到對(duì)象定義,它就生成一小段代碼,這段代碼知道對(duì)象在棧楨上的地址(就是它相對(duì)于棧楨指針的偏移),并能銷(xiāo)毀它。unwind結(jié)構(gòu)中有一個(gè)
34、字段用于保存這段代碼的入口地址: typedef void (*CLEANUP_FUNC(; struct unwind int prev; CLEANUP_FUNC cf; ; try塊對(duì)應(yīng)的unwind結(jié)構(gòu)的cf字段是空值NULL,因?yàn)闆](méi)有與它對(duì)應(yīng)的對(duì)象,所以也沒(méi)有東西需要它去銷(xiāo)毀。通過(guò)prev字段,這些unwind結(jié)構(gòu)也形成了一個(gè)鏈表。異常處理程序清理?xiàng)E時(shí),會(huì)讀取當(dāng)前的id值,以它為索引取得展開(kāi)表中對(duì)應(yīng)的項(xiàng),并調(diào)用其第二個(gè)字段指向的清理代碼,這樣,那個(gè)與之關(guān)聯(lián)的對(duì)象就被銷(xiāo)毀了。然后,處理程序?qū)⒁援?dāng)前unwind結(jié)構(gòu)的prev字段為索引,繼續(xù)在展開(kāi)表中找下一個(gè)unwind結(jié)構(gòu),調(diào)用其清理
35、代碼。這一過(guò)程將一直重復(fù),直到鏈表的結(jié)尾(prev的值是-1)。圖6畫(huà)出了本節(jié)開(kāi)始時(shí)提到的那段代碼的堆棧展開(kāi)表。 現(xiàn)在把new運(yùn)算符也加進(jìn)來(lái),對(duì)于下面的代碼: T* p = new T(; 系統(tǒng)會(huì)首先為T(mén)分配內(nèi)存,然后調(diào)用它的構(gòu)造函數(shù)。所以,如果構(gòu)造函數(shù)拋出了異常,系統(tǒng)就必須釋放這些內(nèi)存。因此,動(dòng)態(tài)創(chuàng)建那些擁有“有為的構(gòu)造函數(shù)”的類(lèi)型時(shí),VC+也為new運(yùn)算符分配了id,并且堆棧展開(kāi)表中也有與其對(duì)應(yīng)的項(xiàng),其清理代碼將釋放分配的內(nèi)存空間。調(diào)用構(gòu)造函數(shù)前,編譯器把new運(yùn)算符的id存到EXCEPTION_REGISTRATION結(jié)構(gòu)中,構(gòu)造函數(shù)順利返回后,它再把id恢復(fù)成原來(lái)的值。更進(jìn)一步說(shuō),構(gòu)造
36、函數(shù)拋出異常時(shí),對(duì)象可能剛剛構(gòu)造了一部分,如果它有子成員對(duì)象或子基類(lèi)對(duì)象,并且發(fā)生異常時(shí)它們中的一部分已經(jīng)構(gòu)造完成的話(huà),就必須調(diào)用這些對(duì)象的析構(gòu)函數(shù)。和普通函數(shù)一樣,編譯器也給構(gòu)造函數(shù)生成了相關(guān)的數(shù)據(jù)來(lái)幫助完成這個(gè)任務(wù)。展開(kāi)堆棧時(shí),異常處理程序調(diào)用的是用戶(hù)定義的析構(gòu)函數(shù),這一點(diǎn)你必須注意,因?yàn)樗灿锌赡軖伋霎惓?!C+標(biāo)準(zhǔn)規(guī)定堆棧展開(kāi)過(guò)程中,析構(gòu)函數(shù)不能拋出異常,否則系統(tǒng)將調(diào)用std:terminate。 實(shí)現(xiàn)本節(jié)我們討論其他三個(gè)有待詳細(xì)解釋的問(wèn)題: a如何安裝異常處理程序 bcatch塊重新拋出異?;驋伋鲂庐惓r(shí)應(yīng)該如何處理 c如何對(duì)所有線(xiàn)程提供異常處理支持 隨同本文,有一個(gè)演示項(xiàng)目,查看其中
37、的文件可以得到一些編譯方面的幫助。 第一項(xiàng)任務(wù)是安裝異常處理程序,也就是把VC+的處理程序替換掉。從前面的討論中,我們已經(jīng)清楚地知道_CxxFrameHandler函數(shù)是VC+所有異常處理工作的入口。編譯器為每個(gè)函數(shù)都生成一段代碼,它們?cè)诎l(fā)生異常時(shí)被調(diào)用,把相應(yīng)的funcinfo結(jié)構(gòu)的指針交給_CxxFrameHandler。 install_my_handler(函數(shù)會(huì)改寫(xiě)_CxxFrameHandler的入口處的代碼,讓程序跳轉(zhuǎn)到my_exc_handler(函數(shù)。不過(guò),_CxxFrameHandler位于只讀的內(nèi)存頁(yè),對(duì)它的任何寫(xiě)操作都會(huì)導(dǎo)致訪(fǎng)問(wèn)違例,所以必須首先用VirtualProt
38、ectEx把該內(nèi)存頁(yè)的保護(hù)方式改成可讀寫(xiě),等改寫(xiě)完畢后,再改回只讀。寫(xiě)入的數(shù)據(jù)是一個(gè)jmp_instr結(jié)構(gòu)。/install_my_handler.cpp #include #include "install_my_handler.h" /C+默認(rèn)的異常處理程序 extern "C" EXCEPTION_DISPOSITION _CxxFrameHandler( struct _EXCEPTION_RECORD* ExceptionRecord, void* EstablisherFrame, struct _CONTEXT* ContextRecord
39、, void* DispatcherContext ; namespace char cpp_handler_instructions5; bool saved_handler_instructions = false; namespace my_handler /我的異常處理程序 EXCEPTION_DISPOSITION my_exc_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherCon
40、text throw(; #pragma pack(push, 1 struct jmp_instr unsigned char jmp; DWORD offset; ; #pragma pack(pop bool WriteMemory(void* loc, void* buffer, int size HANDLE hProcess = GetCurrentProcess(; /把包含內(nèi)存范圍loc,loc+size的頁(yè)面的保護(hù)方式改成可讀寫(xiě) DWORD old_protection; BOOL ret = VirtualProtectEx(hProcess, loc, si
41、ze, PAGE_READWRITE, &old_protection; if(ret = FALSE return false; ret = WriteProcessMemory(hProcess, loc, buffer, size, NULL; /恢復(fù)原來(lái)的保護(hù)方式 DWORD o2; VirtualProtectEx(hProcess, loc, size, old_protection, &o2; return (ret = TRUE; bool ReadMemory(void* loc, void* buffer, DWORD size HANDLE hProces
42、s = GetCurrentProcess(; DWORD bytes_read = 0; BOOL ret = ReadProcessMemory(hProcess, loc, buffer, size, &bytes_read; return (ret = TRUE && bytes_read = size; bool install_my_handler( void* my_hdlr = my_exc_handler; void* cpp_hdlr = _CxxFrameHandler; jmp_instr jmp_my_hdlr; jmp_my_hdlr.jmp
43、 = 0xE9; /從_CxxFrameHandler+5開(kāi)始計(jì)算偏移,因?yàn)閖mp指令長(zhǎng)5字節(jié) jmp_my_hdlr.offset = reinterpret_cast(my_hdlr - (reinterpret_cast(cpp_hdlr + 5; if(!saved_handler_instructions if(!ReadMemory(cpp_hdlr, cpp_handler_instructions, sizeof(cpp_handler_instructions return false; saved_handler_instructions = true; return Wr
44、iteMemory(cpp_hdlr, &jmp_my_hdlr, sizeof(jmp_my_hdlr; bool restore_cpp_handler( if(!saved_handler_instructions return false; else void* loc = _CxxFrameHandler; return WriteMemory(loc, cpp_handler_instructions, sizeof(cpp_handler_instructions; 編譯指令#pragma pack(push, 1告訴編譯器不要在jmp_instr結(jié)構(gòu)中填充任何用于對(duì)齊的
45、空間。沒(méi)有這條指令,jmp_instr的大小將是8字節(jié),而我們需要它是5字節(jié)?,F(xiàn)在重新回到異常處理這個(gè)主題上來(lái)。調(diào)用catch塊時(shí),它可能重新拋出異?;驋伋鲂庐惓?。前一種情況下,異常處理程序必須繼續(xù)傳播(propagate)當(dāng)前異常;后一種情況下,它需要在繼續(xù)之前銷(xiāo)毀原來(lái)的異常。此時(shí),處理程序要面對(duì)兩個(gè)難題:“如何知道異常是源于catch塊還是程序的其他部分”和“如何跟蹤原來(lái)的異?!?。我的解決方法是:在調(diào)用catch塊之前,把當(dāng)前異常保存在exception_storage對(duì)象中,并注冊(cè)一個(gè)專(zhuān)用于catch塊的異常處理程序catch_block_protector。調(diào)用get_exceptio
46、n_storage(函數(shù),就能得到exception_storage對(duì)象: exception_storage* p = get_exception_storage(; p->set(pexc, pexc_info; 注冊(cè) catch_block_protector; 調(diào)用catch塊; /.這樣,當(dāng)catch塊(重新)拋出異常時(shí),程序?qū)?huì)執(zhí)行catch_block_protector。如果是拋出了新異常,這個(gè)函數(shù)可以從exception_storage對(duì)象中分離出前一個(gè)異常并銷(xiāo)毀它;如果是重新拋出原來(lái)的異常(可以通過(guò)ExceptionInformation數(shù)組的前兩個(gè)元素知道是新異常還
47、是舊異常,后一種情況下著兩個(gè)元素都是0,參見(jiàn)下面的代碼),就通過(guò)拷貝ExceptionInformation數(shù)組來(lái)繼續(xù)傳播它。下面的代碼就是catch_block_protector(函數(shù)的實(shí)現(xiàn)。/- / 如果這個(gè)處理程序被調(diào)用了,可以斷定是catch塊(重新)拋出了異常。 / 異常處理程序(my_handler)在調(diào)用catch塊之前注冊(cè)了它。其任務(wù)是判斷 / catch塊拋出了新異常還是重新拋出了原來(lái)的異常,并采取相應(yīng)的操作。 / 在前一種情況下,它需要銷(xiāo)毀傳遞給catch塊的前一個(gè)異常對(duì)象;在后一種 / 情況下,它必須找到原來(lái)的異常并將其保存到ExceptionRecord中供異常 /
48、處理程序使用。 /- EXCEPTION_DISPOSITION catch_block_protector( _EXCEPTION_RECORD* ExceptionRecord, void* EstablisherFrame, struct _CONTEXT *ContextRecord, void* DispatcherContext throw ( EXCEPTION_REGISTRATION *pFrame; pFrame= reinterpret_cast(EstablisherFrame; if(!(ExceptionRecord->ExceptionFlags &
49、 (_EXCEPTION_UNWINDING | _EXCEPTION_EXIT_UNWIND void *pcur_exc = 0, *pprev_exc = 0; const excpt_info *pexc_info = 0, *pprev_excinfo = 0; exception_storage* p = get_exception_storage(; pprev_exc = p->get_exception(; pprev_excinfo = p->get_exception_info(; p->set(0, 0; bool cpp_exc = Exceptio
50、nRecord->ExceptionCode = MS_CPP_EXC; get_exception(ExceptionRecord, &pcur_exc; get_excpt_info(ExceptionRecord, &pexc_info; if(cpp_exc && 0 = pcur_exc && 0 = pexc_info /重新拋出 ExceptionRecord->ExceptionInformation1 = reinterpret_cast(pprev_exc; ExceptionRecord->Exceptio
51、nInformation2 = reinterpret_cast(pprev_excinfo; else exception_helper:destroy(pprev_exc, pprev_excinfo; return ExceptionContinueSearch; 下面是get_exception_storage(函數(shù)的一個(gè)實(shí)現(xiàn): exception_storage* get_exception_storage( static exception_storage es; return &es;在單線(xiàn)程程序中,
52、這是一個(gè)完美的實(shí)現(xiàn)。但在多線(xiàn)程中,這就是個(gè)災(zāi)難了,想象一下多個(gè)線(xiàn)程訪(fǎng)問(wèn)它,并把異常對(duì)象保存在里面的情景吧。由于每個(gè)線(xiàn)程都有自己的堆棧和異常處理鏈,我們需要一個(gè)線(xiàn)程安全的get_exception_storage實(shí)現(xiàn):每個(gè)線(xiàn)程都有自己?jiǎn)为?dú)的exception_storage,它在線(xiàn)程啟動(dòng)時(shí)被創(chuàng)建,并在結(jié)束時(shí)被銷(xiāo)毀。Windows提供的線(xiàn)程局部存儲(chǔ)(thread local storage,TLS)可以滿(mǎn)足這個(gè)要求,它能讓每個(gè)線(xiàn)程通過(guò)一個(gè)全局鍵值來(lái)訪(fǎng)問(wèn)為這個(gè)線(xiàn)程所私有的對(duì)象副本,這是通過(guò)TlsGetValue(和TlsSetValue這兩個(gè)API來(lái)完成的。中給出了get_exception_sto
53、rage(函數(shù)的實(shí)現(xiàn)。它會(huì)被編譯成動(dòng)態(tài)鏈接庫(kù),因?yàn)槲覀兛梢约酥谰€(xiàn)程的創(chuàng)建和退出系統(tǒng)在這兩種情況下都會(huì)調(diào)用所有(當(dāng)前進(jìn)程加載的)dll的DllMain(函數(shù),這讓我們有機(jī)會(huì)創(chuàng)建特定于線(xiàn)程的數(shù)據(jù),也就是exception_storage對(duì)象。 #include "excptstorage.h"#include namespace DWORD dwstorage;namespace my_handler _declspec(dllexport exception_storage* get_exception_storage( throw ( void * p = TlsGetValue(dwstorage; return reinterpret_cast (p; BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶(hù)所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶(hù)上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶(hù)上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶(hù)因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 二零二五年度配音演員聘用合同
- 二零二五年度珠寶店安全保衛(wèi)人員聘用合同
- 二零二五年度影視聲音后期制作合同(封面設(shè)計(jì)新穎)
- 二零二五年度美發(fā)行業(yè)國(guó)際交流與合作協(xié)議
- 二零二五年度國(guó)際貿(mào)易知識(shí)產(chǎn)權(quán)傭金協(xié)議
- 二零二五年度分手補(bǔ)償協(xié)議書(shū)及子女教育費(fèi)用承擔(dān)
- 2025年度股份代持股份占比調(diào)整合同協(xié)議書(shū)模板
- 2025年度酒店餐飲服務(wù)兼職員工合同
- 二零二五年度隱名股東股權(quán)轉(zhuǎn)讓及管理權(quán)移交協(xié)議
- 二零二五年度足療養(yǎng)生店轉(zhuǎn)讓與品牌授權(quán)使用合同
- 2024年玩具陀螺項(xiàng)目可行性研究報(bào)告
- 城區(qū)綠地養(yǎng)護(hù)服務(wù)費(fèi)項(xiàng)目成本預(yù)算績(jī)效分析報(bào)告
- v建筑主墩雙壁鋼圍堰施工工藝資料
- 新部編人教版六年級(jí)道德與法治下冊(cè)全冊(cè)全套課件
- 我國(guó)互聯(lián)網(wǎng)公司資本結(jié)構(gòu)分析-以新浪公司為例
- 【藍(lán)天幼兒園小一班早期閱讀現(xiàn)狀的調(diào)查報(bào)告(含問(wèn)卷)7800字(論文)】
- 糧油機(jī)械設(shè)備更新項(xiàng)目資金申請(qǐng)報(bào)告-超長(zhǎng)期特別國(guó)債投資專(zhuān)項(xiàng)
- 個(gè)體戶(hù)的食品安全管理制度文本
- 部編版道德與法治七年級(jí)下冊(cè)每課教學(xué)反思
- 自考14237《手機(jī)媒體概論》備考試題庫(kù)(含答案)
- 第二次全國(guó)土地調(diào)查技術(shù)規(guī)程完整版
評(píng)論
0/150
提交評(píng)論