• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            road420

            導航

            <2025年5月>
            27282930123
            45678910
            11121314151617
            18192021222324
            25262728293031
            1234567

            統計

            常用鏈接

            留言簿(2)

            隨筆檔案

            文章檔案

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            2013年4月14日 #

            轉載(不要一輩子靠技術生存)

            我現在是自己做,但我此前有多年在從事軟件開發工作,當回過頭來想一想自己,覺得特別想對那些初學JAVA/DOT。NET技術的朋友說點心里話,希望你們能從我們的體會中,多少受點啟發(也許我說的不好,你不贊同但看在我真心的份上別扔磚頭啊).

                一。 在中國你千萬不要因為學習技術就可以換來穩定的生活和高的薪水待遇,你千萬更不要認為哪些從事 市場開發,跑腿的人,沒有前途。

                不知道你是不是知道,咱們中國有相當大的一部分軟件公司,他們的軟件開發團隊都小的可憐,甚至只有1-3個人,連一個項目小組都算不上,而這樣的團隊卻要承擔一個軟件公司所有的軟件開發任務,在軟件上線和開發的關鍵階段需要團隊的成員沒日沒夜的加班,還需要為測試出的BUG和不能按時提交的軟件模塊功能而心懷忐忑,有的時候如果你不幸加入現場開發的團隊你則需要背井離鄉告別你的女友,進行封閉開發,你平時除了編碼之外就是吃飯和睡覺(有錢的公司甚至請個保姆為你做飯,以讓你節省出更多的時間來投入到工作中,讓你一直在那種累了就休息,不累就立即工作的狀態)

                更可怕的是,會讓你接觸的人際關系非常單一,除了有限的技術人員之外你幾乎見不到做其他行業工作和職位的人,你的朋友圈子小且單一,甚至破壞你原有的愛情(想象一下,你在外地做現場開發2個月以上,卻從沒跟女友見過一面的話,你的女友是不是會對你呲牙裂嘴)。

             

                也許你拿到了所謂的白領的工資,但你卻從此失去享受生活的自由,如果你想做技術人員尤其是開發人員,我想你很快就會理解,你多么想在一個地方長期待一段時間,認識一些朋友,多一些生活時間的愿望。

                比之于我們的生活和人際關系及工作,那些從事售前和市場開發的朋友,卻有比我們多的多的工作之外的時間,甚至他們工作的時間有的時候是和生活的時間是可以兼顧的,他們可以通過市場開發,認識各個行業的人士,可以認識各種各樣的朋友,他們比我們坦率說更有發財和發展的機會,只要他們跟我們一樣勤奮。(有一種勤奮的普通人,如果給他換個地方,他馬上會成為一個勤奮且出眾的人。)

                二。在學習技術的時候千萬不要認為如果做到技術最強,就可以成為100%受尊重的人。

                有一次一個人在面試項目經理的時候說了這么一段話:我只用最聽話的人,按照我的要求做只要是聽話就要,如果不聽話不管他技術再好也不要。隨后這個人得到了試用機會,如果沒意外的話,他一定會是下一個項目經理的繼任者。

                朋友們你知道嗎?不管你技術有多強,你也不可能自由的騰出時間象別人那樣研究一下LINUX源碼,甚至寫一個LINUX樣的杰作來表現你的才能。你需要做的就是按照要求寫代碼,寫代碼的含義就是都規定好,你按照規定寫,你很快就會發現你昨天寫的代碼,跟今天寫的代碼有很多類似,等你寫過一段時間的代碼,你將領略:復制,拷貝,粘貼那樣的技術對你來說是何等重要。(如果你沒有做過1年以上的真正意義上的開發不要反駁我)。

                如果你幸運的能夠聽到市場人員的談話,或是領導們的談話,你會隱約覺得他們都在把技術人員當作編碼的機器來看,你的價值并沒有你想象的那么重要。而在你所在的團隊內部,你可能正在為一個技術問題的討論再跟同事搞內耗,因為他不服你,你也不服他,你們都認為自己的對,其實你們兩個都對,而爭論的目的就是為了在關鍵場合證明一下自己比對方技術好,比對方強。(在一個項目開發中,沒有人愿意長期聽別人的,總想換個位置領導別人。)

                三。你更不要認為,如果我技術夠好,我就自己創業,自己有創業的資本,因為自己是搞技術的。

                如果你那樣認為,真的是大錯特錯了,你可以做個調查在非技術人群中,沒有幾個人知道C#與JAVA的,更談不上來欣賞你的技術是好還是不好。一句話,技術僅僅是一個工具,善于運用這個工具為別人干活的人,卻往往不太擅長用這個工具來為自己創業,因為這是兩個概念,訓練的技能也是完全不同的。

                創業最開始的時候,你的人際關系,你處理人際關系的能力,你對社會潛規則的認識,還有你明白不明白別人的心,你會不會說讓人喜歡的話,還有你對自己所提供的服務的策劃和推銷等等,也許有一萬,一百萬個值得我們重視的問題,但你會發現技術卻很少有可能包含在這一萬或一百萬之內,如果你創業到了一個快成功的階段,你會這樣告訴自己:我干嗎要親自做技術,我聘一個人不就行了,這時候你才真正會理解技術的作用,和你以前做技術人員的作用。

                [小結]

                基于上面的討論,我奉勸那些學習技術的朋友,千萬不要拿科舉考試樣的心態去學習技術,對技術的學習幾近的癡迷,想掌握所有所有的技術,以讓自己成為技術領域的權威和專家,以在必要的時候或是心里不暢快的時候到網上對著菜鳥說自己是前輩。

                技術僅僅是一個工具,是你在人生一個階段生存的工具,你可以一輩子喜歡他,但最好不要一輩子靠它生存。

                掌握技術的唯一目的就是拿它找工作(如果你不想把技術當作你第二生命的話),就是干活。所以你在學習的時候千萬不要去做那些所謂的技術習題或是研究那些帽泡算法,最大數算法了,什么叫干活?

                就是做一個東西讓別人用,別人用了,可以提高他們的工作效率,想象吧,你做1萬道技術習題有什么用?只會讓人覺得酸腐,還是在學習的時候,多培養些自己務實的態度吧,比如研究一下當地市場目前有哪些軟件公司用人,自己離他們的要求到底有多遠,自己具體應該怎么做才可以達到他們的要求。等你分析完這些,你就會發現,找工作成功,技術的貢獻率其實并沒有你原來想象的那么高。

                不管你是學習技術為了找工作還是創業,你都要對技術本身有個清醒的認識,在中國不會出現BILL GATES,因為,中國目前還不是十分的尊重技術人才,還僅僅的停留在把軟件技術人才當作人才機器來用的尷尬境地。(如果你不理解,一種可能是你目前僅僅從事過技術工作,你的朋友圈子里技術類的朋友占了大多數,一種可能是你還沒有工作,但喜歡讀比爾。蓋茨的傳記)。

            posted @ 2013-04-14 22:27 深邃者 閱讀(446) | 評論 (1)編輯 收藏

            2010年9月16日 #

            五種程序設計方法

            五種程序設計方法

            1.        結構化程序設計

            為了提高程序的可讀性、可重用性等,逐漸出現了將程序開發中經常用到的相同的功能,比如數學函數運算、字符串操作等,獨立出來編寫成函數,然后按照相互關系或應用領域匯集在相同的文件里,這些文件構成了函數庫

            函數庫是一種對信息的封裝,將常用的函數封裝起來,人們不必知道如何實現它們。只需要了解如何調用它們即可。函數庫可以被多個應用程序共享,在具體編程環境中,一般都有一個頭文件相伴,在這個頭文件中以標準的方式定義了庫中每個函數的接口,根據這些接口形式可以在程序中的任何地方調用所需的函數。

            由于函數、庫、模塊等一系列概念和技術的出現,程序設計逐漸變成如圖所示的風格。程序被分解成一個個函數模塊,其中既有系統函數,也有用戶定義的函數。通過對函數的調用,程序的運行逐步被展開。閱讀程序時,由于每一塊的功能相對獨立,因此對程序結構的理解相對容易,在一定程度上緩解了程序代碼可讀性和可重用件的矛盾,但并未徹底解決矛盾。隨著計算機程序的規模越來越大,這個問題變得更加尖銳,于是出現了另一種編程風格——結構化程序設計

            在結構化程序設計中,任何程序段的編寫都基于3種結構:分支結構、循環結構和順序結構。程序具有明顯的模塊化特征,每個程序模塊具有惟一的出口和入口語句。結構化程序的結構簡單清晰,模塊化強,描述方式貼近人們習慣的推理式思維方式。因此可讀性強,在軟件重用性、軟件維護等方面都有所進步,在大型軟件開發尤其是大型科學與工程運算軟件的開發中發揮了重要作用。因此到目前為止,仍有許多應用程序的開發采用結構化程序設計技術和方法。即使在目前流行的面向對象軟件開發中也不能完全脫離結構化程序設計。

             

            2.        面向對象程序設計

            面向對象的程序役計方法是程序設計的一種新方法。所有面向對象的程序設計語言一般都含有三個方面的語法機制,即對象和類、多態性、繼承性。

            1.對象和類

            對象的概念、原理和方法是面向對象的理序設計語言暈重要的特征。對象是用戶定義的類型(稱為類)的變量。一個對象是既包含數據又包合操作該數據的代碼(函數)的邏輯實體。對象中的這些數據和函數稱為對象的成員,即成員數據和成員函數。對象中的成員分為公有的和私有的。公有成員是對象與外界的接口界面。外界只能通過調用訪問一個對象的公有成員來實現該對象的功能。私有成員體現一個對象的組織形式和功能的實現細節。外界無法對私有成員進行操作。類對象按照規范進行操作,將描述客觀事物的數據表達及對數據的操作處理封裝在一起,成功地實現了面向對象的程序設計。當用戶定義了一個類類型后,就可以在該類型的名下定義變量(即對象)了。類是結構體類型的擴充。結構體中引入成員函數并規定了其訪問和繼承原則后便成了類。

            2.多態性

            面向對象的程序設計語言支持多態性,把一個接口用于一類活動。即一個接口多種算法。具體實施時該選擇哪一個算法是由特定的語法機制確定的。C++編譯時和運行時都支持多態性。編譯時的多態性體現在重載函數和重載運算符等方面。運行時的多態性體現在繼承關系及虛函數等方面。

            3.繼承性

            C++程序中,由一個類(稱為基類)可以派生出新類(稱為派生類)。這種派生的語法機制使得新類的出現輕松自然,使得一個復雜事物可以被順理成章地歸結為由逐層派生的對象描述。派生使得程序中定義的類呈層次結構。處于子層的對參既具有其父層對象的共性.又具有自身的特性。繼承性是一個類對象獲得其基類對象特性的過程。C++中嚴格地規定了派生類對其基類的繼承原則和訪問權限,使得程序中對數據和函數的訪間,需在家族和朋友間嚴格區分。

             

            3.        事件驅動的程序設計

            事件驅動的程序設計實際上是面向對象程序設計的一個應用,但它目前僅適用于windows系列操作系統windows環境中的應用程序與MSDOS環境中的應用程序運行機制不同、設計程序的方式也不一樣。windows程序采用事件驅動機制運行,這種事件驅動程序由事件的發生與否來控制,系統中每個對象狀態副改變都是事件發生的原由或結果,設計程序時需以一種非順序方式處理事件,與順序的、過程驅動的傳統程序設計方法迥異

            事件也稱消息,含義比較廣泛,常見的事件有鼠標事件(如民標移動、單擊、掠過窗口邊界)、鍵盤事件(如按鍵的壓下與拾起)等多種。應用程序運行經過一系列必要的初始化后,將進入等待狀態,等待有事件發生,一旦事件出現,程序就被激活并進行相應處理。

            事件驅動程序設計是圍繞著消息的產生與處理進行的.消息可來自程序中的某個對象,也可由用戶、wlndow s或運行著的其他應用程序產生。每當事件發生時,Windows俘獲有關事件,然后將消息分別轉發到相關應用程序中的有關對象,需要對消息作出反應的對象應該提供消息處理函數,通過這個消息處理函數實現對象的一種功能或行為。所以編寫事件驅動程序的大部分工作是為各個對象()添加各種消息的處理函數。由于一個對象可以是消息的接收者,同時也可能是消息的發送者,所發送的消息與接收到的消息也可以是相同的消息,而有些消息的發出時間是無法預知的(比如關于鍵盤的消息),因此應用程序的執行順序是無法預知的。

            4.        邏輯式對象程序設計

            邏輯式程序設計的概念來自邏輯式程序設計語言Prolog這一曾經在計算機領域引起震動的日本第五代計算機的基本系統語言,在這種第五代計算機中,Prolog的地位相當于當前計算機中的機器語言。

            Prolog主要應用在人工智能領域,在自然語言處理、數據庫查詢、算法描述等方面都有應用,尤其適于作為專家系統的開發工具。

            Prolog是一種陳述式語言,它不是一種嚴格的通用程序設計語言,使用Prolog編寫程序不需要描述具體的解題過程、只需結出一些必要的事實和規則,這些規則是解決問題方法的規范說明,根據這些規則和事實.計算機利用渭詞邏輯,通過演繹推理得到求解問題的執行序列。

            5.        并行程序設計

            一個有實際應用的并行算法,最終總要在并行機上實現,為此首先就要將并行算法轉化為并行程序,此過程就是所謂的并行程序設計(Parallel Program)。它要求算法設計者、系統結構師和軟件工作者廣泛頻繁的交互。因為設計并行程序涉及到的知識面較廣,主要包括操作系統中的有關知識和優化編譯方面的知識。操作系統內容非常豐富,并行程序中最基本的計算要素如任務、進程、線程等基本概念、同步機制和通信操作等。

            目前并行程序設計的狀況是:并行軟件的發展落后于并行硬件;和串行系統與應用軟件相比,現今的并行系統與應用軟件甚少且不成熟;并行軟件的缺乏是發展并行計算的主要障礙;不幸的是,這種狀態似乎仍在繼續著。究其原因是并行程序設計遠比串行程序設計復雜:并行程序設計不但包含了串行程序設計,面且還包含了更多的富有挑戰性的問題;串行程序設計僅有一個普遍被接受的馮·諾依曼計算模型,而并行計算模型雖有好多,但沒有一個可被共同認可的像馮·諾依曼那樣的優秀模型;并行程序設計對環境工具(如編譯、查錯等)的要求遠比串行程序設計先進得多;串行程序設計比較適合于自然習慣,且人們在過去積累了大量的編程知識、經驗和寶貴的軟件財富。

            posted @ 2010-09-16 14:45 深邃者 閱讀(283) | 評論 (0)編輯 收藏

            程序的內存分配

            一個由c/C++編譯的程序占用的內存分為以下幾個部分
            1、棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似于數據結構中的棧。
            2、堆區(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似于鏈表,呵呵。
            3、全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。 - 程序結束后有系統釋放
            4、文字常量區 —常量字符串就是放在這里的。 程序結束后由系統釋放
            5、程序代碼區—存放函數體的二進制代碼。

            堆和棧的理論知識
            2.1申請方式
            stack:
            由系統自動分配。
            例如,聲明在函數中一個局部變量 int b; 系統自動在棧中為b開辟空間
            heap:
            需要程序員自己申請
            ,并指明大小,在c中malloc函數
            如p1 = (char *)malloc(10);
            在C++中用new運算符
            如p2 = (char *)malloc(10);
            但是注意p1、p2本身是在棧中的。
            2.2申請后系統的響應
            棧:只要棧的剩余空間大于所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。
            堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,
            會 遍歷該鏈表,尋找第一個空間大于所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,并將該結點的空間分配給程序,另外,對于大多數系統,會在這塊內 存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。另外,由于找到的堆結點的大小不一定正好等于申請的大 小,系統會自動的將多余的那部分重新放入空閑鏈表中。
            2.3申請大小的限制
            棧:在Windows下,棧是向低地址擴展的數據結構,是一塊 連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因 此,能從棧獲得的空間較小。
            堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由于系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
            2.4申請效率的比較:
            棧由系統自動分配,速度較快。但程序員是無法控制的。
            堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便.
            另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是直接在進程的地址空間中保留一快內存,雖然用起來最不方便。但是速度, 也最靈活
            2.5堆和棧中的存儲內容
            棧: 在函數調用時,第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。
            當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
            堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。
            2.6存取效率的比較

            char s1[] = "aaaaaaaaaaaaaaa";
            char *s2 = "bbbbbbbbbbbbbbbbb";
            aaaaaaaaaaa是在運行時刻賦值的;
            而bbbbbbbbbbb是在編譯時就確定的;
            但是,在以后的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。
            比如:
            #include
            void main()
            {
            char a = 1;
            char c[] = "1234567890";
            char *p ="1234567890";
            a = c[1];
            a = p[1];
            return;
            }
            對應的匯編代碼
            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
            第一種在讀取時直接就把字符串中的元素讀到寄存器cl中,而第二種則要先把指edx中,在根據edx讀取字符,顯然慢了。

            2.7小結:
            堆和棧的區別可以用如下的比喻來看出:
            使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
            使用堆就象是自己動手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由度大

            posted @ 2010-09-16 14:22 深邃者 閱讀(316) | 評論 (1)編輯 收藏

            代碼注入的三種方法

            簡介

              本文將討論如何把代碼注入不同的進程地址空間,然后在該進程的上下文中執行注入的代碼。 我們在網上可以查到一些窗口/密碼偵測的應用例子,網上的這些程序大多都依賴 Windows 鉤子技術來實現。本文將討論除了使用 Windows 鉤子技術以外的其它技術來實現這個功能。如圖一所示:

            圖一 WinSpy 密碼偵測程序

            為了找到解決問題的方法。首先讓我們簡單回顧一下問題背景。
              要“讀取”某個控件的內容——無論這個控件是否屬于當前的應用程序——通常都是發送 WM_GETTEXT 消息來實現。這個技術也同樣應用到編輯控件,但是如果該編輯控件屬于另外一個進程并設置了 ES_PASSWORD 式樣,那么上面講的方法就行不通了。用 WM_GETTEXT 來獲取控件的內容只適用于進程“擁有”密碼控件的情況。所以我們的問題變成了如何在另外一個進程的地址空間執行:

            ::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );

            通常有三種可能性來解決這個問題。

            1. 將你的代碼放入某個 DLL,然后通過 Windows 鉤子映射該DLL到遠程進程;
            2. 將你的代碼放入某個 DLL,然后通過 CreateRemoteThread 和 LoadLibrary 技術映射該DLL到遠程進程;
            3. 如果不寫單獨的 DLL,可以直接將你的代碼拷貝到遠程進程——通過 WriteProcessMemory——并用 CreateRemoteThread 啟動它的執行。本文將在第三部分詳細描述該技術實現細節;

            第一部分: Windows 鉤子

            范例程序——參見HookSpy 和HookInjEx

              Windows 鉤子主要作用是監控某些線程的消息流。通常我們將鉤子分為本地鉤子和遠程鉤子以及系統級鉤子,本地鉤子一般監控屬于本進程的線程的消息流,遠程鉤子是線程專用的,用于監控屬于另外進程的線程消息流。系統級鉤子監控運行在當前系統中的所有線程的消息流。
              如果鉤子作用的線程屬于另外的進程,那么你的鉤子過程必須駐留在某個動態鏈接庫(DLL)中。然后系統映射包含鉤子過程的DLL到鉤子作用的線程的地址空間。Windows將映射整個 DLL,而不僅僅是鉤子過程。這就是為什么 Windows 鉤子能被用于將代碼注入到別的進程地址空間的原因。
              本文我不打算涉及鉤子的具體細節(關于鉤子的細節請參見 MSDN 庫中的 SetWindowHookEx API),但我在此要給出兩個很有用心得,在相關文檔中你是找不到這些內容的:

            1. 在成功調用 SetWindowsHookEx 后,系統自動映射 DLL 到鉤子作用的線程地址空間,但不必立即發生映射,因為 Windows 鉤子都是消息,DLL 在消息事件發生前并沒有產生實際的映射。例如:
                如果你安裝一個鉤子監控某些線程(WH_CALLWNDPROC)的非隊列消息,在消息被實際發送到(某些窗口的)鉤子作用的線程之前,該DLL 是不會被映射到遠程進程的。換句話說,如果 UnhookWindowsHookEx 在某個消息被發送到鉤子作用的線程之前被調用,DLL 根本不會被映射到遠程進程(即使 SetWindowsHookEx 本身調用成功)。為了強制進行映射,在調用 SetWindowsHookEx 之后馬上發送一個事件到相關的線程。
                在UnhookWindowsHookEx了之后,對于沒有映射的DLL處理方法也一樣。只有在足夠的事件發生后,DLL才會有真正的映射。
            2. 當你安裝鉤子后,它們可能影響整個系統得性能(尤其是系統級鉤子),但是你可以很容易解決這個問題,如果你使用線程專用鉤子的DLL映射機制,并不截獲消息。考慮使用如下代碼:
              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;
                  }			
                那么會發生什么呢?首先我們通過Windows 鉤子將DLL映射到遠程進程。然后,在DLL被實際映射之后,我們解開鉤子。通常當第一個消息到達鉤子作用線程時,DLL此時也不會被映射。這里的處理技巧是調用LoadLibrary通過增加 DLLs的引用計數來防止映射不成功。
                現在剩下的問題是如何卸載DLL,UnhookWindowsHookEx 是不會做這個事情的,因為鉤子已經不作用于線程了。你可以像下面這樣做:

              • 就在你想要解除DLL映射前,安裝另一個鉤子;
              • 發送一個“特殊”消息到遠程線程;
              • 在鉤子過程中截獲這個消息,響應該消息時調用 FreeLibrary 和 UnhookWindowsHookEx;

               

                目前只使用了鉤子來從處理遠程進程中DLL的映射和解除映射。在此“作用于線程的”鉤子對性能沒有影響。
              下面我們將討論另外一種方法,這個方法與 LoadLibrary 技術的不同之處是DLL的映射機制不會干預目標進程。相對LoadLibrary 技術,這部分描述的方法適用于 WinNT和Win9x。
                但是,什么時候使用這個技巧呢?答案是當DLL必須在遠程進程中駐留較長時間(即如果你子類化某個屬于另外一個進程的控件時)以及你想盡可能少的干涉目標進程時。我在 HookSpy 中沒有使用它,因為注入DLL 的時間并不長——注入時間只要足夠得到密碼即可。我提供了另外一個例子程序——HookInjEx——來示范。HookInjEx 將DLL映射到資源管理器“explorer.exe”,并從中/解除影射,它子類化“開始”按鈕,并交換鼠標左右鍵單擊“開始”按鈕的功能。

            HookSpy 和 HookInjEx 的源代碼都可以從本文的下載源代碼中獲得。

            第二部分:CreateRemoteThread 和 LoadLibrary 技術

            范例程序——LibSpy

            通常,任何進程都可以通過 LoadLibrary API 動態加載DLL。但是,如何強制一個外部進程調用這個函數呢?答案是:CreateRemoteThread。
            首先,讓我們看一下 LoadLibrary 和FreeLibrary API 的聲明:

            HINSTANCE LoadLibrary(
            LPCTSTR lpLibFileName // 庫模塊文件名的地址
            );
            BOOL FreeLibrary(
            HMODULE hLibModule // 要加載的庫模塊的句柄
            );

            現在將它們與傳遞到 CreateRemoteThread 的線程例程——ThreadProc 的聲明進行比較。

            DWORD WINAPI ThreadProc(
            LPVOID lpParameter // 線程數據
            );

            你可以看到,所有函數都使用相同的調用規范并都接受 32位參數,返回值的大小都相同。也就是說,我們可以傳遞一個指針到LoadLibrary/FreeLibrary 作為到 CreateRemoteThread 的線程例程。但這里有兩個問題,請看下面對CreateRemoteThread 的描述:

            1. CreateRemoteThread 的 lpStartAddress 參數必須表示遠程進程中線程例程的開始地址。
            2. 如果傳遞到 ThreadFunc 的參數lpParameter——被解釋為常規的 32位值(FreeLibrary將它解釋為一個 HMODULE),一切OK。但是,如果 lpParameter 被解釋為一個指針(LoadLibraryA將它解釋為一個串指針)。它必須指向遠程進程的某些數據。

              第一個問題實際上是由它自己解決的。LoadLibrary 和 FreeLibray 兩個函數都在 kernel32.dll 中。因為必須保證kernel32存在并且在每個“常規”進程中的加載地址要相同,LoadLibrary/FreeLibray 的地址在每個進程中的地址要相同,這就保證了有效的指針被傳遞到遠程進程。
              第二個問題也很容易解決。只要通過 WriteProcessMemory 將 DLL 模塊名(LoadLibrary需要的DLL模塊名)拷貝到遠程進程即可。

            所以,為了使用CreateRemoteThread 和 LoadLibrary 技術,需要按照下列步驟來做:

            1. 獲取遠程進程(OpenProcess)的 HANDLE;
            2. 為遠程進程中的 DLL名分配內存(VirtualAllocEx);
            3. 將 DLL 名,包含全路徑名,寫入分配的內存(WriteProcessMemory);
            4. 用 CreateRemoteThread 和 LoadLibrary. 將你的DLL映射到遠程進程;
            5. 等待直到線程終止(WaitForSingleObject),也就是說直到 LoadLibrary 調用返回。另一種方法是,一旦 DllMain(用DLL_PROCESS_ATTACH調用)返回,線程就會終止;
            6. 獲取遠程線程的退出代碼(GetExitCodeThread)。注意這是一個 LoadLibrary 返回的值,因此是所映射 DLL 的基地址(HMODULE)。
              在第二步中釋放分配的地址(VirtualFreeEx);
            7. 用 CreateRemoteThread 和 FreeLibrary從遠程進程中卸載 DLL。傳遞在第六步獲取的 HMODULE 句柄到 FreeLibrary(通過 CreateRemoteThread 的lpParameter參數);
            8. 注意:如果你注入的 DLL 產生任何新的線程,一定要在卸載DLL 之前將它們都終止掉;
            9. 等待直到線程終止(WaitForSingleObject);

              此外,處理完成后不要忘了關閉所有句柄,包括在第四步和第八步創建的兩個線程以及在第一步獲取的遠程線程句柄。現在讓我們看一下 LibSpy 的部分代碼,為了簡單起見,上述步驟的實現細節中的錯誤處理以及 UNICODE 支持部分被略掉。

            HANDLE hThread;
            char    szLibPath[_MAX_PATH];  // “LibSpy.dll”模塊的名稱 (包括全路徑);
            void*   pLibRemote;   // 遠程進程中的地址,szLibPath 將被拷貝到此處;
            DWORD   hLibModule;   // 要加載的模塊的基地址(HMODULE)
            HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
            // 初始化szLibPath
            //...
            // 1. 在遠程進程中為szLibPath 分配內存
            // 2. 將szLibPath 寫入分配的內存
            pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
            MEM_COMMIT, PAGE_READWRITE );
            ::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
            sizeof(szLibPath), NULL );
            // 將"LibSpy.dll" 加載到遠程進程(使用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 );		
              假設我們實際想要注入的代碼——SendMessage ——被放在DllMain (DLL_PROCESS_ATTACH)中,現在它已經被執行。那么現在應該從目標進程中將DLL 卸載:
            // 從目標進程中卸載"LibSpy.dll"  (使用 CreateRemoteThread 和 FreeLibrary)
            hThread = ::CreateRemoteThread( hProcess, NULL, 0,
            (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
            "FreeLibrary" ),
            (void*)hLibModule, 0, NULL );
            ::WaitForSingleObject( hThread, INFINITE );
            // 清除
            ::CloseHandle( hThread );		
            進程間通信

              到目前為止,我們只討論了關于如何將DLL 注入到遠程進程的內容,但是,在大多數情況下,注入的 DLL 都需要與原應用程序進行某種方式的通信(回想一下,我們的DLL是被映射到某個遠程進程的地址空間里了,不是在本地應用程序的地址空間中)。比如秘密偵測程序,DLL必須要知道實際包含密碼的控件句柄,顯然,編譯時無法將這個值進行硬編碼。同樣,一旦DLL獲得了秘密,它必須將它發送回原應用程序,以便能正確顯示出來。
              幸運的是,有許多方法處理這個問題,文件映射,WM_COPYDATA,剪貼板以及很簡單的 #pragma data_seg 共享數據段等,本文我不打算使用這些技術,因為MSDN(“進程間通信”部分)以及其它渠道可以找到很多文檔參考。不過我在 LibSpy例子中還是使用了 #pragma data_seg。細節請參考 LibSpy 源代碼。

            第三部分:CreateRemoteThread 和 WriteProcessMemory 技術

            范例程序——WinSpy

              另外一個將代碼拷貝到另一個進程地址空間并在該進程上下文中執行的方法是使用遠程線程和 WriteProcessMemory API。這種方法不用編寫單獨的DLL,而是用 WriteProcessMemory 直接將代碼拷貝到遠程進程——然后用 CreateRemoteThread 啟動它執行。先來看看 CreateRemoteThread 的聲明:

            HANDLE CreateRemoteThread(
            HANDLE hProcess,        // 傳入創建新線程的進程句柄
            LPSECURITY_ATTRIBUTES lpThreadAttributes,  // 安全屬性指針
            DWORD dwStackSize,      // 字節為單位的初始線程堆棧
            LPTHREAD_START_ROUTINE lpStartAddress,     // 指向線程函數的指針
            LPVOID lpParameter,     // 新線程使用的參數
            DWORD dwCreationFlags,  // 創建標志
            LPDWORD lpThreadId      // 指向返回的線程ID
            );				
            如果你比較它與 CreateThread(MSDN)的聲明,你會注意到如下的差別:
            • 在 CreateRemoteThread中,hProcess是額外的一個參數,一個進程句柄,新線程就是在這個進程中創建的;
            • 在 CreateRemoteThread中,lpStartAddress 表示的是在遠程進程地址空間中的線程起始地址。線程函數必須要存在于遠程進程中,所以我們不能簡單地傳遞一個指針到本地的 ThreadFunc。必須得先拷貝代碼到遠程進程;
            • 同樣,lpParameter 指向的數據也必須要存在于遠程進程,所以也得將它拷貝到那。

            綜上所述,我們得按照如下的步驟來做:

            1. 獲取一個遠程進程的HANDLE (OpenProces) ;
            2. 在遠程進程地址空間中為注入的數據分配內存(VirtualAllocEx);
            3. 將初始的 INDATA 數據結構的一個拷貝寫入分配的內存中(WriteProcessMemory);
            4. 在遠程進程地址空間中為注入的代碼分配內存;
            5. 將 ThreadFunc 的一個拷貝寫入分配的內存;
            6. 用 CreateRemoteThread啟動遠程的 ThreadFunc 拷貝;
            7. 等待遠程線程終止(WaitForSingleObject);
            8. 獲取遠程來自遠程進程的結果(ReadProcessMemory 或 GetExitCodeThread);
            9. 釋放在第二步和第四步中分配的內存(VirtualFreeEx);
            10. 關閉在第六步和第一步獲取的句柄(CloseHandle);

            ThreadFunc 必須要遵循的原則:

            1. 除了kernel32.dll 和user32.dll 中的函數之外,ThreadFunc 不要調用任何其它函數,只有 kernel32.dll 和user32.dll被保證在本地和目標進程中的加載地址相同(注意,user32.dll并不是被映射到每個 Win32 的進程)。如果你需要來自其它庫中的函數,將LoadLibrary 和 GetProcAddress 的地址傳給注入的代碼,然后放手讓它自己去做。如果映射到目標進程中的DLL有沖突,你也可以用 GetModuleHandle 來代替 LoadLibrary。
                同樣,如果你想在 ThreadFunc 中調用自己的子例程,要單獨把每個例程的代碼拷貝到遠程進程并用 INJDATA為 ThreadFunc 提供代碼的地址。
            2. 不要使用靜態字符串,而要用 INJDATA 來傳遞所有字符串。之所以要這樣,是因為編譯器將靜態字符串放在可執行程序的“數據段”中,可是引用(指針)是保留在代碼中的。那么,遠程進程中ThreadFunc 的拷貝指向的內容在遠程進程的地址空間中是不存在的。
            3. 去掉 /GZ 編譯器開關,它在調試版本中是默認設置的。
            4. 將 ThreadFunc 和 AfterThreadFunc 聲明為靜態類型,或者不啟用增量鏈接。
            5. ThreadFunc 中的局部變量一定不能超過一頁(也就是 4KB)。
              注意在調試版本中4KB的空間有大約10個字節是用于內部變量的。
            6. 如果你有一個開關語句塊大于3個case 語句,將它們像下面這樣拆分開:
              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:				
              或者將它們修改成一個 if-else if 結構語句(參見附錄E)。

              如果你沒有按照這些規則來做,目標進程很可能會崩潰。所以務必牢記。在目標進程中不要假設任何事情都會像在本地進程中那樣 (參見附錄F)。

            GetWindowTextRemote(A/W)

            要想從“遠程”編輯框獲得密碼,你需要做的就是將所有功能都封裝在GetWindowTextRemot(A/W):中。

            int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );
            int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
            參數說明:
            hProcess:編輯框控件所屬的進程句柄;
            hWnd:包含密碼的編輯框控件句柄;
            lpString:接收文本的緩沖指針;
            返回值:返回值是拷貝的字符數;

              下面讓我們看看它的部分代碼——尤其是注入數據的代碼——以便明白 GetWindowTextRemote 的工作原理。此處為簡單起見,略掉了 UNICODE 支持部分。

            INJDATA
            typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);
            typedef struct {
            HWND hwnd; // 編輯框句柄
            SENDMESSAGE fnSendMessage; // 指向user32.dll 中 SendMessageA 的指針
            char psText[128]; // 接收密碼的緩沖
            } INJDATA;

              INJDATA 是一個被注入到遠程進程的數據結構。但在注入之前,結構中指向 SendMessageA 的指針是在本地應用程序中初始化的。因為對于每個使用user32.dll的進程來說,user32.dll總是被映射到相同的地址,因此,SendMessageA 的地址也肯定是相同的。這就保證了被傳遞到遠程進程的是一個有效的指針。

            ThreadFunc函數

            static DWORD WINAPI ThreadFunc (INJDATA *pData)
            {
            pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // Get password
            sizeof(pData->psText),
            (LPARAM)pData->psText );
            return 0;
            }
            // 該函數在ThreadFunc之后標記內存地址
            // int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc.
            static void AfterThreadFunc (void)
            {
            }
            

            ThradFunc 是被遠程線程執行的代碼。

            • 注釋:注意AfterThreadFunc 是如何計算 ThreadFunc 大小的。通常這樣做并不是一個好辦法,因為鏈接器可以隨意更改函數的順序(也就是說ThreadFunc可能被放在 AfterThreadFunc之后)。這一點你可以在小項目中很好地保證函數的順序是預先設想好的,比如 WinSpy 程序。在必要的情況下,你還可以使用 /ORDER 鏈接器選項來解決函數鏈接順序問題。或者用反匯編確定 ThreadFunc 函數的大小。
            如何使用該技術子類化遠程控件

            范例程序——InjectEx

            下面我們將討論一些更復雜的內容,如何子類化屬于另一個進程的控件。

            首先,你得拷貝兩個函數到遠程進程來完成此任務

            1. ThreadFunc實際上是通過 SetWindowLong子類化遠程進程中的控件;
            2. NewProc是子類化控件的新窗口過程;

              這里主要的問題是如何將數據傳到遠程窗口過程 NewProc,因為 NewProc 是一個回調函數,它必須遵循特定的規范和原則,我們不能簡單地在參數中傳遞 INJDATA指針。幸運的是我找到了有兩個方法來解決這個問題,只不過要借助匯編語言,所以不要忽略了匯編,關鍵時候它是很有用的!

            方法一:

            如下圖所示:

              在遠程進程中,INJDATA 被放在NewProc 之前,這樣 NewProc 在編譯時便知道 INJDATA 在遠程進程地址空間中的內存位置。更確切地說,它知道相對于其自身位置的 INJDATA 的地址,我們需要所有這些信息。下面是 NewProc 的代碼:

            static LRESULT CALLBACK NewProc(
            HWND hwnd,       // 窗口句柄
            UINT uMsg,       // 消息標示符
            WPARAM wParam,   // 第一個消息參數
            LPARAM lParam )  // 第二個消息參數
            {
            INJDATA* pData = (INJDATA*) NewProc;  // pData 指向 NewProc
            pData--;              // 現在pData 指向INJDATA;
            // 回想一下INJDATA 被置于遠程進程NewProc之前;
            //-----------------------------
            // 此處是子類化代碼
            // ........
            //-----------------------------
            // 調用原窗口過程;
            // fnOldProc (由SetWindowLong 返回) 被(遠程)ThreadFunc初始化
            // 并被保存在(遠程)INJDATA;中
            return pData->fnCallWindowProc( pData->fnOldProc,
            hwnd,uMsg,wParam,lParam );
            }
            
            但這里還有一個問題,見第一行代碼:
            INJDATA* pData = (INJDATA*) NewProc;

              這種方式 pData得到的是硬編碼值(在我們的進程中是原 NewProc 的內存地址)。這不是我們十分想要的。在遠程進程中,NewProc “當前”拷貝的內存地址與它被移到的實際位置是無關的,換句話說,我們會需要某種類型的“this 指針”。
            雖然用 C/C++ 無法解決這個問題,但借助內聯匯編可以解決,下面是對 NewProc的修改:

            static LRESULT CALLBACK NewProc(
            HWND hwnd,       // 窗口句柄
            UINT uMsg,       // 消息標示符
            WPARAM wParam,   // 第一個消息參數
            LPARAM lParam )  // 第二個消息參數
            {
            // 計算INJDATA 結構的位置
            // 在遠程進程中記住這個INJDATA
            // 被放在NewProc之前
            INJDATA* pData;
            _asm {
            call    dummy
            dummy:
            pop     ecx         // <- ECX 包含當前的EIP
            sub     ecx, 9      // <- ECX 包含NewProc的地址
            mov     pData, ecx
            }
            pData--;
            //-----------------------------
            // 此處是子類化代碼
            // ........
            //-----------------------------
            // 調用原來的窗口過程
            return pData->fnCallWindowProc( pData->fnOldProc,
            hwnd,uMsg,wParam,lParam );
            }
            
              那么,接下來該怎么辦呢?事實上,每個進程都有一個特殊的寄存器,它指向下一條要執行的指令的內存位置。即所謂的指令指針,在32位 Intel 和 AMD 處理器上被表示為 EIP。因為 EIP是一個專用寄存器,你無法象操作一般常規存儲器(如:EAX,EBX等)那樣通過編程存取它。也就是說沒有操作代碼來尋址 EIP,以便直接讀取或修改其內容。但是,EIP 仍然還是可以通過間接方法修改的(并且隨時可以修改),通過JMP,CALL和RET這些指令實現。下面我們就通過例子來解釋通過 CALL/RET 子例程調用機制在32位 Intel 和 AMD 處理器上是如何工作的。
              當你調用(通過 CALL)某個子例程時,子例程的地址被加載到 EIP,但即便是在 EIP杯修改之前,其舊的那個值被自動PUSH到堆棧(被用于后面作為指令指針返回)。在子例程執行完時,RET 指令自動將堆棧頂POP到 EIP。
              現在你知道了如何通過 CALL 和 RET 實現 EIP 的修改,但如何獲取其當前的值呢?下面就來解決這個問題,前面講過,CALL PUSH EIP 到堆棧,所以,為了獲取其當前值,調用“啞函數”,然后再POP堆棧頂。讓我們用編譯后的 NewProc 來解釋這個竅門。
            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
            
            • 啞函數調用;就是JUMP到下一個指令并PUSH EIP到堆棧;
            • 然后將堆棧頂POP到 ECX,ECX再保存EIP;這也是 POP EIP指令的真正地址;
            • 注意 NewProc 的入口點和 “POP ECX”之間的“距離”是9 個字節;因此為了計算 NewProc的地址,要從 ECX 減9。

              這樣一來,不管 NewProc 被移到什么地方,它總能計算出其自己的地址。但是,NewProc 的入口點和 “POP ECX”之間的距離可能會隨著你對編譯/鏈接選項的改變而變化,由此造成 RELEASE和DEBUG版本之間也會有差別。但關鍵是你仍然確切地知道編譯時的值。

            1. 首先,編譯函數
            2. 用反匯編確定正確的距離
            3. 最后,用正確的距離值重新編譯

            此即為 InjecEx 中使用的解決方案,類似于 HookInjEx,交換鼠標點擊“開始”左右鍵時的功能。

            方法二:

            對于我們的問題,在遠程進程地址空間中將 INJDATA 放在 NewProc 前面不是唯一的解決辦法。看下面 NewProc的變異版本:

            static LRESULT CALLBACK NewProc(
            HWND hwnd,      // 窗口句柄
            UINT uMsg,      // 消息標示符
            WPARAM wParam,  // 第一個消息參數
            LPARAM lParam ) // 第二個消息參數
            {
            INJDATA* pData = 0xA0B0C0D0;    // 虛構值
            //-----------------------------
            // 子類化代碼
            // ........
            //-----------------------------
            // 調用原來的窗口過程
            return pData->fnCallWindowProc( pData->fnOldProc,
            hwnd,uMsg,wParam,lParam );
            }
            
              此處 0xA0B0C0D0 只是遠程進程地址空間中真實(絕對)INJDATA地址的占位符。前面講過,你無法在編譯時知道該地址。但你可以在調用 VirtualAllocEx (為INJDATA)之后得到 INJDATA 在遠程進程中的位置。編譯我們的 NewProc 后,可以得到如下結果:
             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
            
            因此,其編譯的代碼(十六進制)將是:
            558BECC745FCD0C0B0A0......8BE55DC21000.
            
            現在你可以象下面這樣繼續:
            1. 將INJDATA,ThreadFunc和NewProc 拷貝到目標進程;
            2. 修改 NewProc 的代碼,以便 pData 中保存的是 INJDATA 的真實地址。
              例如,假設 INJDATA 的地址(VirtualAllocEx返回的值)在目標進程中是 0x008a0000。然后象下面這樣修改NewProc的代碼:
              	558BECC745FCD0C0B0A0......8BE55DC21000 <- 原來的NewProc (注1)
                  558BECC745FC00008A00......8BE55DC21000 <- 修改后的NewProc,使用的是INJDATA的實際地址。
              也就是說,你用真正的 INJDATA(注2) 地址替代了虛擬值 A0B0C0D0(注2)。
            3. 開始執行遠程的 ThreadFunc,它負責子類化遠程進程中的控件。
            • 注1、有人可能會問,為什么地址 A0B0C0D0 和 008a0000 在編譯時順序是相反的。因為 Intel 和 AMD 處理器使用 little-endian 符號來表示(多字節)數據。換句話說,某個數字的低位字節被存儲在內存的最小地址處,而高位字節被存儲在最高位地址。
              假設“UNIX”這個詞存儲用4個字節,在 big-endian 系統中,它被存為“UNIX”,在 little-endian 系統中,它將被存為“XINU”。
            • 注2、某些破解(很糟)以類似的方式修改可執行代碼,但是一旦加載到內存,一個程序是無法修改自己的代碼的(代碼駐留在可執行程序的“.text” 區域,這個區域是寫保護的)。但仍可以修改遠程的 NewProc,因為它是先前以 PAGE_EXECUTE_READWRITE 許可方式被拷貝到某個內存塊中的。

            何時使用 CreateRemoteThread 和 WriteProcessMemory 技術

              與其它方法比較,使用 CreateRemoteThread 和 WriteProcessMemory 技術進行代碼注入更靈活,這種方法不需要額外的 dll,不幸的是,該方法更復雜并且風險更大,只要ThreadFunc出現哪怕一丁點錯誤,很容易就讓(并且最大可能地會)使遠程進程崩潰(參見附錄 F),因為調試遠程 ThreadFunc 將是一個可怕的夢魘,只有在注入的指令數很少時,你才應該考慮使用這種技術進行注入,對于大塊的代碼注入,最好用 I.和II 部分討論的方法。

            WinSpy 以及 InjectEx 請從這里下載源代碼

            結束語

             

            到目前為止,有幾個問題是我們未提及的,現總結如下:

            解決方案 OS 進程
            I、Hooks Win9x 和 WinNT 僅僅與 USER32.DLL (注3)鏈接的進程
            II、CreateRemoteThread & LoadLibrary 僅 WinNT(注4) 所有進程(注5), 包括系統服務(注6)
            III、CreateRemoteThread & WriteProcessMemory
             
            僅 WinNT 所有進程, 包括系統服務
            • 注3:顯然,你無法hook一個沒有消息隊列的線程,此外,SetWindowsHookEx不能與系統服務一起工作,即使它們與 USER32.DLL 進行鏈接;
            • 注4:Win9x 中沒有 CreateRemoteThread,也沒有 VirtualAllocEx (實際上,在Win9x 中可以仿真,但不是本文討論的問題了);
            • 注5:所有進程 = 所有 Win32 進程 + csrss.exe
              本地應用 (smss.exe, os2ss.exe, autochk.exe 等)不使用 Win32 API,所以也不會與 kernel32.dll 鏈接。唯一一個例外是 csrss.exe,Win32 子系統本身,它是本地應用程序,但其某些庫(~winsrv.dll)需要 Win32 DLLs,包括 kernel32.dll;
            • 注6:如果你想要將代碼注入到系統服務中(lsass.exe, services.exe, winlogon.exe 等)或csrss.exe,在打開遠程句柄(OpenProcess)之前,將你的進程優先級置為 “SeDebugPrivilege”(AdjustTokenPrivileges)。

              最后,有幾件事情一定要了然于心:你的注入代碼很容易摧毀目標進程,尤其是注入代碼本身出錯的時候,所以要記住:權力帶來責任!
              因為本文中的許多例子是關于密碼的,你也許還讀過 Zhefu Zhang 寫的另外一篇文章“Super Password Spy++” ,在該文中,他解釋了如何獲取IE 密碼框中的內容,此外,他還示范了如何保護你的密碼控件免受類似的攻擊。

            附錄A

            為什么 kernel32.dll 和user32.dll 總是被映射到相同的地址。

              我的假定:因為Microsoft 的程序員認為這樣做有助于速度優化,為什么呢?我的解釋是——通常一個可執行程序是由幾個部分組成,其中包括“.reloc” 。當鏈接器創建 EXE 或者 DLL文件時,它對文件被映射到哪個內存地址做了一個假設。這就是所謂的首選加載/基地址。在映像文件中所有絕對地址都是基于鏈接器首選的加載地址,如果由于某種原因,映像文件沒有被加載到該地址,那么這時“.reloc”就起作用了,它包含映像文件中的所有地址的清單,這個清單中的地址反映了鏈接器首選加載地址和實際加載地址的差別(無論如何,要注意編譯器產生的大多數指令使用某種相對地址尋址,因此,并沒有你想象的那么多地址可供重新分配),另一方面,如果加載器能夠按照鏈接器首選地址加載映像文件,那么“.reloc”就被完全忽略掉了。
              但kernel32.dll 和user32.dll 及其加載地址為何要以這種方式加載呢?因為每一個 Win32 程序都需要kernel32.dll,并且大多數Win32 程序也需要 user32.dll,那么總是將它們(kernel32.dll 和user32.dll)映射到首選地址可以改進所有可執行程序的加載時間。這樣一來,加載器絕不能修改kernel32.dll and user32.dll.中的任何(絕對)地址。我們用下面的例子來說明:
              將某個應用程序 App.exe 的映像基地址設置成 KERNEL32的地址(/base:"0x77e80000")或 USER32的首選基地址(/base:"0x77e10000"),如果 App.exe 不是從 USER32 導入方式來使用 USER32,而是通過LoadLibrary 加載,那么編譯并運行App.exe 后,會報出錯誤信息("Illegal System DLL Relocation"——非法系統DLL地址重分配),App.exe 加載失敗。
            為什么會這樣呢?當創建進程時,Win 2000、Win XP 和Win 2003系統的加載器要檢查 kernel32.dll 和user32.dll 是否被映射到首選基地址(實際上,它們的名字都被硬編碼進了加載器),如果沒有被加載到首選基地址,將發出錯誤。在 WinNT4中,也會檢查ole32.dll,在WinNT 3.51 和較低版本的Windows中,由于不會做這樣的檢查,所以kernel32.dll 和user32.dll可以被加載任何地方。只有ntdll.dll總是被加載到其基地址,加載器不進行檢查,一旦ntdll.dll沒有在其基地址,進程就無法創建。

            總之,對于 WinNT 4 和較高的版本中

            • 一定要被加載到基地址的DLLs 有:kernel32.dll、user32.dll 和ntdll.dll;
            • 每個Win32 程序都要使用的 DLLs+ csrss.exe:kernel32.dll 和ntdll.dll;
            • 每個進程都要使用的DLL只有一個,即使是本地應用:ntdll.dll;

            附錄B

            /GZ 編譯器開關

              在生成 Debug 版本時,/GZ 編譯器特性是默認打開的。你可以用它來捕獲某些錯誤(具體細節請參考相關文檔)。但對我們的可執行程序意味著什么呢?
              當打開 /GZ 開關,編譯器會添加一些額外的代碼到可執行程序中每個函數所在的地方,包括一個函數調用(被加到每個函數的最后)——檢查已經被我們的函數修改的 ESP堆棧指針。什么!難道有一個函數調用被添加到 ThreadFunc 嗎?那將導致災難。ThreadFunc 的遠程拷貝將調用一個在遠程進程中不存在的函數(至少是在相同的地址空間中不存在)

            附錄C

             

            靜態函數和增量鏈接

              增量鏈接主要作用是在生成應用程序時縮短鏈接時間。常規鏈接和增量鏈接的可執行程序之間的差別是——增量鏈接時,每個函數調用經由一個額外的JMP指令,該指令由鏈接器發出(該規則的一個例外是函數聲明為靜態)。這些 JMP 指令允許鏈接器在內存中移動函數,這種移動無需修改引用函數的 CALL指令。但這些JMP指令也確實導致了一些問題:如 ThreadFunc 和 AfterThreadFunc 將指向JMP指令而不是實際的代碼。所以當計算ThreadFunc 的大小時:

            const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc)
            
              你實際上計算的是指向 ThreadFunc 的JMPs 和AfterThreadFunc之間的“距離” (通常它們會緊挨著,不用考慮距離問題)。現在假設 ThreadFunc 的地址位于004014C0 而伴隨的 JMP指令位于 00401020。
            :00401020   jmp  004014C0
            ...
            :004014C0   push EBP          ; ThreadFunc 的實際地址
            :004014C1   mov  EBP, ESP
            ...
            
            那么
            WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);
            
              將拷貝“JMP 004014C0”指令(以及隨后cbCodeSize范圍內的所有指令)到遠程進程——不是實際的 ThreadFunc。遠程進程要執行的第一件事情將是“JMP 004014C0” 。它將會在其最后幾條指令當中——遠程進程和所有進程均如此。但 JMP指令的這個“規則”也有例外。如果某個函數被聲明為靜態的,它將會被直接調用,即使增量鏈接也是如此。這就是為什么規則#4要將 ThreadFunc 和 AfterThreadFunc 聲明為靜態或禁用增量鏈接的緣故。(有關增量鏈接的其它信息參見 Matt Pietrek的文章“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools” )

            附錄D

            為什么 ThreadFunc的局部變量只有 4k?

              局部變量總是存儲在堆棧中,如果某個函數有256個字節的局部變量,當進入該函數時,堆棧指針就減少256個字節(更精確地說,在函數開始處)。例如,下面這個函數:

            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
            
              注意上述例子中,堆棧指針是如何被修改的?而如果某個函數需要4KB以上局部變量內存空間又會怎么樣呢?其實,堆棧指針并不是被直接修改,而是通過另一個函數調用來修改的。就是這個額外的函數調用使得我們的 ThreadFunc “被破壞”了,因為其遠程拷貝會調用一個不存在的東西。
              我們看看文檔中對堆棧探測和 /Gs編譯器選項是怎么說的:
            ——“/GS是一個控制堆棧探測的高級特性,堆棧探測是一系列編譯器插入到每個函數調用的代碼。當函數被激活時,堆棧探測需要的內存空間來存儲相關函數的局部變量。
              如果函數需要的空間大于為局部變量分配的堆棧空間,其堆棧探測被激活。默認的大小是一個頁面(在80x86處理器上4kb)。這個值允許在Win32 應用程序和Windows NT虛擬內存管理器之間進行謹慎調整以便增加運行時承諾給程序堆棧的內存。”
            我確信有人會問:文檔中的“……堆棧探測到一塊需要的內存空間來存儲相關函數的局部變量……”那些編譯器選項(它們的描述)在你完全弄明白之前有時真的讓人氣憤。例如,如果某個函數需要12KB的局部變量存儲空間,堆棧內存將進行如下方式的分配(更精確地說是“承諾” )。
            sub    esp, 0x1000    ; "分配" 第一次 4 Kb
            test  [esp], eax      ; 承諾一個新頁內存(如果還沒有承諾)
            sub    esp, 0x1000    ; "分配" 第二次4 Kb
            test  [esp], eax      ; ...
            sub    esp, 0x1000
            test  [esp], eax
            
              注意4KB堆棧指針是如何被修改的,更重要的是,每一步之后堆棧底是如何被“觸及”(要經過檢查)。這樣保證在“分配”(承諾)另一頁面之前,當前頁面承諾的范圍也包含堆棧底。

            注意事項
              “每一個線程到達其自己的堆棧空間,默認情況下,此空間由承諾的以及預留的內存組成,每個線程使用 1 MB預留的內存,以及一頁承諾的內存,系統將根據需要從預留的堆棧內存中承諾一頁內存區域” (參見 MSDN CreateThread > dwStackSize > Thread Stack Size)
              還應該清楚為什么有關 /GS 的文檔說在堆棧探針在 Win32 應用程序和Windows NT虛擬內存管理器之間進行謹慎調整。

            現在回到我們的ThreadFunc以及 4KB 限制
              雖然你可以用 /Gs 防止調用堆棧探測例程,但在文檔對于這樣的做法給出了警告,此外,文件描述可以用 #pragma check_stack 指令關閉或打開堆棧探測。但是這個指令好像一點作用都沒有(要么這個文檔是垃圾,要么我疏忽了其它一些信息?)。總之,CreateRemoteThread 和 WriteProcessMemory 技術只能用于注入小塊代碼,所以你的局部變量應該盡量少耗費一些內存字節,最好不要超過 4KB限制。

            附錄E

             

            為什么要將開關語句拆分成三個以上?

            用下面這個例子很容易解釋這個問題,假設有如下這么一個函數:

            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;
            }
            
            編譯后變成下面這個樣子:
            地址      操作碼/參數       解釋后的指令
            --------------------------------------------------
            ; 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
            
            注意如何實現這個開關語句?

              與其單獨檢查每個CASE語句,不如創建一個地址表,然后通過簡單地計算地址表的偏移量而跳轉到正確的CASE語句。這實際上是一種改進。假設你有50個CASE語句。如果不使用上述的技巧,你得執行50次 CMP和JMP指令來達到最后一個CASE。相反,有了地址表后,你可以通過表查詢跳轉到任何CASE語句,從計算機算法角度和時間復雜度看,我們用O(5)代替了O(2n)算法。其中:
            1. O表示最壞的時間復雜度;
            2. 我們假設需要5條指令來進行表查詢計算偏移量,最終跳到相應的地址;

              現在,你也許認為出現上述情況只是因為CASE常量被有意選擇為連續的(1,2,3,4)。幸運的是,它的這個方案可以應用于大多數現實例子中,只有偏移量的計算稍微有些復雜。但有兩個例外:

            • 如果CASE語句少于等于三個;
            • 如果CASE 常量完全互不相關(如:“"case 1” ,“case 13” ,“case 50” , 和“case 1000” );

              顯然,單獨判斷每個的CASE常量的話,結果代碼繁瑣耗時,但使用CMP和JMP指令則使得結果代碼的執行就像普通的if-else 語句。
            有趣的地方:如果你不明白CASE語句使用常量表達式的理由,那么現在應該弄明白了吧。為了創建地址表,顯然在編譯時就應該知道相關地址。

            現在回到問題!
            注意到地址 0040100C 處的JMP指令了嗎?我們來看看Intel關于十六進制操作碼 FF 的文檔是怎么說的:

            操作碼 指令     描述
            FF /4  JMP r/m32  Jump near, absolute indirect,
                       address given in r/m32

              原來JMP 使用了一種絕對尋址方式,也就是說,它的操作數(CASE語句中的 0040102C)表示一個絕對地址。還用我說什么嗎?遠程 ThreadFunc 會盲目地認為地址表中開關地址是 0040102C,JMP到一個錯誤的地方,造成遠程進程崩潰。

            附錄F

             

            為什么遠程進程會崩潰呢?

            當遠程進程崩潰時,它總是會因為下面這些原因:

            1. 在ThreadFunc 中引用了一個不存在的串;
            2. 在在ThreadFunc 中 中一個或多個指令使用絕對尋址(參見附錄E);
            3. ThreadFunc 調用某個不存在的函數(該調用可能是編譯器或鏈接器添加的)。你在反匯編器中可以看到這樣的情形:
              :004014C0    push EBP         ; ThreadFunc 的入口點
                  :004014C1    mov EBP, ESP
                  ...
                  :004014C5    call 0041550     ;  這里將使遠程進程崩潰
                  ...
                  :00401502    ret
                  
              如果 CALL 是由編譯器添加的指令(因為某些“禁忌” 開關如/GZ是打開的),它將被定位在 ThreadFunc 的開始的某個地方或者結尾處。

              不管哪種情況,你都要小心翼翼地使用 CreateRemoteThread 和 WriteProcessMemory 技術。尤其要注意你的編譯器/鏈接器選項,一不小心它們就會在 ThreadFunc 添加內容。

            posted @ 2010-09-16 14:06 深邃者 閱讀(571) | 評論 (0)編輯 收藏

            CreateRemoteThread

            CreateRemoteThread提供了一個在遠程進程中執行代碼的方法,就像代碼長出翅膀飛到別處運行。本文將做一個入門介紹,希望對廣大編程愛好者有所幫助。

            先解釋一下遠程進程,其實就是要植入你的代碼的進程,相對于你的工作進程(如果叫本地進程的話)它就叫遠程進程,可理解為宿主。

            首先介紹一下我們的主要工具CreateRemoteThread,這里先將函數原型簡單介紹以下。

            CreateRemoteThread可將線程創建在遠程進程中。

            函數原型
            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
            );
            參數說明:
            hProcess
            [輸入] 進程句柄
            lpThreadAttributes
            [輸入] 線程安全描述字,指向SECURITY_ATTRIBUTES結構的指針
            dwStackSize
            [輸入] 線程棧大小,以字節表示
            lpStartAddress
            [輸入] 一個LPTHREAD_START_ROUTINE類型的指針,指向在遠程進程中執行的函數地址
            lpParameter
            [輸入] 傳入參數
            dwCreationFlags
            [輸入] 創建線程的其它標志

            lpThreadId
            [輸出] 線程身份標志,如果為NULL,則不返回

            返回值
            成功返回新線程句柄,失敗返回NULL,并且可調用GetLastError獲得錯誤值。

            接下來我們將以兩種方式使用CreateRemoteThread,大家可以領略到CreateRemoteThread的神通,它使你的代碼可以脫離你的進程,植入到別的進程中運行。


            第一種方式

            第一種方式,我們使用函數的形式。即我們將自己程序中的一個函數植入到遠程進程中。

            步驟1:首先在你的進程中創建函數MyFunc,我們將把它放在另一個進程中運行,這里以windows

            計算器為目標進程。
            static DWORD WINAPI MyFunc (LPVOID pData)
            {
            //do something
            //...
            //pData輸入項可以是任何類型值
            //這里我們會傳入一個DWORD的值做示例,并且簡單返回
            return *(DWORD*)pData;
            }
            static void AfterMyFunc (void) {
            }
            這里有個小技巧,定義了一個static void AfterMyFunc (void);為了下面確定我們的代碼大小

            步驟2:定位目標進程,這里是一個計算器
            HWND hStart = ::FindWindow (TEXT("SciCalc"),NULL);

            步驟3:獲得目標進程句柄,這里用到兩個不太常用的函數(當然如果經常做線程/進程等方面的 項目的話,就很面熟了),但及有用
            DWORD PID, TID;
            TID = ::GetWindowThreadProcessId (hStart, &PID);

            HANDLE hProcess;
            hProcess = OpenProcess(PROCESS_ALL_ACCESS,false,PID);

            步驟4:在目標進程中配變量地址空間,這里我們分配10個字節,并且設定為可以讀

            寫PAGE_READWRITE,當然也可設為只讀等其它標志,這里就不一一說明了。
            char szBuffer[10];
            *(DWORD*)szBuffer=1000;//for test
            void *pDataRemote =(char*) VirtualAllocEx( hProcess, 0, sizeof(szBuffer), MEM_COMMIT,

            PAGE_READWRITE );

            步驟5:寫內容到目標進程中分配的變量空間
            ::WriteProcessMemory( hProcess, pDataRemote, szBuffer,(sizeof(szBuffer),NULL);

            步驟6:在目標進程中分配代碼地址空間
            計算代碼大小
            DWORD cbCodeSize=((LPBYTE) AfterMyFunc - (LPBYTE) MyFunc);
            分配代碼地址空間
            PDWORD pCodeRemote = (PDWORD) VirtualAllocEx( hProcess, 0, cbCodeSize, MEM_COMMIT,

            PAGE_EXECUTE_READWRITE );

            步驟7:寫內容到目標進程中分配的代碼地址空間
            WriteProcessMemory( hProcess, pCodeRemote, &MyFunc, cbCodeSize, NULL);

            步驟8:在目標進程中執行代碼

            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 );
            }

            這里有幾個值得說明的地方:
            使用WaitForSingleObject等待線程結束;
            使用GetExitCodeThread獲得返回值;
            最后關閉句柄CloseHandle。

            步驟9:清理現場

            釋放空間
            ::VirtualFreeEx( hProcess, pCodeRemote,
                             cbCodeSize,MEM_RELEASE );

            ::VirtualFreeEx( hProcess, pDataRemote,
                             cbParamSize,MEM_RELEASE );

            關閉進程句柄
            ::CloseHandle( hProcess );

             

            第二種方式

            第二種方式,我們使用動態庫的形式。即我們將自己一個動態庫植入到遠程進程中。

            這里不再重復上面相同的步驟,只寫出其中關鍵的地方.
            關鍵1:
            在步驟5中將動態庫的路徑作為變量傳入變量空間.
            關鍵2:
            在步驟8中,將GetProcAddress作為目標執行函數.

            hThread = ::CreateRemoteThread( hProcess, NULL, 0,
                        (LPTHREAD_START_ROUTINE )::GetProcAddress(
                         hModule, "LoadLibraryA"),
                         pDataRemote, 0, NULL );


            另外在步驟9,清理現場中首先要先進行釋放我們的動態庫.也即類似步驟8執行函數FreeLibrary

            hThread = ::CreateRemoteThread( hProcess, NULL, 0,
            (LPTHREAD_START_ROUTINE )::GetProcAddress(
            hModule, "FreeLibrary"),
            (void*)hLibModule, 0, NULL );

            好了,限于篇幅不能夠介紹的很細,在使用過程中如有疑問可向作者咨詢.(開發環境:windows2000/vc6.0)
             

             


            本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/fangchao918628/archive/2008/08/30/2852744.aspx

            posted @ 2010-09-16 14:05 深邃者 閱讀(528) | 評論 (0)編輯 收藏

            dll #pragma data_seg 實現數據的共享

            #pragma data_seg("flag_data")
               int count=0;
            #pragma data_seg()
            #pragma comment(linker,"/SECTION:flag_data,RWS")
            這種方法只能在沒有def文件時使用,如果通過def文件進行導出的話,那么設置就要在def文件內設置而不能
            在代碼里設置了。
            SETCTIONS
            flag_data READ WRITE SHARED

             

            在主文件中,用#pragma data_seg建立一

            個新的數據段并定義共享數據,其具體格式為:

            #pragma data_seg ("shareddata") //名稱可以

            //自己定義,但必須與下面的一致。

            HWND sharedwnd=NULL;//共享數據

            #pragma data_seg()



            僅定義一個數據段還不能達到共享數據的目的,還要告訴編譯器該段的屬性,有兩種方法可以實現該目的 (其效果是相同的),一種方法是在.DEF文件中加入如下語句: SETCTIONS shareddata READ WRITE SHARED 另一種方法是在項目設置鏈接選項(Project Setting --〉Link)中加入如下語句: /SECTION:shareddata,rws

            第一點:什么是共享數據段?為什么要用共享數據段??它有什么用途??
            在Win16環境中,DLL的全局數據對每個載入它的進程來說都是相同的;而在Win32環境中,情況卻發生了變化,DLL函數中的代碼所創建的任何對象(包括變量)都歸調用它的線程或進程所有。當進程在載入DLL時,操作系統自動把DLL地址映射到該進程的私有空間,也就是進程的虛擬地址空間,而且也復制該DLL的全局數據的一份拷貝到該進程空間。也就是說每個進程所擁有的相同的DLL的全局數據,它們的名稱相同,但其值卻并不一定是相同的,而且是互不干涉的。

            因此,在Win32環境下要想在多個進程中共享數據,就必須進行必要的設置。在訪問同一個Dll的各進程之間共享存儲器是通過存儲器映射文件技術實現的。也可以把這些需要共享的數據分離出來,放置在一個獨立的數據段里,并把該段的屬性設置為共享。必須給這些變量賦初值,否則編譯器會把沒有賦初始值的變量放在一個叫未被初始化的數據段中。



            #pragma data_seg預處理指令用于設置共享數據段。例如:

            #pragma data_seg("SharedDataName") HHOOK hHook=NULL; //必須在定義的同時進行初始化!!!!#pragma data_seg()

            在#pragma data_seg("SharedDataName")和#pragma data_seg()之間的所有變量將被訪問該Dll的所有進程看到和共享。再加上一條指令#pragma comment(linker,"/section:.SharedDataName,rws"),[注意:數據節的名稱is case sensitive]那么這個數據節中的數據可以在所有DLL的實例之間共享。所有對這些數據的操作都針對同一個實例的,而不是在每個進程的地址空間中都有一份。



            當進程隱式或顯式調用一個動態庫里的函數時,系統都要把這個動態庫映射到這個進程的虛擬地址空間里(以下簡稱"地址空間")。這使得DLL成為進程的一部分,以這個進程的身份執行,使用這個進程的堆棧。(這項技術又叫code Injection技術,被廣泛地應用在了病毒、黑客領域!呵呵^_^)



            第二點:在具體使用共享數據段時需要注意的一些問題!

            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. (注意: 即使是全局變量和靜態變量也都不是共享的!) 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. (這個編譯參數既可以使用pragma指令來指定,也可以在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.

            posted @ 2010-09-16 10:25 深邃者 閱讀(423) | 評論 (0)編輯 收藏

            #Pragma 指令介紹

            在所有的預處理指令中,#Pragma 指令可能是最復雜的了,它的作用是設定編譯器的狀態或者是指示編譯器完成一些特定的動作。#pragma指令對每個編譯器給出了一個方法,在保持與C和C ++語言完全兼容的情況下,給出主機或操作系統專有的特征。依據定義,編譯指示是機器或操作系統專有的,且對于每個編譯器都是不同的。

            其格式一般為: #Pragma Para
            其中Para 為參數,下面來看一些常用的參數。

            (1)message 參數。

            Message 參數是我最喜歡的一個參數,它能夠在編譯信息輸出窗口中輸出相應的信息,這對于源代碼信息的控制是非常重要的。其使用方法為:
            #Pragma message(“消息文本”)
            當編譯器遇到這條指令時就在編譯輸出窗口中將消息文本打印出來。
            當我們在程序中定義了許多宏來控制源代碼版本的時候,我們自己有可能都會忘記有沒有正確的設置這些宏,此時我們可以用這條指令在編譯的時候就進行檢查。
            假設我們希望判斷自己有沒有在源代碼的什么地方定義了_X86這個宏可以用下面的方法
            #ifdef _X86
            #Pragma message(“_X86 macro activated!”)
            #endif
            當我們定義了_X86這個宏以后,應用程序在編譯時就會在編譯輸出窗口里顯示“_
            X86 macro activated!”。我們就不會因為不記得自己定義的一些特定的宏而抓耳撓腮了。

            (2)另一個使用得比較多的pragma參數是code_seg。

            格式如:
            #pragma code_seg( ["section-name"[,"section-class"] ] )
            它能夠設置程序中函數代碼存放的代碼段,當我們開發驅動程序的時候就會使用到它。

            (3)#pragma once (比較常用)

            只要在頭文件的最開始加入這條指令就能夠保證頭文件被編譯一次,這條指令實際上在VC6中就已經有了,但是考慮到兼容性并沒有太多的使用它。

            (4)#pragma hdrstop

            表示預編譯頭文件到此為止,后面的頭文件不進行預編譯。BCB可以預編譯頭文件以加快鏈接的速度,但如果所有頭文件都進行預編譯又可能占太多磁盤空間,所以使用這個選項排除一些頭文件。
            有時單元之間有依賴關系,比如單元A依賴單元B,所以單元B要先于單元A編譯。你可以用#pragma startup指定編譯優先級,如果使用了#pragma package(smart_init) ,BCB就會根據優先級的大小先后編譯。

            (5)#pragma resource

            #pragma resource "*.dfm"表示把*.dfm文件中的資源加入工程。*.dfm中包括窗體外觀的定義。

            (6)#pragma warning

            #pragma warning( disable : 4507 34; once : 4385; error : 164 )
            等價于:
            #pragma warning(disable:4507 34) // 不顯示4507和34號警告信息
            #pragma warning(once:4385) // 4385號警告信息僅報告一次
            #pragma warning(error:164) // 把164號警告信息作為一個錯誤。
            同時這個pragma warning 也支持如下格式:
            #pragma warning( push [ ,n ] )
            #pragma warning( pop )
            這里n代表一個警告等級(1---4)。
            #pragma warning( push )保存所有警告信息的現有的警告狀態。
            #pragma warning( push, n)保存所有警告信息的現有的警告狀態,并且把全局警告等級設定為n。
            #pragma warning( pop )向棧中彈出最后一個警告信息,在入棧和出棧之間所作的一切改動取消。例如:
            #pragma warning( push )
            #pragma warning( disable : 4705 )
            #pragma warning( disable : 4706 )
            #pragma warning( disable : 4707 )
            //.......
            #pragma warning( pop )
            在這段代碼的最后,重新保存所有的警告信息(包括4705,4706和4707)。

            (7)pragma comment(...)

            該指令將一個注釋記錄放入一個對象文件或可執行文件中。
            常用的lib關鍵字,可以幫我們連入一個庫文件。

            (8)progma data_seg

            有的時候我們可能想讓一個應用程序只啟動一次,就像單件模式(singleton)一樣,實現的方法可能有多種,這里說說用#pragma data_seg來實現的方法,很是簡潔便利。

            應用程序的入口文件前面加上

            #pragma data_seg("flag_data")
            int app_count = 0;
            #pragma data_seg()
            #pragma comment(linker,"/SECTION:flag_data,RWS")

            然后程序啟動的地方加上

            if(app_count>0)     // 如果計數大于0,則退出應用程序。
            {
               //MessageBox(NULL, "已經啟動一個應用程序", "Warning", MB_OK);

               //printf("no%d application", app_count);

               return FALSE;
            }
            app_count++;

            Windows 在一個Win32程序的地址空間周圍筑了一道墻。通常,一個程序的地址空間中的數據是私有的,對別的程序而言是不可見的。但是執行STRPROG的多個執行實體表示了STRLIB在程序的所有執行實體之間共享數據是毫無問題的。當您在一個STRPROG窗口中增加或者刪除一個字符串時,這種改變將立即反映在其它的窗口中。

            在全部例程之間,STRLIB共享兩個變量:一個字符數組和一個整數(記錄已儲存的有效字符串的個數)。STRLIB將這兩個變量儲存在共享的一個特殊內存區段中:

            #pragma      data_seg ("shared")
                    
            int                 iTotal = 0 ;
                    
            WCHAR               szStrings [MAX_STRINGS][MAX_LENGTH + 1] = { '\0' } ;
                    
            #pragma      data_seg ()
                    

            第一個#pragma敘述建立數據段,這里命名為shared。您可以將這段命名為任何一個您喜歡的名字。在這里的#pragma敘述之后的所有初始化了的變量都放在shared數據段中。第二個#pragma敘述標示段的結束。對變量進行專門的初始化是很重要的,否則編譯器將把它們放在普通的未初始化數據段中而不是放在shared中。

            連結器必須知道有一個「shared」共享數據段。在「Project Settings」對話框選擇「Link」頁面卷標。選中「STRLIB」時在「Project Options」字段(在Release和Debug設定中均可),包含下面的連結敘述:

            /SECTION:shared,RWS
                    

            字母RWS表示段具有讀、寫和共享屬性。或者,您也可以直接用DLL原始碼指定連結選項,就像我們在STRLIB.C那樣:

            #pragma comment(linker,"/SECTION:shared,RWS")
                    

            共享的內存段允許iTotal變量和szStrings字符串數組在STRLIB的所有例程之間共享。因為MAX_STRINGS等于256,而 MAX_LENGTH等于63,所以,共享內存段的長度為32,772字節-iTotal變量需要4字節,256個指針中的每一個都需要128字節。

            posted @ 2010-09-16 10:25 深邃者 閱讀(230) | 評論 (0)編輯 收藏

            2010年8月10日 #

            RTTI、虛函數和虛基類的實現方式、開銷分析及使用指導

            RTTI、虛函數和虛基類的實現方式、開銷分析及使用指導

            白楊

            http://baiy.cn

             

            “在正確的場合使用恰當的特性” 對稱職的C++程序員來說是一個基本標準。想要做到這點,首先要了解語言中每個特性的實現方式及其開銷。本文主要討論相對于傳統 C 而言,對效率有影響的幾個C++新特性。

            相對于傳統的 C 語言,C++ 引入的額外開銷體現在以下兩個方面:

            編譯時開銷

            模板、類層次結構、強類型檢查等新特性,以及大量使用了這些新特性的 STL 標準庫都增加了編譯器負擔。但是應當看到,這些新機能在不降低,甚至(由于模板的內聯能力)提升了程序執行效率的前提下,明顯減輕了廣大 C++ 程序員的工作量。

            用幾秒鐘的CPU時間換取幾人日的辛勤勞動,附帶節省了日后調試和維護代碼的時間,這點開銷當算超值。

            當然,在使用這些特性的時候,也有不少優化技巧。比如:編譯一個 廣泛依賴模板庫的大型軟件時,幾條顯式實例化指令就可能使編譯速度提高幾十倍;恰當地組合使用部分專門化和完全專門化,不但可以最優化程序的執行效率,還可以讓同時使用多種不同參數實例化一套模板的程序體積顯著減小……

             

            運行時開銷

            運行時開銷恐怕是程序員最關心的問題之一了。相對與傳統C程序而言,C++中有可能引入額外運行時開銷的新特性包括:
            1. 虛基類
            2. 虛函數
            3. RTTI(dynamic_cast和typeid)
            4. 異常
            5. 對象的構造和析構

            關于其中第四點:異常,對于大多數現代編譯器來說,在正常情況(未拋出異常)下,try塊中的代碼執行效率和普通代碼一樣高,而且由于不再需要使用傳統上通過返回值或函數調用來判斷錯誤的方式,代碼的實際執行效率還可能進一步提高。拋出和捕捉異常的效率也只是在某些情況下才會稍低于函數正常返回的效率,何況對于一個編寫良好的程序,拋出和捕捉異常的機會應該不多。關于異常使用的詳細討論,參見:C++編碼規范正文中的相關部分和C++異常機制的實現方式和開銷分析一節。

            而第五點,對象的構造和析構開銷也不總是存在。對于不需要初始化/銷毀的類型,并沒有構造和析構的開銷,相反對于那些需要初始化/銷毀的類型來說,即使用傳統的C方式實現,也至少需要與之相當的開銷。這里要注意的一點是盡量不要讓構造和析構函數過于臃腫,特別是在一個類層次結構中更要注意。時刻保持你的構造、析構函數中只有最必要的初始化和銷毀操作,把那些并不是每個(子)對象都需要執行的操作留給其他方法和派生類去解決。

            其實對一個優秀的編譯器而言,C++的各種特性本身就是使用C/匯編加以千錘百煉而最優化實現的。可以說,想用C甚至匯編比編譯器更高效地實現某個C++特性幾乎是不可能的。要是真能做到這一點的話,大俠就應該去寫個編譯器造福廣大程序員才對~

            C++之所以 被廣泛認為比C“低效”,其根本原因在于:由于程序員對某些特性的實現方式及其產生的開銷不夠了解,致使他們在錯誤的場合使用了錯誤的特性。而這些錯誤基本都集中在:

            • 把異常當作另一種流控機制,而不是僅將其用于錯誤處理中
            • 一個類和/或其基類的構造、析構函數過于臃腫,包含了很多非初始化/銷毀范疇的代碼
            • 濫用或不正確地使用RTTI、虛函數和虛基類機制

            其中前兩點上文已經講過,下面討論第三點。

            為了說明RTTI、虛函數和虛基類的實現方式,這里首先給出一個經典的菱形繼承實例,及其具體實現(為了便于理解,這里故意忽略了一些無關緊要的優化):


            圖中虛箭頭代表偏移,實箭頭代表指針

            由上圖得到每種特性的運行時開銷如下:
             
            特性 時間開銷 空間開銷
            RTTI 幾次整形比較和一次取址操作(可能還會有1、2次整形加法) 每類型一個type_info對象(包括類型ID和類名稱),典型情況下小于32字節

             

            虛函數 一次整形加法和一次指針間接引用 每類型一個虛表,典型情況下小于128字節

            每對象若干個(大部分情況下是一個)虛表指針,典型情況下小于8字節

             

            虛基類 從虛繼承的子類中訪問虛基類的數據成員或其虛函數時,將增加兩次指針間接引用和一次整形加法(部分情況下可以優化為一次指針間接引用)。 每類型一個虛基類表,典型情況下小于32字節

            每對象若干虛基類表指針,典型情況下小于8字節

            在同時使用了虛函數的時候,虛基類表可以合并到虛表(virtual table)中,每對象的虛基類表指針(vbptr)也可以省略(只需vptr即可)。實際上, 很多實現都是這么做的。

             

             * 其中“每類型”或“每對象”是指用到該特性的類型/對象。對于未用到這些功能的類型及其對象,則不會增加上述開銷

            可見,關于老天“餓時掉餡餅、睡時掉老婆”等美好傳說純屬謠言。但凡人工制品必不完美,總有設計上的取舍,有其適應的場合也有其不適用的地方。

            C++中的每個特性,都是從程序員平時的生產生活中逐漸精化而來的。在不正確的場合使用它們必然會引起邏輯、行為和性能上的問題。對于上述特性,應該只在必要、合理的前提下才使用。

            "dynamic_cast" 用于在類層次結構中漫游,對指針或引用進行自由的向上、向下或交叉強制。"typeid" 則用于獲取一個對象或引用的確切類型,與 "dynamic_cast" 不同,將 "typeid" 作用于指針通常是一個錯誤,要得到一個指針指向之對象的type_info,應當先將其解引用(例如:"typeid(*p);")。

            一般地講,能用虛函數解決的問題就不要用 "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" 寫會稍好一點,當然最好的方式還是在CShape里定義名為 "rotate" 的虛函數。

            虛函數是C++眾多運行時多態特性中開銷最小,也最常用的機制。虛函數的好處和作用這里不再多說,應當注意在對性能有苛刻要求的場合,或者需要頻繁調用,對性能影響較大的地方(比如每秒鐘要調用成千上萬次,而自身內容又很簡單的事件處理函數)要慎用虛函數。

            需要特別說明的一點是:虛函數的調用開銷與通過函數指針的間接函數調用(例如:經典C程序中常見的,通過指向結構中的一個函數指針成員調用;以及調用DLL/SO中的函數等常見情況)是相當的。比起函數調用本身的開銷(保存現場->傳遞參數->傳遞返回值->恢復現場)來說,一次指針間接引用是微不足道的。這就使得在絕大部分可以使用函數的場合中都能夠負擔得起虛方法的些微額外開銷。

            作為一種支持多繼承的面向對象語言,虛基類有時是保證類層次結構正確一致的一種必不可少的手段。但在需要頻繁使用基類提供的服務,又對性能要求較高的場合,應該盡量避免使用它。在基類中沒有數據成員的場合,也可以解除使用虛基類。例如,在上圖中,如果類 "BB" 中不存在數據成員,那么 "BB" 就可以作為一個普通基類分別被 "B1" 和 "B2" 繼承。這樣的優化在達到相同效果的前提下,解除了虛基類引起的開銷。不過這種優化也會帶來一些問題:從 "DD" 向上強制到 "BB" 時會引起歧義,破壞了類層次結構的邏輯關系。

            上述特性的空間開銷一般都是可以接受的,當然也存在一些特例,比如:在存儲布局需要和傳統C結構兼容的場合、在考慮對齊的場合、在需要為一個本來尺寸很小的類同時實例化許多對象的場合等等。

            posted @ 2010-08-10 17:35 深邃者 閱讀(428) | 評論 (0)編輯 收藏

            2009年11月21日 #

            Debug 和 Release

            Debug版本包括調試信息,所以要比Release版本大很多(可能大數百K至數M)。至于是否需要DLL支持,主要看你采用的編譯選項。如果是基于ATL的,則Debug和Release版本對DLL的要求差不多。如果采用的編譯選項為使用MFC動態庫,則需要MFC42D.DLL等庫支持,而Release版本需要MFC42.DLL支持。Release  Build不對源代碼進行調試,不考慮MFC的診斷宏,使用的是MFC  Release庫,編譯十對應用程序的速度進行優化,而Debug  Build則正好相反,它允許對源代碼進行調試,可以定義和使用MFC的診斷宏,采用MFC  Debug庫,對速度沒有優化。    


            一、Debug  和  Release  編譯方式的本質區別  

            Debug  通常稱為調試版本,它包含調試信息,并且不作任何優化,便于程序員調試程序。Release  稱為發布版本,它往往是進行了各種優化,使得程序在代碼大小和運行速度上都是最優的,以便用戶很好地使用。  
            Debug  和  Release  的真正秘密,在于一組編譯選項。下面列出了分別針對二者的選項(當然除此之外還有其他一些,如/Fd  /Fo,但區別并不重要,通常他們也不會引起  Release  版錯誤,在此不討論)  

            Debug  版本:  
            /MDd  /MLd  或  /MTd  使用  Debug  runtime  library(調試版本的運行時刻函數庫)  
            /Od  關閉優化開關  
            /D  &quot;_DEBUG&quot;  相當于  #define  _DEBUG,打開編譯調試代碼開關(主要針對  
            assert函數)  
            /ZI  創建  Edit  and  continue(編輯繼續)數據庫,這樣在調試過  
            程中如果修改了源代碼不需重新編譯  
            /GZ  可以幫助捕獲內存錯誤  
            /Gm  打開最小化重鏈接開關,減少鏈接時間  

            Release  版本:    
            /MD  /ML  或  /MT  使用發布版本的運行時刻函數庫  
            /O1  或  /O2  優化開關,使程序最小或最快  
            /D  &quot;NDEBUG&quot;  關閉條件編譯調試代碼開關(即不編譯assert函數)  
            /GF  合并重復的字符串,并將字符串常量放到只讀內存,防止  
            被修改  

            實際上,Debug  和  Release  并沒有本質的界限,他們只是一組編譯選項的集合,編譯器只是按照預定的選項行動。事實上,我們甚至可以修改這些選項,從而得到優化過的調試版本或是帶跟蹤語句的發布版本。  

            二、哪些情況下  Release  版會出錯  

            有了上面的介紹,我們再來逐個對照這些選項看看  Release  版錯誤是怎樣產生的  

            1.  Runtime  Library:鏈接哪種運行時刻函數庫通常只對程序的性能產生影響。調試版本的  Runtime  Library  包含了調試信息,并采用了一些保護機制以幫助發現錯誤,因此性能不如發布版本。編譯器提供的  Runtime  Library  通常很穩定,不會造成  Release  版錯誤;倒是由于  Debug  的  Runtime  Library  加強了對錯誤的檢測,如堆內存分配,有時會出現  Debug  有錯但  Release  正常的現象。應當指出的是,如果  Debug  有錯,即使  Release  正常,程序肯定是有  Bug  的,只不過可能是  Release  版的某次運行沒有表現出來而已。  

            2.  優化:這是造成錯誤的主要原因,因為關閉優化時源程序基本上是直接翻譯的,而打開優化后編譯器會作出一系列假設。這類錯誤主要有以下幾種:  

            (1)  幀指針(Frame  Pointer)省略(簡稱  FPO  ):在函數調用過程中,所有調用信息(返回地址、參數)以及自動變量都是放在棧中的。若函數的聲明與實現不同(參數、返回值、調用方式),就會產生錯誤————但  Debug  方式下,棧的訪問通過  EBP  寄存器保存的地址實現,如果沒有發生數組越界之類的錯誤(或是越界“不多”),函數通常能正常執行;Release  方式下,優化會省略  EBP  棧基址指針,這樣通過一個全局指針訪問棧就會造成返回地址錯誤是程序崩潰。C++  的強類型特性能檢查出大多數這樣的錯誤,但如果用了強制類型轉換,就不行了。你可以在  Release  版本中強制加入  /Oy-  編譯選項來關掉幀指針省略,以確定是否此類錯誤。此類錯誤通常有:  

            ●  MFC  消息響應函數書寫錯誤。正確的應為  
            afx_msg  LRESULT  OnMessageOwn(WPARAM  wparam,  LPARAM  lparam);  
            ON_MESSAGE  宏包含強制類型轉換。防止這種錯誤的方法之一是重定義  ON_MESSAGE  宏,把下列代碼加到  stdafx.h  中(在#include  &quot;afxwin.h&quot;之后),函數原形錯誤時編譯會報錯  
            #undef  ON_MESSAGE  
            #define  ON_MESSAGE(message,  memberFxn)  \  
            {  message,  0,  0,  0,  AfxSig_lwl,  \  
            (AFX_PMSG)(AFX_PMSGW)(static_cast&lt;  LRESULT  (AFX_MSG_CALL  \  
            CWnd::*)(WPARAM,  LPARAM)  &gt;  (&memberFxn)  },  

            (2)  volatile  型變量:volatile  告訴編譯器該變量可能被程序之外的未知方式修改(如系統、其他進程和線程)。優化程序為了使程序性能提高,常把一些變量放在寄存器中(類似于  register  關鍵字),而其他進程只能對該變量所在的內存進行修改,而寄存器中的值沒變。如果你的程序是多線程的,或者你發現某個變量的值與預期的不符而你確信已正確的設置了,則很可能遇到這樣的問題。這種錯誤有時會表現為程序在最快優化出錯而最小優化正常。把你認為可疑的變量加上  volatile  試試。  

            (3)  變量優化:優化程序會根據變量的使用情況優化變量。例如,函數中有一個未被使用的變量,在  Debug  版中它有可能掩蓋一個數組越界,而在  Release  版中,這個變量很可能被優化調,此時數組越界會破壞棧中有用的數據。當然,實際的情況會比這復雜得多。與此有關的錯誤有:  
            ●  非法訪問,包括數組越界、指針錯誤等。例如  
            void  fn(void)  
            {  
            int  i;  
            i  =  1;  
            int  a[4];  
            {  
            int  j;  
            j  =  1;  
            }  
            a[-1]  =  1;//當然錯誤不會這么明顯,例如下標是變量  
            a[4]  =  1;  
            }  
            j  雖然在數組越界時已出了作用域,但其空間并未收回,因而  i  和  j  就會掩蓋越界。而  Release  版由于  i、j  并未其很大作用可能會被優化掉,從而使棧被破壞。  

            3.  _DEBUG  與  NDEBUG  :當定義了  _DEBUG  時,assert()  函數會被編譯,而  NDEBUG  時不被編譯。除此之外,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()  。事實上,這些宏都是調用了  assert()  函數,只不過附加了一些與庫有關的調試代碼。如果你在這些宏中加入了任何程序代碼,而不只是布爾表達式(例如賦值、能改變變量值的函數調用  等),那么  Release  版都不會執行這些操作,從而造成錯誤。初學者很容易犯這類錯誤,查找的方法也很簡單,因為這些宏都已在上面列出,只要利用  VC++  的  Find  in  Files  功能在工程所有文件中找到用這些宏的地方再一一檢查即可。另外,有些高手可能還會加入  #ifdef  _DEBUG  之類的條件編譯,也要注意一下。  
            順便值得一提的是  VERIFY()  宏,這個宏允許你將程序代碼放在布爾表達式里。這個宏通常用來檢查  Windows  API  的返回值。有些人可能為這個原因而濫用  VERIFY()  ,事實上這是危險的,因為  VERIFY()  違反了斷言的思想,不能使程序代碼和調試代碼完全分離,最終可能會帶來很多麻煩。因此,專家們建議盡量少用這個宏。  

            4.  /GZ  選項:這個選項會做以下這些事  

            (1)  初始化內存和變量。包括用  0xCC  初始化所有自動變量,0xCD  (  Cleared  Data  )  初始化堆中分配的內存(即動態分配的內存,例如  new  ),0xDD  (  Dead  Data  )  填充已被釋放的堆內存(例如  delete  ),0xFD(  deFencde  Data  )  初始化受保護的內存(debug  版在動態分配內存的前后加入保護內存以防止越界訪問),其中括號中的詞是微軟建議的助記詞。這樣做的好處是這些值都很大,作為指針是不可能的(而且  32  位系統中指針很少是奇數值,在有些系統中奇數的指針會產生運行時錯誤),作為數值也很少遇到,而且這些值也很容易辨認,因此這很有利于在  Debug  版中發現  Release  版才會遇到的錯誤。要特別注意的是,很多人認為編譯器會用  0  來初始化變量,這是錯誤的(而且這樣很不利于查找錯誤)。  
            (2)  通過函數指針調用函數時,會通過檢查棧指針驗證函數調用的匹配性。(防止原形不匹配)  
            (3)  函數返回前檢查棧指針,確認未被修改。(防止越界訪問和原形不匹配,與第二項合在一起可大致模擬幀指針省略  FPO  )  

            通常  /GZ  選項會造成  Debug  版出錯而  Release  版正常的現象,因為  Release  版中未初始化的變量是隨機的,這有可能使指針指向一個有效地址而掩蓋了非法訪問。  

            除此之外,/Gm  /GF  等選項造成錯誤的情況比較少,而且他們的效果顯而易見,比較容易發現。  
            --------------------------------------------------------------  
            Release是發行版本,比Debug版本有一些優化,文件比Debug文件小  
            Debug是調試版本,包括的程序信息更多  
            Release方法:  
            build-&gt;batch  build-&gt;build就OK.  

            -----------------------------------------------------  

            一、&quot;Debug是調試版本,包括的程序信息更多&quot;  

            補充:只有DEBUG版的程序才能設置斷點、單步執行、使用TRACE/ASSERT等調試輸出語句。REALEASE不包含任何調試信息,所以體積小、運行速度快。  

            二、一般發布release的方法除了hzh_shat(水)  所說的之外,還可以project-&gt;Set  Active  Config,選中release版本。此后,按F5或F7編譯所得的結果就是release版本

            posted @ 2009-11-21 22:39 深邃者 閱讀(286) | 評論 (0)編輯 收藏

            2009年10月26日 #

            DLL 線程本地存儲

            DLL, 線程本地存儲

            1.概覽
            .構造DLL  
               (1)僅導出函數
                  DLL可以導出全局變量和類,但我們不建議這么做,建議導出函數。
               (2).lib
                  每個DLL都有與之相對應的.lib文件,該文件中列出了DLL中導出的函數和變量的符號名
               (3)指定要導出的函數名
                   因為不同編譯器的Name mangle規則不同,這就導致DLL不能跨編譯器使用。
                   有以下兩種方法可以解決這個問題:
                        1..def文件中指定要導出的函數名
                        2.在編譯指中指定要導出的函數名:
                            #pragma comment(linker, "/export:MyFunc=_MyFunc@8")
            .DLL加載路徑
                當需要加載一個DLL時,系統會依照下面的順序去尋找所需DLL直到找到為止,然后加載,否則加載失敗。
                          (1)當前可執行文件路徑
                          (2)GetWindowsDirectory返回的Windows系統路徑
                          (3)16位系統的路徑 windows"system
                          (4)GetSystemDirectory返回的Windows系統路徑
                          (5)當前進程所在路徑
                          (6)PATH環境中所指定的路徑
            ­
            .創建\使用動態鏈接庫
            首先必須創建一個包含需要導出的符號的頭文件,以便其他程序鏈接到該dll上:
            // dllexample.h
            #ifdef DLLEXAMPLE_EXPORTS // 在編譯命令中已定義,所以實際用的是 __declspec(dllexport)
            #define DLLEXAMPLE_API __declspec(dllexport)
            #else
            #define DLLEXAMPLE_API __declspec(dllimport)
            #endif
            DLLEXAMPLE_API int fnDllexample(void);
            當其他應用包含該頭文件,意圖使用該dll的導出符號時,因為沒有定義DLLEXAMPLE_EXPORTS,所以使用的是__declspec(dllimport),這樣編譯器編譯時便知道這是從外部引入的函數。在鏈接時,鏈接程序將生成導入表(ImportAddressTable),該表羅列了所有調用到的函數,以及一個空白的對應地址。在程序執行時,加載器將動態的填入每個函數符號在本進程中的地址,使得程序能正確的調用到dll中的函數上。
            這種通過dll提供的.h和.lib文件進行鏈接dll的使用方式,稱為隱式鏈接。用vc開發程序時,幾乎所有的系統API調用都用了隱式鏈接。
            .顯式鏈接
            在exe創建時不引用.lib文件中的符號,當然也不必包含.h頭文件,而是由程序調用LoadLibrary(Ex)以及GetProcAddress函數來獲取每個需要使用的函數地址,從而進行dll中的函數調用,這種dll使用方法稱為顯式鏈接。顯式鏈接時不生成對應dll的IAT.
            當決定不再使用該dll時,通過調用FreeLibrary來卸載。需要注意的是,同一個進程中共計調用LoadLibrary的次數要和調用FreeLibrary的次數相等,因為系統維護了一個使用計數,當計數為0時,才會真正的卸載該dll.
            如果想確認一個dll是否已經被映射到進程空間中,盡量使用GetModuleHandle,最好不要冒然使用LoadLibrary(Ex).
            GetProcAddress可以傳遞函數名或者序號(通過MAKEINTRESOURCE(2)來"制作"序號).
            ­
            1.1動態加載DLL文件 LoadLibraryEx
            HMODULE LoadLibraryEx( //返回DLL加載到進程空間原首地址。
            PCTSTR pszDLLPathName,
            HANDLE hFile,
            DWORD dwFlags);
            dwFlags 可以有以下幾個值
                          (1) DONT_RESOLVE_DLL_REFERENCES
                                                建議永遠不要使有這個值,它的存在僅僅是為了向后兼容、
                              更多內容請訪問:http://blogs.msdn.com/oldnewthing/archive/2005/02/14/372266.aspx
                          (2) LOAD_LIBRARY_AS_DATAFILE
                              把要加載的DLL文件以數據文件的形式加載到進程中。
                              GetModuleHandleGetProcAddress返回NULL
                          (3) LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE
                              與前者相同,不同的時獨占打開,禁止其它進程訪問和修改該DLL中的內容。
                          (4) LOAD_LIBRARY_AS_IMAGE_RESOURCE
                              不修改DLL中的RVA,以image的形式加載到進程中。常與LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE一起使用。
                          (5) LOAD_WITH_ALTERED_SEARCH_PATH
                               修改DLL的加載路徑
            1.2 DLL的加載與卸載
                (1)加載
                   不要在同一進程中,同時使用LoadLIbraryLoadLibraryEx加載同一DLL文件。
                   DLL的引用計數是以進程為單位的。LoadLibrary會把DLL文件加載到內存,然后映射到進程空間中。
                   多次加載同一DLL只會增加引用計數而不會多次映射。當所有進程對DLL的引用計數都為0時,系統會在內存中釋放該DLL
                (2)卸載
                     FreeLibrary,FreeLibraryAndExitThread對當前進程的DLL的引用計數減1
                (3) GetProcAddress
                     取得函數地址。它只接受ANSI字符串。
            2.DLL的入口函數
                  2.1 DllMain
                          BOOL WINAPI DllMain(
                          HINSTANCE hInstDll, ""加載后在進程中的虛擬地址
                          DWORD fdwReason, ""系統因何而調用該函數
                          PVOID fImpLoad ""查看是隱工還是動態加載該DLL
                DLLsDllMain方法來初始化他們自已。DllMain中的代碼應盡量簡單,只做一些簡單的初始化工作。
                不要在DllMain中調用LoadLibrary,FreeLibraryShell, ODBC, COM, RPC, socket 函數,從而避免不可預期的錯誤。
              2.2 fdwReason的值
                (1)DLL_PROCESS_ATTACH
                  系統在為每個進程第一次加載該DLL時會,執行DLL_PROCESS_ATTACH后面的語句來初始化DLL,DllMain的返回值僅由它決定。
                 系統會忽略DLL_THREAD_ATTACH等執行后DllMain的返回值。
                 如果DllMain返回FALSE,系統會自動調用DLL_PROCESS_DETACH的代碼并解除DLL文件中進程中的內存映射。        
                (2)DLL_PROCESS_DETACH
                    如果DLL是因進程終止而卸載其在進程中的映射,那么負責調用ExitProcess的線程會調用DllMainDLL_PROCESS_DETACH所對應的代碼。
                    如果DLL是因FreeLibraryFreeLibraryAndExitThread,而卸載其在進程中的映射, 那么FreeLibraryFreeLibraryAndExitThread會負責調用DllMainDLL_PROCESS_DETACH所對應的代碼。
                    如果DLL是因TerminateProcess而卸載其在進程中的映射,系統不會調用DllMainDLL_PROCESS_DETACH所對應的代碼。
                (3) DLL_THREAD_ATTACH
                    若進程是先加載的DLL,后創建的線程
                    那么在進程中創建新線程時(主線程除外),系統會執行該進程已載的所有DLLDllMainDLL_THREAD_ATTACH對應的代碼。
                     若進程是先創建的線程,后加載的DLL
                     那么系統不會調用DLLDllMain中的代碼。
                 (4) DLL_THREAD_DETACH
                     進程中的線程退出時,會先執行所有已加載DLLDllMainDLL_THREAD_DETACH所對應的代碼。若該代碼中有死循環,線程不會退出。              
            2.3 同步化DllMain的調用
                  同一時間只能有一個線程調用DllMain中的代碼,所以下面的代碼會導致死循環
            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);// CreateThreadDLL_THREAD_ATTACH中的代碼,但是由于當前線程并未執行完畢,
                  //所以DLL_THREAD_ATTACH 中的代碼不會被執行,且CreateThread永無不會返回。
                  // 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.延時加載DLL
            (1)延時加載DLL的限制
                     延遲加載的D L L是個隱含鏈接的D L L,它實際上要等到你的代碼試圖引用D L L中包含的一個符號時才進行加載,它與動態加載不同。
                    http://msdn2.microsoft.com/en-us/library/yx1x886y(VS.80).aspx
            4.已知的DLL (Known DLLs)
                位置:HKEY_LOCAL_MACHINE"SYSTEM"CurrentControlSet"Control"Session Manager"KnownDLLs
                LoadLibrary在查找DLL會先去該位置查找有無相應的鍵值與DLL要對應,若有則根據鏈值去%SystemRoot%"System32加載鍵值對應的DLL
                若無則根據默認規去尋找DLL
            5.Bind and Rebase Module
                它可以程序啟動的速度。ReBaseImage
            ­
            DLL 注入和API(DLL Injection and API Hooking)
            1.概覽
              每個進程都有自已獨立的地址空間,一個進程不可能創建一個指向其它進程地址空間的指針。
               然而如果我們把自已的DLL注射到另一個進程的地址空間去,我們就可以在那個被注入的進程里為所欲為了。
               Subclass同一進程中的窗體:http://msdn2.microsoft.com/en-us/library/ms649784.aspx.
            2.用注冊表注入DLL
                 該方法適用于給GUI的程序注入DLL
                 所有的GUI應用程序在啟動時都會加載User32.dll,而在User32.dllDLL_PROCESS_ATTACH代碼根據注冊表中的信息
            來注入用戶指定的DLL
            注冊表項 HKEY_LOCAL_MACHINE"Software"Microsoft"Windows NT"CurrentVersion"Windows"
            中有兩個值:
                LoadAppInit_Dlls:鍵值中指定要注入的DLL 如:c:"inject.dll
            AppInit_Dlls:若其鍵值為1,則注入LoadAppInit_Dlls中指定的DLL,否則若為0則不注入。
                :
            (1)LoadAppInit_Dlls中的值是以空格或分號分隔的,所以DLL的路徑中最好不要有空格,最后不指定路徑,直接將DLL放到windows系統目錄中。
            (2) 用注冊表注入DLL的方式有很大的局限性,Kernel32.dllNtdll.dll中有的函數才能調用
            一.注入dll
            1.通過注冊表項 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs 來指定你的dll的路徑,那么當一個GUI程序啟動時就要加載User32.dll,而User32.dll將會檢查這個值,如果有的話就LoadLibrary該Dll。這個方法不好,因為大多數情況我們只需要針對性的注入,并且沒辦法注入到不使用User32.dll的進程中;
            ­
            2.用SetWindowsHookEx函數,并傳遞目標線程ID、需要掛載的Dll在本進程中的映射地址(hInstance)、替換函數在本進程中的地址。這樣,當被掛載進程的這個線程要執行相應的操作時(GETMESSAGE、鍵盤消息之類的),就會發現已經安裝了WH_XX,The system checks to see whether the DLL containing the GetMsgProc function is mapped into Process B's address space,如果還未映射該Dll,則強制LoadLibrary。然后系統調用hThisInstance + (GetMsgProc - hInstance),從而實現了事件的通知。這種方法的好處是可以針對某個進程安裝Hook,缺點是容易被目標進程發現、同樣只適用于GUI進程。如果不再想使用掛鉤了,那么需要調用UnhookWindowsHookEx,卸載Hook。
            ­
            3.使用遠程線程注入Dll(Injecting a DLL Using Remote Threads)
            這個方法比較好。流程是這樣的:
            ?調用VirtualAllocEx,在目標進程保留一塊內存,并提交,其長度是你要注入Dll的全路徑長度nLen + 1,返回地址pv;
            ?調用WriteProcessMemory,在目標進程的pv處寫入Dll的全路徑,注意要添加\0結束符;
            ?獲取本進程的LoadLibrary函數的地址,方法是調用pfn = GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA")——之所以獲取本進程的地址,是因為kernel32.dll在每個進程的映射地址都相同,倘若不同,那么此方法則無效;
            ?調用HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,  pfn, pv, 0, NULL)來創建遠程線程,其實這個線程函數就是LoadLibrary函數,因此將執行映射Dll到目標進程的操作;
            ?調用VirtuallFreeEx(hProcessRemote, pv)釋放提交的內存;
            這便完成了dll注入。
            缺點是不能用在windows98上。但是對于xp都要被微軟拋棄的年代,windows98地影響不大了。
            ­
            4.披著羊皮的狼:使用特洛伊Dll來注入Dll(Injecting a DLL with a Trojan DLL)
            其實就是替換某個目標進程要加載的a.dll,并把a.dll的所有引出函數用函數轉發器在自己的dll引出。
            ­
            5.用調試函數插入Dll
            ReadProcessMemory和WriteProcessMemory是windows提供的調試函數。如果在方法3中調用WriteProcessMemory寫入的不是字串而是精心編排好的機器指令,并且寫在目標進程特定的地址空間,那么這段機器指令就有機會執行——而這段機器指令恰好完成了LoadLibrary功能;
            ­
            6.其他方法(略)
            ­
            二.掛接API(API Hooking)
            其實,這是許多注入的Dll都愿意做的事情。
            所謂掛接API就是在目標進程調用windows API之前,先執行我們的仿API函數,從而控制系統API的行為,達到特殊的目的。
            我們的仿造函數必須與要替換的系統API有相同的型參表以及相同的返回值類型.
            ­
            1.改寫系統API代碼的前幾個字節,通過寫入jmp指令來跳轉到我們的函數。在我們的函數里執行操作,可以直接返回一個值,也可以將系統API的前幾個字節復原,調用系統API,并返回系統API的值——隨便你想怎么做。
            此方法的缺點是對于搶占式多線程的系統不太管用。
            ­
            2.通過改寫目標進程IAT中要調用的函數地址來達到目的。具體操作見書中示例
            ­
            ­
            ­
            線程本地存儲(Thread-Local Storage)
            例子C / C + +運行期庫要使用線程本地存儲器( T L S)。由于運行期庫是在多線程應用程序出現前的許多年設計的,因此運行期庫中的大多數函數是用于單線程應用程序的。函數s t r t o k就是個很好的例子。
            盡可能避免使用全局變量和靜態變量
            1.動態TLS
            圖21-1 用于管理T L S的內部數據結構
            ­
            在創建線程時,進程會為當前創建的線程分配一個void *的數組作為TLS用。它用于存儲只限當前線程可見的全局變量。
            從而使進程中的每個線程都可以有自已的(不能其它線程訪問的)全局變量。
            TlsAlloc在返回時會先把槽中的值置為0。每個線程至少有64個槽。
            2.靜態TLS
                          __declspec(thread)關鍵字用于聲明,線程本地的全局變量。
                          要求聲明的變量必須是全局變量或靜態變量。
            3.Common API:
                          TlsAlloc   TlsFree
                          TlsSetValue   TlsGetValue
                          __declspec(thread)

            posted @ 2009-10-26 18:33 深邃者 閱讀(1031) | 評論 (0)編輯 收藏

            僅列出標題  下一頁
            婷婷久久精品国产| 亚洲国产精品一区二区久久| 久久99精品久久久久久秒播| 久久91精品国产91久久麻豆| 99久久精品国产免看国产一区| 97精品伊人久久久大香线蕉| 国产成人精品综合久久久久 | 日韩欧美亚洲综合久久影院Ds| 国产真实乱对白精彩久久| 日本精品久久久中文字幕 | 久久电影网2021| 国产美女久久精品香蕉69| 久久福利青草精品资源站| 夜夜亚洲天天久久| 久久精品国产99久久久香蕉| 久久久久久极精品久久久 | 久久综合给合久久狠狠狠97色69| 亚洲精品高清国产一线久久| 国产Av激情久久无码天堂| 日本精品久久久久中文字幕| 日韩久久久久中文字幕人妻 | 性做久久久久久久久浪潮| 亚洲AV日韩精品久久久久久| 久久国产免费观看精品3| 久久99精品久久久久久| 久久婷婷五月综合成人D啪| 日韩人妻无码精品久久免费一| 99久久这里只有精品| 久久亚洲AV无码西西人体| 中文字幕久久久久人妻| 99久久精品九九亚洲精品| 综合久久给合久久狠狠狠97色| 久久久久久久人妻无码中文字幕爆| 久久精品国产一区| 精品人妻伦九区久久AAA片69| 久久美女人爽女人爽| 久久久久久国产精品美女| 97超级碰碰碰碰久久久久| 思思久久99热只有频精品66| 亚洲国产精品热久久| 久久综合九色综合网站|