2013年4月14日
#
我現(xiàn)在是自己做,但我此前有多年在從事軟件開(kāi)發(fā)工作,當(dāng)回過(guò)頭來(lái)想一想自己,覺(jué)得特別想對(duì)那些初學(xué)JAVA/DOT。NET技術(shù)的朋友說(shuō)點(diǎn)心里話(huà),希望你們能從我們的體會(huì)中,多少受點(diǎn)啟發(fā)(也許我說(shuō)的不好,你不贊同但看在我真心的份上別扔磚頭啊).
一。 在中國(guó)你千萬(wàn)不要因?yàn)閷W(xué)習(xí)技術(shù)就可以換來(lái)穩(wěn)定的生活和高的薪水待遇,你千萬(wàn)更不要認(rèn)為哪些從事 市場(chǎng)開(kāi)發(fā),跑腿的人,沒(méi)有前途。
不知道你是不是知道,咱們中國(guó)有相當(dāng)大的一部分軟件公司,他們的軟件開(kāi)發(fā)團(tuán)隊(duì)都小的可憐,甚至只有1-3個(gè)人,連一個(gè)項(xiàng)目小組都算不上,而這樣的團(tuán)隊(duì)卻要承擔(dān)一個(gè)軟件公司所有的軟件開(kāi)發(fā)任務(wù),在軟件上線(xiàn)和開(kāi)發(fā)的關(guān)鍵階段需要團(tuán)隊(duì)的成員沒(méi)日沒(méi)夜的加班,還需要為測(cè)試出的BUG和不能按時(shí)提交的軟件模塊功能而心懷忐忑,有的時(shí)候如果你不幸加入現(xiàn)場(chǎng)開(kāi)發(fā)的團(tuán)隊(duì)你則需要背井離鄉(xiāng)告別你的女友,進(jìn)行封閉開(kāi)發(fā),你平時(shí)除了編碼之外就是吃飯和睡覺(jué)(有錢(qián)的公司甚至請(qǐng)個(gè)保姆為你做飯,以讓你節(jié)省出更多的時(shí)間來(lái)投入到工作中,讓你一直在那種累了就休息,不累就立即工作的狀態(tài))
更可怕的是,會(huì)讓你接觸的人際關(guān)系非常單一,除了有限的技術(shù)人員之外你幾乎見(jiàn)不到做其他行業(yè)工作和職位的人,你的朋友圈子小且單一,甚至破壞你原有的愛(ài)情(想象一下,你在外地做現(xiàn)場(chǎng)開(kāi)發(fā)2個(gè)月以上,卻從沒(méi)跟女友見(jiàn)過(guò)一面的話(huà),你的女友是不是會(huì)對(duì)你呲牙裂嘴)。
也許你拿到了所謂的白領(lǐng)的工資,但你卻從此失去享受生活的自由,如果你想做技術(shù)人員尤其是開(kāi)發(fā)人員,我想你很快就會(huì)理解,你多么想在一個(gè)地方長(zhǎng)期待一段時(shí)間,認(rèn)識(shí)一些朋友,多一些生活時(shí)間的愿望。
比之于我們的生活和人際關(guān)系及工作,那些從事售前和市場(chǎng)開(kāi)發(fā)的朋友,卻有比我們多的多的工作之外的時(shí)間,甚至他們工作的時(shí)間有的時(shí)候是和生活的時(shí)間是可以兼顧的,他們可以通過(guò)市場(chǎng)開(kāi)發(fā),認(rèn)識(shí)各個(gè)行業(yè)的人士,可以認(rèn)識(shí)各種各樣的朋友,他們比我們坦率說(shuō)更有發(fā)財(cái)和發(fā)展的機(jī)會(huì),只要他們跟我們一樣勤奮。(有一種勤奮的普通人,如果給他換個(gè)地方,他馬上會(huì)成為一個(gè)勤奮且出眾的人。)
二。在學(xué)習(xí)技術(shù)的時(shí)候千萬(wàn)不要認(rèn)為如果做到技術(shù)最強(qiáng),就可以成為100%受尊重的人。
有一次一個(gè)人在面試項(xiàng)目經(jīng)理的時(shí)候說(shuō)了這么一段話(huà):我只用最聽(tīng)話(huà)的人,按照我的要求做只要是聽(tīng)話(huà)就要,如果不聽(tīng)話(huà)不管他技術(shù)再好也不要。隨后這個(gè)人得到了試用機(jī)會(huì),如果沒(méi)意外的話(huà),他一定會(huì)是下一個(gè)項(xiàng)目經(jīng)理的繼任者。
朋友們你知道嗎?不管你技術(shù)有多強(qiáng),你也不可能自由的騰出時(shí)間象別人那樣研究一下LINUX源碼,甚至寫(xiě)一個(gè)LINUX樣的杰作來(lái)表現(xiàn)你的才能。你需要做的就是按照要求寫(xiě)代碼,寫(xiě)代碼的含義就是都規(guī)定好,你按照規(guī)定寫(xiě),你很快就會(huì)發(fā)現(xiàn)你昨天寫(xiě)的代碼,跟今天寫(xiě)的代碼有很多類(lèi)似,等你寫(xiě)過(guò)一段時(shí)間的代碼,你將領(lǐng)略:復(fù)制,拷貝,粘貼那樣的技術(shù)對(duì)你來(lái)說(shuō)是何等重要。(如果你沒(méi)有做過(guò)1年以上的真正意義上的開(kāi)發(fā)不要反駁我)。
如果你幸運(yùn)的能夠聽(tīng)到市場(chǎng)人員的談話(huà),或是領(lǐng)導(dǎo)們的談話(huà),你會(huì)隱約覺(jué)得他們都在把技術(shù)人員當(dāng)作編碼的機(jī)器來(lái)看,你的價(jià)值并沒(méi)有你想象的那么重要。而在你所在的團(tuán)隊(duì)內(nèi)部,你可能正在為一個(gè)技術(shù)問(wèn)題的討論再跟同事搞內(nèi)耗,因?yàn)樗环悖阋膊环銈兌颊J(rèn)為自己的對(duì),其實(shí)你們兩個(gè)都對(duì),而爭(zhēng)論的目的就是為了在關(guān)鍵場(chǎng)合證明一下自己比對(duì)方技術(shù)好,比對(duì)方強(qiáng)。(在一個(gè)項(xiàng)目開(kāi)發(fā)中,沒(méi)有人愿意長(zhǎng)期聽(tīng)別人的,總想換個(gè)位置領(lǐng)導(dǎo)別人。)
三。你更不要認(rèn)為,如果我技術(shù)夠好,我就自己創(chuàng)業(yè),自己有創(chuàng)業(yè)的資本,因?yàn)樽约菏歉慵夹g(shù)的。
如果你那樣認(rèn)為,真的是大錯(cuò)特錯(cuò)了,你可以做個(gè)調(diào)查在非技術(shù)人群中,沒(méi)有幾個(gè)人知道C#與JAVA的,更談不上來(lái)欣賞你的技術(shù)是好還是不好。一句話(huà),技術(shù)僅僅是一個(gè)工具,善于運(yùn)用這個(gè)工具為別人干活的人,卻往往不太擅長(zhǎng)用這個(gè)工具來(lái)為自己創(chuàng)業(yè),因?yàn)檫@是兩個(gè)概念,訓(xùn)練的技能也是完全不同的。
創(chuàng)業(yè)最開(kāi)始的時(shí)候,你的人際關(guān)系,你處理人際關(guān)系的能力,你對(duì)社會(huì)潛規(guī)則的認(rèn)識(shí),還有你明白不明白別人的心,你會(huì)不會(huì)說(shuō)讓人喜歡的話(huà),還有你對(duì)自己所提供的服務(wù)的策劃和推銷(xiāo)等等,也許有一萬(wàn),一百萬(wàn)個(gè)值得我們重視的問(wèn)題,但你會(huì)發(fā)現(xiàn)技術(shù)卻很少有可能包含在這一萬(wàn)或一百萬(wàn)之內(nèi),如果你創(chuàng)業(yè)到了一個(gè)快成功的階段,你會(huì)這樣告訴自己:我干嗎要親自做技術(shù),我聘一個(gè)人不就行了,這時(shí)候你才真正會(huì)理解技術(shù)的作用,和你以前做技術(shù)人員的作用。
[小結(jié)]
基于上面的討論,我奉勸那些學(xué)習(xí)技術(shù)的朋友,千萬(wàn)不要拿科舉考試樣的心態(tài)去學(xué)習(xí)技術(shù),對(duì)技術(shù)的學(xué)習(xí)幾近的癡迷,想掌握所有所有的技術(shù),以讓自己成為技術(shù)領(lǐng)域的權(quán)威和專(zhuān)家,以在必要的時(shí)候或是心里不暢快的時(shí)候到網(wǎng)上對(duì)著菜鳥(niǎo)說(shuō)自己是前輩。
技術(shù)僅僅是一個(gè)工具,是你在人生一個(gè)階段生存的工具,你可以一輩子喜歡他,但最好不要一輩子靠它生存。
掌握技術(shù)的唯一目的就是拿它找工作(如果你不想把技術(shù)當(dāng)作你第二生命的話(huà)),就是干活。所以你在學(xué)習(xí)的時(shí)候千萬(wàn)不要去做那些所謂的技術(shù)習(xí)題或是研究那些帽泡算法,最大數(shù)算法了,什么叫干活?
就是做一個(gè)東西讓別人用,別人用了,可以提高他們的工作效率,想象吧,你做1萬(wàn)道技術(shù)習(xí)題有什么用?只會(huì)讓人覺(jué)得酸腐,還是在學(xué)習(xí)的時(shí)候,多培養(yǎng)些自己務(wù)實(shí)的態(tài)度吧,比如研究一下當(dāng)?shù)厥袌?chǎng)目前有哪些軟件公司用人,自己離他們的要求到底有多遠(yuǎn),自己具體應(yīng)該怎么做才可以達(dá)到他們的要求。等你分析完這些,你就會(huì)發(fā)現(xiàn),找工作成功,技術(shù)的貢獻(xiàn)率其實(shí)并沒(méi)有你原來(lái)想象的那么高。
不管你是學(xué)習(xí)技術(shù)為了找工作還是創(chuàng)業(yè),你都要對(duì)技術(shù)本身有個(gè)清醒的認(rèn)識(shí),在中國(guó)不會(huì)出現(xiàn)BILL GATES,因?yàn)椋袊?guó)目前還不是十分的尊重技術(shù)人才,還僅僅的停留在把軟件技術(shù)人才當(dāng)作人才機(jī)器來(lái)用的尷尬境地。(如果你不理解,一種可能是你目前僅僅從事過(guò)技術(shù)工作,你的朋友圈子里技術(shù)類(lèi)的朋友占了大多數(shù),一種可能是你還沒(méi)有工作,但喜歡讀比爾。蓋茨的傳記)。
2010年9月16日
#
|
|
|
|
1. 結(jié)構(gòu)化程序設(shè)計(jì)
|
為了提高程序的可讀性、可重用性等,逐漸出現(xiàn)了將程序開(kāi)發(fā)中經(jīng)常用到的相同的功能,比如數(shù)學(xué)函數(shù)運(yùn)算、字符串操作等,獨(dú)立出來(lái)編寫(xiě)成函數(shù),然后按照相互關(guān)系或應(yīng)用領(lǐng)域匯集在相同的文件里,這些文件構(gòu)成了函數(shù)庫(kù)。
函數(shù)庫(kù)是一種對(duì)信息的封裝,將常用的函數(shù)封裝起來(lái),人們不必知道如何實(shí)現(xiàn)它們。只需要了解如何調(diào)用它們即可。函數(shù)庫(kù)可以被多個(gè)應(yīng)用程序共享,在具體編程環(huán)境中,一般都有一個(gè)頭文件相伴,在這個(gè)頭文件中以標(biāo)準(zhǔn)的方式定義了庫(kù)中每個(gè)函數(shù)的接口,根據(jù)這些接口形式可以在程序中的任何地方調(diào)用所需的函數(shù)。
由于函數(shù)、庫(kù)、模塊等一系列概念和技術(shù)的出現(xiàn),程序設(shè)計(jì)逐漸變成如圖所示的風(fēng)格。程序被分解成一個(gè)個(gè)函數(shù)模塊,其中既有系統(tǒng)函數(shù),也有用戶(hù)定義的函數(shù)。通過(guò)對(duì)函數(shù)的調(diào)用,程序的運(yùn)行逐步被展開(kāi)。閱讀程序時(shí),由于每一塊的功能相對(duì)獨(dú)立,因此對(duì)程序結(jié)構(gòu)的理解相對(duì)容易,在一定程度上緩解了程序代碼可讀性和可重用件的矛盾,但并未徹底解決矛盾。隨著計(jì)算機(jī)程序的規(guī)模越來(lái)越大,這個(gè)問(wèn)題變得更加尖銳,于是出現(xiàn)了另一種編程風(fēng)格——結(jié)構(gòu)化程序設(shè)計(jì)。
在結(jié)構(gòu)化程序設(shè)計(jì)中,任何程序段的編寫(xiě)都基于3種結(jié)構(gòu):分支結(jié)構(gòu)、循環(huán)結(jié)構(gòu)和順序結(jié)構(gòu)。程序具有明顯的模塊化特征,每個(gè)程序模塊具有惟一的出口和入口語(yǔ)句。結(jié)構(gòu)化程序的結(jié)構(gòu)簡(jiǎn)單清晰,模塊化強(qiáng),描述方式貼近人們習(xí)慣的推理式思維方式。因此可讀性強(qiáng),在軟件重用性、軟件維護(hù)等方面都有所進(jìn)步,在大型軟件開(kāi)發(fā)尤其是大型科學(xué)與工程運(yùn)算軟件的開(kāi)發(fā)中發(fā)揮了重要作用。因此到目前為止,仍有許多應(yīng)用程序的開(kāi)發(fā)采用結(jié)構(gòu)化程序設(shè)計(jì)技術(shù)和方法。即使在目前流行的面向?qū)ο筌浖_(kāi)發(fā)中也不能完全脫離結(jié)構(gòu)化程序設(shè)計(jì)。
|
|
|
面向?qū)ο蟮某绦蛞塾?jì)方法是程序設(shè)計(jì)的一種新方法。所有面向?qū)ο蟮某绦蛟O(shè)計(jì)語(yǔ)言一般都含有三個(gè)方面的語(yǔ)法機(jī)制,即對(duì)象和類(lèi)、多態(tài)性、繼承性。
1.對(duì)象和類(lèi)
對(duì)象的概念、原理和方法是面向?qū)ο蟮睦硇蛟O(shè)計(jì)語(yǔ)言暈重要的特征。對(duì)象是用戶(hù)定義的類(lèi)型(稱(chēng)為類(lèi))的變量。一個(gè)對(duì)象是既包含數(shù)據(jù)又包合操作該數(shù)據(jù)的代碼(函數(shù))的邏輯實(shí)體。對(duì)象中的這些數(shù)據(jù)和函數(shù)稱(chēng)為對(duì)象的成員,即成員數(shù)據(jù)和成員函數(shù)。對(duì)象中的成員分為公有的和私有的。公有成員是對(duì)象與外界的接口界面。外界只能通過(guò)調(diào)用訪(fǎng)問(wèn)一個(gè)對(duì)象的公有成員來(lái)實(shí)現(xiàn)該對(duì)象的功能。私有成員體現(xiàn)一個(gè)對(duì)象的組織形式和功能的實(shí)現(xiàn)細(xì)節(jié)。外界無(wú)法對(duì)私有成員進(jìn)行操作。類(lèi)對(duì)象按照規(guī)范進(jìn)行操作,將描述客觀(guān)事物的數(shù)據(jù)表達(dá)及對(duì)數(shù)據(jù)的操作處理封裝在一起,成功地實(shí)現(xiàn)了面向?qū)ο蟮某绦蛟O(shè)計(jì)。當(dāng)用戶(hù)定義了一個(gè)類(lèi)類(lèi)型后,就可以在該類(lèi)型的名下定義變量(即對(duì)象)了。類(lèi)是結(jié)構(gòu)體類(lèi)型的擴(kuò)充。結(jié)構(gòu)體中引入成員函數(shù)并規(guī)定了其訪(fǎng)問(wèn)和繼承原則后便成了類(lèi)。
2.多態(tài)性
面向?qū)ο蟮某绦蛟O(shè)計(jì)語(yǔ)言支持“多態(tài)性”,把一個(gè)接口用于一類(lèi)活動(dòng)。即“一個(gè)接口多種算法”。具體實(shí)施時(shí)該選擇哪一個(gè)算法是由特定的語(yǔ)法機(jī)制確定的。C++編譯時(shí)和運(yùn)行時(shí)都支持多態(tài)性。編譯時(shí)的多態(tài)性體現(xiàn)在重載函數(shù)和重載運(yùn)算符等方面。運(yùn)行時(shí)的多態(tài)性體現(xiàn)在繼承關(guān)系及虛函數(shù)等方面。
3.繼承性
C++程序中,由一個(gè)類(lèi)(稱(chēng)為基類(lèi))可以派生出新類(lèi)(稱(chēng)為派生類(lèi))。這種派生的語(yǔ)法機(jī)制使得新類(lèi)的出現(xiàn)輕松自然,使得一個(gè)復(fù)雜事物可以被順理成章地歸結(jié)為由逐層派生的對(duì)象描述。“派生”使得程序中定義的類(lèi)呈層次結(jié)構(gòu)。處于子層的對(duì)參既具有其父層對(duì)象的共性.又具有自身的特性。繼承性是一個(gè)類(lèi)對(duì)象獲得其基類(lèi)對(duì)象特性的過(guò)程。C++中嚴(yán)格地規(guī)定了派生類(lèi)對(duì)其基類(lèi)的繼承原則和訪(fǎng)問(wèn)權(quán)限,使得程序中對(duì)數(shù)據(jù)和函數(shù)的訪(fǎng)間,需在家族和朋友間嚴(yán)格區(qū)分。
|
|
|
3. 事件驅(qū)動(dòng)的程序設(shè)計(jì)
|
事件驅(qū)動(dòng)的程序設(shè)計(jì)實(shí)際上是面向?qū)ο蟪绦蛟O(shè)計(jì)的一個(gè)應(yīng)用,但它目前僅適用于windows系列操作系統(tǒng)。windows環(huán)境中的應(yīng)用程序與MS-DOS環(huán)境中的應(yīng)用程序運(yùn)行機(jī)制不同、設(shè)計(jì)程序的方式也不一樣。windows程序采用事件驅(qū)動(dòng)機(jī)制運(yùn)行,這種事件驅(qū)動(dòng)程序由事件的發(fā)生與否來(lái)控制,系統(tǒng)中每個(gè)對(duì)象狀態(tài)副改變都是事件發(fā)生的原由或結(jié)果,設(shè)計(jì)程序時(shí)需以一種非順序方式處理事件,與順序的、過(guò)程驅(qū)動(dòng)的傳統(tǒng)程序設(shè)計(jì)方法迥異。
事件也稱(chēng)消息,含義比較廣泛,常見(jiàn)的事件有鼠標(biāo)事件(如民標(biāo)移動(dòng)、單擊、掠過(guò)窗口邊界)、鍵盤(pán)事件(如按鍵的壓下與拾起)等多種。應(yīng)用程序運(yùn)行經(jīng)過(guò)一系列必要的初始化后,將進(jìn)入等待狀態(tài),等待有事件發(fā)生,一旦事件出現(xiàn),程序就被激活并進(jìn)行相應(yīng)處理。
事件驅(qū)動(dòng)程序設(shè)計(jì)是圍繞著消息的產(chǎn)生與處理進(jìn)行的.消息可來(lái)自程序中的某個(gè)對(duì)象,也可由用戶(hù)、wlndow s或運(yùn)行著的其他應(yīng)用程序產(chǎn)生。每當(dāng)事件發(fā)生時(shí),Windows俘獲有關(guān)事件,然后將消息分別轉(zhuǎn)發(fā)到相關(guān)應(yīng)用程序中的有關(guān)對(duì)象,需要對(duì)消息作出反應(yīng)的對(duì)象應(yīng)該提供消息處理函數(shù),通過(guò)這個(gè)消息處理函數(shù)實(shí)現(xiàn)對(duì)象的一種功能或行為。所以編寫(xiě)事件驅(qū)動(dòng)程序的大部分工作是為各個(gè)對(duì)象(類(lèi))添加各種消息的處理函數(shù)。由于一個(gè)對(duì)象可以是消息的接收者,同時(shí)也可能是消息的發(fā)送者,所發(fā)送的消息與接收到的消息也可以是相同的消息,而有些消息的發(fā)出時(shí)間是無(wú)法預(yù)知的(比如關(guān)于鍵盤(pán)的消息),因此應(yīng)用程序的執(zhí)行順序是無(wú)法預(yù)知的。
|
|
|
4. 邏輯式對(duì)象程序設(shè)計(jì)
|
邏輯式程序設(shè)計(jì)的概念來(lái)自邏輯式程序設(shè)計(jì)語(yǔ)言Prolog這一曾經(jīng)在計(jì)算機(jī)領(lǐng)域引起震動(dòng)的日本“第五代”計(jì)算機(jī)的基本系統(tǒng)語(yǔ)言,在這種“第五代”計(jì)算機(jī)中,Prolog的地位相當(dāng)于當(dāng)前計(jì)算機(jī)中的機(jī)器語(yǔ)言。
Prolog主要應(yīng)用在人工智能領(lǐng)域,在自然語(yǔ)言處理、數(shù)據(jù)庫(kù)查詢(xún)、算法描述等方面都有應(yīng)用,尤其適于作為專(zhuān)家系統(tǒng)的開(kāi)發(fā)工具。
Prolog是一種陳述式語(yǔ)言,它不是一種嚴(yán)格的通用程序設(shè)計(jì)語(yǔ)言,使用Prolog編寫(xiě)程序不需要描述具體的解題過(guò)程、只需結(jié)出一些必要的事實(shí)和規(guī)則,這些規(guī)則是解決問(wèn)題方法的規(guī)范說(shuō)明,根據(jù)這些規(guī)則和事實(shí).計(jì)算機(jī)利用渭詞邏輯,通過(guò)演繹推理得到求解問(wèn)題的執(zhí)行序列。
|
|
|
一個(gè)有實(shí)際應(yīng)用的并行算法,最終總要在并行機(jī)上實(shí)現(xiàn),為此首先就要將并行算法轉(zhuǎn)化為并行程序,此過(guò)程就是所謂的并行程序設(shè)計(jì)(Parallel Program)。它要求算法設(shè)計(jì)者、系統(tǒng)結(jié)構(gòu)師和軟件工作者廣泛頻繁的交互。因?yàn)樵O(shè)計(jì)并行程序涉及到的知識(shí)面較廣,主要包括操作系統(tǒng)中的有關(guān)知識(shí)和優(yōu)化編譯方面的知識(shí)。操作系統(tǒng)內(nèi)容非常豐富,并行程序中最基本的計(jì)算要素如任務(wù)、進(jìn)程、線(xiàn)程等基本概念、同步機(jī)制和通信操作等。
目前并行程序設(shè)計(jì)的狀況是:⑴并行軟件的發(fā)展落后于并行硬件;⑵和串行系統(tǒng)與應(yīng)用軟件相比,現(xiàn)今的并行系統(tǒng)與應(yīng)用軟件甚少且不成熟;⑶并行軟件的缺乏是發(fā)展并行計(jì)算的主要障礙;⑷不幸的是,這種狀態(tài)似乎仍在繼續(xù)著。究其原因是并行程序設(shè)計(jì)遠(yuǎn)比串行程序設(shè)計(jì)復(fù)雜:⑴并行程序設(shè)計(jì)不但包含了串行程序設(shè)計(jì),面且還包含了更多的富有挑戰(zhàn)性的問(wèn)題;⑵串行程序設(shè)計(jì)僅有一個(gè)普遍被接受的馮·諾依曼計(jì)算模型,而并行計(jì)算模型雖有好多,但沒(méi)有一個(gè)可被共同認(rèn)可的像馮·諾依曼那樣的優(yōu)秀模型;⑶并行程序設(shè)計(jì)對(duì)環(huán)境工具(如編譯、查錯(cuò)等)的要求遠(yuǎn)比串行程序設(shè)計(jì)先進(jìn)得多;⑷串行程序設(shè)計(jì)比較適合于自然習(xí)慣,且人們?cè)谶^(guò)去積累了大量的編程知識(shí)、經(jīng)驗(yàn)和寶貴的軟件財(cái)富。
|
一個(gè)由c/C++編譯的程序占用的內(nèi)存分為以下幾個(gè)部分
1、棧區(qū)(stack)— 由編譯器自動(dòng)分配釋放 ,存放函數(shù)的參數(shù)值,局部變量的值等。其操作方式類(lèi)似于數(shù)據(jù)結(jié)構(gòu)中的棧。
2、堆區(qū)(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結(jié)束時(shí)可能由OS回收 。注意它與數(shù)據(jù)結(jié)構(gòu)中的堆是兩回事,分配方式倒是類(lèi)似于鏈表,呵呵。
3、全局區(qū)(靜態(tài)區(qū))(static)—,全局變量和靜態(tài)變量的存儲(chǔ)是放在一塊的,初始化的全局變量和靜態(tài)變量在一塊區(qū)域, 未初始化的全局變量和未初始化的靜態(tài)變量在相鄰的另一塊區(qū)域。 - 程序結(jié)束后有系統(tǒng)釋放
4、文字常量區(qū) —常量字符串就是放在這里的。 程序結(jié)束后由系統(tǒng)釋放
5、程序代碼區(qū)—存放函數(shù)體的二進(jìn)制代碼。
堆和棧的理論知識(shí)
2.1申請(qǐng)方式
stack:
由系統(tǒng)自動(dòng)分配。 例如,聲明在函數(shù)中一個(gè)局部變量 int b; 系統(tǒng)自動(dòng)在棧中為b開(kāi)辟空間
heap:
需要程序員自己申請(qǐng),并指明大小,在c中malloc函數(shù)
如p1 = (char *)malloc(10);
在C++中用new運(yùn)算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在棧中的。
2.2申請(qǐng)后系統(tǒng)的響應(yīng)
棧:只要棧的剩余空間大于所申請(qǐng)空間,系統(tǒng)將為程序提供內(nèi)存,否則將報(bào)異常提示棧溢出。
堆:首先應(yīng)該知道操作系統(tǒng)有一個(gè)記錄空閑內(nèi)存地址的鏈表,當(dāng)系統(tǒng)收到程序的申請(qǐng)時(shí),
會(huì) 遍歷該鏈表,尋找第一個(gè)空間大于所申請(qǐng)空間的堆結(jié)點(diǎn),然后將該結(jié)點(diǎn)從空閑結(jié)點(diǎn)鏈表中刪除,并將該結(jié)點(diǎn)的空間分配給程序,另外,對(duì)于大多數(shù)系統(tǒng),會(huì)在這塊內(nèi) 存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語(yǔ)句才能正確的釋放本內(nèi)存空間。另外,由于找到的堆結(jié)點(diǎn)的大小不一定正好等于申請(qǐng)的大 小,系統(tǒng)會(huì)自動(dòng)的將多余的那部分重新放入空閑鏈表中。
2.3申請(qǐng)大小的限制
棧:在Windows下,棧是向低地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是一塊 連續(xù)的內(nèi)存的區(qū)域。這句話(huà)的意思是棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的,在 WINDOWS下,棧的大小是2M(也有的說(shuō)是1M,總之是一個(gè)編譯時(shí)就確定的常數(shù)),如果申請(qǐng)的空間超過(guò)棧的剩余空間時(shí),將提示overflow。因 此,能從棧獲得的空間較小。
堆:堆是向高地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是不連續(xù)的內(nèi)存區(qū)域。這是由于系統(tǒng)是用鏈表來(lái)存儲(chǔ)的空閑內(nèi)存地址的,自然是不連續(xù)的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存。由此可見(jiàn),堆獲得的空間比較靈活,也比較大。
2.4申請(qǐng)效率的比較:
棧由系統(tǒng)自動(dòng)分配,速度較快。但程序員是無(wú)法控制的。
堆是由new分配的內(nèi)存,一般速度比較慢,而且容易產(chǎn)生內(nèi)存碎片,不過(guò)用起來(lái)最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內(nèi)存,他不是在堆,也不是在棧是直接在進(jìn)程的地址空間中保留一快內(nèi)存,雖然用起來(lái)最不方便。但是速度, 也最靈活
2.5堆和棧中的存儲(chǔ)內(nèi)容
棧: 在函數(shù)調(diào)用時(shí),第一個(gè)進(jìn)棧的是主函數(shù)中后的下一條指令(函數(shù)調(diào)用語(yǔ)句的下一條可執(zhí)行語(yǔ)句)的地址,然后是函數(shù)的各個(gè)參數(shù),在大多數(shù)的C編譯器中,參數(shù)是由右往左入棧的,然后是函數(shù)中的局部變量。注意靜態(tài)變量是不入棧的。
當(dāng)本次函數(shù)調(diào)用結(jié)束后,局部變量先出棧,然后是參數(shù),最后棧頂指針指向最開(kāi)始存的地址,也就是主函數(shù)中的下一條指令,程序由該點(diǎn)繼續(xù)運(yùn)行。
堆:一般是在堆的頭部用一個(gè)字節(jié)存放堆的大小。堆中的具體內(nèi)容有程序員安排。
2.6存取效率的比較
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在運(yùn)行時(shí)刻賦值的;
而bbbbbbbbbbb是在編譯時(shí)就確定的;
但是,在以后的存取中,在棧上的數(shù)組比指針?biāo)赶虻淖址?例如堆)快。
比如:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
對(duì)應(yīng)的匯編代碼
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一種在讀取時(shí)直接就把字符串中的元素讀到寄存器cl中,而第二種則要先把指edx中,在根據(jù)edx讀取字符,顯然慢了。
2.7小結(jié):
堆和棧的區(qū)別可以用如下的比喻來(lái)看出:
使用棧就象我們?nèi)ワ堭^里吃飯,只管點(diǎn)菜(發(fā)出申請(qǐng))、付錢(qián)、和吃(使用),吃飽了就走,不必理會(huì)切菜、洗菜等準(zhǔn)備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
使用堆就象是自己動(dòng)手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由度大。
簡(jiǎn)介
本文將討論如何把代碼注入不同的進(jìn)程地址空間,然后在該進(jìn)程的上下文中執(zhí)行注入的代碼。 我們?cè)诰W(wǎng)上可以查到一些窗口/密碼偵測(cè)的應(yīng)用例子,網(wǎng)上的這些程序大多都依賴(lài) Windows 鉤子技術(shù)來(lái)實(shí)現(xiàn)。本文將討論除了使用 Windows 鉤子技術(shù)以外的其它技術(shù)來(lái)實(shí)現(xiàn)這個(gè)功能。如圖一所示:

圖一 WinSpy 密碼偵測(cè)程序
為了找到解決問(wèn)題的方法。首先讓我們簡(jiǎn)單回顧一下問(wèn)題背景。
要“讀取”某個(gè)控件的內(nèi)容——無(wú)論這個(gè)控件是否屬于當(dāng)前的應(yīng)用程序——通常都是發(fā)送 WM_GETTEXT 消息來(lái)實(shí)現(xiàn)。這個(gè)技術(shù)也同樣應(yīng)用到編輯控件,但是如果該編輯控件屬于另外一個(gè)進(jìn)程并設(shè)置了 ES_PASSWORD 式樣,那么上面講的方法就行不通了。用 WM_GETTEXT 來(lái)獲取控件的內(nèi)容只適用于進(jìn)程“擁有”密碼控件的情況。所以我們的問(wèn)題變成了如何在另外一個(gè)進(jìn)程的地址空間執(zhí)行:
::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
通常有三種可能性來(lái)解決這個(gè)問(wèn)題。
- 將你的代碼放入某個(gè) DLL,然后通過(guò) Windows 鉤子映射該DLL到遠(yuǎn)程進(jìn)程;
- 將你的代碼放入某個(gè) DLL,然后通過(guò) CreateRemoteThread 和 LoadLibrary 技術(shù)映射該DLL到遠(yuǎn)程進(jìn)程;
- 如果不寫(xiě)單獨(dú)的 DLL,可以直接將你的代碼拷貝到遠(yuǎn)程進(jìn)程——通過(guò) WriteProcessMemory——并用 CreateRemoteThread 啟動(dòng)它的執(zhí)行。本文將在第三部分詳細(xì)描述該技術(shù)實(shí)現(xiàn)細(xì)節(jié);
第一部分: Windows 鉤子
范例程序——參見(jiàn)HookSpy 和HookInjEx
Windows 鉤子主要作用是監(jiān)控某些線(xiàn)程的消息流。通常我們將鉤子分為本地鉤子和遠(yuǎn)程鉤子以及系統(tǒng)級(jí)鉤子,本地鉤子一般監(jiān)控屬于本進(jìn)程的線(xiàn)程的消息流,遠(yuǎn)程鉤子是線(xiàn)程專(zhuān)用的,用于監(jiān)控屬于另外進(jìn)程的線(xiàn)程消息流。系統(tǒng)級(jí)鉤子監(jiān)控運(yùn)行在當(dāng)前系統(tǒng)中的所有線(xiàn)程的消息流。
如果鉤子作用的線(xiàn)程屬于另外的進(jìn)程,那么你的鉤子過(guò)程必須駐留在某個(gè)動(dòng)態(tài)鏈接庫(kù)(DLL)中。然后系統(tǒng)映射包含鉤子過(guò)程的DLL到鉤子作用的線(xiàn)程的地址空間。Windows將映射整個(gè) DLL,而不僅僅是鉤子過(guò)程。這就是為什么 Windows 鉤子能被用于將代碼注入到別的進(jìn)程地址空間的原因。
本文我不打算涉及鉤子的具體細(xì)節(jié)(關(guān)于鉤子的細(xì)節(jié)請(qǐng)參見(jiàn) MSDN 庫(kù)中的 SetWindowHookEx API),但我在此要給出兩個(gè)很有用心得,在相關(guān)文檔中你是找不到這些內(nèi)容的:
- 在成功調(diào)用 SetWindowsHookEx 后,系統(tǒng)自動(dòng)映射 DLL 到鉤子作用的線(xiàn)程地址空間,但不必立即發(fā)生映射,因?yàn)?Windows 鉤子都是消息,DLL 在消息事件發(fā)生前并沒(méi)有產(chǎn)生實(shí)際的映射。例如:
如果你安裝一個(gè)鉤子監(jiān)控某些線(xiàn)程(WH_CALLWNDPROC)的非隊(duì)列消息,在消息被實(shí)際發(fā)送到(某些窗口的)鉤子作用的線(xiàn)程之前,該DLL 是不會(huì)被映射到遠(yuǎn)程進(jìn)程的。換句話(huà)說(shuō),如果 UnhookWindowsHookEx 在某個(gè)消息被發(fā)送到鉤子作用的線(xiàn)程之前被調(diào)用,DLL 根本不會(huì)被映射到遠(yuǎn)程進(jìn)程(即使 SetWindowsHookEx 本身調(diào)用成功)。為了強(qiáng)制進(jìn)行映射,在調(diào)用 SetWindowsHookEx 之后馬上發(fā)送一個(gè)事件到相關(guān)的線(xiàn)程。
在UnhookWindowsHookEx了之后,對(duì)于沒(méi)有映射的DLL處理方法也一樣。只有在足夠的事件發(fā)生后,DLL才會(huì)有真正的映射。
- 當(dāng)你安裝鉤子后,它們可能影響整個(gè)系統(tǒng)得性能(尤其是系統(tǒng)級(jí)鉤子),但是你可以很容易解決這個(gè)問(wèn)題,如果你使用線(xiàn)程專(zhuān)用鉤子的DLL映射機(jī)制,并不截獲消息。考慮使用如下代碼:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved )
{
if( ul_reason_for_call == DLL_PROCESS_ATTACH )
{
// Increase reference count via LoadLibrary
char lib_name[MAX_PATH];
::GetModuleFileName( hModule, lib_name, MAX_PATH );
::LoadLibrary( lib_name );
// Safely remove hook
::UnhookWindowsHookEx( g_hHook );
}
return TRUE;
}
那么會(huì)發(fā)生什么呢?首先我們通過(guò)Windows 鉤子將DLL映射到遠(yuǎn)程進(jìn)程。然后,在DLL被實(shí)際映射之后,我們解開(kāi)鉤子。通常當(dāng)?shù)谝粋€(gè)消息到達(dá)鉤子作用線(xiàn)程時(shí),DLL此時(shí)也不會(huì)被映射。這里的處理技巧是調(diào)用LoadLibrary通過(guò)增加 DLLs的引用計(jì)數(shù)來(lái)防止映射不成功。
現(xiàn)在剩下的問(wèn)題是如何卸載DLL,UnhookWindowsHookEx 是不會(huì)做這個(gè)事情的,因?yàn)殂^子已經(jīng)不作用于線(xiàn)程了。你可以像下面這樣做:
- 就在你想要解除DLL映射前,安裝另一個(gè)鉤子;
- 發(fā)送一個(gè)“特殊”消息到遠(yuǎn)程線(xiàn)程;
- 在鉤子過(guò)程中截獲這個(gè)消息,響應(yīng)該消息時(shí)調(diào)用 FreeLibrary 和 UnhookWindowsHookEx;
目前只使用了鉤子來(lái)從處理遠(yuǎn)程進(jìn)程中DLL的映射和解除映射。在此“作用于線(xiàn)程的”鉤子對(duì)性能沒(méi)有影響。
下面我們將討論另外一種方法,這個(gè)方法與 LoadLibrary 技術(shù)的不同之處是DLL的映射機(jī)制不會(huì)干預(yù)目標(biāo)進(jìn)程。相對(duì)LoadLibrary 技術(shù),這部分描述的方法適用于 WinNT和Win9x。
但是,什么時(shí)候使用這個(gè)技巧呢?答案是當(dāng)DLL必須在遠(yuǎn)程進(jìn)程中駐留較長(zhǎng)時(shí)間(即如果你子類(lèi)化某個(gè)屬于另外一個(gè)進(jìn)程的控件時(shí))以及你想盡可能少的干涉目標(biāo)進(jìn)程時(shí)。我在 HookSpy 中沒(méi)有使用它,因?yàn)樽⑷隓LL 的時(shí)間并不長(zhǎng)——注入時(shí)間只要足夠得到密碼即可。我提供了另外一個(gè)例子程序——HookInjEx——來(lái)示范。HookInjEx 將DLL映射到資源管理器“explorer.exe”,并從中/解除影射,它子類(lèi)化“開(kāi)始”按鈕,并交換鼠標(biāo)左右鍵單擊“開(kāi)始”按鈕的功能。
HookSpy 和 HookInjEx 的源代碼都可以從本文的下載源代碼中獲得。
第二部分:CreateRemoteThread 和 LoadLibrary 技術(shù)
范例程序——LibSpy
通常,任何進(jìn)程都可以通過(guò) LoadLibrary API 動(dòng)態(tài)加載DLL。但是,如何強(qiáng)制一個(gè)外部進(jìn)程調(diào)用這個(gè)函數(shù)呢?答案是:CreateRemoteThread。
首先,讓我們看一下 LoadLibrary 和FreeLibrary API 的聲明:
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName // 庫(kù)模塊文件名的地址
);
BOOL FreeLibrary(
HMODULE hLibModule // 要加載的庫(kù)模塊的句柄
);
現(xiàn)在將它們與傳遞到 CreateRemoteThread 的線(xiàn)程例程——ThreadProc 的聲明進(jìn)行比較。
DWORD WINAPI ThreadProc(
LPVOID lpParameter // 線(xiàn)程數(shù)據(jù)
);
你可以看到,所有函數(shù)都使用相同的調(diào)用規(guī)范并都接受 32位參數(shù),返回值的大小都相同。也就是說(shuō),我們可以傳遞一個(gè)指針到LoadLibrary/FreeLibrary 作為到 CreateRemoteThread 的線(xiàn)程例程。但這里有兩個(gè)問(wèn)題,請(qǐng)看下面對(duì)CreateRemoteThread 的描述:
- CreateRemoteThread 的 lpStartAddress 參數(shù)必須表示遠(yuǎn)程進(jìn)程中線(xiàn)程例程的開(kāi)始地址。
- 如果傳遞到 ThreadFunc 的參數(shù)lpParameter——被解釋為常規(guī)的 32位值(FreeLibrary將它解釋為一個(gè) HMODULE),一切OK。但是,如果 lpParameter 被解釋為一個(gè)指針(LoadLibraryA將它解釋為一個(gè)串指針)。它必須指向遠(yuǎn)程進(jìn)程的某些數(shù)據(jù)。
第一個(gè)問(wèn)題實(shí)際上是由它自己解決的。LoadLibrary 和 FreeLibray 兩個(gè)函數(shù)都在 kernel32.dll 中。因?yàn)楸仨毐WCkernel32存在并且在每個(gè)“常規(guī)”進(jìn)程中的加載地址要相同,LoadLibrary/FreeLibray 的地址在每個(gè)進(jìn)程中的地址要相同,這就保證了有效的指針被傳遞到遠(yuǎn)程進(jìn)程。
第二個(gè)問(wèn)題也很容易解決。只要通過(guò) WriteProcessMemory 將 DLL 模塊名(LoadLibrary需要的DLL模塊名)拷貝到遠(yuǎn)程進(jìn)程即可。
所以,為了使用CreateRemoteThread 和 LoadLibrary 技術(shù),需要按照下列步驟來(lái)做:
- 獲取遠(yuǎn)程進(jìn)程(OpenProcess)的 HANDLE;
- 為遠(yuǎn)程進(jìn)程中的 DLL名分配內(nèi)存(VirtualAllocEx);
- 將 DLL 名,包含全路徑名,寫(xiě)入分配的內(nèi)存(WriteProcessMemory);
- 用 CreateRemoteThread 和 LoadLibrary. 將你的DLL映射到遠(yuǎn)程進(jìn)程;
- 等待直到線(xiàn)程終止(WaitForSingleObject),也就是說(shuō)直到 LoadLibrary 調(diào)用返回。另一種方法是,一旦 DllMain(用DLL_PROCESS_ATTACH調(diào)用)返回,線(xiàn)程就會(huì)終止;
- 獲取遠(yuǎn)程線(xiàn)程的退出代碼(GetExitCodeThread)。注意這是一個(gè) LoadLibrary 返回的值,因此是所映射 DLL 的基地址(HMODULE)。
在第二步中釋放分配的地址(VirtualFreeEx);
- 用 CreateRemoteThread 和 FreeLibrary從遠(yuǎn)程進(jìn)程中卸載 DLL。傳遞在第六步獲取的 HMODULE 句柄到 FreeLibrary(通過(guò) CreateRemoteThread 的lpParameter參數(shù));
- 注意:如果你注入的 DLL 產(chǎn)生任何新的線(xiàn)程,一定要在卸載DLL 之前將它們都終止掉;
- 等待直到線(xiàn)程終止(WaitForSingleObject);
此外,處理完成后不要忘了關(guān)閉所有句柄,包括在第四步和第八步創(chuàng)建的兩個(gè)線(xiàn)程以及在第一步獲取的遠(yuǎn)程線(xiàn)程句柄。現(xiàn)在讓我們看一下 LibSpy 的部分代碼,為了簡(jiǎn)單起見(jiàn),上述步驟的實(shí)現(xiàn)細(xì)節(jié)中的錯(cuò)誤處理以及 UNICODE 支持部分被略掉。
HANDLE hThread;
char szLibPath[_MAX_PATH]; // “LibSpy.dll”模塊的名稱(chēng) (包括全路徑);
void* pLibRemote; // 遠(yuǎn)程進(jìn)程中的地址,szLibPath 將被拷貝到此處;
DWORD hLibModule; // 要加載的模塊的基地址(HMODULE)
HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
// 初始化szLibPath
//...
// 1. 在遠(yuǎn)程進(jìn)程中為szLibPath 分配內(nèi)存
// 2. 將szLibPath 寫(xiě)入分配的內(nèi)存
pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
sizeof(szLibPath), NULL );
// 將"LibSpy.dll" 加載到遠(yuǎn)程進(jìn)程(使用CreateRemoteThread 和 LoadLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"LoadLibraryA" ),
pLibRemote, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 獲取所加載的模塊的句柄
::GetExitCodeThread( hThread, &hLibModule );
// 清除
::CloseHandle( hThread );
::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );
假設(shè)我們實(shí)際想要注入的代碼——SendMessage ——被放在DllMain (DLL_PROCESS_ATTACH)中,現(xiàn)在它已經(jīng)被執(zhí)行。那么現(xiàn)在應(yīng)該從目標(biāo)進(jìn)程中將DLL 卸載:
// 從目標(biāo)進(jìn)程中卸載"LibSpy.dll" (使用 CreateRemoteThread 和 FreeLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"FreeLibrary" ),
(void*)hLibModule, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 清除
::CloseHandle( hThread );
進(jìn)程間通信 到目前為止,我們只討論了關(guān)于如何將DLL 注入到遠(yuǎn)程進(jìn)程的內(nèi)容,但是,在大多數(shù)情況下,注入的 DLL 都需要與原應(yīng)用程序進(jìn)行某種方式的通信(回想一下,我們的DLL是被映射到某個(gè)遠(yuǎn)程進(jìn)程的地址空間里了,不是在本地應(yīng)用程序的地址空間中)。比如秘密偵測(cè)程序,DLL必須要知道實(shí)際包含密碼的控件句柄,顯然,編譯時(shí)無(wú)法將這個(gè)值進(jìn)行硬編碼。同樣,一旦DLL獲得了秘密,它必須將它發(fā)送回原應(yīng)用程序,以便能正確顯示出來(lái)。
幸運(yùn)的是,有許多方法處理這個(gè)問(wèn)題,文件映射,WM_COPYDATA,剪貼板以及很簡(jiǎn)單的 #pragma data_seg 共享數(shù)據(jù)段等,本文我不打算使用這些技術(shù),因?yàn)镸SDN(“進(jìn)程間通信”部分)以及其它渠道可以找到很多文檔參考。不過(guò)我在 LibSpy例子中還是使用了 #pragma data_seg。細(xì)節(jié)請(qǐng)參考 LibSpy 源代碼。
第三部分:CreateRemoteThread 和 WriteProcessMemory 技術(shù)
范例程序——WinSpy
另外一個(gè)將代碼拷貝到另一個(gè)進(jìn)程地址空間并在該進(jìn)程上下文中執(zhí)行的方法是使用遠(yuǎn)程線(xiàn)程和 WriteProcessMemory API。這種方法不用編寫(xiě)單獨(dú)的DLL,而是用 WriteProcessMemory 直接將代碼拷貝到遠(yuǎn)程進(jìn)程——然后用 CreateRemoteThread 啟動(dòng)它執(zhí)行。先來(lái)看看 CreateRemoteThread 的聲明:
HANDLE CreateRemoteThread(
HANDLE hProcess, // 傳入創(chuàng)建新線(xiàn)程的進(jìn)程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全屬性指針
DWORD dwStackSize, // 字節(jié)為單位的初始線(xiàn)程堆棧
LPTHREAD_START_ROUTINE lpStartAddress, // 指向線(xiàn)程函數(shù)的指針
LPVOID lpParameter, // 新線(xiàn)程使用的參數(shù)
DWORD dwCreationFlags, // 創(chuàng)建標(biāo)志
LPDWORD lpThreadId // 指向返回的線(xiàn)程ID
);
如果你比較它與 CreateThread(MSDN)的聲明,你會(huì)注意到如下的差別:
- 在 CreateRemoteThread中,hProcess是額外的一個(gè)參數(shù),一個(gè)進(jìn)程句柄,新線(xiàn)程就是在這個(gè)進(jìn)程中創(chuàng)建的;
- 在 CreateRemoteThread中,lpStartAddress 表示的是在遠(yuǎn)程進(jìn)程地址空間中的線(xiàn)程起始地址。線(xiàn)程函數(shù)必須要存在于遠(yuǎn)程進(jìn)程中,所以我們不能簡(jiǎn)單地傳遞一個(gè)指針到本地的 ThreadFunc。必須得先拷貝代碼到遠(yuǎn)程進(jìn)程;
- 同樣,lpParameter 指向的數(shù)據(jù)也必須要存在于遠(yuǎn)程進(jìn)程,所以也得將它拷貝到那。
綜上所述,我們得按照如下的步驟來(lái)做:
- 獲取一個(gè)遠(yuǎn)程進(jìn)程的HANDLE (OpenProces) ;
- 在遠(yuǎn)程進(jìn)程地址空間中為注入的數(shù)據(jù)分配內(nèi)存(VirtualAllocEx);
- 將初始的 INDATA 數(shù)據(jù)結(jié)構(gòu)的一個(gè)拷貝寫(xiě)入分配的內(nèi)存中(WriteProcessMemory);
- 在遠(yuǎn)程進(jìn)程地址空間中為注入的代碼分配內(nèi)存;
- 將 ThreadFunc 的一個(gè)拷貝寫(xiě)入分配的內(nèi)存;
- 用 CreateRemoteThread啟動(dòng)遠(yuǎn)程的 ThreadFunc 拷貝;
- 等待遠(yuǎn)程線(xiàn)程終止(WaitForSingleObject);
- 獲取遠(yuǎn)程來(lái)自遠(yuǎn)程進(jìn)程的結(jié)果(ReadProcessMemory 或 GetExitCodeThread);
- 釋放在第二步和第四步中分配的內(nèi)存(VirtualFreeEx);
- 關(guān)閉在第六步和第一步獲取的句柄(CloseHandle);
ThreadFunc 必須要遵循的原則:
- 除了kernel32.dll 和user32.dll 中的函數(shù)之外,ThreadFunc 不要調(diào)用任何其它函數(shù),只有 kernel32.dll 和user32.dll被保證在本地和目標(biāo)進(jìn)程中的加載地址相同(注意,user32.dll并不是被映射到每個(gè) Win32 的進(jìn)程)。如果你需要來(lái)自其它庫(kù)中的函數(shù),將LoadLibrary 和 GetProcAddress 的地址傳給注入的代碼,然后放手讓它自己去做。如果映射到目標(biāo)進(jìn)程中的DLL有沖突,你也可以用 GetModuleHandle 來(lái)代替 LoadLibrary。
同樣,如果你想在 ThreadFunc 中調(diào)用自己的子例程,要單獨(dú)把每個(gè)例程的代碼拷貝到遠(yuǎn)程進(jìn)程并用 INJDATA為 ThreadFunc 提供代碼的地址。
- 不要使用靜態(tài)字符串,而要用 INJDATA 來(lái)傳遞所有字符串。之所以要這樣,是因?yàn)榫幾g器將靜態(tài)字符串放在可執(zhí)行程序的“數(shù)據(jù)段”中,可是引用(指針)是保留在代碼中的。那么,遠(yuǎn)程進(jìn)程中ThreadFunc 的拷貝指向的內(nèi)容在遠(yuǎn)程進(jìn)程的地址空間中是不存在的。
- 去掉 /GZ 編譯器開(kāi)關(guān),它在調(diào)試版本中是默認(rèn)設(shè)置的。
- 將 ThreadFunc 和 AfterThreadFunc 聲明為靜態(tài)類(lèi)型,或者不啟用增量鏈接。
- ThreadFunc 中的局部變量一定不能超過(guò)一頁(yè)(也就是 4KB)。
注意在調(diào)試版本中4KB的空間有大約10個(gè)字節(jié)是用于內(nèi)部變量的。
- 如果你有一個(gè)開(kāi)關(guān)語(yǔ)句塊大于3個(gè)case 語(yǔ)句,將它們像下面這樣拆分開(kāi):
switch( expression ) {
case constant1: statement1; goto END;
case constant2: statement2; goto END;
case constant3: statement2; goto END;
}
switch( expression ) {
case constant4: statement4; goto END;
case constant5: statement5; goto END;
case constant6: statement6; goto END;
}
END:
或者將它們修改成一個(gè) if-else if 結(jié)構(gòu)語(yǔ)句(參見(jiàn)附錄E)。
如果你沒(méi)有按照這些規(guī)則來(lái)做,目標(biāo)進(jìn)程很可能會(huì)崩潰。所以務(wù)必牢記。在目標(biāo)進(jìn)程中不要假設(shè)任何事情都會(huì)像在本地進(jìn)程中那樣 (參見(jiàn)附錄F)。
GetWindowTextRemote(A/W)
要想從“遠(yuǎn)程”編輯框獲得密碼,你需要做的就是將所有功能都封裝在GetWindowTextRemot(A/W):中。
int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );
int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
參數(shù)說(shuō)明:
hProcess:編輯框控件所屬的進(jìn)程句柄;
hWnd:包含密碼的編輯框控件句柄;
lpString:接收文本的緩沖指針;
返回值:返回值是拷貝的字符數(shù);
下面讓我們看看它的部分代碼——尤其是注入數(shù)據(jù)的代碼——以便明白 GetWindowTextRemote 的工作原理。此處為簡(jiǎn)單起見(jiàn),略掉了 UNICODE 支持部分。
INJDATA
typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);
typedef struct {
HWND hwnd; // 編輯框句柄
SENDMESSAGE fnSendMessage; // 指向user32.dll 中 SendMessageA 的指針
char psText[128]; // 接收密碼的緩沖
} INJDATA;
INJDATA 是一個(gè)被注入到遠(yuǎn)程進(jìn)程的數(shù)據(jù)結(jié)構(gòu)。但在注入之前,結(jié)構(gòu)中指向 SendMessageA 的指針是在本地應(yīng)用程序中初始化的。因?yàn)閷?duì)于每個(gè)使用user32.dll的進(jìn)程來(lái)說(shuō),user32.dll總是被映射到相同的地址,因此,SendMessageA 的地址也肯定是相同的。這就保證了被傳遞到遠(yuǎn)程進(jìn)程的是一個(gè)有效的指針。
ThreadFunc函數(shù)
static DWORD WINAPI ThreadFunc (INJDATA *pData)
{
pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // Get password
sizeof(pData->psText),
(LPARAM)pData->psText );
return 0;
}
// 該函數(shù)在ThreadFunc之后標(biāo)記內(nèi)存地址
// int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc.
static void AfterThreadFunc (void)
{
}
ThradFunc 是被遠(yuǎn)程線(xiàn)程執(zhí)行的代碼。
- 注釋?zhuān)鹤⒁釧fterThreadFunc 是如何計(jì)算 ThreadFunc 大小的。通常這樣做并不是一個(gè)好辦法,因?yàn)殒溄悠骺梢噪S意更改函數(shù)的順序(也就是說(shuō)ThreadFunc可能被放在 AfterThreadFunc之后)。這一點(diǎn)你可以在小項(xiàng)目中很好地保證函數(shù)的順序是預(yù)先設(shè)想好的,比如 WinSpy 程序。在必要的情況下,你還可以使用 /ORDER 鏈接器選項(xiàng)來(lái)解決函數(shù)鏈接順序問(wèn)題。或者用反匯編確定 ThreadFunc 函數(shù)的大小。
如何使用該技術(shù)子類(lèi)化遠(yuǎn)程控件
范例程序——InjectEx
下面我們將討論一些更復(fù)雜的內(nèi)容,如何子類(lèi)化屬于另一個(gè)進(jìn)程的控件。
首先,你得拷貝兩個(gè)函數(shù)到遠(yuǎn)程進(jìn)程來(lái)完成此任務(wù)
- ThreadFunc實(shí)際上是通過(guò) SetWindowLong子類(lèi)化遠(yuǎn)程進(jìn)程中的控件;
- NewProc是子類(lèi)化控件的新窗口過(guò)程;
這里主要的問(wèn)題是如何將數(shù)據(jù)傳到遠(yuǎn)程窗口過(guò)程 NewProc,因?yàn)?NewProc 是一個(gè)回調(diào)函數(shù),它必須遵循特定的規(guī)范和原則,我們不能簡(jiǎn)單地在參數(shù)中傳遞 INJDATA指針。幸運(yùn)的是我找到了有兩個(gè)方法來(lái)解決這個(gè)問(wèn)題,只不過(guò)要借助匯編語(yǔ)言,所以不要忽略了匯編,關(guān)鍵時(shí)候它是很有用的!
方法一:
如下圖所示:

在遠(yuǎn)程進(jìn)程中,INJDATA 被放在NewProc 之前,這樣 NewProc 在編譯時(shí)便知道 INJDATA 在遠(yuǎn)程進(jìn)程地址空間中的內(nèi)存位置。更確切地說(shuō),它知道相對(duì)于其自身位置的 INJDATA 的地址,我們需要所有這些信息。下面是 NewProc 的代碼:
static LRESULT CALLBACK NewProc(
HWND hwnd, // 窗口句柄
UINT uMsg, // 消息標(biāo)示符
WPARAM wParam, // 第一個(gè)消息參數(shù)
LPARAM lParam ) // 第二個(gè)消息參數(shù)
{
INJDATA* pData = (INJDATA*) NewProc; // pData 指向 NewProc
pData--; // 現(xiàn)在pData 指向INJDATA;
// 回想一下INJDATA 被置于遠(yuǎn)程進(jìn)程N(yùn)ewProc之前;
//-----------------------------
// 此處是子類(lèi)化代碼
// ........
//-----------------------------
// 調(diào)用原窗口過(guò)程;
// fnOldProc (由SetWindowLong 返回) 被(遠(yuǎn)程)ThreadFunc初始化
// 并被保存在(遠(yuǎn)程)INJDATA;中
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
但這里還有一個(gè)問(wèn)題,見(jiàn)第一行代碼:
INJDATA* pData = (INJDATA*) NewProc;
這種方式 pData得到的是硬編碼值(在我們的進(jìn)程中是原 NewProc 的內(nèi)存地址)。這不是我們十分想要的。在遠(yuǎn)程進(jìn)程中,NewProc “當(dāng)前”拷貝的內(nèi)存地址與它被移到的實(shí)際位置是無(wú)關(guān)的,換句話(huà)說(shuō),我們會(huì)需要某種類(lèi)型的“this 指針”。
雖然用 C/C++ 無(wú)法解決這個(gè)問(wèn)題,但借助內(nèi)聯(lián)匯編可以解決,下面是對(duì) NewProc的修改:
static LRESULT CALLBACK NewProc(
HWND hwnd, // 窗口句柄
UINT uMsg, // 消息標(biāo)示符
WPARAM wParam, // 第一個(gè)消息參數(shù)
LPARAM lParam ) // 第二個(gè)消息參數(shù)
{
// 計(jì)算INJDATA 結(jié)構(gòu)的位置
// 在遠(yuǎn)程進(jìn)程中記住這個(gè)INJDATA
// 被放在NewProc之前
INJDATA* pData;
_asm {
call dummy
dummy:
pop ecx // <- ECX 包含當(dāng)前的EIP
sub ecx, 9 // <- ECX 包含NewProc的地址
mov pData, ecx
}
pData--;
//-----------------------------
// 此處是子類(lèi)化代碼
// ........
//-----------------------------
// 調(diào)用原來(lái)的窗口過(guò)程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
那么,接下來(lái)該怎么辦呢?事實(shí)上,每個(gè)進(jìn)程都有一個(gè)特殊的寄存器,它指向下一條要執(zhí)行的指令的內(nèi)存位置。即所謂的指令指針,在32位 Intel 和 AMD 處理器上被表示為 EIP。因?yàn)?EIP是一個(gè)專(zhuān)用寄存器,你無(wú)法象操作一般常規(guī)存儲(chǔ)器(如:EAX,EBX等)那樣通過(guò)編程存取它。也就是說(shuō)沒(méi)有操作代碼來(lái)尋址 EIP,以便直接讀取或修改其內(nèi)容。但是,EIP 仍然還是可以通過(guò)間接方法修改的(并且隨時(shí)可以修改),通過(guò)JMP,CALL和RET這些指令實(shí)現(xiàn)。下面我們就通過(guò)例子來(lái)解釋通過(guò) CALL/RET 子例程調(diào)用機(jī)制在32位 Intel 和 AMD 處理器上是如何工作的。
當(dāng)你調(diào)用(通過(guò) CALL)某個(gè)子例程時(shí),子例程的地址被加載到 EIP,但即便是在 EIP杯修改之前,其舊的那個(gè)值被自動(dòng)PUSH到堆棧(被用于后面作為指令指針?lè)祷兀T谧永虉?zhí)行完時(shí),RET 指令自動(dòng)將堆棧頂POP到 EIP。
現(xiàn)在你知道了如何通過(guò) CALL 和 RET 實(shí)現(xiàn) EIP 的修改,但如何獲取其當(dāng)前的值呢?下面就來(lái)解決這個(gè)問(wèn)題,前面講過(guò),CALL PUSH EIP 到堆棧,所以,為了獲取其當(dāng)前值,調(diào)用“啞函數(shù)”,然后再POP堆棧頂。讓我們用編譯后的 NewProc 來(lái)解釋這個(gè)竅門(mén)。
Address OpCode/Params Decoded instruction
--------------------------------------------------
:00401000 55 push ebp ; entry point of
; NewProc
:00401001 8BEC mov ebp, esp
:00401003 51 push ecx
:00401004 E800000000 call 00401009 ; *a* call dummy
:00401009 59 pop ecx ; *b*
:0040100A 83E909 sub ecx, 00000009 ; *c*
:0040100D 894DFC mov [ebp-04], ecx ; mov pData, ECX
:00401010 8B45FC mov eax, [ebp-04]
:00401013 83E814 sub eax, 00000014 ; pData--;
.....
.....
:0040102D 8BE5 mov esp, ebp
:0040102F 5D pop ebp
:00401030 C21000 ret 0010
- 啞函數(shù)調(diào)用;就是JUMP到下一個(gè)指令并PUSH EIP到堆棧;
- 然后將堆棧頂POP到 ECX,ECX再保存EIP;這也是 POP EIP指令的真正地址;
- 注意 NewProc 的入口點(diǎn)和 “POP ECX”之間的“距離”是9 個(gè)字節(jié);因此為了計(jì)算 NewProc的地址,要從 ECX 減9。
這樣一來(lái),不管 NewProc 被移到什么地方,它總能計(jì)算出其自己的地址。但是,NewProc 的入口點(diǎn)和 “POP ECX”之間的距離可能會(huì)隨著你對(duì)編譯/鏈接選項(xiàng)的改變而變化,由此造成 RELEASE和DEBUG版本之間也會(huì)有差別。但關(guān)鍵是你仍然確切地知道編譯時(shí)的值。
- 首先,編譯函數(shù)
- 用反匯編確定正確的距離
- 最后,用正確的距離值重新編譯
此即為 InjecEx 中使用的解決方案,類(lèi)似于 HookInjEx,交換鼠標(biāo)點(diǎn)擊“開(kāi)始”左右鍵時(shí)的功能。
方法二:
對(duì)于我們的問(wèn)題,在遠(yuǎn)程進(jìn)程地址空間中將 INJDATA 放在 NewProc 前面不是唯一的解決辦法。看下面 NewProc的變異版本:
static LRESULT CALLBACK NewProc(
HWND hwnd, // 窗口句柄
UINT uMsg, // 消息標(biāo)示符
WPARAM wParam, // 第一個(gè)消息參數(shù)
LPARAM lParam ) // 第二個(gè)消息參數(shù)
{
INJDATA* pData = 0xA0B0C0D0; // 虛構(gòu)值
//-----------------------------
// 子類(lèi)化代碼
// ........
//-----------------------------
// 調(diào)用原來(lái)的窗口過(guò)程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
此處 0xA0B0C0D0 只是遠(yuǎn)程進(jìn)程地址空間中真實(shí)(絕對(duì))INJDATA地址的占位符。前面講過(guò),你無(wú)法在編譯時(shí)知道該地址。但你可以在調(diào)用 VirtualAllocEx (為INJDATA)之后得到 INJDATA 在遠(yuǎn)程進(jìn)程中的位置。編譯我們的 NewProc 后,可以得到如下結(jié)果:
Address OpCode/Params Decoded instruction
--------------------------------------------------
:00401000 55 push ebp
:00401001 8BEC mov ebp, esp
:00401003 C745FCD0C0B0A0 mov [ebp-04], A0B0C0D0
:0040100A ...
....
:0040102D 8BE5 mov esp, ebp
:0040102F 5D pop ebp
:00401030 C21000 ret 0010
因此,其編譯的代碼(十六進(jìn)制)將是:
558BECC745FCD0C0B0A0......8BE55DC21000.
現(xiàn)在你可以象下面這樣繼續(xù):
- 將INJDATA,ThreadFunc和NewProc 拷貝到目標(biāo)進(jìn)程;
- 修改 NewProc 的代碼,以便 pData 中保存的是 INJDATA 的真實(shí)地址。
例如,假設(shè) INJDATA 的地址(VirtualAllocEx返回的值)在目標(biāo)進(jìn)程中是 0x008a0000。然后象下面這樣修改NewProc的代碼:
558BECC745FCD0C0B0A0......8BE55DC21000 <- 原來(lái)的NewProc (注1)
558BECC745FC00008A00......8BE55DC21000 <- 修改后的NewProc,使用的是INJDATA的實(shí)際地址。
也就是說(shuō),你用真正的 INJDATA(注2) 地址替代了虛擬值 A0B0C0D0(注2)。
- 開(kāi)始執(zhí)行遠(yuǎn)程的 ThreadFunc,它負(fù)責(zé)子類(lèi)化遠(yuǎn)程進(jìn)程中的控件。
- 注1、有人可能會(huì)問(wèn),為什么地址 A0B0C0D0 和 008a0000 在編譯時(shí)順序是相反的。因?yàn)?Intel 和 AMD 處理器使用 little-endian 符號(hào)來(lái)表示(多字節(jié))數(shù)據(jù)。換句話(huà)說(shuō),某個(gè)數(shù)字的低位字節(jié)被存儲(chǔ)在內(nèi)存的最小地址處,而高位字節(jié)被存儲(chǔ)在最高位地址。
假設(shè)“UNIX”這個(gè)詞存儲(chǔ)用4個(gè)字節(jié),在 big-endian 系統(tǒng)中,它被存為“UNIX”,在 little-endian 系統(tǒng)中,它將被存為“XINU”。
- 注2、某些破解(很糟)以類(lèi)似的方式修改可執(zhí)行代碼,但是一旦加載到內(nèi)存,一個(gè)程序是無(wú)法修改自己的代碼的(代碼駐留在可執(zhí)行程序的“.text” 區(qū)域,這個(gè)區(qū)域是寫(xiě)保護(hù)的)。但仍可以修改遠(yuǎn)程的 NewProc,因?yàn)樗窍惹耙?PAGE_EXECUTE_READWRITE 許可方式被拷貝到某個(gè)內(nèi)存塊中的。
何時(shí)使用 CreateRemoteThread 和 WriteProcessMemory 技術(shù)
與其它方法比較,使用 CreateRemoteThread 和 WriteProcessMemory 技術(shù)進(jìn)行代碼注入更靈活,這種方法不需要額外的 dll,不幸的是,該方法更復(fù)雜并且風(fēng)險(xiǎn)更大,只要ThreadFunc出現(xiàn)哪怕一丁點(diǎn)錯(cuò)誤,很容易就讓?zhuān)ú⑶易畲罂赡艿貢?huì))使遠(yuǎn)程進(jìn)程崩潰(參見(jiàn)附錄 F),因?yàn)檎{(diào)試遠(yuǎn)程 ThreadFunc 將是一個(gè)可怕的夢(mèng)魘,只有在注入的指令數(shù)很少時(shí),你才應(yīng)該考慮使用這種技術(shù)進(jìn)行注入,對(duì)于大塊的代碼注入,最好用 I.和II 部分討論的方法。
WinSpy 以及 InjectEx 請(qǐng)從這里下載源代碼。
結(jié)束語(yǔ)
到目前為止,有幾個(gè)問(wèn)題是我們未提及的,現(xiàn)總結(jié)如下:
| 解決方案 |
OS |
進(jìn)程 |
| I、Hooks |
Win9x 和 WinNT |
僅僅與 USER32.DLL (注3)鏈接的進(jìn)程 |
| II、CreateRemoteThread & LoadLibrary |
僅 WinNT(注4) |
所有進(jìn)程(注5), 包括系統(tǒng)服務(wù)(注6) |
III、CreateRemoteThread & WriteProcessMemory |
僅 WinNT |
所有進(jìn)程, 包括系統(tǒng)服務(wù) |
- 注3:顯然,你無(wú)法hook一個(gè)沒(méi)有消息隊(duì)列的線(xiàn)程,此外,SetWindowsHookEx不能與系統(tǒng)服務(wù)一起工作,即使它們與 USER32.DLL 進(jìn)行鏈接;
- 注4:Win9x 中沒(méi)有 CreateRemoteThread,也沒(méi)有 VirtualAllocEx (實(shí)際上,在Win9x 中可以仿真,但不是本文討論的問(wèn)題了);
- 注5:所有進(jìn)程 = 所有 Win32 進(jìn)程 + csrss.exe
本地應(yīng)用 (smss.exe, os2ss.exe, autochk.exe 等)不使用 Win32 API,所以也不會(huì)與 kernel32.dll 鏈接。唯一一個(gè)例外是 csrss.exe,Win32 子系統(tǒng)本身,它是本地應(yīng)用程序,但其某些庫(kù)(~winsrv.dll)需要 Win32 DLLs,包括 kernel32.dll;
- 注6:如果你想要將代碼注入到系統(tǒng)服務(wù)中(lsass.exe, services.exe, winlogon.exe 等)或csrss.exe,在打開(kāi)遠(yuǎn)程句柄(OpenProcess)之前,將你的進(jìn)程優(yōu)先級(jí)置為 “SeDebugPrivilege”(AdjustTokenPrivileges)。
最后,有幾件事情一定要了然于心:你的注入代碼很容易摧毀目標(biāo)進(jìn)程,尤其是注入代碼本身出錯(cuò)的時(shí)候,所以要記住:權(quán)力帶來(lái)責(zé)任!
因?yàn)楸疚闹械脑S多例子是關(guān)于密碼的,你也許還讀過(guò) Zhefu Zhang 寫(xiě)的另外一篇文章“Super Password Spy++” ,在該文中,他解釋了如何獲取IE 密碼框中的內(nèi)容,此外,他還示范了如何保護(hù)你的密碼控件免受類(lèi)似的攻擊。
附錄A:
為什么 kernel32.dll 和user32.dll 總是被映射到相同的地址。
我的假定:因?yàn)镸icrosoft 的程序員認(rèn)為這樣做有助于速度優(yōu)化,為什么呢?我的解釋是——通常一個(gè)可執(zhí)行程序是由幾個(gè)部分組成,其中包括“.reloc” 。當(dāng)鏈接器創(chuàng)建 EXE 或者 DLL文件時(shí),它對(duì)文件被映射到哪個(gè)內(nèi)存地址做了一個(gè)假設(shè)。這就是所謂的首選加載/基地址。在映像文件中所有絕對(duì)地址都是基于鏈接器首選的加載地址,如果由于某種原因,映像文件沒(méi)有被加載到該地址,那么這時(shí)“.reloc”就起作用了,它包含映像文件中的所有地址的清單,這個(gè)清單中的地址反映了鏈接器首選加載地址和實(shí)際加載地址的差別(無(wú)論如何,要注意編譯器產(chǎn)生的大多數(shù)指令使用某種相對(duì)地址尋址,因此,并沒(méi)有你想象的那么多地址可供重新分配),另一方面,如果加載器能夠按照鏈接器首選地址加載映像文件,那么“.reloc”就被完全忽略掉了。
但kernel32.dll 和user32.dll 及其加載地址為何要以這種方式加載呢?因?yàn)槊恳粋€(gè) Win32 程序都需要kernel32.dll,并且大多數(shù)Win32 程序也需要 user32.dll,那么總是將它們(kernel32.dll 和user32.dll)映射到首選地址可以改進(jìn)所有可執(zhí)行程序的加載時(shí)間。這樣一來(lái),加載器絕不能修改kernel32.dll and user32.dll.中的任何(絕對(duì))地址。我們用下面的例子來(lái)說(shuō)明:
將某個(gè)應(yīng)用程序 App.exe 的映像基地址設(shè)置成 KERNEL32的地址(/base:"0x77e80000")或 USER32的首選基地址(/base:"0x77e10000"),如果 App.exe 不是從 USER32 導(dǎo)入方式來(lái)使用 USER32,而是通過(guò)LoadLibrary 加載,那么編譯并運(yùn)行App.exe 后,會(huì)報(bào)出錯(cuò)誤信息("Illegal System DLL Relocation"——非法系統(tǒng)DLL地址重分配),App.exe 加載失敗。
為什么會(huì)這樣呢?當(dāng)創(chuàng)建進(jìn)程時(shí),Win 2000、Win XP 和Win 2003系統(tǒng)的加載器要檢查 kernel32.dll 和user32.dll 是否被映射到首選基地址(實(shí)際上,它們的名字都被硬編碼進(jìn)了加載器),如果沒(méi)有被加載到首選基地址,將發(fā)出錯(cuò)誤。在 WinNT4中,也會(huì)檢查ole32.dll,在WinNT 3.51 和較低版本的Windows中,由于不會(huì)做這樣的檢查,所以kernel32.dll 和user32.dll可以被加載任何地方。只有ntdll.dll總是被加載到其基地址,加載器不進(jìn)行檢查,一旦ntdll.dll沒(méi)有在其基地址,進(jìn)程就無(wú)法創(chuàng)建。
總之,對(duì)于 WinNT 4 和較高的版本中
- 一定要被加載到基地址的DLLs 有:kernel32.dll、user32.dll 和ntdll.dll;
- 每個(gè)Win32 程序都要使用的 DLLs+ csrss.exe:kernel32.dll 和ntdll.dll;
- 每個(gè)進(jìn)程都要使用的DLL只有一個(gè),即使是本地應(yīng)用:ntdll.dll;
附錄B:
/GZ 編譯器開(kāi)關(guān)
在生成 Debug 版本時(shí),/GZ 編譯器特性是默認(rèn)打開(kāi)的。你可以用它來(lái)捕獲某些錯(cuò)誤(具體細(xì)節(jié)請(qǐng)參考相關(guān)文檔)。但對(duì)我們的可執(zhí)行程序意味著什么呢?
當(dāng)打開(kāi) /GZ 開(kāi)關(guān),編譯器會(huì)添加一些額外的代碼到可執(zhí)行程序中每個(gè)函數(shù)所在的地方,包括一個(gè)函數(shù)調(diào)用(被加到每個(gè)函數(shù)的最后)——檢查已經(jīng)被我們的函數(shù)修改的 ESP堆棧指針。什么!難道有一個(gè)函數(shù)調(diào)用被添加到 ThreadFunc 嗎?那將導(dǎo)致災(zāi)難。ThreadFunc 的遠(yuǎn)程拷貝將調(diào)用一個(gè)在遠(yuǎn)程進(jìn)程中不存在的函數(shù)(至少是在相同的地址空間中不存在)
附錄C:
靜態(tài)函數(shù)和增量鏈接
增量鏈接主要作用是在生成應(yīng)用程序時(shí)縮短鏈接時(shí)間。常規(guī)鏈接和增量鏈接的可執(zhí)行程序之間的差別是——增量鏈接時(shí),每個(gè)函數(shù)調(diào)用經(jīng)由一個(gè)額外的JMP指令,該指令由鏈接器發(fā)出(該規(guī)則的一個(gè)例外是函數(shù)聲明為靜態(tài))。這些 JMP 指令允許鏈接器在內(nèi)存中移動(dòng)函數(shù),這種移動(dòng)無(wú)需修改引用函數(shù)的 CALL指令。但這些JMP指令也確實(shí)導(dǎo)致了一些問(wèn)題:如 ThreadFunc 和 AfterThreadFunc 將指向JMP指令而不是實(shí)際的代碼。所以當(dāng)計(jì)算ThreadFunc 的大小時(shí):
const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc)
你實(shí)際上計(jì)算的是指向 ThreadFunc 的JMPs 和AfterThreadFunc之間的“距離” (通常它們會(huì)緊挨著,不用考慮距離問(wèn)題)。現(xiàn)在假設(shè) ThreadFunc 的地址位于004014C0 而伴隨的 JMP指令位于 00401020。
:00401020 jmp 004014C0
...
:004014C0 push EBP ; ThreadFunc 的實(shí)際地址
:004014C1 mov EBP, ESP
...
那么
WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);
將拷貝“JMP 004014C0”指令(以及隨后cbCodeSize范圍內(nèi)的所有指令)到遠(yuǎn)程進(jìn)程——不是實(shí)際的 ThreadFunc。遠(yuǎn)程進(jìn)程要執(zhí)行的第一件事情將是“JMP 004014C0” 。它將會(huì)在其最后幾條指令當(dāng)中——遠(yuǎn)程進(jìn)程和所有進(jìn)程均如此。但 JMP指令的這個(gè)“規(guī)則”也有例外。如果某個(gè)函數(shù)被聲明為靜態(tài)的,它將會(huì)被直接調(diào)用,即使增量鏈接也是如此。這就是為什么規(guī)則#4要將 ThreadFunc 和 AfterThreadFunc 聲明為靜態(tài)或禁用增量鏈接的緣故。(有關(guān)增量鏈接的其它信息參見(jiàn) Matt Pietrek的文章“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools” )
附錄D:
為什么 ThreadFunc的局部變量只有 4k?
局部變量總是存儲(chǔ)在堆棧中,如果某個(gè)函數(shù)有256個(gè)字節(jié)的局部變量,當(dāng)進(jìn)入該函數(shù)時(shí),堆棧指針就減少256個(gè)字節(jié)(更精確地說(shuō),在函數(shù)開(kāi)始處)。例如,下面這個(gè)函數(shù):
void Dummy(void) {
BYTE var[256];
var[0] = 0;
var[1] = 1;
var[255] = 255;
}
編譯后的匯編如下:
:00401000 push ebp
:00401001 mov ebp, esp
:00401003 sub esp, 00000100 ; change ESP as storage for
; local variables is needed
:00401006 mov byte ptr [esp], 00 ; var[0] = 0;
:0040100A mov byte ptr [esp+01], 01 ; var[1] = 1;
:0040100F mov byte ptr [esp+FF], FF ; var[255] = 255;
:00401017 mov esp, ebp ; restore stack pointer
:00401019 pop ebp
:0040101A ret
注意上述例子中,堆棧指針是如何被修改的?而如果某個(gè)函數(shù)需要4KB以上局部變量?jī)?nèi)存空間又會(huì)怎么樣呢?其實(shí),堆棧指針并不是被直接修改,而是通過(guò)另一個(gè)函數(shù)調(diào)用來(lái)修改的。就是這個(gè)額外的函數(shù)調(diào)用使得我們的 ThreadFunc “被破壞”了,因?yàn)槠溥h(yuǎn)程拷貝會(huì)調(diào)用一個(gè)不存在的東西。
我們看看文檔中對(duì)堆棧探測(cè)和 /Gs編譯器選項(xiàng)是怎么說(shuō)的:
——“/GS是一個(gè)控制堆棧探測(cè)的高級(jí)特性,堆棧探測(cè)是一系列編譯器插入到每個(gè)函數(shù)調(diào)用的代碼。當(dāng)函數(shù)被激活時(shí),堆棧探測(cè)需要的內(nèi)存空間來(lái)存儲(chǔ)相關(guān)函數(shù)的局部變量。
如果函數(shù)需要的空間大于為局部變量分配的堆棧空間,其堆棧探測(cè)被激活。默認(rèn)的大小是一個(gè)頁(yè)面(在80x86處理器上4kb)。這個(gè)值允許在Win32 應(yīng)用程序和Windows NT虛擬內(nèi)存管理器之間進(jìn)行謹(jǐn)慎調(diào)整以便增加運(yùn)行時(shí)承諾給程序堆棧的內(nèi)存。”
我確信有人會(huì)問(wèn):文檔中的“……堆棧探測(cè)到一塊需要的內(nèi)存空間來(lái)存儲(chǔ)相關(guān)函數(shù)的局部變量……”那些編譯器選項(xiàng)(它們的描述)在你完全弄明白之前有時(shí)真的讓人氣憤。例如,如果某個(gè)函數(shù)需要12KB的局部變量存儲(chǔ)空間,堆棧內(nèi)存將進(jìn)行如下方式的分配(更精確地說(shuō)是“承諾” )。
sub esp, 0x1000 ; "分配" 第一次 4 Kb
test [esp], eax ; 承諾一個(gè)新頁(yè)內(nèi)存(如果還沒(méi)有承諾)
sub esp, 0x1000 ; "分配" 第二次4 Kb
test [esp], eax ; ...
sub esp, 0x1000
test [esp], eax
注意4KB堆棧指針是如何被修改的,更重要的是,每一步之后堆棧底是如何被“觸及”(要經(jīng)過(guò)檢查)。這樣保證在“分配”(承諾)另一頁(yè)面之前,當(dāng)前頁(yè)面承諾的范圍也包含堆棧底。
注意事項(xiàng)
“每一個(gè)線(xiàn)程到達(dá)其自己的堆棧空間,默認(rèn)情況下,此空間由承諾的以及預(yù)留的內(nèi)存組成,每個(gè)線(xiàn)程使用 1 MB預(yù)留的內(nèi)存,以及一頁(yè)承諾的內(nèi)存,系統(tǒng)將根據(jù)需要從預(yù)留的堆棧內(nèi)存中承諾一頁(yè)內(nèi)存區(qū)域” (參見(jiàn) MSDN CreateThread > dwStackSize > Thread Stack Size)
還應(yīng)該清楚為什么有關(guān) /GS 的文檔說(shuō)在堆棧探針在 Win32 應(yīng)用程序和Windows NT虛擬內(nèi)存管理器之間進(jìn)行謹(jǐn)慎調(diào)整。
現(xiàn)在回到我們的ThreadFunc以及 4KB 限制
雖然你可以用 /Gs 防止調(diào)用堆棧探測(cè)例程,但在文檔對(duì)于這樣的做法給出了警告,此外,文件描述可以用 #pragma check_stack 指令關(guān)閉或打開(kāi)堆棧探測(cè)。但是這個(gè)指令好像一點(diǎn)作用都沒(méi)有(要么這個(gè)文檔是垃圾,要么我疏忽了其它一些信息?)。總之,CreateRemoteThread 和 WriteProcessMemory 技術(shù)只能用于注入小塊代碼,所以你的局部變量應(yīng)該盡量少耗費(fèi)一些內(nèi)存字節(jié),最好不要超過(guò) 4KB限制。
附錄E:
為什么要將開(kāi)關(guān)語(yǔ)句拆分成三個(gè)以上?
用下面這個(gè)例子很容易解釋這個(gè)問(wèn)題,假設(shè)有如下這么一個(gè)函數(shù):
int Dummy( int arg1 )
{
int ret =0;
switch( arg1 ) {
case 1: ret = 1; break;
case 2: ret = 2; break;
case 3: ret = 3; break;
case 4: ret = 0xA0B0; break;
}
return ret;
}
編譯后變成下面這個(gè)樣子:
地址 操作碼/參數(shù) 解釋后的指令
--------------------------------------------------
; arg1 -> ECX
:00401000 8B4C2404 mov ecx, dword ptr [esp+04]
:00401004 33C0 xor eax, eax ; EAX = 0
:00401006 49 dec ecx ; ECX --
:00401007 83F903 cmp ecx, 00000003
:0040100A 771E ja 0040102A
; JMP 到表***中的地址之一
; 注意 ECX 包含的偏移
:0040100C FF248D2C104000 jmp dword ptr [4*ecx+0040102C]
:00401013 B801000000 mov eax, 00000001 ; case 1: eax = 1;
:00401018 C3 ret
:00401019 B802000000 mov eax, 00000002 ; case 2: eax = 2;
:0040101E C3 ret
:0040101F B803000000 mov eax, 00000003 ; case 3: eax = 3;
:00401024 C3 ret
:00401025 B8B0A00000 mov eax, 0000A0B0 ; case 4: eax = 0xA0B0;
:0040102A C3 ret
:0040102B 90 nop
; 地址表***
:0040102C 13104000 DWORD 00401013 ; jump to case 1
:00401030 19104000 DWORD 00401019 ; jump to case 2
:00401034 1F104000 DWORD 0040101F ; jump to case 3
:00401038 25104000 DWORD 00401025 ; jump to case 4
注意如何實(shí)現(xiàn)這個(gè)開(kāi)關(guān)語(yǔ)句?
與其單獨(dú)檢查每個(gè)CASE語(yǔ)句,不如創(chuàng)建一個(gè)地址表,然后通過(guò)簡(jiǎn)單地計(jì)算地址表的偏移量而跳轉(zhuǎn)到正確的CASE語(yǔ)句。這實(shí)際上是一種改進(jìn)。假設(shè)你有50個(gè)CASE語(yǔ)句。如果不使用上述的技巧,你得執(zhí)行50次 CMP和JMP指令來(lái)達(dá)到最后一個(gè)CASE。相反,有了地址表后,你可以通過(guò)表查詢(xún)跳轉(zhuǎn)到任何CASE語(yǔ)句,從計(jì)算機(jī)算法角度和時(shí)間復(fù)雜度看,我們用O(5)代替了O(2n)算法。其中:
- O表示最壞的時(shí)間復(fù)雜度;
- 我們假設(shè)需要5條指令來(lái)進(jìn)行表查詢(xún)計(jì)算偏移量,最終跳到相應(yīng)的地址;
現(xiàn)在,你也許認(rèn)為出現(xiàn)上述情況只是因?yàn)镃ASE常量被有意選擇為連續(xù)的(1,2,3,4)。幸運(yùn)的是,它的這個(gè)方案可以應(yīng)用于大多數(shù)現(xiàn)實(shí)例子中,只有偏移量的計(jì)算稍微有些復(fù)雜。但有兩個(gè)例外:
- 如果CASE語(yǔ)句少于等于三個(gè);
- 如果CASE 常量完全互不相關(guān)(如:“"case 1” ,“case 13” ,“case 50” , 和“case 1000” );
顯然,單獨(dú)判斷每個(gè)的CASE常量的話(huà),結(jié)果代碼繁瑣耗時(shí),但使用CMP和JMP指令則使得結(jié)果代碼的執(zhí)行就像普通的if-else 語(yǔ)句。
有趣的地方:如果你不明白CASE語(yǔ)句使用常量表達(dá)式的理由,那么現(xiàn)在應(yīng)該弄明白了吧。為了創(chuàng)建地址表,顯然在編譯時(shí)就應(yīng)該知道相關(guān)地址。
現(xiàn)在回到問(wèn)題!
注意到地址 0040100C 處的JMP指令了嗎?我們來(lái)看看Intel關(guān)于十六進(jìn)制操作碼 FF 的文檔是怎么說(shuō)的:
操作碼 指令 描述
FF /4 JMP r/m32 Jump near, absolute indirect,
address given in r/m32
原來(lái)JMP 使用了一種絕對(duì)尋址方式,也就是說(shuō),它的操作數(shù)(CASE語(yǔ)句中的 0040102C)表示一個(gè)絕對(duì)地址。還用我說(shuō)什么嗎?遠(yuǎn)程 ThreadFunc 會(huì)盲目地認(rèn)為地址表中開(kāi)關(guān)地址是 0040102C,JMP到一個(gè)錯(cuò)誤的地方,造成遠(yuǎn)程進(jìn)程崩潰。
附錄F:
為什么遠(yuǎn)程進(jìn)程會(huì)崩潰呢?
當(dāng)遠(yuǎn)程進(jìn)程崩潰時(shí),它總是會(huì)因?yàn)橄旅孢@些原因:
- 在ThreadFunc 中引用了一個(gè)不存在的串;
- 在在ThreadFunc 中 中一個(gè)或多個(gè)指令使用絕對(duì)尋址(參見(jiàn)附錄E);
- ThreadFunc 調(diào)用某個(gè)不存在的函數(shù)(該調(diào)用可能是編譯器或鏈接器添加的)。你在反匯編器中可以看到這樣的情形:
:004014C0 push EBP ; ThreadFunc 的入口點(diǎn)
:004014C1 mov EBP, ESP
...
:004014C5 call 0041550 ; 這里將使遠(yuǎn)程進(jìn)程崩潰
...
:00401502 ret
如果 CALL 是由編譯器添加的指令(因?yàn)槟承?#8220;禁忌” 開(kāi)關(guān)如/GZ是打開(kāi)的),它將被定位在 ThreadFunc 的開(kāi)始的某個(gè)地方或者結(jié)尾處。
不管哪種情況,你都要小心翼翼地使用 CreateRemoteThread 和 WriteProcessMemory 技術(shù)。尤其要注意你的編譯器/鏈接器選項(xiàng),一不小心它們就會(huì)在 ThreadFunc 添加內(nèi)容。
CreateRemoteThread提供了一個(gè)在遠(yuǎn)程進(jìn)程中執(zhí)行代碼的方法,就像代碼長(zhǎng)出翅膀飛到別處運(yùn)行。本文將做一個(gè)入門(mén)介紹,希望對(duì)廣大編程愛(ài)好者有所幫助。
先解釋一下遠(yuǎn)程進(jìn)程,其實(shí)就是要植入你的代碼的進(jìn)程,相對(duì)于你的工作進(jìn)程(如果叫本地進(jìn)程的話(huà))它就叫遠(yuǎn)程進(jìn)程,可理解為宿主。
首先介紹一下我們的主要工具CreateRemoteThread,這里先將函數(shù)原型簡(jiǎn)單介紹以下。
CreateRemoteThread可將線(xiàn)程創(chuàng)建在遠(yuǎn)程進(jìn)程中。
函數(shù)原型
HANDLE CreateRemoteThread(
HANDLE hProcess, // handle to process
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
SIZE_T dwStackSize, // initial stack size
LPTHREAD_START_ROUTINE lpStartAddress, // thread function
LPVOID lpParameter, // thread argument
DWORD dwCreationFlags, // creation option
LPDWORD lpThreadId // thread identifier
);
參數(shù)說(shuō)明:
hProcess
[輸入] 進(jìn)程句柄
lpThreadAttributes
[輸入] 線(xiàn)程安全描述字,指向SECURITY_ATTRIBUTES結(jié)構(gòu)的指針
dwStackSize
[輸入] 線(xiàn)程棧大小,以字節(jié)表示
lpStartAddress
[輸入] 一個(gè)LPTHREAD_START_ROUTINE類(lèi)型的指針,指向在遠(yuǎn)程進(jìn)程中執(zhí)行的函數(shù)地址
lpParameter
[輸入] 傳入?yún)?shù)
dwCreationFlags
[輸入] 創(chuàng)建線(xiàn)程的其它標(biāo)志
lpThreadId
[輸出] 線(xiàn)程身份標(biāo)志,如果為NULL,則不返回
返回值
成功返回新線(xiàn)程句柄,失敗返回NULL,并且可調(diào)用GetLastError獲得錯(cuò)誤值。
接下來(lái)我們將以?xún)煞N方式使用CreateRemoteThread,大家可以領(lǐng)略到CreateRemoteThread的神通,它使你的代碼可以脫離你的進(jìn)程,植入到別的進(jìn)程中運(yùn)行。
第一種方式
第一種方式,我們使用函數(shù)的形式。即我們將自己程序中的一個(gè)函數(shù)植入到遠(yuǎn)程進(jìn)程中。
步驟1:首先在你的進(jìn)程中創(chuàng)建函數(shù)MyFunc,我們將把它放在另一個(gè)進(jìn)程中運(yùn)行,這里以windows
計(jì)算器為目標(biāo)進(jìn)程。
static DWORD WINAPI MyFunc (LPVOID pData)
{
//do something
//...
//pData輸入項(xiàng)可以是任何類(lèi)型值
//這里我們會(huì)傳入一個(gè)DWORD的值做示例,并且簡(jiǎn)單返回
return *(DWORD*)pData;
}
static void AfterMyFunc (void) {
}
這里有個(gè)小技巧,定義了一個(gè)static void AfterMyFunc (void);為了下面確定我們的代碼大小
步驟2:定位目標(biāo)進(jìn)程,這里是一個(gè)計(jì)算器
HWND hStart = ::FindWindow (TEXT("SciCalc"),NULL);
步驟3:獲得目標(biāo)進(jìn)程句柄,這里用到兩個(gè)不太常用的函數(shù)(當(dāng)然如果經(jīng)常做線(xiàn)程/進(jìn)程等方面的 項(xiàng)目的話(huà),就很面熟了),但及有用
DWORD PID, TID;
TID = ::GetWindowThreadProcessId (hStart, &PID);
HANDLE hProcess;
hProcess = OpenProcess(PROCESS_ALL_ACCESS,false,PID);
步驟4:在目標(biāo)進(jìn)程中配變量地址空間,這里我們分配10個(gè)字節(jié),并且設(shè)定為可以讀
寫(xiě)PAGE_READWRITE,當(dāng)然也可設(shè)為只讀等其它標(biāo)志,這里就不一一說(shuō)明了。
char szBuffer[10];
*(DWORD*)szBuffer=1000;//for test
void *pDataRemote =(char*) VirtualAllocEx( hProcess, 0, sizeof(szBuffer), MEM_COMMIT,
PAGE_READWRITE );
步驟5:寫(xiě)內(nèi)容到目標(biāo)進(jìn)程中分配的變量空間
::WriteProcessMemory( hProcess, pDataRemote, szBuffer,(sizeof(szBuffer),NULL);
步驟6:在目標(biāo)進(jìn)程中分配代碼地址空間
計(jì)算代碼大小
DWORD cbCodeSize=((LPBYTE) AfterMyFunc - (LPBYTE) MyFunc);
分配代碼地址空間
PDWORD pCodeRemote = (PDWORD) VirtualAllocEx( hProcess, 0, cbCodeSize, MEM_COMMIT,
PAGE_EXECUTE_READWRITE );
步驟7:寫(xiě)內(nèi)容到目標(biāo)進(jìn)程中分配的代碼地址空間
WriteProcessMemory( hProcess, pCodeRemote, &MyFunc, cbCodeSize, NULL);
步驟8:在目標(biāo)進(jìn)程中執(zhí)行代碼
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) pCodeRemote,
pDataRemote, 0 , NULL);
DWORD h;
if (hThread)
{
::WaitForSingleObject( hThread, INFINITE );
::GetExitCodeThread( hThread, &h );
TRACE("run and return %d\n",h);
::CloseHandle( hThread );
}
這里有幾個(gè)值得說(shuō)明的地方:
使用WaitForSingleObject等待線(xiàn)程結(jié)束;
使用GetExitCodeThread獲得返回值;
最后關(guān)閉句柄CloseHandle。
步驟9:清理現(xiàn)場(chǎng)
釋放空間
::VirtualFreeEx( hProcess, pCodeRemote,
cbCodeSize,MEM_RELEASE );
::VirtualFreeEx( hProcess, pDataRemote,
cbParamSize,MEM_RELEASE );
關(guān)閉進(jìn)程句柄
::CloseHandle( hProcess );
第二種方式
第二種方式,我們使用動(dòng)態(tài)庫(kù)的形式。即我們將自己一個(gè)動(dòng)態(tài)庫(kù)植入到遠(yuǎn)程進(jìn)程中。
這里不再重復(fù)上面相同的步驟,只寫(xiě)出其中關(guān)鍵的地方.
關(guān)鍵1:
在步驟5中將動(dòng)態(tài)庫(kù)的路徑作為變量傳入變量空間.
關(guān)鍵2:
在步驟8中,將GetProcAddress作為目標(biāo)執(zhí)行函數(shù).
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE )::GetProcAddress(
hModule, "LoadLibraryA"),
pDataRemote, 0, NULL );
另外在步驟9,清理現(xiàn)場(chǎng)中首先要先進(jìn)行釋放我們的動(dòng)態(tài)庫(kù).也即類(lèi)似步驟8執(zhí)行函數(shù)FreeLibrary
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE )::GetProcAddress(
hModule, "FreeLibrary"),
(void*)hLibModule, 0, NULL );
好了,限于篇幅不能夠介紹的很細(xì),在使用過(guò)程中如有疑問(wèn)可向作者咨詢(xún).(開(kāi)發(fā)環(huán)境:windows2000/vc6.0)
本文來(lái)自CSDN博客,轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/fangchao918628/archive/2008/08/30/2852744.aspx
在所有的預(yù)處理指令中,#Pragma 指令可能是最復(fù)雜的了,它的作用是設(shè)定編譯器的狀態(tài)或者是指示編譯器完成一些特定的動(dòng)作。#pragma指令對(duì)每個(gè)編譯器給出了一個(gè)方法,在保持與C和C ++語(yǔ)言完全兼容的情況下,給出主機(jī)或操作系統(tǒng)專(zhuān)有的特征。依據(jù)定義,編譯指示是機(jī)器或操作系統(tǒng)專(zhuān)有的,且對(duì)于每個(gè)編譯器都是不同的。
其格式一般為: #Pragma Para
其中Para 為參數(shù),下面來(lái)看一些常用的參數(shù)。
(1)message 參數(shù)。
Message 參數(shù)是我最喜歡的一個(gè)參數(shù),它能夠在編譯信息輸出窗口中輸出相應(yīng)的信息,這對(duì)于源代碼信息的控制是非常重要的。其使用方法為:
#Pragma message(“消息文本”)
當(dāng)編譯器遇到這條指令時(shí)就在編譯輸出窗口中將消息文本打印出來(lái)。
當(dāng)我們?cè)诔绦蛑卸x了許多宏來(lái)控制源代碼版本的時(shí)候,我們自己有可能都會(huì)忘記有沒(méi)有正確的設(shè)置這些宏,此時(shí)我們可以用這條指令在編譯的時(shí)候就進(jìn)行檢查。
假設(shè)我們希望判斷自己有沒(méi)有在源代碼的什么地方定義了_X86這個(gè)宏可以用下面的方法
#ifdef _X86
#Pragma message(“_X86 macro activated!”)
#endif
當(dāng)我們定義了_X86這個(gè)宏以后,應(yīng)用程序在編譯時(shí)就會(huì)在編譯輸出窗口里顯示“_
X86 macro activated!”。我們就不會(huì)因?yàn)椴挥浀米约憾x的一些特定的宏而抓耳撓腮了。
(2)另一個(gè)使用得比較多的pragma參數(shù)是code_seg。
格式如:
#pragma code_seg( ["section-name"[,"section-class"] ] )
它能夠設(shè)置程序中函數(shù)代碼存放的代碼段,當(dāng)我們開(kāi)發(fā)驅(qū)動(dòng)程序的時(shí)候就會(huì)使用到它。
(3)#pragma once (比較常用)
只要在頭文件的最開(kāi)始加入這條指令就能夠保證頭文件被編譯一次,這條指令實(shí)際上在VC6中就已經(jīng)有了,但是考慮到兼容性并沒(méi)有太多的使用它。
(4)#pragma hdrstop
表示預(yù)編譯頭文件到此為止,后面的頭文件不進(jìn)行預(yù)編譯。BCB可以預(yù)編譯頭文件以加快鏈接的速度,但如果所有頭文件都進(jìn)行預(yù)編譯又可能占太多磁盤(pán)空間,所以使用這個(gè)選項(xiàng)排除一些頭文件。
有時(shí)單元之間有依賴(lài)關(guān)系,比如單元A依賴(lài)單元B,所以單元B要先于單元A編譯。你可以用#pragma startup指定編譯優(yōu)先級(jí),如果使用了#pragma package(smart_init) ,BCB就會(huì)根據(jù)優(yōu)先級(jí)的大小先后編譯。
(5)#pragma resource
#pragma resource "*.dfm"表示把*.dfm文件中的資源加入工程。*.dfm中包括窗體外觀(guān)的定義。
(6)#pragma warning
#pragma warning( disable : 4507 34; once : 4385; error : 164 )
等價(jià)于:
#pragma warning(disable:4507 34) // 不顯示4507和34號(hào)警告信息
#pragma warning(once:4385) // 4385號(hào)警告信息僅報(bào)告一次
#pragma warning(error:164) // 把164號(hào)警告信息作為一個(gè)錯(cuò)誤。
同時(shí)這個(gè)pragma warning 也支持如下格式:
#pragma warning( push [ ,n ] )
#pragma warning( pop )
這里n代表一個(gè)警告等級(jí)(1---4)。
#pragma warning( push )保存所有警告信息的現(xiàn)有的警告狀態(tài)。
#pragma warning( push, n)保存所有警告信息的現(xiàn)有的警告狀態(tài),并且把全局警告等級(jí)設(shè)定為n。
#pragma warning( pop )向棧中彈出最后一個(gè)警告信息,在入棧和出棧之間所作的一切改動(dòng)取消。例如:
#pragma warning( push )
#pragma warning( disable : 4705 )
#pragma warning( disable : 4706 )
#pragma warning( disable : 4707 )
//.......
#pragma warning( pop )
在這段代碼的最后,重新保存所有的警告信息(包括4705,4706和4707)。
(7)pragma comment(...)
該指令將一個(gè)注釋記錄放入一個(gè)對(duì)象文件或可執(zhí)行文件中。
常用的lib關(guān)鍵字,可以幫我們連入一個(gè)庫(kù)文件。
(8)progma data_seg
有的時(shí)候我們可能想讓一個(gè)應(yīng)用程序只啟動(dòng)一次,就像單件模式(singleton)一樣,實(shí)現(xiàn)的方法可能有多種,這里說(shuō)說(shuō)用#pragma data_seg來(lái)實(shí)現(xiàn)的方法,很是簡(jiǎn)潔便利。
應(yīng)用程序的入口文件前面加上
#pragma data_seg("flag_data")
int app_count = 0;
#pragma data_seg()
#pragma comment(linker,"/SECTION:flag_data,RWS")
然后程序啟動(dòng)的地方加上
if(app_count>0) // 如果計(jì)數(shù)大于0,則退出應(yīng)用程序。
{
//MessageBox(NULL, "已經(jīng)啟動(dòng)一個(gè)應(yīng)用程序", "Warning", MB_OK);
//printf("no%d application", app_count);
return FALSE;
}
app_count++;
Windows 在一個(gè)Win32程序的地址空間周?chē)艘坏缐ΑMǔ#粋€(gè)程序的地址空間中的數(shù)據(jù)是私有的,對(duì)別的程序而言是不可見(jiàn)的。但是執(zhí)行STRPROG的多個(gè)執(zhí)行實(shí)體表示了STRLIB在程序的所有執(zhí)行實(shí)體之間共享數(shù)據(jù)是毫無(wú)問(wèn)題的。當(dāng)您在一個(gè)STRPROG窗口中增加或者刪除一個(gè)字符串時(shí),這種改變將立即反映在其它的窗口中。
在全部例程之間,STRLIB共享兩個(gè)變量:一個(gè)字符數(shù)組和一個(gè)整數(shù)(記錄已儲(chǔ)存的有效字符串的個(gè)數(shù))。STRLIB將這兩個(gè)變量?jī)?chǔ)存在共享的一個(gè)特殊內(nèi)存區(qū)段中:
#pragma data_seg ("shared")
int iTotal = 0 ;
WCHAR szStrings [MAX_STRINGS][MAX_LENGTH + 1] = { '\0' } ;
#pragma data_seg ()
第一個(gè)#pragma敘述建立數(shù)據(jù)段,這里命名為shared。您可以將這段命名為任何一個(gè)您喜歡的名字。在這里的#pragma敘述之后的所有初始化了的變量都放在shared數(shù)據(jù)段中。第二個(gè)#pragma敘述標(biāo)示段的結(jié)束。對(duì)變量進(jìn)行專(zhuān)門(mén)的初始化是很重要的,否則編譯器將把它們放在普通的未初始化數(shù)據(jù)段中而不是放在shared中。
連結(jié)器必須知道有一個(gè)「shared」共享數(shù)據(jù)段。在「Project Settings」對(duì)話(huà)框選擇「Link」頁(yè)面卷標(biāo)。選中「STRLIB」時(shí)在「Project Options」字段(在Release和Debug設(shè)定中均可),包含下面的連結(jié)敘述:
字母RWS表示段具有讀、寫(xiě)和共享屬性。或者,您也可以直接用DLL原始碼指定連結(jié)選項(xiàng),就像我們?cè)赟TRLIB.C那樣:
#pragma comment(linker,"/SECTION:shared,RWS")
共享的內(nèi)存段允許iTotal變量和szStrings字符串?dāng)?shù)組在STRLIB的所有例程之間共享。因?yàn)镸AX_STRINGS等于256,而 MAX_LENGTH等于63,所以,共享內(nèi)存段的長(zhǎng)度為32,772字節(jié)-iTotal變量需要4字節(jié),256個(gè)指針中的每一個(gè)都需要128字節(jié)。
#pragma data_seg("flag_data")
int count=0;
#pragma data_seg()
#pragma comment(linker,"/SECTION:flag_data,RWS")
這種方法只能在沒(méi)有def文件時(shí)使用,如果通過(guò)def文件進(jìn)行導(dǎo)出的話(huà),那么設(shè)置就要在def文件內(nèi)設(shè)置而不能
在代碼里設(shè)置了。
SETCTIONS
flag_data READ WRITE SHARED
在主文件中,用#pragma data_seg建立一
個(gè)新的數(shù)據(jù)段并定義共享數(shù)據(jù),其具體格式為:
#pragma data_seg ("shareddata") //名稱(chēng)可以
//自己定義,但必須與下面的一致。
HWND sharedwnd=NULL;//共享數(shù)據(jù)
#pragma data_seg()
僅定義一個(gè)數(shù)據(jù)段還不能達(dá)到共享數(shù)據(jù)的目的,還要告訴編譯器該段的屬性,有兩種方法可以實(shí)現(xiàn)該目的 (其效果是相同的),一種方法是在.DEF文件中加入如下語(yǔ)句: SETCTIONS shareddata READ WRITE SHARED 另一種方法是在項(xiàng)目設(shè)置鏈接選項(xiàng)(Project Setting --〉Link)中加入如下語(yǔ)句: /SECTION:shareddata,rws
第一點(diǎn):什么是共享數(shù)據(jù)段?為什么要用共享數(shù)據(jù)段??它有什么用途??
在Win16環(huán)境中,DLL的全局?jǐn)?shù)據(jù)對(duì)每個(gè)載入它的進(jìn)程來(lái)說(shuō)都是相同的;而在Win32環(huán)境中,情況卻發(fā)生了變化,DLL函數(shù)中的代碼所創(chuàng)建的任何對(duì)象(包括變量)都?xì)w調(diào)用它的線(xiàn)程或進(jìn)程所有。當(dāng)進(jìn)程在載入DLL時(shí),操作系統(tǒng)自動(dòng)把DLL地址映射到該進(jìn)程的私有空間,也就是進(jìn)程的虛擬地址空間,而且也復(fù)制該DLL的全局?jǐn)?shù)據(jù)的一份拷貝到該進(jìn)程空間。也就是說(shuō)每個(gè)進(jìn)程所擁有的相同的DLL的全局?jǐn)?shù)據(jù),它們的名稱(chēng)相同,但其值卻并不一定是相同的,而且是互不干涉的。
因此,在Win32環(huán)境下要想在多個(gè)進(jìn)程中共享數(shù)據(jù),就必須進(jìn)行必要的設(shè)置。在訪(fǎng)問(wèn)同一個(gè)Dll的各進(jìn)程之間共享存儲(chǔ)器是通過(guò)存儲(chǔ)器映射文件技術(shù)實(shí)現(xiàn)的。也可以把這些需要共享的數(shù)據(jù)分離出來(lái),放置在一個(gè)獨(dú)立的數(shù)據(jù)段里,并把該段的屬性設(shè)置為共享。必須給這些變量賦初值,否則編譯器會(huì)把沒(méi)有賦初始值的變量放在一個(gè)叫未被初始化的數(shù)據(jù)段中。
#pragma data_seg預(yù)處理指令用于設(shè)置共享數(shù)據(jù)段。例如:
#pragma data_seg("SharedDataName") HHOOK hHook=NULL; //必須在定義的同時(shí)進(jìn)行初始化!!!!#pragma data_seg()
在#pragma data_seg("SharedDataName")和#pragma data_seg()之間的所有變量將被訪(fǎng)問(wèn)該Dll的所有進(jìn)程看到和共享。再加上一條指令#pragma comment(linker,"/section:.SharedDataName,rws"),[注意:數(shù)據(jù)節(jié)的名稱(chēng)is case sensitive]那么這個(gè)數(shù)據(jù)節(jié)中的數(shù)據(jù)可以在所有DLL的實(shí)例之間共享。所有對(duì)這些數(shù)據(jù)的操作都針對(duì)同一個(gè)實(shí)例的,而不是在每個(gè)進(jìn)程的地址空間中都有一份。
當(dāng)進(jìn)程隱式或顯式調(diào)用一個(gè)動(dòng)態(tài)庫(kù)里的函數(shù)時(shí),系統(tǒng)都要把這個(gè)動(dòng)態(tài)庫(kù)映射到這個(gè)進(jìn)程的虛擬地址空間里(以下簡(jiǎn)稱(chēng)"地址空間")。這使得DLL成為進(jìn)程的一部分,以這個(gè)進(jìn)程的身份執(zhí)行,使用這個(gè)進(jìn)程的堆棧。(這項(xiàng)技術(shù)又叫code Injection技術(shù),被廣泛地應(yīng)用在了病毒、黑客領(lǐng)域!呵呵^_^)
第二點(diǎn):在具體使用共享數(shù)據(jù)段時(shí)需要注意的一些問(wèn)題!
Win32 DLLs are mapped into the address space of the calling process. By default, each process using a DLL has its own instance of all the DLLs global and static variables. (注意: 即使是全局變量和靜態(tài)變量也都不是共享的!) If your DLL needs to share data with other instances of it loaded by other applications, you can use either of the following approaches:
· Create named data sections using the data_seg pragma.
· Use memory mapped files. See the Win32 documentation about memory mapped files.
Here is an example of using the data_seg pragma:
#pragma data_seg (".myseg")
int i = 0;
char a[32] = "hello world";
#pragma data_seg()
data_seg can be used to create a new named section (.myseg in this example). The most typical usage is to call the data segment .shared for clarity. You then must specify the correct sharing attributes for this new named data section in your .def file or with the linker option /SECTION:.MYSEC,RWS. (這個(gè)編譯參數(shù)既可以使用pragma指令來(lái)指定,也可以在VC的IDE中指定!)
There are restrictions to consider before using a shared data segment:
· Any variables in a shared data segment must be statically initialized. In the above example, i is initialized to 0 and a is 32 characters initialized to hello world.
· All shared variables are placed in the compiled DLL in the specified data segment. Very large arrays can result in very large DLLs. This is true of all initialized global variables.
· Never store process-specific information in a shared data segment. Most Win32 data structures or values (such as HANDLEs) are really valid only within the context of a single process.
· Each process gets its own address space. It is very important that pointers are never stored in a variable contained in a shared data segment. A pointer might be perfectly valid in one application but not in another.
· It is possible that the DLL itself could get loaded at a different address in the virtual address spaces of each process. It is not safe to have pointers to functions in the DLL or to other shared variables.
2010年8月10日
#
RTTI、虛函數(shù)和虛基類(lèi)的實(shí)現(xiàn)方式、開(kāi)銷(xiāo)分析及使用指導(dǎo)
白楊
http://baiy.cn
“在正確的場(chǎng)合使用恰當(dāng)?shù)奶匦?#8221; 對(duì)稱(chēng)職的C++程序員來(lái)說(shuō)是一個(gè)基本標(biāo)準(zhǔn)。想要做到這點(diǎn),首先要了解語(yǔ)言中每個(gè)特性的實(shí)現(xiàn)方式及其開(kāi)銷(xiāo)。本文主要討論相對(duì)于傳統(tǒng) C 而言,對(duì)效率有影響的幾個(gè)C++新特性。
相對(duì)于傳統(tǒng)的 C 語(yǔ)言,C++ 引入的額外開(kāi)銷(xiāo)體現(xiàn)在以下兩個(gè)方面:
編譯時(shí)開(kāi)銷(xiāo)
01.png)
| 模板、類(lèi)層次結(jié)構(gòu)、強(qiáng)類(lèi)型檢查等新特性,以及大量使用了這些新特性的 STL 標(biāo)準(zhǔn)庫(kù)都增加了編譯器負(fù)擔(dān)。但是應(yīng)當(dāng)看到,這些新機(jī)能在不降低,甚至(由于模板的內(nèi)聯(lián)能力)提升了程序執(zhí)行效率的前提下,明顯減輕了廣大 C++ 程序員的工作量。
用幾秒鐘的CPU時(shí)間換取幾人日的辛勤勞動(dòng),附帶節(jié)省了日后調(diào)試和維護(hù)代碼的時(shí)間,這點(diǎn)開(kāi)銷(xiāo)當(dāng)算超值。
當(dāng)然,在使用這些特性的時(shí)候,也有不少優(yōu)化技巧。比如:編譯一個(gè) 廣泛依賴(lài)模板庫(kù)的大型軟件時(shí),幾條顯式實(shí)例化指令就可能使編譯速度提高幾十倍;恰當(dāng)?shù)亟M合使用部分專(zhuān)門(mén)化和完全專(zhuān)門(mén)化,不但可以最優(yōu)化程序的執(zhí)行效率,還可以讓同時(shí)使用多種不同參數(shù)實(shí)例化一套模板的程序體積顯著減小……
|
運(yùn)行時(shí)開(kāi)銷(xiāo)
01.png)
運(yùn)行時(shí)開(kāi)銷(xiāo)恐怕是程序員最關(guān)心的問(wèn)題之一了。相對(duì)與傳統(tǒng)C程序而言,C++中有可能引入額外運(yùn)行時(shí)開(kāi)銷(xiāo)的新特性包括:
- 虛基類(lèi)
- 虛函數(shù)
- RTTI(dynamic_cast和typeid)
- 異常
- 對(duì)象的構(gòu)造和析構(gòu)
關(guān)于其中第四點(diǎn):異常,對(duì)于大多數(shù)現(xiàn)代編譯器來(lái)說(shuō),在正常情況(未拋出異常)下,try塊中的代碼執(zhí)行效率和普通代碼一樣高,而且由于不再需要使用傳統(tǒng)上通過(guò)返回值或函數(shù)調(diào)用來(lái)判斷錯(cuò)誤的方式,代碼的實(shí)際執(zhí)行效率還可能進(jìn)一步提高。拋出和捕捉異常的效率也只是在某些情況下才會(huì)稍低于函數(shù)正常返回的效率,何況對(duì)于一個(gè)編寫(xiě)良好的程序,拋出和捕捉異常的機(jī)會(huì)應(yīng)該不多。關(guān)于異常使用的詳細(xì)討論,參見(jiàn):C++編碼規(guī)范正文中的相關(guān)部分和C++異常機(jī)制的實(shí)現(xiàn)方式和開(kāi)銷(xiāo)分析一節(jié)。
而第五點(diǎn),對(duì)象的構(gòu)造和析構(gòu)開(kāi)銷(xiāo)也不總是存在。對(duì)于不需要初始化/銷(xiāo)毀的類(lèi)型,并沒(méi)有構(gòu)造和析構(gòu)的開(kāi)銷(xiāo),相反對(duì)于那些需要初始化/銷(xiāo)毀的類(lèi)型來(lái)說(shuō),即使用傳統(tǒng)的C方式實(shí)現(xiàn),也至少需要與之相當(dāng)?shù)拈_(kāi)銷(xiāo)。這里要注意的一點(diǎn)是盡量不要讓構(gòu)造和析構(gòu)函數(shù)過(guò)于臃腫,特別是在一個(gè)類(lèi)層次結(jié)構(gòu)中更要注意。時(shí)刻保持你的構(gòu)造、析構(gòu)函數(shù)中只有最必要的初始化和銷(xiāo)毀操作,把那些并不是每個(gè)(子)對(duì)象都需要執(zhí)行的操作留給其他方法和派生類(lèi)去解決。
其實(shí)對(duì)一個(gè)優(yōu)秀的編譯器而言,C++的各種特性本身就是使用C/匯編加以千錘百煉而最優(yōu)化實(shí)現(xiàn)的。可以說(shuō),想用C甚至匯編比編譯器更高效地實(shí)現(xiàn)某個(gè)C++特性幾乎是不可能的。要是真能做到這一點(diǎn)的話(huà),大俠就應(yīng)該去寫(xiě)個(gè)編譯器造福廣大程序員才對(duì)~
C++之所以 被廣泛認(rèn)為比C“低效”,其根本原因在于:由于程序員對(duì)某些特性的實(shí)現(xiàn)方式及其產(chǎn)生的開(kāi)銷(xiāo)不夠了解,致使他們?cè)阱e(cuò)誤的場(chǎng)合使用了錯(cuò)誤的特性。而這些錯(cuò)誤基本都集中在:
- 把異常當(dāng)作另一種流控機(jī)制,而不是僅將其用于錯(cuò)誤處理中
- 一個(gè)類(lèi)和/或其基類(lèi)的構(gòu)造、析構(gòu)函數(shù)過(guò)于臃腫,包含了很多非初始化/銷(xiāo)毀范疇的代碼
- 濫用或不正確地使用RTTI、虛函數(shù)和虛基類(lèi)機(jī)制
其中前兩點(diǎn)上文已經(jīng)講過(guò),下面討論第三點(diǎn)。
為了說(shuō)明RTTI、虛函數(shù)和虛基類(lèi)的實(shí)現(xiàn)方式,這里首先給出一個(gè)經(jīng)典的菱形繼承實(shí)例,及其具體實(shí)現(xiàn)(為了便于理解,這里故意忽略了一些無(wú)關(guān)緊要的優(yōu)化):
|
存布局.png)
圖中虛箭頭代表偏移,實(shí)箭頭代表指針
由上圖得到每種特性的運(yùn)行時(shí)開(kāi)銷(xiāo)如下:
| 特性 |
時(shí)間開(kāi)銷(xiāo) |
空間開(kāi)銷(xiāo) |
| RTTI |
幾次整形比較和一次取址操作(可能還會(huì)有1、2次整形加法) |
每類(lèi)型一個(gè)type_info對(duì)象(包括類(lèi)型ID和類(lèi)名稱(chēng)),典型情況下小于32字節(jié)
|
| 虛函數(shù) |
一次整形加法和一次指針間接引用 |
每類(lèi)型一個(gè)虛表,典型情況下小于128字節(jié)
每對(duì)象若干個(gè)(大部分情況下是一個(gè))虛表指針,典型情況下小于8字節(jié)
|
| 虛基類(lèi) |
從虛繼承的子類(lèi)中訪(fǎng)問(wèn)虛基類(lèi)的數(shù)據(jù)成員或其虛函數(shù)時(shí),將增加兩次指針間接引用和一次整形加法(部分情況下可以?xún)?yōu)化為一次指針間接引用)。 |
每類(lèi)型一個(gè)虛基類(lèi)表,典型情況下小于32字節(jié)
每對(duì)象若干虛基類(lèi)表指針,典型情況下小于8字節(jié)
在同時(shí)使用了虛函數(shù)的時(shí)候,虛基類(lèi)表可以合并到虛表(virtual table)中,每對(duì)象的虛基類(lèi)表指針(vbptr)也可以省略(只需vptr即可)。實(shí)際上, 很多實(shí)現(xiàn)都是這么做的。
|
| * 其中“每類(lèi)型”或“每對(duì)象”是指用到該特性的類(lèi)型/對(duì)象。對(duì)于未用到這些功能的類(lèi)型及其對(duì)象,則不會(huì)增加上述開(kāi)銷(xiāo) |
可見(jiàn),關(guān)于老天“餓時(shí)掉餡餅、睡時(shí)掉老婆”等美好傳說(shuō)純屬謠言。但凡人工制品必不完美,總有設(shè)計(jì)上的取舍,有其適應(yīng)的場(chǎng)合也有其不適用的地方。
C++中的每個(gè)特性,都是從程序員平時(shí)的生產(chǎn)生活中逐漸精化而來(lái)的。在不正確的場(chǎng)合使用它們必然會(huì)引起邏輯、行為和性能上的問(wèn)題。對(duì)于上述特性,應(yīng)該只在必要、合理的前提下才使用。
"dynamic_cast" 用于在類(lèi)層次結(jié)構(gòu)中漫游,對(duì)指針或引用進(jìn)行自由的向上、向下或交叉強(qiáng)制。"typeid" 則用于獲取一個(gè)對(duì)象或引用的確切類(lèi)型,與 "dynamic_cast" 不同,將 "typeid" 作用于指針通常是一個(gè)錯(cuò)誤,要得到一個(gè)指針指向之對(duì)象的type_info,應(yīng)當(dāng)先將其解引用(例如:"typeid(*p);")。
一般地講,能用虛函數(shù)解決的問(wèn)題就不要用 "dynamic_cast",能夠用 "dynamic_cast" 解決的就不要用 "typeid"。比如:

void rotate(IN const CShape& iS) { if (typeid(iS) == typeid(CCircle)) { // ... } else if (typeid(iS) == typeid(CTriangle)) { // ... } else if (typeid(iS) == typeid(CSqucre)) { // ... }
// ... } |
以上代碼用 "dynamic_cast" 寫(xiě)會(huì)稍好一點(diǎn),當(dāng)然最好的方式還是在CShape里定義名為 "rotate" 的虛函數(shù)。
虛函數(shù)是C++眾多運(yùn)行時(shí)多態(tài)特性中開(kāi)銷(xiāo)最小,也最常用的機(jī)制。虛函數(shù)的好處和作用這里不再多說(shuō),應(yīng)當(dāng)注意在對(duì)性能有苛刻要求的場(chǎng)合,或者需要頻繁調(diào)用,對(duì)性能影響較大的地方(比如每秒鐘要調(diào)用成千上萬(wàn)次,而自身內(nèi)容又很簡(jiǎn)單的事件處理函數(shù))要慎用虛函數(shù)。
需要特別說(shuō)明的一點(diǎn)是:虛函數(shù)的調(diào)用開(kāi)銷(xiāo)與通過(guò)函數(shù)指針的間接函數(shù)調(diào)用(例如:經(jīng)典C程序中常見(jiàn)的,通過(guò)指向結(jié)構(gòu)中的一個(gè)函數(shù)指針成員調(diào)用;以及調(diào)用DLL/SO中的函數(shù)等常見(jiàn)情況)是相當(dāng)?shù)摹1绕鸷瘮?shù)調(diào)用本身的開(kāi)銷(xiāo)(保存現(xiàn)場(chǎng)->傳遞參數(shù)->傳遞返回值->恢復(fù)現(xiàn)場(chǎng))來(lái)說(shuō),一次指針間接引用是微不足道的。這就使得在絕大部分可以使用函數(shù)的場(chǎng)合中都能夠負(fù)擔(dān)得起虛方法的些微額外開(kāi)銷(xiāo)。
作為一種支持多繼承的面向?qū)ο笳Z(yǔ)言,虛基類(lèi)有時(shí)是保證類(lèi)層次結(jié)構(gòu)正確一致的一種必不可少的手段。但在需要頻繁使用基類(lèi)提供的服務(wù),又對(duì)性能要求較高的場(chǎng)合,應(yīng)該盡量避免使用它。在基類(lèi)中沒(méi)有數(shù)據(jù)成員的場(chǎng)合,也可以解除使用虛基類(lèi)。例如,在上圖中,如果類(lèi) "BB" 中不存在數(shù)據(jù)成員,那么 "BB" 就可以作為一個(gè)普通基類(lèi)分別被 "B1" 和 "B2" 繼承。這樣的優(yōu)化在達(dá)到相同效果的前提下,解除了虛基類(lèi)引起的開(kāi)銷(xiāo)。不過(guò)這種優(yōu)化也會(huì)帶來(lái)一些問(wèn)題:從 "DD" 向上強(qiáng)制到 "BB" 時(shí)會(huì)引起歧義,破壞了類(lèi)層次結(jié)構(gòu)的邏輯關(guān)系。
上述特性的空間開(kāi)銷(xiāo)一般都是可以接受的,當(dāng)然也存在一些特例,比如:在存儲(chǔ)布局需要和傳統(tǒng)C結(jié)構(gòu)兼容的場(chǎng)合、在考慮對(duì)齊的場(chǎng)合、在需要為一個(gè)本來(lái)尺寸很小的類(lèi)同時(shí)實(shí)例化許多對(duì)象的場(chǎng)合等等。
|
2009年11月21日
#
Debug版本包括調(diào)試信息,所以要比Release版本大很多(可能大數(shù)百K至數(shù)M)。至于是否需要DLL支持,主要看你采用的編譯選項(xiàng)。如果是基于ATL的,則Debug和Release版本對(duì)DLL的要求差不多。如果采用的編譯選項(xiàng)為使用MFC動(dòng)態(tài)庫(kù),則需要MFC42D.DLL等庫(kù)支持,而Release版本需要MFC42.DLL支持。Release Build不對(duì)源代碼進(jìn)行調(diào)試,不考慮MFC的診斷宏,使用的是MFC Release庫(kù),編譯十對(duì)應(yīng)用程序的速度進(jìn)行優(yōu)化,而Debug Build則正好相反,它允許對(duì)源代碼進(jìn)行調(diào)試,可以定義和使用MFC的診斷宏,采用MFC Debug庫(kù),對(duì)速度沒(méi)有優(yōu)化。
一、Debug 和 Release 編譯方式的本質(zhì)區(qū)別
Debug 通常稱(chēng)為調(diào)試版本,它包含調(diào)試信息,并且不作任何優(yōu)化,便于程序員調(diào)試程序。Release 稱(chēng)為發(fā)布版本,它往往是進(jìn)行了各種優(yōu)化,使得程序在代碼大小和運(yùn)行速度上都是最優(yōu)的,以便用戶(hù)很好地使用。
Debug 和 Release 的真正秘密,在于一組編譯選項(xiàng)。下面列出了分別針對(duì)二者的選項(xiàng)(當(dāng)然除此之外還有其他一些,如/Fd /Fo,但區(qū)別并不重要,通常他們也不會(huì)引起 Release 版錯(cuò)誤,在此不討論)
Debug 版本:
/MDd /MLd 或 /MTd 使用 Debug runtime library(調(diào)試版本的運(yùn)行時(shí)刻函數(shù)庫(kù))
/Od 關(guān)閉優(yōu)化開(kāi)關(guān)
/D "_DEBUG" 相當(dāng)于 #define _DEBUG,打開(kāi)編譯調(diào)試代碼開(kāi)關(guān)(主要針對(duì)
assert函數(shù))
/ZI 創(chuàng)建 Edit and continue(編輯繼續(xù))數(shù)據(jù)庫(kù),這樣在調(diào)試過(guò)
程中如果修改了源代碼不需重新編譯
/GZ 可以幫助捕獲內(nèi)存錯(cuò)誤
/Gm 打開(kāi)最小化重鏈接開(kāi)關(guān),減少鏈接時(shí)間
Release 版本:
/MD /ML 或 /MT 使用發(fā)布版本的運(yùn)行時(shí)刻函數(shù)庫(kù)
/O1 或 /O2 優(yōu)化開(kāi)關(guān),使程序最小或最快
/D "NDEBUG" 關(guān)閉條件編譯調(diào)試代碼開(kāi)關(guān)(即不編譯assert函數(shù))
/GF 合并重復(fù)的字符串,并將字符串常量放到只讀內(nèi)存,防止
被修改
實(shí)際上,Debug 和 Release 并沒(méi)有本質(zhì)的界限,他們只是一組編譯選項(xiàng)的集合,編譯器只是按照預(yù)定的選項(xiàng)行動(dòng)。事實(shí)上,我們甚至可以修改這些選項(xiàng),從而得到優(yōu)化過(guò)的調(diào)試版本或是帶跟蹤語(yǔ)句的發(fā)布版本。
二、哪些情況下 Release 版會(huì)出錯(cuò)
有了上面的介紹,我們?cè)賮?lái)逐個(gè)對(duì)照這些選項(xiàng)看看 Release 版錯(cuò)誤是怎樣產(chǎn)生的
1. Runtime Library:鏈接哪種運(yùn)行時(shí)刻函數(shù)庫(kù)通常只對(duì)程序的性能產(chǎn)生影響。調(diào)試版本的 Runtime Library 包含了調(diào)試信息,并采用了一些保護(hù)機(jī)制以幫助發(fā)現(xiàn)錯(cuò)誤,因此性能不如發(fā)布版本。編譯器提供的 Runtime Library 通常很穩(wěn)定,不會(huì)造成 Release 版錯(cuò)誤;倒是由于 Debug 的 Runtime Library 加強(qiáng)了對(duì)錯(cuò)誤的檢測(cè),如堆內(nèi)存分配,有時(shí)會(huì)出現(xiàn) Debug 有錯(cuò)但 Release 正常的現(xiàn)象。應(yīng)當(dāng)指出的是,如果 Debug 有錯(cuò),即使 Release 正常,程序肯定是有 Bug 的,只不過(guò)可能是 Release 版的某次運(yùn)行沒(méi)有表現(xiàn)出來(lái)而已。
2. 優(yōu)化:這是造成錯(cuò)誤的主要原因,因?yàn)殛P(guān)閉優(yōu)化時(shí)源程序基本上是直接翻譯的,而打開(kāi)優(yōu)化后編譯器會(huì)作出一系列假設(shè)。這類(lèi)錯(cuò)誤主要有以下幾種:
(1) 幀指針(Frame Pointer)省略(簡(jiǎn)稱(chēng) FPO ):在函數(shù)調(diào)用過(guò)程中,所有調(diào)用信息(返回地址、參數(shù))以及自動(dòng)變量都是放在棧中的。若函數(shù)的聲明與實(shí)現(xiàn)不同(參數(shù)、返回值、調(diào)用方式),就會(huì)產(chǎn)生錯(cuò)誤————但 Debug 方式下,棧的訪(fǎng)問(wèn)通過(guò) EBP 寄存器保存的地址實(shí)現(xiàn),如果沒(méi)有發(fā)生數(shù)組越界之類(lèi)的錯(cuò)誤(或是越界“不多”),函數(shù)通常能正常執(zhí)行;Release 方式下,優(yōu)化會(huì)省略 EBP 棧基址指針,這樣通過(guò)一個(gè)全局指針訪(fǎng)問(wèn)棧就會(huì)造成返回地址錯(cuò)誤是程序崩潰。C++ 的強(qiáng)類(lèi)型特性能檢查出大多數(shù)這樣的錯(cuò)誤,但如果用了強(qiáng)制類(lèi)型轉(zhuǎn)換,就不行了。你可以在 Release 版本中強(qiáng)制加入 /Oy- 編譯選項(xiàng)來(lái)關(guān)掉幀指針省略,以確定是否此類(lèi)錯(cuò)誤。此類(lèi)錯(cuò)誤通常有:
● MFC 消息響應(yīng)函數(shù)書(shū)寫(xiě)錯(cuò)誤。正確的應(yīng)為
afx_msg LRESULT OnMessageOwn(WPARAM wparam, LPARAM lparam);
ON_MESSAGE 宏包含強(qiáng)制類(lèi)型轉(zhuǎn)換。防止這種錯(cuò)誤的方法之一是重定義 ON_MESSAGE 宏,把下列代碼加到 stdafx.h 中(在#include "afxwin.h"之后),函數(shù)原形錯(cuò)誤時(shí)編譯會(huì)報(bào)錯(cuò)
#undef ON_MESSAGE
#define ON_MESSAGE(message, memberFxn) \
{ message, 0, 0, 0, AfxSig_lwl, \
(AFX_PMSG)(AFX_PMSGW)(static_cast< LRESULT (AFX_MSG_CALL \
CWnd::*)(WPARAM, LPARAM) > (&memberFxn) },
(2) volatile 型變量:volatile 告訴編譯器該變量可能被程序之外的未知方式修改(如系統(tǒng)、其他進(jìn)程和線(xiàn)程)。優(yōu)化程序?yàn)榱耸钩绦蛐阅芴岣撸0岩恍┳兞糠旁诩拇嫫髦校?lèi)似于 register 關(guān)鍵字),而其他進(jìn)程只能對(duì)該變量所在的內(nèi)存進(jìn)行修改,而寄存器中的值沒(méi)變。如果你的程序是多線(xiàn)程的,或者你發(fā)現(xiàn)某個(gè)變量的值與預(yù)期的不符而你確信已正確的設(shè)置了,則很可能遇到這樣的問(wèn)題。這種錯(cuò)誤有時(shí)會(huì)表現(xiàn)為程序在最快優(yōu)化出錯(cuò)而最小優(yōu)化正常。把你認(rèn)為可疑的變量加上 volatile 試試。
(3) 變量?jī)?yōu)化:優(yōu)化程序會(huì)根據(jù)變量的使用情況優(yōu)化變量。例如,函數(shù)中有一個(gè)未被使用的變量,在 Debug 版中它有可能掩蓋一個(gè)數(shù)組越界,而在 Release 版中,這個(gè)變量很可能被優(yōu)化調(diào),此時(shí)數(shù)組越界會(huì)破壞棧中有用的數(shù)據(jù)。當(dāng)然,實(shí)際的情況會(huì)比這復(fù)雜得多。與此有關(guān)的錯(cuò)誤有:
● 非法訪(fǎng)問(wèn),包括數(shù)組越界、指針錯(cuò)誤等。例如
void fn(void)
{
int i;
i = 1;
int a[4];
{
int j;
j = 1;
}
a[-1] = 1;//當(dāng)然錯(cuò)誤不會(huì)這么明顯,例如下標(biāo)是變量
a[4] = 1;
}
j 雖然在數(shù)組越界時(shí)已出了作用域,但其空間并未收回,因而 i 和 j 就會(huì)掩蓋越界。而 Release 版由于 i、j 并未其很大作用可能會(huì)被優(yōu)化掉,從而使棧被破壞。
3. _DEBUG 與 NDEBUG :當(dāng)定義了 _DEBUG 時(shí),assert() 函數(shù)會(huì)被編譯,而 NDEBUG 時(shí)不被編譯。除此之外,VC++中還有一系列斷言宏。這包括:
ANSI C 斷言 void assert(int expression );
C Runtime Lib 斷言 _ASSERT( booleanExpression );
_ASSERTE( booleanExpression );
MFC 斷言 ASSERT( booleanExpression );
VERIFY( booleanExpression );
ASSERT_VALID( pObject );
ASSERT_KINDOF( classname, pobject );
ATL 斷言 ATLASSERT( booleanExpression );
此外,TRACE() 宏的編譯也受 _DEBUG 控制。
所有這些斷言都只在 Debug版中才被編譯,而在 Release 版中被忽略。唯一的例外是 VERIFY() 。事實(shí)上,這些宏都是調(diào)用了 assert() 函數(shù),只不過(guò)附加了一些與庫(kù)有關(guān)的調(diào)試代碼。如果你在這些宏中加入了任何程序代碼,而不只是布爾表達(dá)式(例如賦值、能改變變量值的函數(shù)調(diào)用 等),那么 Release 版都不會(huì)執(zhí)行這些操作,從而造成錯(cuò)誤。初學(xué)者很容易犯這類(lèi)錯(cuò)誤,查找的方法也很簡(jiǎn)單,因?yàn)檫@些宏都已在上面列出,只要利用 VC++ 的 Find in Files 功能在工程所有文件中找到用這些宏的地方再一一檢查即可。另外,有些高手可能還會(huì)加入 #ifdef _DEBUG 之類(lèi)的條件編譯,也要注意一下。
順便值得一提的是 VERIFY() 宏,這個(gè)宏允許你將程序代碼放在布爾表達(dá)式里。這個(gè)宏通常用來(lái)檢查 Windows API 的返回值。有些人可能為這個(gè)原因而濫用 VERIFY() ,事實(shí)上這是危險(xiǎn)的,因?yàn)? VERIFY() 違反了斷言的思想,不能使程序代碼和調(diào)試代碼完全分離,最終可能會(huì)帶來(lái)很多麻煩。因此,專(zhuān)家們建議盡量少用這個(gè)宏。
4. /GZ 選項(xiàng):這個(gè)選項(xiàng)會(huì)做以下這些事
(1) 初始化內(nèi)存和變量。包括用 0xCC 初始化所有自動(dòng)變量,0xCD ( Cleared Data ) 初始化堆中分配的內(nèi)存(即動(dòng)態(tài)分配的內(nèi)存,例如 new ),0xDD ( Dead Data ) 填充已被釋放的堆內(nèi)存(例如 delete ),0xFD( deFencde Data ) 初始化受保護(hù)的內(nèi)存(debug 版在動(dòng)態(tài)分配內(nèi)存的前后加入保護(hù)內(nèi)存以防止越界訪(fǎng)問(wèn)),其中括號(hào)中的詞是微軟建議的助記詞。這樣做的好處是這些值都很大,作為指針是不可能的(而且 32 位系統(tǒng)中指針很少是奇數(shù)值,在有些系統(tǒng)中奇數(shù)的指針會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤),作為數(shù)值也很少遇到,而且這些值也很容易辨認(rèn),因此這很有利于在 Debug 版中發(fā)現(xiàn) Release 版才會(huì)遇到的錯(cuò)誤。要特別注意的是,很多人認(rèn)為編譯器會(huì)用 0 來(lái)初始化變量,這是錯(cuò)誤的(而且這樣很不利于查找錯(cuò)誤)。
(2) 通過(guò)函數(shù)指針調(diào)用函數(shù)時(shí),會(huì)通過(guò)檢查棧指針驗(yàn)證函數(shù)調(diào)用的匹配性。(防止原形不匹配)
(3) 函數(shù)返回前檢查棧指針,確認(rèn)未被修改。(防止越界訪(fǎng)問(wèn)和原形不匹配,與第二項(xiàng)合在一起可大致模擬幀指針省略 FPO )
通常 /GZ 選項(xiàng)會(huì)造成 Debug 版出錯(cuò)而 Release 版正常的現(xiàn)象,因?yàn)? Release 版中未初始化的變量是隨機(jī)的,這有可能使指針指向一個(gè)有效地址而掩蓋了非法訪(fǎng)問(wèn)。
除此之外,/Gm /GF 等選項(xiàng)造成錯(cuò)誤的情況比較少,而且他們的效果顯而易見(jiàn),比較容易發(fā)現(xiàn)。
--------------------------------------------------------------
Release是發(fā)行版本,比Debug版本有一些優(yōu)化,文件比Debug文件小
Debug是調(diào)試版本,包括的程序信息更多
Release方法:
build->batch build->build就OK.
-----------------------------------------------------
一、"Debug是調(diào)試版本,包括的程序信息更多"
補(bǔ)充:只有DEBUG版的程序才能設(shè)置斷點(diǎn)、單步執(zhí)行、使用TRACE/ASSERT等調(diào)試輸出語(yǔ)句。REALEASE不包含任何調(diào)試信息,所以體積小、運(yùn)行速度快。
二、一般發(fā)布release的方法除了hzh_shat(水) 所說(shuō)的之外,還可以project->Set Active Config,選中release版本。此后,按F5或F7編譯所得的結(jié)果就是release版本
2009年10月26日
#
1.概覽
.構(gòu)造DLL
(1)僅導(dǎo)出函數(shù)
DLL可以導(dǎo)出全局變量和類(lèi),但我們不建議這么做,建議導(dǎo)出函數(shù)。
(2).lib
每個(gè)DLL都有與之相對(duì)應(yīng)的.lib文件,該文件中列出了DLL中導(dǎo)出的函數(shù)和變量的符號(hào)名
(3)指定要導(dǎo)出的函數(shù)名
因?yàn)椴煌幾g器的Name mangle規(guī)則不同,這就導(dǎo)致DLL不能跨編譯器使用。
有以下兩種方法可以解決這個(gè)問(wèn)題:
1.在.def文件中指定要導(dǎo)出的函數(shù)名
2.在編譯指中指定要導(dǎo)出的函數(shù)名:
#pragma comment(linker, "/export:MyFunc=_MyFunc@8")
.DLL加載路徑
當(dāng)需要加載一個(gè)DLL時(shí),系統(tǒng)會(huì)依照下面的順序去尋找所需DLL直到找到為止,然后加載,否則加載失敗。
(1)當(dāng)前可執(zhí)行文件路徑
(2)GetWindowsDirectory返回的Windows系統(tǒng)路徑
(3)16位系統(tǒng)的路徑 windows"system
(4)GetSystemDirectory返回的Windows系統(tǒng)路徑
(5)當(dāng)前進(jìn)程所在路徑
(6)PATH環(huán)境中所指定的路徑
.創(chuàng)建\使用動(dòng)態(tài)鏈接庫(kù)
首先必須創(chuàng)建一個(gè)包含需要導(dǎo)出的符號(hào)的頭文件,以便其他程序鏈接到該dll上:
// dllexample.h
#ifdef DLLEXAMPLE_EXPORTS // 在編譯命令中已定義,所以實(shí)際用的是 __declspec(dllexport)
#define DLLEXAMPLE_API __declspec(dllexport)
#else
#define DLLEXAMPLE_API __declspec(dllimport)
#endif
DLLEXAMPLE_API int fnDllexample(void);
當(dāng)其他應(yīng)用包含該頭文件,意圖使用該dll的導(dǎo)出符號(hào)時(shí),因?yàn)闆](méi)有定義DLLEXAMPLE_EXPORTS,所以使用的是__declspec(dllimport),這樣編譯器編譯時(shí)便知道這是從外部引入的函數(shù)。在鏈接時(shí),鏈接程序?qū)⑸蓪?dǎo)入表(ImportAddressTable),該表羅列了所有調(diào)用到的函數(shù),以及一個(gè)空白的對(duì)應(yīng)地址。在程序執(zhí)行時(shí),加載器將動(dòng)態(tài)的填入每個(gè)函數(shù)符號(hào)在本進(jìn)程中的地址,使得程序能正確的調(diào)用到dll中的函數(shù)上。
這種通過(guò)dll提供的.h和.lib文件進(jìn)行鏈接dll的使用方式,稱(chēng)為隱式鏈接。用vc開(kāi)發(fā)程序時(shí),幾乎所有的系統(tǒng)API調(diào)用都用了隱式鏈接。
.顯式鏈接
在exe創(chuàng)建時(shí)不引用.lib文件中的符號(hào),當(dāng)然也不必包含.h頭文件,而是由程序調(diào)用LoadLibrary(Ex)以及GetProcAddress函數(shù)來(lái)獲取每個(gè)需要使用的函數(shù)地址,從而進(jìn)行dll中的函數(shù)調(diào)用,這種dll使用方法稱(chēng)為顯式鏈接。顯式鏈接時(shí)不生成對(duì)應(yīng)dll的IAT.
當(dāng)決定不再使用該dll時(shí),通過(guò)調(diào)用FreeLibrary來(lái)卸載。需要注意的是,同一個(gè)進(jìn)程中共計(jì)調(diào)用LoadLibrary的次數(shù)要和調(diào)用FreeLibrary的次數(shù)相等,因?yàn)橄到y(tǒng)維護(hù)了一個(gè)使用計(jì)數(shù),當(dāng)計(jì)數(shù)為0時(shí),才會(huì)真正的卸載該dll.
如果想確認(rèn)一個(gè)dll是否已經(jīng)被映射到進(jìn)程空間中,盡量使用GetModuleHandle,最好不要冒然使用LoadLibrary(Ex).
GetProcAddress可以傳遞函數(shù)名或者序號(hào)(通過(guò)MAKEINTRESOURCE(2)來(lái)"制作"序號(hào)).
1.1動(dòng)態(tài)加載DLL文件 LoadLibraryEx
HMODULE LoadLibraryEx( //返回DLL加載到進(jìn)程空間原首地址。
PCTSTR pszDLLPathName,
HANDLE hFile,
DWORD dwFlags);
dwFlags 可以有以下幾個(gè)值
(1) DONT_RESOLVE_DLL_REFERENCES
建議永遠(yuǎn)不要使有這個(gè)值,它的存在僅僅是為了向后兼容、
(2) LOAD_LIBRARY_AS_DATAFILE
把要加載的DLL文件以數(shù)據(jù)文件的形式加載到進(jìn)程中。
GetModuleHandle和GetProcAddress返回NULL
(3) LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE
與前者相同,不同的時(shí)獨(dú)占打開(kāi),禁止其它進(jìn)程訪(fǎng)問(wèn)和修改該DLL中的內(nèi)容。
(4) LOAD_LIBRARY_AS_IMAGE_RESOURCE
不修改DLL中的RVA,以image的形式加載到進(jìn)程中。常與LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE一起使用。
(5) LOAD_WITH_ALTERED_SEARCH_PATH
修改DLL的加載路徑
1.2 DLL的加載與卸載
(1)加載
不要在同一進(jìn)程中,同時(shí)使用LoadLIbrary和LoadLibraryEx加載同一DLL文件。
DLL的引用計(jì)數(shù)是以進(jìn)程為單位的。LoadLibrary會(huì)把DLL文件加載到內(nèi)存,然后映射到進(jìn)程空間中。
多次加載同一DLL只會(huì)增加引用計(jì)數(shù)而不會(huì)多次映射。當(dāng)所有進(jìn)程對(duì)DLL的引用計(jì)數(shù)都為0時(shí),系統(tǒng)會(huì)在內(nèi)存中釋放該DLL。
(2)卸載
FreeLibrary,FreeLibraryAndExitThread對(duì)當(dāng)前進(jìn)程的DLL的引用計(jì)數(shù)減1
(3) GetProcAddress
取得函數(shù)地址。它只接受ANSI字符串。
2.DLL的入口函數(shù)
2.1 DllMain
BOOL WINAPI DllMain(
HINSTANCE hInstDll, ""加載后在進(jìn)程中的虛擬地址
DWORD fdwReason, ""系統(tǒng)因何而調(diào)用該函數(shù)
PVOID fImpLoad ""查看是隱工還是動(dòng)態(tài)加載該DLL
DLLs用DllMain方法來(lái)初始化他們自已。DllMain中的代碼應(yīng)盡量簡(jiǎn)單,只做一些簡(jiǎn)單的初始化工作。
不要在DllMain中調(diào)用LoadLibrary,FreeLibrary及Shell, ODBC, COM, RPC, 和 socket 函數(shù),從而避免不可預(yù)期的錯(cuò)誤。
2.2 fdwReason的值
(1)DLL_PROCESS_ATTACH
系統(tǒng)在為每個(gè)進(jìn)程第一次加載該DLL時(shí)會(huì),執(zhí)行DLL_PROCESS_ATTACH后面的語(yǔ)句來(lái)初始化DLL,DllMain的返回值僅由它決定。
系統(tǒng)會(huì)忽略DLL_THREAD_ATTACH等執(zhí)行后DllMain的返回值。
如果DllMain返回FALSE,系統(tǒng)會(huì)自動(dòng)調(diào)用DLL_PROCESS_DETACH的代碼并解除DLL文件中進(jìn)程中的內(nèi)存映射。
(2)DLL_PROCESS_DETACH
如果DLL是因進(jìn)程終止而卸載其在進(jìn)程中的映射,那么負(fù)責(zé)調(diào)用ExitProcess的線(xiàn)程會(huì)調(diào)用DllMain中DLL_PROCESS_DETACH所對(duì)應(yīng)的代碼。
如果DLL是因FreeLibrary或FreeLibraryAndExitThread,而卸載其在進(jìn)程中的映射, 那么FreeLibrary或FreeLibraryAndExitThread會(huì)負(fù)責(zé)調(diào)用DllMain中DLL_PROCESS_DETACH所對(duì)應(yīng)的代碼。
如果DLL是因TerminateProcess而卸載其在進(jìn)程中的映射,系統(tǒng)不會(huì)調(diào)用DllMain中DLL_PROCESS_DETACH所對(duì)應(yīng)的代碼。
(3) DLL_THREAD_ATTACH
若進(jìn)程是先加載的DLL,后創(chuàng)建的線(xiàn)程
那么在進(jìn)程中創(chuàng)建新線(xiàn)程時(shí)(主線(xiàn)程除外),系統(tǒng)會(huì)執(zhí)行該進(jìn)程已載的所有DLL的DllMain中DLL_THREAD_ATTACH對(duì)應(yīng)的代碼。
若進(jìn)程是先創(chuàng)建的線(xiàn)程,后加載的DLL
那么系統(tǒng)不會(huì)調(diào)用DLL的DllMain中的代碼。
(4) DLL_THREAD_DETACH
進(jìn)程中的線(xiàn)程退出時(shí),會(huì)先執(zhí)行所有已加載DLL的DllMain中DLL_THREAD_DETACH所對(duì)應(yīng)的代碼。若該代碼中有死循環(huán),線(xiàn)程不會(huì)退出。
2.3 同步化DllMain的調(diào)用
同一時(shí)間只能有一個(gè)線(xiàn)程調(diào)用DllMain中的代碼,所以下面的代碼會(huì)導(dǎo)致死循環(huán)
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {
HANDLE hThread;
DWORD dwThreadId;
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// The DLL is being mapped into the process' address space.
// Create a thread to do some stuff.
hThread = CreateThread(NULL, 0, SomeFunction, NULL,
0, &dwThreadId);// CreateThread會(huì)DLL_THREAD_ATTACH中的代碼,但是由于當(dāng)前線(xiàn)程并未執(zhí)行完畢,
//所以DLL_THREAD_ATTACH 中的代碼不會(huì)被執(zhí)行,且CreateThread永無(wú)不會(huì)返回。
// Suspend our thread until the new thread terminates.
WaitForSingleObject(hThread, INFINITE);
// We no longer need access to the new thread.
CloseHandle(hThread);
break;
case DLL_THREAD_ATTACH:
// A thread is being created.
break;
case DLL_THREAD_DETACH:
// A thread is exiting cleanly.
break;
case DLL_PROCESS_DETACH:
// The DLL is being unmapped from the process' address space.
break;
}
return(TRUE);
}
3.延時(shí)加載DLL
(1)延時(shí)加載DLL的限制
延遲加載的D L L是個(gè)隱含鏈接的D L L,它實(shí)際上要等到你的代碼試圖引用D L L中包含的一個(gè)符號(hào)時(shí)才進(jìn)行加載,它與動(dòng)態(tài)加載不同。
4.已知的DLL (Known DLLs)
位置:HKEY_LOCAL_MACHINE"SYSTEM"CurrentControlSet"Control"Session Manager"KnownDLLs
LoadLibrary在查找DLL會(huì)先去該位置查找有無(wú)相應(yīng)的鍵值與DLL要對(duì)應(yīng),若有則根據(jù)鏈值去%SystemRoot%"System32加載鍵值對(duì)應(yīng)的DLL
若無(wú)則根據(jù)默認(rèn)規(guī)去尋找DLL
5.Bind and Rebase Module
它可以程序啟動(dòng)的速度。ReBaseImage
DLL 注入和API鉤(DLL Injection and API Hooking)
1.概覽
每個(gè)進(jìn)程都有自已獨(dú)立的地址空間,一個(gè)進(jìn)程不可能創(chuàng)建一個(gè)指向其它進(jìn)程地址空間的指針。
然而如果我們把自已的DLL注射到另一個(gè)進(jìn)程的地址空間去,我們就可以在那個(gè)被注入的進(jìn)程里為所欲為了。
2.用注冊(cè)表注入DLL
該方法適用于給GUI的程序注入DLL
所有的GUI應(yīng)用程序在啟動(dòng)時(shí)都會(huì)加載User32.dll,而在User32.dll的DLL_PROCESS_ATTACH代碼根據(jù)注冊(cè)表中的信息
來(lái)注入用戶(hù)指定的DLL
注冊(cè)表項(xiàng) HKEY_LOCAL_MACHINE"Software"Microsoft"Windows NT"CurrentVersion"Windows"
中有兩個(gè)值:
LoadAppInit_Dlls:鍵值中指定要注入的DLL 如:c:"inject.dll
AppInit_Dlls:若其鍵值為1,則注入LoadAppInit_Dlls中指定的DLL,否則若為0則不注入。
注:
(1)LoadAppInit_Dlls中的值是以空格或分號(hào)分隔的,所以DLL的路徑中最好不要有空格,最后不指定路徑,直接將DLL放到windows系統(tǒng)目錄中。
(2) 用注冊(cè)表注入DLL的方式有很大的局限性,Kernel32.dll和Ntdll.dll中有的函數(shù)才能調(diào)用
一.注入dll
1.通過(guò)注冊(cè)表項(xiàng) HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs 來(lái)指定你的dll的路徑,那么當(dāng)一個(gè)GUI程序啟動(dòng)時(shí)就要加載User32.dll,而User32.dll將會(huì)檢查這個(gè)值,如果有的話(huà)就LoadLibrary該Dll。這個(gè)方法不好,因?yàn)榇蠖鄶?shù)情況我們只需要針對(duì)性的注入,并且沒(méi)辦法注入到不使用User32.dll的進(jìn)程中;
2.用SetWindowsHookEx函數(shù),并傳遞目標(biāo)線(xiàn)程ID、需要掛載的Dll在本進(jìn)程中的映射地址(hInstance)、替換函數(shù)在本進(jìn)程中的地址。這樣,當(dāng)被掛載進(jìn)程的這個(gè)線(xiàn)程要執(zhí)行相應(yīng)的操作時(shí)(GETMESSAGE、鍵盤(pán)消息之類(lèi)的),就會(huì)發(fā)現(xiàn)已經(jīng)安裝了WH_XX,The system checks to see whether the DLL containing the GetMsgProc function is mapped into Process B's address space,如果還未映射該Dll,則強(qiáng)制LoadLibrary。然后系統(tǒng)調(diào)用hThisInstance + (GetMsgProc - hInstance),從而實(shí)現(xiàn)了事件的通知。這種方法的好處是可以針對(duì)某個(gè)進(jìn)程安裝Hook,缺點(diǎn)是容易被目標(biāo)進(jìn)程發(fā)現(xiàn)、同樣只適用于GUI進(jìn)程。如果不再想使用掛鉤了,那么需要調(diào)用UnhookWindowsHookEx,卸載Hook。
3.使用遠(yuǎn)程線(xiàn)程注入Dll(Injecting a DLL Using Remote Threads)
這個(gè)方法比較好。流程是這樣的:
?調(diào)用VirtualAllocEx,在目標(biāo)進(jìn)程保留一塊內(nèi)存,并提交,其長(zhǎng)度是你要注入Dll的全路徑長(zhǎng)度nLen + 1,返回地址pv;
?調(diào)用WriteProcessMemory,在目標(biāo)進(jìn)程的pv處寫(xiě)入Dll的全路徑,注意要添加\0結(jié)束符;
?獲取本進(jìn)程的LoadLibrary函數(shù)的地址,方法是調(diào)用pfn = GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA")——之所以獲取本進(jìn)程的地址,是因?yàn)閗ernel32.dll在每個(gè)進(jìn)程的映射地址都相同,倘若不同,那么此方法則無(wú)效;
?調(diào)用HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfn, pv, 0, NULL)來(lái)創(chuàng)建遠(yuǎn)程線(xiàn)程,其實(shí)這個(gè)線(xiàn)程函數(shù)就是LoadLibrary函數(shù),因此將執(zhí)行映射Dll到目標(biāo)進(jìn)程的操作;
?調(diào)用VirtuallFreeEx(hProcessRemote, pv)釋放提交的內(nèi)存;
這便完成了dll注入。
缺點(diǎn)是不能用在windows98上。但是對(duì)于xp都要被微軟拋棄的年代,windows98地影響不大了。
4.披著羊皮的狼:使用特洛伊Dll來(lái)注入Dll(Injecting a DLL with a Trojan DLL)
其實(shí)就是替換某個(gè)目標(biāo)進(jìn)程要加載的a.dll,并把a(bǔ).dll的所有引出函數(shù)用函數(shù)轉(zhuǎn)發(fā)器在自己的dll引出。
5.用調(diào)試函數(shù)插入Dll
ReadProcessMemory和WriteProcessMemory是windows提供的調(diào)試函數(shù)。如果在方法3中調(diào)用WriteProcessMemory寫(xiě)入的不是字串而是精心編排好的機(jī)器指令,并且寫(xiě)在目標(biāo)進(jìn)程特定的地址空間,那么這段機(jī)器指令就有機(jī)會(huì)執(zhí)行——而這段機(jī)器指令恰好完成了LoadLibrary功能;
6.其他方法(略)
二.掛接API(API Hooking)
其實(shí),這是許多注入的Dll都愿意做的事情。
所謂掛接API就是在目標(biāo)進(jìn)程調(diào)用windows API之前,先執(zhí)行我們的仿API函數(shù),從而控制系統(tǒng)API的行為,達(dá)到特殊的目的。
我們的仿造函數(shù)必須與要替換的系統(tǒng)API有相同的型參表以及相同的返回值類(lèi)型.
1.改寫(xiě)系統(tǒng)API代碼的前幾個(gè)字節(jié),通過(guò)寫(xiě)入jmp指令來(lái)跳轉(zhuǎn)到我們的函數(shù)。在我們的函數(shù)里執(zhí)行操作,可以直接返回一個(gè)值,也可以將系統(tǒng)API的前幾個(gè)字節(jié)復(fù)原,調(diào)用系統(tǒng)API,并返回系統(tǒng)API的值——隨便你想怎么做。
此方法的缺點(diǎn)是對(duì)于搶占式多線(xiàn)程的系統(tǒng)不太管用。
2.通過(guò)改寫(xiě)目標(biāo)進(jìn)程IAT中要調(diào)用的函數(shù)地址來(lái)達(dá)到目的。具體操作見(jiàn)書(shū)中示例
線(xiàn)程本地存儲(chǔ)(Thread-Local Storage)
例子C / C + +運(yùn)行期庫(kù)要使用線(xiàn)程本地存儲(chǔ)器( T L S)。由于運(yùn)行期庫(kù)是在多線(xiàn)程應(yīng)用程序出現(xiàn)前的許多年設(shè)計(jì)的,因此運(yùn)行期庫(kù)中的大多數(shù)函數(shù)是用于單線(xiàn)程應(yīng)用程序的。函數(shù)s t r t o k就是個(gè)很好的例子。
盡可能避免使用全局變量和靜態(tài)變量
1.動(dòng)態(tài)TLS
圖21-1 用于管理T L S的內(nèi)部數(shù)據(jù)結(jié)構(gòu)
在創(chuàng)建線(xiàn)程時(shí),進(jìn)程會(huì)為當(dāng)前創(chuàng)建的線(xiàn)程分配一個(gè)void *的數(shù)組作為TLS用。它用于存儲(chǔ)只限當(dāng)前線(xiàn)程可見(jiàn)的全局變量。
從而使進(jìn)程中的每個(gè)線(xiàn)程都可以有自已的(不能其它線(xiàn)程訪(fǎng)問(wèn)的)全局變量。
TlsAlloc在返回時(shí)會(huì)先把槽中的值置為0。每個(gè)線(xiàn)程至少有64個(gè)槽。
2.靜態(tài)TLS
__declspec(thread)關(guān)鍵字用于聲明,線(xiàn)程本地的全局變量。
要求聲明的變量必須是全局變量或靜態(tài)變量。
3.Common API:
TlsAlloc TlsFree
TlsSetValue TlsGetValue
__declspec(thread)