編程高手箴言

本書是作者十余年編程生涯中的技術和經驗的總結。內容涵蓋了從認識CPU、Windows運行機理、
編程語言的運行機理,到代碼的規范和風格、分析方法、調試方法和內核優化,內有作者對許多問題
的認知過程和透徹的分析,以及優秀和精彩的編程經驗。
第1章 程序點滴
1.1 程序≠軟件(1)
1.1 程序≠軟件(2)
1.2 高手是怎樣練成的(1)
1.2 高手是怎樣練成的(2)
1.2 高手是怎樣練成的(3)
1.3 正確的入門方法(1)
1.3 正確的入門方法(2)
1.3 正確的入門方法(3)
1.4 開放性思維(1)
1.4 開放性思維(2)
第2章 認識CPU
2.1 8位微處理器回顧/2.2 16位微處理
器(1)
2.2 16位微處理器(2)
2.3 32位微處理器(1)
2.3 32位微處理器(2)
2.3 32位微處理器(3)
2.4 【實例】:在DOS實模式下讀取4GB內
存(1)
2.4 【實例】:在DOS實模式下讀取4GB內
存(2)
第3章 Windows運行機理
3.1 內核分析(1)
3.1 內核分析(2)
3.1 內核分析(3)
3.1 內核分析(4)
3.1 內核分析(5)
3.1 內核分析(6)
3.1 內核分析(7)
3.1 內核分析(8)
3.1 內核分析(9)
3.1 內核分析(10)
3.1 內核分析(11)
3.1 內核分析(12)
3.3 GDI的結構和組成(1)
3.3 GDI的結構和組成(2)
3.4 線程的機制(1)
3.4 線程的機制(2)
3.4 線程的機制(3)
3.4 線程的機制(4)
3.4 線程的機制(5)
3.4 線程的機制(6)
3.4 線程的機制(7)
3.5 PE結構分析(1)
3.5 PE結構分析(2)
3.5 PE結構分析(3)
?

3.1 內核分析(13)
3.2 消息的運行方式(1)
3.2 消息的運行方式(2)
3.2 消息的運行方式(3)
3.5 PE結構分析(4)
3.5 PE結構分析(5)
3.5 PE結構分析(6)
3.5 PE結構分析(7)
第4章 編程語言的運行機理
第5章 代碼的規范和風格
5.1 環境的設置
5.1.1 集成環境的設置
5.1.2 TAB值的設置
5.1.3 編譯環境的設置
5.1.4 設置herosoft.dsm宏
5.2 變量定義的規范
5.2.1 變量的命名規則
5.2.2 變量定義的地方規定
5.2.3 變量的對齊規定
5.3 代碼對齊方式、分塊、換行的規范
5.4 快速的代碼整理方法
5.5 注釋的規范
5.6 頭文件的規范
5.7 建議采用的一些規則
5.8 可靈活運用的一些規則
5.9 標準化代碼示例
5.10 成對編碼規則
5.10.1 成對編碼的實現方法
5.10.2 成對編碼中的幾點問題
5.11 正確的成對編碼的工程編程方法
5.11.1 編碼前的工作
5.11.2 成對編碼的工程方法
5.11.3 兩個問題的解釋
第6章 分析方法
6.1 分析概要
6.1.1 分析案例一:軟件硬盤陣列
6.1.2 分析案例之二:游戲內存修改工具
6.2 接口的提煉
6.2.1 分離接口
6.2.2 參數分析
6.3 主干和分支
6.3.1 主干和分支分析舉例
6.3.2 程序檢驗
6.4 是否對象化
6.5 是否DLL化
6.5.1 DLL的建立和調用
6.5.2 DLL動態與靜態加載的比較
6.5.3 DLL中函數的定義
6.6 COM的結構
6.7 幾種軟件系統的體系結構分析
6.7.1 播放器的解碼組成分析
6.7.2 豪杰大眼睛的體系結構
6.7.3 Windows 9x體系結構
?

?第7章 調試方法
7.1 調試要點
7.1.1 調試和編程同步
7.1.2 匯編代碼確認
7.1.3 Win32的Debug實現方法
7.2 基本調試實例分析
7.3 多線程應用的調試
7.4 非固定錯誤的調試
7.4.1 激活調試環境
7.4.2 正確區分錯誤的類型
7.4.3 常見的偶然錯誤
第8章 內核優化
8.1 數據類型的認識
8.2 X86優化編碼準則
8.2.1 通用的X86優化技術
8.2.2 通用的AMD-K6處理器x86代碼優化
8.2.3 AMD-K6處理器整數x86代碼優化
8.3 MMX指令的優化
8.3.1 MMX的寄存器介紹
8.3.2 MMX的工作原理
8.3.3 MMX的檢測
8.3.4 MMX指令的介紹
8.4 MMX的實例一:圖像的淡入淡出
8.4.1 目的
8.4.2 解決方法
8.4.3 分析
8.4.4 初步實現
8.4.5 MMX的優化實現
8.5 MMX的實例二:MMX類的實現方法
8.5.1 實現方法分析
8.5.2 實現步驟
8.5.3 檢測過程
8.5.4 總結
整理說明:
【獻給CSDN上的朋友們】
在CSDN論壇上多次見到網友搜尋《編程高手箴言》一書,我本人也常常在書店里站著翻閱此書,
雖然對梁先生的部分觀點實在不敢茍同,但里面一些知識點確是講的非常不錯。
所以一直也在尋找電子版。
今天正好看到有朋友帖出地址:http://act.it.sohu.com/book/serialize.php?id=71

雖然只有前三章,但已經相當不錯,(個人認為前三章乃是此書精華之所在)
只不過頁面在網絡上,看起來太麻煩,而且很多廣告鏈接,看得不太舒服。
于是我花了兩個多小時整理出來(主要時間花在清理無用鏈接以及一些腳本錯誤,還有圖片和鏈接的相對地址轉換)。
希望能給大家帶來方便,則是本人莫大欣慰。
整理者:Featured (mail: lizhaozhuo@people.com.cn)
2005.4.21 晚
?

第1章 程序點滴
1.1 程序≠軟件(1)
現在很多人以為程序就是軟件,軟件就是程序。事實上,軟件和程序在20世紀80年代時,還可以說是
等同的,或者說,在非PC領域里它們可能還會是等同的。比如說某個嵌入式軟件領域,軟件和程序可能
是等同的。但是,在PC這個領域內,現在的程序已不等于軟件了。這是什么意思呢?
1. 軟件發展簡述
在20世紀80年代的時候,PC剛誕生,那時國內還沒有幾個人會寫程序。那么,如果你寫個程序,別
人就可以拿來用。那時候的程序就能產生價值,那個程序就直接等同于軟件。
但軟件行業發展到現在,這里以中國的情況為例(美國在20世紀80年代,程序已經不等同于軟件
了),程序也不等同于軟件了。因為現在寫程序很容易,但是你的這個程序很難產生什么樣的商業意義,
也不能產生什么價值,這就很難直接變成軟件。要使一個程序直接變成軟件,中間就面臨著很高的門檻問
題。這個門檻問題來自于整個行業的形成。
現在,你寫了一個程序以后,要面臨商業化的過程。你要宣傳,你要讓用戶知道,你要建立經銷渠
道,可能你還要花很多的時間去說服別人用你的東西。這是程序到軟件的一個過程。這門檻已比較高了。
我們在和國內的大經銷商的銷售渠道的人聊天時,他們的老板說,這幾年做軟件的門檻挺高的,如果
你沒有五六百萬元做軟件,那是“玩”不起來的。我說:“你們就使門檻很高了。”他說:“那肯定是的。如果
你寫個“爛”程序,明天你倒閉了,你的東西還占了我的庫房,我還不知道找誰退去呢。我的庫房是要錢的
呀!現在的軟件又是那么多!”
所以,如果你沒有一定的資產的話,經銷商都不理你。實際情況也是這樣的,如果你的公司比較小,
且沒什么名氣,你的產品放到經銷商庫房,那么他最多給你暫收,產品銷不動的話,一般兩周絕對會退
貨。因為現在經銷商可選擇的余地已很多了,所謂的軟件也已經很多了。而程序則更多,程序都想變成軟
件,誰都說自己的是“金子”。但只有經受住用戶的檢驗,才能成為真正的“金子”。
這就是美國為什么在20世紀90年代幾乎沒有什么新的軟件公司產生的原因。只是原來80年代的大的軟
件公司互相兼并,我吞你,你吃我。但是,寫程序的人很多,美國的程序變軟件的門檻可能比我們還高,
所以很多人寫了程序就丟在網上,就形成了共享軟件。
2. 共享軟件
共享軟件是避開商業渠道的一種方法。它避開了商業的門檻,因為這個行業的門檻發展很高以后就輕
易進不去了。我寫個程序丟在網上,你下載就可以用,這時候程序又等于軟件。共享軟件是這樣產生的,
是因為沒有辦法中的辦法。如果說程序直接等于軟件的話,誰也不會輕易把程序丟到網上去。
開始做共享軟件的人并不認為做它能賺錢,只是后來用的人多了,有人付錢給他了。共享軟件使得程
序和軟件的距離縮短了,但是它與商業軟件的距離會進一步拉大。商業軟件的功能和所要達到的目標就不
是一個人能“玩”得起來的了。這時的軟件也已不是幾個人、一個小組就能做出來的了。這就是在美國新的
軟件公司沒法產生的原因。比如Netscape網景是在1995~1996年產生的新軟件公司,但是,兩三年后它
就不見了。
?

1.1.1 商業軟件門檻的形成
1. 商業軟件門檻的形成
商業軟件門檻的形成是整個行業發展的必然結果。任何一個行業初始階段時的門檻都非常低,但是,
只要發展到一定的階段后,它的門檻就必然抬高。比如,現在國內生產小汽車很困難,但在20世紀50年代
~60年代的時候,你裝4個輪子,再加上柴油機等就形成汽車。那時的萊特兄弟裝個螺旋槳,加兩個機
翼,就能做飛機。整個行業還沒有形成的時候,絕對可以這樣做,但是,到整個行業形成時,你就做不了
了。所有的行業都是這樣的。
為什么網站一出來時那么多人去擠著做?這也是因為一開始的時候,看起來門檻非常低,人人都可以
做。只要有一個服務器,架根網線,就能做網站。這個行業處于初始階段時,情況就是這樣的。但這個行
業形成后,你就輕易地“玩”不了了。
國內的軟件發展也是如此。國內的軟件自從軟件經銷商形成以后,這個行業才真正地形成。有沒有一
個渠道是判斷一個行業是否形成的很重要的環節。任何一個行業都會有一個經銷渠道,如果渠道形成了,
那么這個行業也就形成了。第一名的經銷商是1994年~1995年成立的,也就是說,中國軟件行業大概也
就是在1995年形成的,至今才經歷8年時間的發展。
有一種浮躁的思想認為,中國軟件產業應該很快就能趕上美國。美國軟件行業是20世紀80年代形成
的,到現在已經發展了20多年了。中國軟件行業才8年,8歲才是一個懵懂的小孩,20多歲是一個強壯的
青年,那么他們的力量是不對等的。但也要看到,當8歲變成15歲的時候,它真正的能量才會反映出來。
2. 軟件門檻對程序員的影響
現在中國軟件行業正在形成。所以,現在做一個程序員一定要有耐心,因為現在已經不等于以前了。
你一定要把所有的問題搞清楚,然后再去做程序。
對于程序員來說,最好的工作環境是在現有的或者初始要成立的公司里面,這是最容易成功的。個人
單槍匹馬闖天下已經很困難了。即使現在偶爾做兩個共享軟件放在網上能成名,但是也已經比較困難了。
因為現在做軟件的人已經很多了。這也說明軟件已經不等于程序了,程序也不等于軟件。
程序要變成軟件,這中間是一個商業化的過程。沒有門檻以前,它沒有這個商業過程,現在有這個行
業了,它中間就有商業化的過程。這個商業化的過程就不是一個人能“玩”的。
如果你開始做某一類軟件的時候,別人已經做成了,這時你再決定花力氣去做,那么你就要花雙倍的
力氣去趕上別人。
現在的商業軟件往往是由很多模塊組成的,模塊是整個系統的一部分。個人要完整地寫一個商業系統
幾乎是不可能的。軟件進入Windows平臺后,它已經很復雜了,不像在DOS的時候,你寫兩行程序就能
賣,做個ZIP也能賣。事實上,美國的商業編譯器也不是一個人能“玩”的。現在你可能覺得它是很簡單
的,甚至Linux還帶了一個GCC,且源程序還在。你可以把它改一改,做個VC試一試,看它會有人用嗎?
它能變成軟件嗎?即使你再做個界面,它也還是一個GCC,絕對不會成為Visual C++那樣能商業化的軟
件。
可見,國外軟件行業的門檻要比中國的高很多了。我覺得我們中國即使再去做這樣的東西,也沒有多
大的意義了。這個門檻你是追不過來的。不僅要花雙倍的力氣,而且在這么短的時間內,你還要完成別人
已經完成過的工作,包括別人所做的測試工作。只有這樣,才能做到你的軟件與別人有競爭力,能與它做
比較。
?

第1章 程序點滴
1.1 程序≠軟件(2)
1.1.2 認清自己的發展
如果連以上認識都不清楚,很可能就以為去書店買一本MFC高手速成之類的書,編兩個程序就能成為
軟件高手。就好像這些書是“黃金”,我學兩下,學會了VC、MFC,就能做一個軟件拿出去賣了。這種想
法也不是不行,最后一定能行,但要有耐心,還要有機遇。機遇是從耐心中產生的,越有耐心,就越有機
遇。你得非常努力,要花很多的精力,可能還要走很多的彎路。
如果你是從MFC入手的,或是從VB入手的,則如要做出一個真正的能應用個人領域的通用軟件,就
會走非常多的彎路。直接的捷徑絕對不是走這兩條路。這兩條路看起來很快,而且在很多公司里面確實需
要這樣的東西,比如說我這家公司就是為另一個家公司做系統集成的,那我就需要這樣的東西,我不管你
具體怎么實現,我只需要達到這個目標就行了。
任何軟件的實現都會有n種方法,即使你是用最差的那種方法實現的,也沒有問題,最后它還是能運
行。即使有問題,再改一改就是。但是,做通用軟件就不行了,通用是一對多,你做出來的軟件以后要面
向全國,如果將來自由貿易通到香港也好,通到國外也好,整個產品能銷到全世界的話,這時候,通用軟
件所有做的工作就不是這么簡單了。所以說,正確的入門方法就很關鍵。
如果你僅僅只是想混口飯吃,找個工作,可能教你成為MFC的高手之類的書對你就足夠了。但是,如果你
想做一個很好的軟件,不僅能滿足你謀一碗飯吃,還能使你揚名,最后你的軟件還能成為很多人用,甚至
你還想把它作為一個事業去經營,那么這第一步就非常關鍵。這時就絕對不能找一本MFC或找一本VB的
書學兩下就行,而是要從最低層開始做起,從最基本做起。
?

第1章 程序點滴
1.2 高手是怎樣練成的(1)
1.2.1 高手成長的六個階段
程序員怎樣才能達到編程的最高境界?最高境界絕對不是你去編兩行代碼,或者是幾分鐘能寫幾行代
碼,或者是用什么所謂的可視化工具產生最少的代碼這些工作,這都不是真正的高手境界。即使是這樣的
高手,那也都是無知者的自封。
我認為,一個程序員的成長可分為如下六個階段。
. 第一階段
此階段主要是能熟練地使用某種語言。這就相當于練武中的套路和架式這些表面的東西。
. 第二階段
此階段能精通基于某種平臺的接口(例如我們現在常用的Win 32的API函數)以及所對應語言的自身
的庫函數。到達這個階段后,也就相當于可以進行真實散打對練了,可以真正地在實踐中做些應用。
. 第三階段
此階段能深入地了解某個平臺系統的底層,已經具有了初級的內功的能力,也就是“手中有劍,心中無
劍”。
. 第四階級
此階段能直接在平臺上進行比較深層次的開發。基本上,能達到這個層次就可以說是進入了高層次。
這時進入了高級內功的修煉。比如能進行VxD或操作系統的內核的修改。
這時已經不再有語言的束縛,語言只是一種工具,即使要用自己不會的語言進行開發,也只是簡單地
熟悉一下,就手到擒來,完全不像是第一階段的時候學習語言的那種情況。一般來說,從第三階段過渡到
第四階段是比較困難的。為什么會難呢?這就是因為很多人的思想轉變不過來。
. 第五階級
此階段就已經不再局限于簡單的技術上的問題了,而是能從全局上把握和設計一個比較大的系統體系
結構,從內核到外層界面。可以說是“手中無劍,心中有劍”。到了這個階段以后,能對市面上的任何軟件
進行剖析,并能按自己的要求進行設計,就算是MS Word這樣的大型軟件,只要有充足的時間,也一定會
設計出來。
. 第六階級
此階段也是最高的境界,達到“無招勝有招”。這時候,任何問題就純粹變成了一個思路的問題,不是
用什么代碼就能表示的。也就是“手中無劍,心中也無劍”。
此時,對于練功的人來說,他已不用再去學什么少林拳,只是在旁看一下少林拳的對戰,就能把
此拳拿來就用。這就是真正的大師級的人物。這時,Win 32或Linux在你眼里是沒有什么差別的。
每一個階段再向上發展時都要按一定的方法。第一、第二個階段通過自學就可以完成,只要多用心去
研究,耐心地去學習。
?

要想從第二個階段過渡到第三個階段,就要有一個好的學習環境。例如有一個高手帶領或公司里有一
個好的練手環境。經過二、三年的積累就能達到第三個階段。但是,有些人到達第三個階段后,常常就很
難有境界上的突破了。他們這時會產生一種觀念,認為軟件無非如此,認為自己已無所不能。其實,這時
如果遇到大的或難些的軟件,他們往往還是無從下手。
現在我們國家大部分程序員都是在第二、三級之間。他們大多都是通過自學成才的,不過這樣的程序
員一般在軟件公司也能獨當一面,完成一些軟件的模塊。
但是,也還有一大堆處在第一階段的程序員,他們一般就能玩玩VB,做程序時,去找一堆控件集成一
個軟件。
現在一種流行的說法是,中國軟件人才現在是一個橄欖型的人才結構,有大量的中等水平的程序員,
而初級和高級程序員比較少。而我認為,現在中國絕大多數都是初級的程序員,中級程序員很少,高級的
就更少了。所以,現在的人才結構是“方塔”形,這是一種斷層的不良結構。而真正成熟的軟件人才結構應
該是平滑的三角形結構。這樣,初級、中級、高級程序員才能充分地各施所長。三種人才結構對比如
圖1.1所示。
圖1.1 三種人才結構對比
?

第1章 程序點滴
1.2 高手是怎樣練成的(2)
1.2.2 初級程序員和高級程序員的區別
一般對于一個問題,初級程序員和高級程序員考慮這個問題的方法絕對是不同的。比如,在初級程序
員階段時,他會覺得VB也能做出應用來,且看起來也不錯。
但到了中級程序員時,他可能就不會選擇VB了,可能會用MFC,這時,也能做出效果不錯的程序。
到高級程序員時,他絕對不是首先選擇以上工具,VB也好,VC也好,這些都不是他考慮的問題。這
時考慮的絕對是什么才是具有最快效率、最穩定性能的解決問題的方法。
軟件和別的產品不同。比如,在軟件中要達到某個目標,有n種方法,但是在n種方法
中,只有一種方法或兩種方法是最好的,其他的都很次。所以,要做一個好的系統,是很需要耐心
的。如果沒有耐心,就不會有細活,有細活的東西才是好東西。我覺得做軟件是這樣,做任何事情
也是這樣的,一定要投入。
程序員到達最高境界的時候,想的就是“我就是程序,程序就是我”。這時候我要做一個軟件,不會有
自己主觀的思路,而是以機器的思路來考慮問題,也就是說,就是以程序的思考方式來思考程序,而不是
以我去設計程序的方式去思考程序。這一點如果不到比較高的層次是不能明白的。
你設計程序不就是你思考問題,然后按自己的思路去做程序嗎?
其實不是的。在我設計這個程序的時候,相當于我“鉆”入這個程序里面去了。這時候沒有我自己的任
何思維,我的所有思維都是這個程序,它這步該怎么走,下步該怎么走,它可能會出現什么情況。我動這
個部分的時候,別的部分是否要干擾,也許會動一發而牽全身,它們之間是怎么相互影響的?
也只有到達這個境界,你的程序才能真正地寫好,絕對不是做個什么可視化。可視化本身就是“我去設
計這個程序”,而真正的程序高手是“我就是程序”,這兩種方法絕對是不同的。比如,我要用VB去設計一
個程序,和我本身就是一個程序的思維方式,是不一樣的。別人也許覺得操作系統很深奧,很復雜,其
實,如果你到達高手狀態,你就是操作系統,你就能做任何程序。
對待軟件要有一個全面的分析方法,光說理論是沒有用的。如果你沒有經過第一、第二、第三、第四
這四個階段,則永遠到達不了高境界。因為空中樓閣的理論沒有用,而這些必須是一步一步地去做出來。
一個高級程序員應該具備開放性思維,從里到外的所有的知識都能了解。然后,看到世界最新技術就
能馬上掌握,馬上了解。實際上,技術到達最高的境界后,是沒有分別的。任何東西都是相通的,只要你
到達這個境界以后,什么問題一看就能明白,一看就能抓住最核心的問題,最根本的根本,而不會被其他
的枝葉或表象所迷惑,做到這一步后才算比較成功。
從程序員本身來說,如果它到達這一步以后,他就已經形成了開闊的思維。他有這種開放性思維的
話,他就能做戰略決策,這對他將來做任何事情都有好處。事實上,會做程序后,就會有一種分析問題的
?

方法,學會怎么樣把問題的表象剖開,看到它的本質。這時你碰到任何具體的問題,只要給點時間,都能
輕而易舉地解決。實際上,對開發計算機軟件來說,沒有什么做不了的軟件,所有的軟件都能做,只是看
你有沒有時間,有沒有耐心,有沒有資金做支撐。
這幾年,尤其是這兩三年,估計到2005年前,中國軟件這個行業里面大的軟件公司就能形成。現在就
已經在形成,例如用友,它上市后,地位就更加穩固了。其他大的軟件企業會在這幾年內迅速長大。這時
候,包括流通渠道、經銷商的渠道也會迅速長大。也就是說,到2005年以后,中國軟件這個行業的門檻比
現在還要高很多,與美國不會有太大的差別。此時,中國軟件才真正體現出它的威力來。如果你是這些威
力中的一員,就已經很厲害了。
別人可能知道比爾?蓋茨是個談判的高手,是賣東西的高手,其實,比爾?蓋茨從根本上來
說是個程序高手,這是他根本中的根本。他對所有的技術都非常敏感,一眼就看到本質,而且他本
身也能做程序,時常在看程序。現在他不做董事長,而做首席設計師,這時他就更加接近程序的本
質。因為他本身就有很開闊的思維,又深入到技術的本身,所以他就知道技術的方向。這對于一個
公司,對他這樣的人來說,是非常重要的。
如果他判斷錯誤一步,那公司以后再回頭就很難了。計算機的競爭是非常激烈的,不能走錯半
步。很多公司以前看上去很火,后來就
銷聲匿跡了,就是因為它走錯一步,然后就不行了。為什么它會走錯?因為他不了解技術的本質在
哪里,技術的發展方向在哪里。
比爾?蓋茨因為父母是學法律的,所以他本身就很能“侃”,很有說服力,而他又是做技術的,就
非常清楚技術的方向在哪里,所以他才能把方向把握得很準確,公司越來越大。而別的公司只火一
陣子,他卻火了還會再火。就算微軟再龐大,你如果不把握好軟件技術的最前沿,一樣也會玩完。
就像Intel時刻把握著CPU的最新技術,才能保證自己是行業老大。技術決定它的將來。
所以,程序員要能達到這樣的目標,就要有非常強的耐心和非常好的機遇才有可能。事實上,現在的
機會挺好的,2005年以前機會都非常大,以后機會會比較小。但是,如果有耐心的話,你還是會有機會
的,機會都是出在耐心里。我記得有句話說“雄心的一半是耐心”,我認為雄心的三分之二都是耐心。如果
你越有野心,你就越要有耐心,你的野心才有可能實現。如果你有野心而沒有耐心,那都是胡思亂想,別
人一眼就能看穿。最后在競爭中,對手一眼就看到你的意圖,那你還有什么可競爭的?
1.2.3 程序員是吃青春飯的嗎
很多人都認為程序員是三十歲以前的職業,到了三十歲以后,就不應再做程序員了。現在的很多程序
員也有這種想法,我覺得這種想法很不對。
在20世紀80年代末到90年代初,那時軟件還沒有形成行業,程序員不能以此作為謀生的手段時,你必
須轉行,因為你年輕的時候不用考慮吃飯的問題,天天“玩”都可以,但是以后就不可能了。
據我了解,微軟里面的那些高手,幾乎都是四五十歲的,而且都是做底層的。他們是上世紀70年代就
開始“玩”程序的,所以對于整個計算機,他們是太清楚了。現在有些人主觀臆斷地希望微軟第二天倒閉就
好了,但那可能性太小了。因為那些程序員是從CPU是4004的時候開始,玩到現在奔騰IV,沒有哪一代
東西他們沒有經歷過。
你知道他們現在正在玩什么嗎?現在正在玩64位的CPU。你說你普通的程序員,有這個耐心嗎?沒有
這個耐心,你絕對做不了,你也絕對當不了高手。他為什么能做?因為他不僅是玩過來的,而且他還非常
?

有耐心,每一步技術他都跟得上,所以對他來說,沒有任何的難度和壓力。
?

第1章 程序點滴
1.2 高手是怎樣練成的(3)
因為計算機技術沒有任何時候是突變的。它的今年和去年相差不會很大,但是回過頭來看三年以前的
情況,和現在的距離就很大。所以說,如果你每年都跟著技術進步的話,你的壓力就很小,因為你時刻都
能掌握最新的技術。但是,如果你落下來,別說十年,就是三年,你就趕不上了。
如果你一旦趕不上,就會覺得非常吃力;如果你趕不上,你就會迷失方向;如果你迷失了方向,你就
覺得計算機沒有味道,越做越沒勁。當你還只是有個思路的時候,別人的產品都做出來了,因為你的水平
跟別人相差太遠,人家早就想到的問題,你現在才開始認識。水平越高,他就看得越遠,那么他的思維就
越開闊;水平越低,想的問題就越窄。
64位CPU是這個十年和下個十年最重要的技術之一,誰抓住這個機會,誰就能抓住未來
賺錢的商機。CPU是英特爾設計的,對這一點他肯定清楚。舉例來說,如果從64位的角度來看現在
的32位,就像從現在的角度去看DOS。你說DOS很復雜嗎?當你在DOS年代的時候,你會覺
得DOS很復雜。你說現在的Windows不夠復雜嗎?Windows太復雜了,但是你到了64位的時候再去
看Windows,就如同現在看DOS一樣。
整個64位系統的平臺和思維方式、思路都比現在更開闊,打個比方說,現在的Windows里面能
開n個DOS窗口,每個DOS窗都能運行一個程序。到達64位的時候,操作系統事實上能做到
開n個X86,開n個Windows 98,然后再開n個Windows 95都沒有問題,系統能做到這一步,甚至你
的系統內開n個Windows NT都沒有關系。這就是64位和32位的差別。所以,微軟的那些“老頭”,
四、五十歲的那幾個做核心的人,現在正在玩這些東西。你說微軟的技術它能不先進嗎?是Linux那
幾個玩家能搞定的嗎?
微軟的技術非常雄厚,世界計算機的最新技術絕對集中在這幾個人手里。而且這幾個人的思維
模式非常開闊,誰都沒有意識到的東西他早就開始做了。現在64位的CPU都出來一二年了,你說有
什么人去做這些應用嗎?沒有,有的就是那幾個UNIX廠商做好后給自己用的。
所以,追求技術的最高境界的時候,實際上是沒有年齡限制的。對我來說,現在都三十三了,我從來
沒有想過退出這行,我覺得我就能玩下去,一直玩到退休都沒有問題。我要時刻保持技術的最前端,這樣
的話對我來說是不困難的,沒有任何累的感覺。
很多人說做程序不是人干的事情,是非人的待遇。這樣,他們一旦成立一個公司,做出一點成績,在
輝煌的時候馬上就考慮退出。因為他們太苦了,每天晚上熬夜,每天晚上燒了兩包煙還不夠,屋子里面簡
直就缺氧了,好像還沒有解決問題。
白天睡覺,晚上干活,那當然累死了,這是自己折騰自己。所以,做程序員一定要有一種正常的心
態,就是說,你做程序的時候,不要把自己的生活搞得顛三倒四的。如果非得搞得晚上燒好多煙才行,這
樣你肯定折騰不到三十歲,三十歲以后身體就差了。
?

事實上,我基本上就沒有因為做程序而熬夜的。我只經歷過三次熬夜,一次是在學校的時候,1986年
剛接觸計算機時,一天晚上跟一個同桌在計算機室內玩游戲,研究了半天,搞著搞著就到了天亮,這是第
一次。然后在畢業之前,在286上做一個程序。還有一次就是超級解霸上市前,那時公司已吹得很大了,
那天晚上沒法睡覺。
一般來說,我也是十二點鐘睡覺,第二天七點就起了。所以說,只有具有正常的生活、正常的節奏,
才有正常的心態來做程序員,這樣,你的思路才是正常的,只有正常的東西才能長久。搞疲勞戰或者是黑
白顛倒,時間長久后就玩不轉了,玩著玩著就不想玩了。
只要你不想玩,不了解新技術,你就會落后,一旦落后,你再想追,就很難了。
?

第1章 程序點滴
1.3 正確的入門方法(1)
在這一節中,主要講從我的經驗來看,一般程序員需要注意的地方。教你怎樣去具體學習不是我的責
任,你可以去任何一個書店去找一本書回來自己看就可以了。這里只是對這些書做一些補充以及一些平常
從來沒注意的內容。
入門最基本的方法就是從C語言入手。如果以前學過BASIC語言的話,那么從C語言入手是非常容易
的。我就經歷了一個過程,根本不覺得這中間有太大的難度。其實,C語言本身和BASIC沒有什么兩
樣。BASIC每個所謂的命令在C語言里面都可以做成一個函數來實現,那么你就能用那個命令組合成整個
程序。從這個角度來看,BASIC和C語言沒有本質的差別。C語言就是入門的正確方法,沒有其他。
現在的C語言本身就包含了嵌入匯編,使學習匯編語言的時候更加方便。你可以忽略掉純匯編里面的
很多操作。也許有人覺得這個方法太慢了。但要知道,工欲善其事,必先利其器,要想成功,沒有一個艱
苦的過程是不可能的,所以一開始的時候就要有耐心。如果你準備花5年的時間成為高手,那我敢說,你
根本不用等到5年,你只要有這個耐心就足夠了,你可能2年~3年內就能達到目標。但如果你想在一年時
間內就成為高手,即使5年后,你還是成不了高手。
我們公司1998年招的開發人員都是應屆大學畢業生。很明顯,有人好像什么都會,又
會CorelDraw,又會Photoshop,又會Flash,又會C++,甚至VB也會。可是這樣的人到現在還是
全都會,但是什么事情也做不好,做的東西“臭”死了。但其中有一個人就不同,他以前甚至
連Windows的程序都沒有做過,只會在DOS下做幾個小程序。但當我們把超級解霸的程序給他看,
讓他去研究的時候,他只用一周的時間,就迅速掌握。他那個月進步非常快,幾乎就是一生中進步
最快的階段,這就是一個質的飛躍。
從基本入手以后,當你的積累到達一個階段以后,就會有一個質的飛躍的階段。事實上,我也
有這么一個階段,這個階段也是我離開大學以后,真正去公司做事的時候。當我真正擁有一臺計算
機后,我把所有以前積累的問題在一個月內做了探討以后,感覺自己的水平迅速提高。
入門和積累是很重要的。事實上,到達高手的境界以后,不管什么語言不語言的,其實都根本不用去
學,只要拿過來看兩天,就全部精通。如果你沒有入門,即使去書店找n本書,天天背它,你也不會成為
高手。
所有的語言只是很花哨的表面東西。高手馬上就能透過它的表象而看到它的本質。這樣才是真正的高
手。他不需要再去學什么Java,或者其他什么語言。當他真正要寫個Java程序的時候,只要把Java程序
拿過來看一看,瞄一瞄書,就全都清楚了。如果這時他學VB就更容易了,我想他不用一天的時間,就能
學會。到達高手的境界以后,所有的事物都是觸類旁通的。
當你成為C語言的高手,那么就你很容易進入到操作系統的平臺里面去;當你進入到操作系統的平臺
里去實際做程序時,就會懂得進行調試;當你懂得調試的時候,你就會發現能輕而易舉地了解整個平臺的
?

架構。這時候,計算機基本上一切都在你的掌握之中了,沒有什么東西能逃得出你的手掌心。
上面只是針對程序的角度說明,另外一點也很重要,即好的程序員必須具備開放性思維,也就是思考
問題的方法。程序員,尤其現在很多的程序員,都被誤導從MFC入手,這就很容易形成一種封閉式的思維
模式。這也是微軟希望很多人只能學點表面的東西,不致成為高手,所以他大力推薦MFC之類的工具,但
也真有很多人愿意去上他的當,最后真正迷失方向。說他做不了程序吧,他也能做程序,但是如果那個程
序復雜一點,出現問題時,問題出在哪里就搞不清楚了,反正是不清楚。如果你真正有一種開放性的思
維,在你能夠成為高級程序員的時候,對MFC這些是不屑一顧的,MFC、VB根本不會在考慮的范圍之
內。
事實上很多人,包括外面很多公司里面工資挺高的人,可能一個月能拿五、六萬的這些人,他們的思
維也不一定能達到很高的境界。但是,他確實做了很多的事情,已經有很好的積累了。但要上升到更高的
境界上,就要有正確的思維方法。這就是為什么比爾?蓋茨說,他招人的時候寧愿招一個學物理,而不是
學編程的。學物理的人會有非常非常廣的思維,他考慮的小到粒子,大到宇宙,思維空間非常廣闊,這
樣,他思考問題的時候,就會很有深度。
有人研究物理研究得比較深的時候,他能針對某個問題一直深入進去。很多寫程序的人只會注意到這
行代碼或那行代碼,則比較起來則顯得膚淺。所以,編程的時候也要深入進去,把你的愛好、你的所有思
維都放進去,努力做到物我合一的境界。
?

第1章 程序點滴
1.3 正確的入門方法(2)
1.3.1 規范的格式是入門的基礎
以前所有的C語言的書中,不太重視格式的問題,寫的程序像一堆堆的垃圾一樣。這也導致了現在的
很多程序員的程序中有很多是廢碼、垃圾代碼,這和那些入門的書非常有關系。因為這些書從不強調代碼
規范,而真正的商業程序絕對是規范的。你寫的程序和他寫的程序應該格式大致相同,否則誰也看不懂。
如果寫出來的代碼大家都看不懂,那絕對是垃圾。如果把那些垃圾“翻”半天,勉強才能把里面“金子”找出
來,那這樣的程序不如不要,還不如重新寫過,這樣,思路還會更清楚一點。這是入門首先要注意的事
情,即規范的格式是入門的基礎。
1. 成對編碼
正確的程序設計思路是成對編碼,先寫上面的大括號,然后馬上寫下面的大括號。這樣一個函數體就
已經形成了。它沒有任何問題。然后,比如你要寫個for循環,這時候先申明一個變量I,再寫這個for循
環。寫上面的大括號,馬上寫下面的大括號,然后再在中間插一二行代碼。插這段代碼后,如果你又要用
到新變量,則再在頭上添加新的變量,然后再讓它進行工作。這就是一種成對編碼。
這樣,當你用到一個內存的時候,寫一個分配函數分配一塊內存,馬上就接著寫釋放這塊內存的代
碼。然后你再在中間插上你要用這個內存做什么。這是正確的快速的編程方法。否則,你去查或調試代碼
都無從下手。針對這個程序來說,如果用成對編碼,則它任何時候都是可以調試的,不需要你整個程序都
寫完后才能進行調試。
它是任何時候都可以編譯調試的,甚至你寫了兩個大括號,中間什么也沒有,它是空的時,你都可以
進行調試。你寫了第一個for循環,它也可以進行調試,當你又寫了一個分配內存、釋放內存以后,它還可
以進行調試。它可以編譯運行,里面可以放斷點,這就是成對編碼。
成對編碼就涉及到代碼規范的問題。為什么我說上面一個大括號,下面一個大括號,而不說成是前面
一個大括號,后面一個大括號呢?如果是一般C語言的書,則它絕對說是后面加個大括號,回過頭前面加
個大括號。事實上,這就是垃圾程序的寫法。正確的思路是寫完行給它回車,給它大括號獨立的一行,下
面大括號也是獨立的一行,而且這兩個大括號跟那個for單詞中間錯開一個TAB。
集成環境的TAB首先要設成8,因為TAB的基本定義就是8,而現在的VC把它設成了4,這樣
使得你編出的程序放到一個標準的環境里看的時候就是亂的。
代碼一定不能亂,一定要格式非常清楚,這點使你寫的程序我能讀,我寫的程序你也能讀,不需要再
去習慣彼此的不同寫法。
而且結合成對編碼思維,這時候你去讀一個程序的時候,你會發現,你讀程序的方法變了。以前讀程
序的時候,你可以先去讀它的變量是什么,然后再讀第一行、第二行,讀到最后一個大括號,這是一種讀
程序的方法。現在就不一樣了,現在讀程序的時候就養成了一種習慣,就是分塊閱讀程序,很明顯兩個大
括號之間就是一塊代碼。
?

那么寫出一個程序后,你要讀這個程序是干什么的,只要看這個大括號和那個大括號之間的部分就可
以了,不需要再去讀其他的代碼是干什么的。比如,你從Linux中或網上下載了一個“爛”程序后,該怎么去
閱讀它?最好的方法是先把程序所有的格式都整理好,先別去讀它。把所有的格式按照這種規范化的方
法,把它的括號全部整理好。這時候你再讀那個程序,只要半分鐘就讀懂了,但是你可能要整理一個小
時。但如果不這樣做,你可能讀兩個小時都讀不清楚該程序。
這點絕對不會有人告訴你,現在沒有人去講解這方面的技巧。這也是我寫了那么多的程
序,才總結出來的。一開始的時候,我也像那些教科書所教導那樣寫,后面放個大括號,前面放個
大括號,甚至括號連括號,一連四個括號,每個括號對哪個最后都找不清楚。編譯告訴你好像少了
一個括號,于是找呀,找呀,上面找,下面找,而這個程序又很大,只有一個函數,上面在上屏,
下面在下屏,最后翻來翻去也翻不出。
所以我就想,大括號之間要互相對應,即使不在一個屏幕內,也能很容易地看到它,因為只要
光標落在這個大括號里面,往上去找,即能找到它頭上的那個與此對正的,而且這些代碼是在一起
的。這一層代碼和下一層代碼是互相隔開的,我只要讀這層代碼,下面那一層代碼就不需要了。
比如,它有n個for循環的時候,我只想看某一個for循環,這時我只要對正大括號,它的光標往
上走,一下就能找到了。如果按照教科書那樣寫的話,你要讀呀,讀呀,要把所有的代碼,把所有
的for
循環都讀一遍,才可能找到你要的東西。這就是成對編碼和規范化的方法(詳細敘述請參考代碼規
范一章)。
代碼中如果不包括正確的思路,那該代碼就沒有什么用。如果是一個代碼愛好者去收集代碼,而現在
網絡上代碼成群,Linux本身就帶了一大堆的程序,那些程序對你真的有用嗎?我看不見得。而且那些程
序還在不斷地升級,那程序還會有新版,如果你把它拿來看一下,對你來說其實沒什么價值。
那怎么樣使得它對你有用?就必須用上面所說的方法,經過這么處理以后,你就能真正取到它其中的
設計思路,這樣才能變廢為寶。如果是MFC之類的東西,那你就不用找了,因為即使找,也找不出有價值
的東西,全部是VC自動給你生成的一堆堆的垃圾框架,相對于網上Linux程序來說,它可能更“臭”一些。
?

第1章 程序點滴
1.3 正確的入門方法(3)
在軟件沒有形成行業,程序等同于軟件的時候,那時候程序很容易體現出價值來。只要得到代碼,就
相當于得到這個軟件。但現在就不同了。現在的程序都不是幾行,你寫出的程序,如果又沒有注釋,格式
又很亂,你拿過來給我,我還得花很長的時間才能讀得清楚,那這樣的程序的代碼有價值嗎?
我經常聽到一些程序員在外面兜銷代碼,很多是學校的學生,尤其那些素質比較差的研
究生,和老師做了一個項目后,他拿出來到外面到處去賣,但是他最后可能賣出去嗎?最后可能還
是沒賣出去,因為那個程序很龐大。如果某個公司買了這個程序以后,該公司還得招一個人去讀這
個程序,當這個人讀懂以后,他又離職了,那公司買這個代碼干嘛?
2. 代碼的注釋
代碼本身體現不出價值來,有價值的代碼一定是不僅格式非常規范,而且還要有很詳細的設計思路和
注釋,這個是很重要的。首先要養成這種習慣,教科書里面很少講為什么要做注釋,注釋應該怎么注。有
些人愛在哪兒下注釋就在哪兒下注釋,甚至在語句中間也加,中間也可弄兩個斜杠放兩個花括號寫點注
釋。
注釋格式是非常重要的,但很少有人去注意它。現在的程序如果沒有注釋,則基本上是沒法用的,也
就跟你拿一個可執行程序沒什么兩樣,你拿過來還不能隨便改,你改了后編出來的程序絕對不能用。所
以,程序如果沒有詳細的注釋,別人就算拿到了代碼也沒有用,體現不出它的價值來。
Linux是個操作系統,很厲害呀!其實那些程序你拿回來,耐心地去讀它,會發現,它里面亂得很,那
個內核程序除了作者自己能讀懂外,別人可能要花很長的時間才能讀懂。Apache的作者對自己Apache那
套代碼是很清楚,但換一個做瀏覽器的人去讀,也會很困難。一般人只把代碼復制下來后,打個BUILD命
令看看能不能正確地編譯,最后能正確編譯的程序就是好的,如果不能正確編譯的程序就刪掉吧,再下載
一個,因為他沒有正確的對待代碼的那種思維,而只是認為那代碼本身才有很大的價值,不用關心有沒有
注釋。
如果代碼沒有注釋和規范,是沒有價值的,這也是現在為什么很多的個人跑去賣源程序的時候,很多
的公司都不要。我們不是說沒有技術,任何程序都能做,只是時間的問題,而且像視頻中有的技術,比那
些賣代碼的技術還要深得多。真正要做一個有價值的程序,開發程序的思維就很重要,這種思維的具體體
現就在注釋及規范的代碼本身。
1.3.2 調試的重要性
調試是很重要的一個部分。所有的程序都是調試出來的,不是寫出來的。講怎么去調試,實際上就是
講一種解決問題的思路。所有的程序寫出來后一定是有問題的,既然有問題,就一定會有一個解決問題的
思路。解決問題的方法就是調試的方法。
用VB或者是MFC做出來的程序,先運行一遍看看什么地方有問題,如果發現有問題,重新改一改,
然后又重新運行。這種方法是還沒有入門的調試方法,即是看直接的表象。這種方法既浪費時間,又不能
?

消除隱患。
調試是很重要的內容,如果要進入高深境界,調試是除了了解設計程序、平臺以外,一個非常重要的
難關。如果要成為高級程序員,就必須過這一關。如果不懂調試,則永遠成不了高手。在學習調試的過程
中,對匯編語言、體系結構會有進一步的了解。
你可能覺得我把調試的作用說得言過其實了,舉例子說明一下吧。請把以下的C程序改寫成匯編代
碼:
int i;
extern int In[],Out[];
for(i=0;i<100;i++)
{
Out[i]*=In[i];
}
我發現90%的人寫出來的匯編代碼可能是不正常的或有錯誤的。要么是不了解32位匯編,要么是不循
環,要么只有循環沒有處理等。這是為什么呢?因為就算是一段小小的代碼,如果沒有經過調試,也可能
錯誤百出。
如果你是初級一點的程序員,則如果程序出了問題,也不知道原因所在。怎么回事呀?我就是搞不清
楚。要搞清楚首先要調試,這就涉及到調試的問題。比如說,放到一個文件里面的,它出錯了,我查程序
看了n遍,它就是沒有任何問題,這時候該怎么辦呢?這時的解決方法就是調試,調試能使得一個程序正
常地運轉起來。如果對于程序員來說寫這個程序可能只用了一天的時間,但是調試可能會花他二三天的時
間。一個程序絕對是調試出來的,不是編出來的。如果說哪個系統是編出來的,那它肯定會有很多性能方
面的問題,包括可能有不可預測的各種各樣的問題。
程序出現問題的話,要能考慮到各種各樣可能的情況,絕對沒有任何臆測。比如,有可能完全是編譯
器的錯誤,也有可能因你程序里面增加了什么,而對程序產生干擾,甚至還有一種可能是你的指針基本就
沒有給它賦值,指向了別的地方,把別的東西破壞了。這些情況太多了。還有一種常見的錯誤,即MFC里
面很常見的一種設計思維,就是任何一個東西,只管創建,不管釋放、銷毀。這種思路是現在很多程序員
做的程序沒用幾下就會死機的原因。這絕對是錯誤的設計思路,而MFC讓你這么做,就是讓你永遠成不了
高手,你寫的程序永遠不可能穩定。
MFC里面的所有的結構也好,變量也好,只需要你去分配一個,幾乎就不需要你去釋放
它。這絕對是錯誤的,程序一定要成對編寫。成對編碼是快速編寫程序的一種方法,而教科書里面
講的那些都是從頭到尾去編。先把那個什么變量編寫上,再寫第一行,再寫第二行,再寫第三行,
最后再寫個大括號。這種方法絕對是錯誤的。對于現在的程序來說,它效率很慢,沒法即時調試,
因為只有最后把所有的程序做完以后,才能進行調試,所以在這中間出現錯誤的幾率就積累得非常
大了。
?

第1章 程序點滴
1.4 開放性思維(1)
要具備開放性思維,就必須了解包括從CPU的執行方法,到Windows平臺的運轉,到你的程序的調
試,最后到你要實現的功能這一整套的內容,只有做到這樣,才能真正提高。如果你的知識范圍很窄,什
么也不了解,純粹只了解語言,那你的思維就會很狹隘,就會只想到這個語言有這個函數,那個語言沒有
那個函數,這個C++有這個類,那個語言沒有這個類等。而真正要做一個系統,思維一定要是全面的,游
離于平臺之上的系統和實際的應用軟件是不現實的。
這種所謂理想化,已經有很多人提出是不現實的。所以,任何一個軟件一定都是跟一個平臺相關聯
的,脫離平臺之上的軟件幾乎都是不能用的。這就必須對平臺的本身非常了解。如果你有平臺這些方面的
知識,這樣在思考一個問題的時候,能馬上想到操作系統能提供些什么功能,我再需要做些什么,然后就
能達到這個目標。這就是一種開放的思維。
在開放的思維下,我要做這個程序的時候,就會考慮怎么把它拆成幾個獨立的、分開的模塊,最簡單
的,怎么把這個模塊盡量能單獨調用,而不是我要做個很大的EXE程序。一個很普通的程序員,如果他能
夠考慮到將程序分成好幾個動態庫,那么它的思維就已經有點開放性了,就已經不是MFC那些思維方式
了。思考問題的時候能把它拆開,就是說,任何一個問題,如果你能把它拆開來思考,這就是簡單的開放
性思維。
但光會拆還是不夠的,盡管有很多人連拆都不會。很多教科書中的程序,要解決問題的時候,就一
個main,以后就是一個非常長的函數。這個main函數把所有的事情都解決了。如果連函數都不會分的
話,則就是典型的封閉式思維。
這樣的人不是沒有,我是碰見過的。一些畢業生做的程序就有這種情況。所有的問題都由一個函數來
解決。他就不會把它拆成幾個模塊。我問他,把一件工作拆成幾件模塊不是更清晰嗎?他說,拆出來后的
模塊執行會更慢些。這就是很明顯的封閉式思維和非封閉式思維的區別。
你看MFC的思路,那就是一層套一層的,要把所有的類都實現了,然后繼承。它從CWnd以后,把所
有的東西都包括進去了,組成一個巨型的類。這個巨型的類連界面到實現統統包括在里面。這時你怎么
拆?根本就沒有拆的方法,這就是封閉式思維。
如果一個系統、一個程序不能拆的話,則它基本上是做不好的。因為任何一個程序,如果它本身的復
雜度越大,它可能出錯的幾率就越大。比如最簡單的,哪個函數越大,則該函數的出錯幾率就越大。但如
果把該函數分成很多小的函數,每個小的函數的出錯幾率就會很小,那么組合起來的整個程序的出錯幾
率就很小。這就是為什么要把它拆出來的原因。
你用C++來實現的方法也是一樣的。你要把它拆成許多的接口,如果能做到這樣,你就能把它獨立起
來,甚至你能把它用動態庫的方法去實現。動態庫是現在的程序非常重要的一塊。
1.4.1 動態庫的重要性
有了動態庫,當你要改進某一項功能的時候,你可以不動任何其他的地方,只要改其中你拆出來的這
?

一塊。這一塊是一個動態庫,然后把它改進,只需要把這個動態庫調試好后,整個系統就可以進行升級。
但如果不是這樣,你的整個程序是獨立的文件,然后,另外的功能也是一個獨立的文件,把這個程序
編譯成一個EXE,這就不是動態庫的思想。按道理,我只改這個文件,其他系統也不需要進行調試。理論
上看起來是一樣的,而實際的結果往往就是因為你改動了這個文件,使得原來跑得很好的整個系統,現在
不能跑了或者出現了很奇怪的現象。如何解釋這個問題?事實上,這就涉及到編譯器產生代碼的方法,如
果不了解這點的話,永遠找不出問題來。
不存在沒有BUG的編譯器,包括VC,它也會產生編譯上的問題。就算把這些問題都排除,你的軟件
也可能因為你加了某些功能,而影響了其他的文件,這個幾率甚至非常大。這又得把你以前的測試工作重
頭再來一遍了。
動態庫和EXE有什么不同呢?
動態庫,包括它的代碼和數據都是獨立的,絕對不會跟其他的動態庫串在一起。但是,如果你把所有
功能放到一個EXE的工程里面,它的數據和代碼就都是放到一起的,最后產生可執行程序的時候,就會互
相干擾。而動態庫就不會,這是由操作系統來保證的。從理論上看,動態庫也是一個文件,我做這個工程
的時候也是一個獨立的文件,但它就會出現這樣的問題。
1.4.2 程序設計流程
程序設計流程其實很簡單。第一步就是要拆出模塊,如果你有開放性思維,則任何軟件都非常容易設
計。怎么設計呢?首先,拿到問題的時候,一定要明確目標;然后,對操作系統所提供哪些功能,程序怎
么跟操作系統接口考慮清楚;接著,就是“砍”,把它分開,要把它拆成一個個的獨立的模塊;最后,再進
一步去實現,從小到大地進行設計。
首先“抓”馬上能進行測試的簡單的模塊,就像剛才說的成對編碼那樣,寫任何一個部分都要進行調
試,每個部分最好能獨立進行調試。這樣,每個部分都是分開的時候,它都有一定的功能。當把所要做的
功能都實現后,組合起來,再進行通調就可以了。
決定一個軟件的成敗還是得看該軟件設計的思維是否正確。我們也試過,即使你把那些所謂的軟件寫
得再明白也沒有用,如果實現這個軟件的思路不對,則下面的工作根本就沒有必要。
做軟件時,一定要把注釋寫進去。這樣寫成的軟件如果要改版的話,就很容易,因為你的整個系統是
開放性的,那么你要增強某些功能的時候,都是針對其中的某個小項做改進,只要改它就是了。如果那個
功能是全新的,則它本身就是一個獨立塊,只要去做即可。
現在很多開發工具都提供了自動化設計的功能,在生成新的程序的時候,只要設置好一些條件,就能
自動產生程序的框架,這是一種趨勢嗎?
其實,這種方法不太適用通用軟件的開發,針對某個公司做個ERP系統,可能會管用,但是那些方法
拿不到通用軟件里面來。通用軟件絕對是一行一行地編碼產生出來的,而且每一行編碼的結果要達到一種
可預測性。
什么叫可預測性?就是你寫程序的時候,如果發現某一種癥狀,馬上就能想到該癥狀是由于哪個地方
出了錯,而不是別的地方,也就是從癥狀就能判斷出是哪些代碼產生了問題,這就是可預測性。
如果你用MFC來“玩”的話,即使它出錯了,你也可能不知道錯誤在哪里,它的可預測性就很差。做軟
件時,如果它的可預測性越高,解決問題的方法就越快。如果某用戶說我出現什么狀況了,你馬上就可以
?

斷定錯誤,而不用去搜索源代碼,就能想到程序可能是什么地方有問題,則這就是可預測性。
?

第1章 程序點滴
1.4 開放性思維(2)
1.4.3 保證程序可預測性
設計程序的時候,如何保證可預測性呢?答案就是我們上面所說的,所有的代碼必須是經過測試的,
必須是一步一步調試過的。只有經過你調試過的代碼,你才能知道這個代碼做某種運算的時候,它是怎樣
的執行方法。如果你不知道它的執行方法,你沒進行過調試,則你就沒有任何預測性。要達到可預測性,
代碼在匯編級是怎么執行的,你都得非常清楚。代碼對哪個部分進行了什么操作,你都得知道。如果達不
到這點,你的可預測性就很差。
比如,有些程序,你看它的C或者C++的源代碼時,都看不出任何的問題。你看靜態的程序時看不出
任何問題,動態的程序調試你也看不出任何問題,這時,你必須把它的匯編打開,看一看它具體的操作,
才能知道。所以說,開放性思維非常重要,你必須從最低層到最上層都要清楚。VC本身提供了一個匯編
的調試環境,但是打開匯編后,如果你都看不懂,那你說怎么調呢?調什么?如果一個程序經過調試出
來,則它會出錯的地方你馬上就會知道,只要看一些表現,就知道它有些什么問題。
比如說,我們做“大眼睛”的時候有個這樣的現象。當要顯示一個很大的圖的時候,屏幕上只能顯示其
中的一小塊,這樣就可能需要拖動整個圖像,但是拖的時候,如果在Windows 2000或Windows XP系統下
就會發現,一旦我將圖像拖到右下角時,圖像就一下到左上角去了。該圖像在右下角沒有到底的時候還是
顯示正確的,但一旦到底,就把右下角轉到左上角去了,如圖1.2所示。
這是怎么回事?在Windows 98和Windows 95下,從來沒有這個問題,而且如果圖像不到右下角這一
行,只差一點,它也不會出現這樣的問題。為什么在Windows 98下沒有這樣的問題,在Windows 2000下
會有呢?難道是我的程序有問題?
圖1.2 圖像顯示問題示意圖
這時,我就做了一個區域的比較,即看這個區域和整個這個圖像的區域,是否中間運算有錯誤。但程
序是調用Windows本身的API,我就懷疑是不是這個API出問題了。于是又重新寫了一個區域相交部分,
一步一步去查它,也沒有任何問題,在任何情況下都是好的,但是到達右下角時,圖像就會翻過來。經過
以上兩個步驟后,我就能確定,這是Windows操作系統的問題,Windows 98下沒有這個問題,Windows
2000有,Windows XP也沒有改過來。這是操作系統的原因,絕對不是軟件的問題。
為什么會出現這樣的問題?這是因為微軟設計系統的那些家伙自以為聰明。只要圖像的左上角是0,
不管三七二十一,肯定往下面放,但是它的圖像是正向位圖,所有的位圖設計的時候是倒過來的。而一個
?

正向位圖的高度是負的,否則它顯示的時候是倒過來的。高度是負的時候,這個0發生了變化,從上向下
的,那么他設計操作系統的時候,只看了0而沒去看高度,這時他沒做條件處理。他的想法是為了加速這
個位圖的速度,是做優化的結果,但結果就出錯了,而到現在他也沒有解決這個問題。
所以,可預測性在這里就顯得很重要了。當出現這個問題時,能想到要么就是區域合并有問題,要么
就是直接顯示的這個函數有問題。區域合并的問題可以解決,我寫個函數還不行嗎?我一步一步地去跟
蹤,就能肯定這個API有沒有問題,最后得出結論是有問題,也的確是它有問題。如果你不會調試的話,
這個問題你永遠也查不出來;如果你不了解操作系統,你永遠不會想到操作系統會出問題;如果你不了解
這個平臺,你根本就不知道問題所在。所以,要成為一個高手,視角一定要從里到外,從點到面非常開
闊。如果你局限在一個封閉的思維里,做系統就很難。
?

第2章 認識CPU
2.1 8位微處理器回顧/2.2 16位微處理器(1)
2.1 8位微處理器回顧
在20世紀70年代中期,開始出現了8位芯片。8位芯片與以前的4位芯片相比,無論在指令還是譯碼數
據,以及數據處理上都能按8位的方式進行處理,并且它提供了更多的寄存器和更快的尋址方式。
當時形成了以Intel的8080、摩托羅拉的MC6800(設計此芯片的人還設計出6502,后被蘋果II采用)
和Z80(此芯片在我國當初應用甚廣)三足鼎立的局面。
以Intel 8080為例,它由6000多個晶體管構成,每秒能執行約60萬次操作。尋址空間達到64KB,指令
多達60條以上。
蘋果Ⅱ使用的是6502芯片。6502的指令比較少,6502 CPU有256Byte的固定堆棧區,內有一些基本
函數的功能。因為6502為8位,所以整個內存只有64KB。6502在蘋果II及任天堂游戲機中被廣泛地使用,
可惜6502沒有后續的兼容性的產品。
在沒有IBM PC之前,個人電腦就是蘋果。其中,蘋果II是成功之作,而它沒有使用Intel的8080及后來
的8086。這令Intel這家CPU廠商倍受壓力。為此,Intel加快了技術的研發,從8位機轉向16位機;相
反,6502的成功沒有令它的廠商進一步開發16位的高性能的CPU。由此可見,機會永遠是留給有心人
的。
2.2 16位微處理器
為了保持在微處理器領域的領先地位,Intel在1978年推出了16位的8086芯片。但當時大部分計算機
外部設備都是為8位微處理器而設計的,所以8086并沒有引起大的反響。為此,Intel于1979年推出了
準16位芯片8088,即它的內部總線為16位,而外部總線為8位。
當IBM進入PC市場時,8086/8088成為首選。盡管后來IBM要自己開發新CPU,并且想
踢開Intel,但Intel 80286卻助Compaq抓住了機會。Compaq迅速推出兼容機并大舉成功
(Compaq可能是Compatibility Quickly的縮寫),IBM自己的CPU也就胎死腹中。
因為當時人們還沒有對計算機產生“代”的概念。當時蘋果機選用6502時,開發6502的那
家CPU公司認為從此可以穩坐泰山了,就沒有投入精力去開發新的或與這一代兼容的16位的下一
代CPU。這時,Intel看到了機會,它迅速地研制出比蘋果機要好得多的16位CPU 8086。這時,蘋
果機發現壓力很大,所以也做了一個16位的也能兼容6502的CPU。但是,這個CPU比8086差些,
所以蘋果公司以后也就一直沒有用生產6502 CPU的公司的CPU了,這個公司就失去了成為生
產CPU的核心公司的一個機會。后來的蘋果選用了68000。
2.2.1 組成結構
8086 CPU內部結構如圖2.1所示。
?

圖2.1 計算機執行單元的主要結構
計算機主要是由總線、I/O、內存、寄存器、運算器這幾個主要部件組成的。
8086/8088與6502之間最大的不同在于指令的體系結構。當我在使用6502的蘋果II時,面臨的最大難
題是64KB的內存限制。同樣的問題從8086(16位)到80386(32位)也出現了,在32位到64位時還將出
現。
8086最頭痛的問題在于段式結構,1MB的內存被它的段偏移所限制。至今我也不明白Intel當初為何要
設計成這么復雜的內存機制,也許是為了與8080兼容的需要。這套笨拙的體系一直延續到IA64為止。
8086的內存機制使得段寄存器IP只要用16位就可以進行工作,否則,IP寄存器就要用20位來工作。
從軟件的角度來看,執行指令如一個個小的函數一般,所以CPU中的指令可以通過軟件的方法來模
擬。也就是有這種思想,計算機界曾經出現過RISC(精簡指令體系)和CISC(復雜指令體系)的爭
論。RISC就是在設計CPU時,只把最常用的指令用硬件來實現,其他的指令都通過微代碼用軟件的方法
模擬實現。CISC是一種指令對應一組執行單元的體系結構。不過,隨著CISC工作頻率的提高和技術的發
展,RISC現在已經黯然失色了。
8086在指令執行的時候引入了流水線的概念。例如,一個運算過程要分為6步來完成,當運算完成第
一步后,CPU就會自動地進入第二步繼續工作,當第三步完成后再運行第四步,這樣一直下去,直到整個
過程結束,這個計算過程就宣告完成。
但當CPU開始運行第一條指令的第一步時,第二條指令就可以進來了,這樣就可以連續不斷地運行。
如果把每一步想像成CPU中的一個周期,那么相當于一個周期就運算完一條指令。如果增加流水線的數
目,就可以相應地增加每個周期所完成的指令運算。
2.2.2 8086寄存器組成
8086/8088包括4個16位的數據寄存器,兩個16位指針寄存器,兩個16位變址寄存器,分成四組,它
們的名稱和分組情況如圖2.2所示。
通用寄存器中,這些寄存器除完成規定的專門用途外,均可用于傳送和暫存數據,可以保存算術邏輯
運算的操作和運算結果。
?

第2章 認識CPU
2.2 16位微處理器(2)
段寄存器能在8086中實現1MB物理空間尋址,并可與8080 CPU進行兼容。段寄存器都是16位的,分
別稱為代碼段(Code Segment)寄存器CS、數據段(Data Segment)寄存器DS、堆棧段(Stack
Segment)寄存器SS和附加段寄存器。
圖2.2 8086寄存器的組成
標志寄存器在8086中有一個16位用于反映處理器的狀態和運算結果的某些特征。其中,包括9個標志
位,如圖2.3所示。
這些標志位分為兩類,其一是運算結果標志,主要用于反映處理器的狀態和運算結果特征,有進位標
志CF(Carry Flags)、零標志ZF(Zero Flag)、符號標志SF(Sign Flag)、溢出標志OF(Over
Flag)、奇偶標志PF(Parity Flag)、輔助進位標志AF(Auxiliary Carry Flag)。
其二是狀態控制標志。它控制著處理器的操作。要通過專門的指令才能使狀態控制標志發生變化。其
中有方向標志DF(Direction Flag)、中斷允許標志IF(Interrupt Flag)、追蹤標志TF(Trap Flag)。
2.2.3 內存的尋址
8086 CPU有20根地址線,可直接尋址的物理地址空間為1MB。系統
內存由以字節為單位內存的存儲單元組成,存儲單元的物理地址長20位,范圍是00000H至FFFFFH。盡
管8086/8088內部的ALU每次最多進行16位運算,但存放存儲單元地址偏移的指針寄存器都是16位的,所
?

以8080/ 8086通過內存分段和使用段寄存器的方法來有效地實現尋址1MB的空間。
邏輯段要求滿足第一邏輯段的開始地址必須是16的整數倍,第二邏輯段最長不超過64KB的空間。段
與段可以相互重疊和聯接。
存儲單元的邏輯地址由段值和偏移兩部分組成,用如下的形式表示:
段值:偏移
所以根據邏輯地址可以方便地得到存儲單元的物理地址,計算公式如下:
物理地址=段值×16+偏移
段值通過邏輯段的段寄存器的值來取得,偏移可由指令指針的IP、堆棧指針SP和其他可作為內存指針
使用的寄存器(SI、DI、BX和BP)給出,偏移還可以直接用16位數給出。指令中不使用物理地址,而使
用邏輯地址,由總線接口單元BIU按需要根據段值和偏移自動形成20位物理地址。物理地址的形成如
圖2.4所示。
圖2.4 物理地址的形成
2.2.4 中斷處理
中斷使CPU暫停正在運行的事件而轉去處理另一事件。其實,中斷還可以認為是一種函數的調用,不
過,這個函數是隨時都可能調用的,這樣,中斷就很好理解了。我們把引起這種操作的事件就叫中斷源。
它們可以是外設的輸入輸出請求,也可是計算機的一些異常事件或者其他的內部原因。
在8086/8088的計算機中,支持256種類型的中斷,其中斷編號依次為0~0FFH。
每種中斷都有一個中斷處理程序與之相對應。這些處理程序的段值和偏移量都被安排在內存的最頂
端。因為它們占用1KB字節空間(256×4),所以當發生中斷時,CPU根據中斷向量表就可以很快地查找
到對應的處理程序來處理中斷事件。中斷向量表如圖2.5所示。
圖2.5 中斷向量表
我們從圖中可以看到,所謂中斷號其實就是中斷處理的入口地址。
?

在IBM PC系列兼容計算機中,中斷分為兩種,一種是可屏蔽中斷,另一種是不可屏蔽中斷。DOS的
部分中斷分配情況如表2.1所示。
表2.1 DOS的部分中斷分配表
向量號 功能 向量號 功能
0H 除法出錯 10H 視頻顯示
01H 單步調試 11H 設備配置
02H 非屏蔽中斷 12H 存儲容量
03H 斷點 13H 硬盤I/O
04H 溢出 14H 串行I/O
05H 打印屏幕 15H 擴充BIOS
06H 保留 16H 鍵盤輸入
07H 保留 17H 打印輸出
08H 定時器 18H ROM BASIC
09H 鍵盤 19H 系統自舉
0AH 保留(從中斷控制器) 1AH 時鐘管理
0BH 串行通信端口2 1BH Ctrl+Break鍵處理
0CH 串行通信端口1 1CH 定時處理
0DH 硬盤(并行口) 1DH—1FH 參數指針
0EH 軟盤 20H~2FH DOS使用
0FH 打印機 30H~3FH 為DOS保留
?

第2章 認識CPU
2.3 32位微處理器(1)
按Intel的定義,0~32個中斷是CPU出錯用的,稱為異常。32~255是給系統自己定義使用的。
在DOS中,系統使用被分成了兩個部分,一個部分是硬件的IRQ,IRQ就是級連的中斷控制器。其他的則
被分配給軟件使用。現在64位的CPU中,中斷擴充成16位,則理論上可有64KB個中斷。
80286芯片能在實模式和保護模式兩種方式下工作。在實模式下,80286與8086芯片一樣,與操作系
統DOS和絕大部分硬件系統兼容;在保護模式下,每個同時運行的程序都在分開的空間內獨自運
行。286的保護模式還是有很多不兼容缺陷,到了386才算有真正的改革,操作系統才真正進一步發揮作
用,從16位真正跨入32位程序。
2.3 32位微處理器
1985年,真正的32位微處理器80386DX誕生,為32位軟件的開發提供了廣闊的舞
臺。1989年,Intel推出80486芯片,把387的浮點運算器合于486之中,并且采用流水線技術,令CPU每
個周期可以執行一條指令,速度上突破100 MHz,超過了RISC的CPU。1992年,Intel發布奔騰芯片,采
用多流水線技術及并行執行的能力,從此,CPU可以每個周期執行多個指令。1995年的奔騰Pro能力上再
進了一步,產生動態執行技術,使CPU可以亂序執行。我們知道,從80386開始到現在的P4的CPU,它
們的體系結構一直都是相同的,增加的只是內部的實現方式,所以,這些體系結構對大多數程序員來說就
是透明的。
2.3.1 寄存器組成
80386寄存器的寬度大多是32位,可分為如下幾組:通用寄存器、段寄存器、指令指針及標志寄存
器、系統地址寄存器、調試寄存器、控制寄存器和測試寄存器。應用程序主要使用前面三組寄存器,只有
系統才會使用其他寄存器。這些寄存器是8080、8086、80286寄存器的超集,所以,80386包含了先前處
理器的全部16位寄存器。80386的部分寄存器如圖2.6所示。
圖2.6 80386的部分寄存器
1. 通用寄存器
80386有8個通用寄存器,這8個寄存器分別定名
為EAX、EBX、ECX、EDX、ESP、EBP、ESI和EDI。它們都由原先的16位寄存器擴展而成。這些通用
寄存器的低16位還是可以作為16位寄存器存取,并不受影響。以前的AX、BX、CX、DX這4個寄存器還
可以單獨使用這16位中的高8位和低8位,即分別是AH、AL、BH、BL、CH、CL、DH和DL。
在80386中,8個32位通用寄存器都可以作為指針寄存器使用,所以32位通用寄存器更加通用。
2. 段寄存器
?

80386中有6個16位的段寄存器,分別命名為CS、SS、DS、ES、FS和GS。其
中,FS和GS是80386新增加的寄存器。
在實模式下,內存的邏輯地址仍是“段值:偏移”形式,而在保護模式下,情況就復雜很多了。它總體
上是通過可見部分寄存器指向不可見的內存部分。有關內容將在2.3.2節中介紹。
所有這些寄存器的可見的部分和不可見的部分在IA64中可以直接處理IA 32位的一切,就像80386中
的VM86一樣,即如在Windows上執行DOS窗一樣。
3. 指令指針和標志寄存器
80386的指令指針寄存器擴展到了32位,記為EIP。EIP的低16位是16位的指令指針IP,與以前
的X86系統相同。
由于在實模式下,段的最大范圍是64KB,所以EIP的高16位必須全是0,仍相當于16位的IP作用。
80386中,標志寄存器也擴展到了32位,記為EFLAG,如圖2.7所示。
圖2.7 80386的標志寄存器
其中,增加了IO特權標志IOPL(I/O Privilege Level)、嵌套任務標志NT(Nest Task)、重啟動標
志RF(Reset Flag)、虛擬8086方式標志VM(Virtual 8086 Mode)。
AMD采用了X86架構并將之擴展至64位,開創了X86-64架構。
(1)處理器在32位的X86位純模式下工作,可以運行現在的32位操作系統和應用軟件。
(2)處理器在“長模式”下工作,運行64位的操作系統,既能執行32位應用程序,又能執行64位
應用程序。
(3)只有在“64位模式”下,才能進行64位尋址和訪問64位 寄存器。
(4)擴展是簡單并且兼容的,所以處理器可以以最高的速度和性能支持X86和X86-64。
所有的用戶都能獲得32位的性能和32位的兼容性。在需要時,客戶可以在不放棄32位兼容性的
情況下遷移至64位的尋址和數據類型,沿用主流PC架構的發展而不是重新創作。AMD-64寄存器如
上圖所示。
?

第2章 認識CPU
2.3 32位微處理器(2)
2.3.2 保護模式
80386提供了兩種工作模式。其一為實模式,在此模式下,80386可以和8086、8088完全地兼容。其
二為保護模式,它是80386提供的一種全新的強的工作模式。在保護模式下,不僅可尋址4GB的內存空
間,擴充了內存的分段管理機制,并可對內存進行分頁管理,而且還可實現虛擬內存,支持多任務。
保護模式最重要的是完善了多任務保護機制。其實在80286開始,就具備了保護工作方式,但當時還
不是很完善,80386才得到真正的完善。有兩種保護模式任務方式。
(1)不同任務之間的保護:通過把每個不同的任務放在不同的虛擬地址空間中,來實現不同任務間的
隔離(即A程序不能訪問和修改B程序的代碼和數據),以達到程序間的隔離。
(2)同一任務的保護:在每一任務之內定義了4種保護級別。分別為0、1、2、3,按環的方式來表
示,如圖2.8所示。
圖2.8 同一任務保護模式
其中,0級代表最高的權限級,3級代表最低的權限級。按環的方式來表示,數字小的在“內環”,數字
大的在外環。其中,環0、1、2為系統級,環3為用戶級。原來的系統都是基于用戶和系統來設計的,所以
一般的系統只使用環0和環3這兩個級。
2.3.3 80386的尋址方式
80386繼續采用分段的方法管理主內存。內存的邏輯地址由段基地址(段的起始地址)和段內偏移兩
部分表示,存儲單元的地址由段基地址加上段偏移得到。段寄存器指示段基地址,各種尋址方式決定段內
偏移。
實模式下,段基地址仍然是16的倍數,段的最大長度仍然是64KB。段寄存器內所含的仍然是段基地
址對應的段值,存儲單元的物理地址仍然是段寄存器內的段值乘上16再加上偏移。所以,盡管386有32根
地址線,可直接尋址物理地址空間達到4GB字節,但在實模式下,仍然與8086/8088相似。
在保護模式下,段基地址可長32位,并且無需是16的倍數,可以是內存內任意一個開始點,段的最大
長度可達4GB。它的尋址就與8086/8088有很大的變化,如圖2.9所示。
?

圖2.9 保護模式下的尋址
1. 描述符
保護模式下的虛擬器由大小可變的存儲塊組成,這樣的存儲塊還是稱“段”。每個段由如下的三個參數
進行定義:基地址、段界限、段屬性。在保護下可以建立多個段。
而描述段的屬性參數就稱為“描述符”。它的格式如圖2.10所示。
圖2.10 描述符的格式
這些描述符會放置在內存的某一塊空間內。
2. 選擇子
在8086/8088和80386實模式下,段寄存器用來表示段值。而在80386的保護模式下,段寄存器就成為
選擇子。可以將選擇子看做一個句柄。
選擇子的作用就是指向對應的描述符。例如,代碼選擇子的值是02H(也就是CS=02H),那么它指
向的就是02H個描述符。
3. 簡單的尋址過程
80386的尋址過程如圖2.11所示。
圖2.11 80386的尋址過程
當在機器運行如下代碼時:
MOV AX,DS:[DX];
假設此時DS=04H,DX=2344H,那么CPU怎樣才能在內存中找DS:[DS]的值呢?其步驟如
下:
(1)從DS選擇子中選取04H。
(2)從對應的描述符空間中查找到第04H個描述符。
(3)取出描述符中的三個參數,分別是段基地址、段界限和段屬性。假設段的基地址等
?

于00012345H,段界限等于5678H。
(4)這時,段基地址就是段的開始位置,通過EIP的32位偏移,
就可得到物理地址,由:
物理地址=段基地址+偏移
可得物理地址就是179BDH(00012345H+5678H)。
(5)此時就可以從179BDH中取出數據放入AX寄存器中。
這個尋址過程是經過簡化后的模型,真實的尋址要比這復雜得多,有興趣的讀者可參考其他
的書籍。
?

第2章 認識CPU
2.3 32位微處理器(3)
4. 中斷處理
80386不但保存了8086/8088的所有中斷,還增強了很多功能。我們把外部中斷稱為“中斷”,把內部中
斷稱為“異常”。
在實模式下,中斷的處理和8086/8088完全一樣。但是,在保護模式下,80386不再使用簡單的中斷
向量表來處理中斷程序,而是引入了“中斷描述符”。中斷描述符的結構如圖2.12所示。
GATE STRUC ;門的數據結構
OFFSETL DW 0 ;32位偏移的低a16位
SELECTOR DW 0 ;選擇子
DCOUNT DW 0 ;雙字計數字段
GTYPE DB 0 ;類型
OFFSETH DW 0 ;32位偏移的高16位
GETE ENDS
中斷的簡單處理過程如下:
(1)當中斷產生時,通過中斷號找到對應的中斷描述表。
(2)從中斷描述表中取出對應的選擇子和偏移。
(3)通過選擇子從描述符中取出段的基值加上偏移,形成中斷處理程序的位置。
(4)轉入中斷處理程序。
(5)中斷處理程序分為以下兩種。
. 當程序出現中斷時,讓中斷自己進行處理,程序跳到中斷點后繼續運行。
. 中斷程序可能先在環1進行一些處理,然后再跳環2進行一些處理,還可能跳用戶層(環3)進
行處理。但是Windows中是沒有環1、環2的過程的,所以這種情況一般發生在異常中。這時就會
?

變成先在系統級進行處理,當處理完后,再返回到用戶級繼續處理,當用戶級完成后,再返回到
中斷點。
中斷處理過程簡圖如圖2.13所示。
圖2.13 中斷處理過程簡圖
?

第2章 認識CPU
2.4 【實例】:在DOS實模式下讀取4GB內存(1)
為了幫助讀者實際了解以上所介紹的一些概念,下面我們來分析一段在DOS實模式下直接讀取4GB內
存的代碼。通過該程序來分析CPU 的工作原理,揭開保護模式的神秘面紗,讀者將會發現,保護模式其
實與實模式一樣簡單和易于控制。在此基礎上用四五十行C 語言程序做到進出保護模式和在實模式之下直
接訪問整個4GB內存空間。
這個訪問4GB內存的程序是在實模式下使用的,它只是讓CPU中的不可見部分有4GB大小訪問權限。
在進入保護模式(CR0成為1)后,如果段寄存器不發生變化的話,則一切和實模式一樣。所
以CPU的保護位為1時,后面的代碼依然可以執行,而不是死機狀態。
同樣的方法就不能用于分頁,如果分頁后的內存與不分頁前時對于執行的地方發生不同,如分頁的指
令在內存0X12345處,分頁后這個地方可能變成不存在,則計算機就只有出錯重啟。對于這個問題,本人
做過多次實驗,屢試不爽。
2.4.1 程序的意義
此程序具有如下功能:
. 不需要在保護模式狀態下就可以直接把386的4GB內存讀出來;
. 利用此程序可直接在DOS中做物理設備的檢測;
. 理解GDT表的對應關系后,所謂386 32位模式也就很容易理解;
. 在DOS下,可根據此類方法將中斷向量表移到任意位置,達到反跟蹤或其他等目的。
2.4.2 程序代碼
程序代碼如下所示。
#include <dos.h>

// 4G Memory Access
// This Program Can Access 4G Bytes in DOS Real
//Mode,Needn't in Protection Mode It Works.
// The Program Enter 32 Bit Flat Mode a moment and
//Only Load FS a 32 Bit Flat Mode Selector,Then Return
//Real Mode.
// Used The FS Can Access All 4G Memory till It be
//reloaded.
//
///
unsigned long GDT_Table[]=
?

{ 0, 0, //NULL - 00H
0x0000FFFF, 0x00CF9A00, //Code32 - 08H Base=0
//Limit=4G-1 Size=4G
0x0000FFFF, 0x00CF9200 //Data32 - 10H Base=0
//Limit=4G-1 Size=4G
};
//Save The IDTR before Enter Protect Mode.
unsigned char OldIDT[6]={0};
//NULL The IDTR,IDTR's Limit=0 will disable all
//Interrupts,include NMI.
unsigned char pdescr_tmp[6]={0};
#define KeyWait() {while(inportb(0x64)&2);}
void A20Enable(void)
{
KeyWait();
outportb(0x64,0xD1);
KeyWait();
outportb(0x60,0xDF); //Enable A20 with 8042.
KeyWait();
outportb(0x64,0xFF);
KeyWait();
}
void LoadFSLimit4G(void)
{
A20Enable(); //Enable A20
//**************************************
//* Disable ints & Null IDT *
//**************************************
asm {
CLI //Disable inerrupts
SIDT OldIDT //Save OLD IDTR
LIDT pdescr_tmp //Set up empty IDT.Disable any
//interrupts,
?

?} //Include NMI.
//***************************************
//* Load GDTR *
//***************************************
asm {
//The right Code is Real,But BC++'s Linker NOT Work
//with 32-bits Code.
db 0x66 //32 bit Operation Prefix in 16 Bit DOS.
MOV CX,DS //MOV ECX,DS
db 0x66 //Get Data segment physical Address
SHL CX,4 //SHL ECX,4
MOV word ptr pdescr_tmp[0],(3*8-1)
//MOV word ptr pdescr_tmp[0],(3*8-1)
db 0x66
XOR AX,AX //XOR EAX,EAX
MOV AX,offset GDT_Table
//MOV AX,offset GDT_Table
db 0x66
ADD AX,CX //ADD EAX,ECX
MOV word ptr pdescr_tmp[2],AX
//GDTR Base high16 bits
db 0x66
SHR AX,16 //SHR EAX,16
MOV word ptr pdescr_tmp[4],AX
//GDTR Base high16 bits
LGDT pdescr_tmp //Load GDTR
}
//**************************************
//* Enter 32 bit Flat Protected Mode *
//**************************************
// Set CR0 Bit-0 to 1 Enter 32 Bit Protection
//Mode,And NOT Clear machine perform cache,It Meaning
//the after Code HAD Ready To RUN in 32 Bit Flat Mode,
//Then Load Flat Selector to FS and Description into it's
//Shadow register,After that,ShutDown Protection Mode
//And ReEnter Real Mode immediately.
// The FS holds Base=0 Size=4G Description and
//it can Work in Real Mode as same as Pretect Mode,
?

?//untill FS be reloaded.
// In that time All the other Segment Registers are
//Not Changed,except FS.(They are ERROR Value holded in CPU).
asm {
MOV DX,0x10 //The Data32 Selector
db 0x66,0x0F,0x20,0xC0 //MOV EAX,CR0
db 0x66
MOV BX,AX //MOV EBX,EAX
OR AX,1
db 0x66,0x0F,0x22,0xC0 //MOV CR0,EAX
//Set Protection enable bit
JMP Flush
} //Clear machine perform cache.
Flush: //Now In Flat Mode,But The
//CS is Real Mode Value.
asm { //And it's attrib is 16-Bit Code
//Segment.
db 0x66
MOV AX,BX //MOV EAX,EBX
db 0x8E,0xE2 //MOV FS,DX //Load FS now
db 0x66,0x0F,0x22,0xC0
//MOV CR0,EAX
//Return Real Mode.Now FS's Base=0 Size=4G
LIDT OldIDT
//LIDT OldIDT Restore IDTR
STI //STI Enable INTR
}
}
//With FS can Access All 4G Memory Now.But if FS be reloaded
//in Real Mode It's Limit will Be Set to FFFFh(Size=64K),
//then Can not used it
// to Access 4G bytes Memory Again,Because FS is Segment:Offset
//Memory type after that.
//If Use it to Access large than 64K will generate Execption 0D.
//unsigned char ReadByte(unsigned long Address)
{
asm db 0x66
asm mov di,word ptr Address //MOV EDI,Address
asm db 0x67 //32 bit Address Prefix
?

?asm db 0x64 //FS:
asm mov al,byte ptr [BX] //=MOV AL,FS:[EDI]
return _AL;
}
unsigned char WriteByte(unsigned long Address)
{
asm db 0x66
asm mov di,word ptr Address //MOV EDI,Address
asm db 0x67 //32 bit Address Prefix
asm db 0x64 //FS:
asm mov byte ptr [BX],al //=MOV FS:[EDI],AL
return _AL;
}
/ Don't Touch Above Code /
#include <stdio.h>
/
//打印出Address指向的內存中的數據
///
void Dump4G(unsigned long Address)
{
int i;
int j;
for(i=0;i<20;i++)
{
printf("%08lX: ",(Address+i*16));
for(j=0;j<16;j++)
printf("%02X ",ReadByte(Address+i*16+j));
printf(" ");
for(j=0;j<16;j++)
{
if(ReadByte(Address+i*16+j)<0x20) printf(".");
else printf("%c",ReadByte(Address+i*16+j));
}
printf("/n");
}
}
main()
{
char KeyBuffer[256];
?

?unsigned long Address=0;
unsigned long tmp;
LoadFSLimit4G();
printf("====Designed By Southern.1995.7.17====/n");
printf("Now you can Access The Machine All 4G Memory./n");
printf("Input the Start Memory Physical to DUMP./n");
printf("Press D to Cuntinue DUMP,0 to End & Quit./n");
do {
printf("-");
gets(KeyBuffer);
sscanf(KeyBuffer,"%lX",&tmp);
if(KeyBuffer[0]=='q') break;
if(KeyBuffer[0]=='d') Address+=(20*16);
else Address=tmp;
Dump4G(Address);
}while(Address!=0);
return 0;
}
程序運行后,等用戶從鍵盤輸入一個字符。當輸入“Q”字符時,整個程序將退出,當輸入“D”時,將在
屏幕上顯示一屏內存的數據,最左邊為絕對地址,其后一列顯示的是以十六進制位表示的內存的數據,后
一列是數據所對應的ASCII碼。
?

第2章 認識CPU
2.4 【實例】:在DOS實模式下讀取4GB內存(2)
2.4.3 程序原理
我們知道,CPU上電后,從ROM 中的BIOS開始運行,而Intel 文檔卻說80x86 CPU上電總是從最高內
存下16字節開始執行,那么,BIOS是處在內存的最頂端64KB(FFFF0000H),還是1MB之下
的64KB(F0000H)處呢?事實上,BIOS在這兩個地方都同時出現(可用后面存取4GB 內存的程序驗
證)。
為了弄清楚以上問題,首先要了解CPU 是如何處理物理地址的。真的是在實模式下用段寄存器左
移4位與偏移量相加,還是在保護模式下用段描述符中的基地址加偏移量,難道兩者是毫無關聯的嗎?
答案是兩者其實是一樣的。當Intel把80286推出時,其地址空間變成了24位,則從8086的20位
到24位,十分自然地要加大段寄存器才行。實際上,段寄存器和指針都被加大了,只是由于保護的原因,
加大的部分沒有被程序看見,到了80386之后,地址又從24位加大到32位(80386 SX是24位)。
在8086中,CPU只有“看得見部分”,但在80286之后,在“看不見部分”中已經包含了地址值,“看得見
部分”就退化為只是一個標號,再也不用參與地址形成運算了。地址的形成總是從“不可看見部分”取出基址
值與偏移相加形成地址。也就是說,在實模式下,當一個段寄存器被裝入一個值時,“看不見部分”的界限
被設成FFFFH,基址部分將裝入值左移4位,屬性部分設成16位0特權級。這個過程與保護模式時裝入一
個段寄存器是同理的,只是保護模式的“不可見部分”是從描述表中取值,而實模式是一套固定的過程。
對于CPU在形成地址時,是沒有實模式與保護模式之分的,它只管用基址(“不可見部分”)去加上偏
移量。實模式與保護模式的差別實際上只是保護處理部件是否工作得更精確而已,比如不允許代碼段的寫
入。實模式下的段寄存裝入有固定的形成辦法,從而也就不需要保護模式的“描述符”了,因此,保持了
與8086/8088的兼容性。而“描述符”也只是為了裝入段寄存器的“不可見部分”而設的。
從上面的“整個段寄存器”可見,CPU的地址形成與“看得見部分”的當前值毫無關系。這也解釋了為什
么在剛進入保護模式時,后面的代碼依然被正確地運行,而這時代碼段寄存器CS的值卻還是進入保護模
式前的實模式值,或者從保護模式回到實模式時,代碼段CS被改變之前程序是正常地工作,而不會“突
變”到CS左移4位的地址上去。比如在保護模式時,CS是08H的選擇子,到了實模式時,CS還是08H,但
地址不會突然變成80H加上偏移量。因為地址的形成不理會段寄存器“看得見部分”的當前值,這一個值只
是在被裝入時對CPU有用。
地址的形成與CPU的工作模式無關,也就是說,實模式與0特權級保護模式不分頁時是一模一樣的。
明白了這一機理后,在實模式下一樣可以處理通常被認為只有在保護模式才能做的事,比如訪問整個機器
的內存。不必理會保護模式下的眾多術語或許會更易于理解,如選擇子就是“看得見部分”,描述符是為了
裝入“不可見部分”而設的。
有一些書籍也介紹有同樣功能的匯編程序,但它們都錯誤地認為是利用80386芯片的設計疏漏。實際
上,Intel本身就在使用這種辦法,使得CPU上電時能從FFFFFFF0H處開始第一條指令,這種技術
286
?

在 之后的每一臺機器每一次冷啟動時都使用,只是我們不知道罷了。
2.4.4 程序中的一些解釋
下面對程序做幾點說明。
(1)IP=0000FFF0H
通過這樣設置,CS∶EIP就形成了FFFFFFF0H的物理地址,當CPU進行一次遠跳轉重新裝入CS時,
基址就變了。
(2)為了訪問4GB內存空間,必須有一個段寄存器的“不可見部分”的界限為4G-1,基址為0,這樣就
包含了4GB內存,不必理會“可見部分”的值。顯然要讓段寄存器在實模式下直接裝入這些值是不可能的。
惟一的辦法是讓CPU進入一會兒保護模式,在裝入了段寄存器之后馬上回到實模式。
進入保護模式十分簡單,只要建好GDT,把CR0寄存器的位0置上1,CPU就在保護模式了。從前面分
析CPU地址形成機理可知,這時不必理會寄存器的“看得見部分”值是否合法,各種段寄存器是一樣可用
的,就像沒進保護模式一樣。在把一個包含有4GB地址空間的值裝入某個段寄存器之后,就可返回實模
式。
(3)預先可建好GDT如下:
unsigned long GDT-Table[]=
{ 0,0, //空描述符,必須為零
0x0000FFFF,0xCF9A00, //32位平面式代碼段
0x0000FFFF,0xCF9200 //32位平面式數據段
}
這只是為了訪問數據只要2個GDT就足夠了,因為并沒有重裝代碼段,所以這里只是為了完整性而給
出3個GDT。
(4)通常,在進入保護模式時要關閉所有的中斷,把IDTR的界限設置為0,CPU自動關閉所有中斷,
包括NMI,返回實模式后恢復IDTR并開中斷。
(5)A20地址線的控制對于正確訪問整個內存也很重要,在進入保護模式前,要讓8042打開A20地址
線,否則會出現4GB內存中的混亂。
在這個例子里,FS段寄存器設成可訪問4GB內存的基址和界限,由于在DOS中很少有程序會用
到GS、FS這兩個386增加的段寄存器,所以當要讀寫4GB范圍中的任一個地方時,都可通過FS段來達
到,直到FS在實模式下被重裝入沖掉為止。
這個例子在386SX、386DX、486上都運行通過。例子里加有十分詳細的注釋,由于這一程序是用BC
3.1編譯連接的,而其連接器不能為DOS程序處理32位寄存器,所以直接在代碼中加入操作碼前綴0x66和
地址前綴0x67,以便讓DOS實模式下的16位程序可用32位寄存器和地址。程序的右邊以注釋形式給出等
效的32位指令。
要注意,16位的指令中,mov al, byte ptr [BX]的指令碼正好是32位的指令mov al, byte ptr[EDI]。
讀者可用這個程序驗證BIOS是否同時在兩個區域出現。如果有線性定址能力的VESA顯示卡
(如TNT2),還可進一步驗證線性顯示緩沖區在1MB之上的工作情況。
?

第3章 Windows運行機理
3.1 內核分析(1)
3.1.1 運行機理
1. 概述
我們知道,DOS是一個開放的操作系統,應用程序和操作系統在同一個級別上,所以應用程序能控制
整個機器的所有資源。這在DOS的早期還沒什么問題,但是,后來隨著應用程序的增加,系統就出現了一
個很嚴重的問題—資源沖突。
當Windows 3.x推出時,市場上已有很多優秀的DOS軟件。為了不失去巨大的市場,微軟公司引入了
全新的方法,讓每個DOS程序和Windows程序都認為自己擁有所有的硬件資源。它們對系統硬件的操作是
通過一些虛擬設備(VxD)來實現的,這就是所謂的虛擬機(VM)。之所以稱為虛擬機,是因為它有完
整的內存空間、I/O端口,以及中斷向量。每個DOS都是一個VM,而所有的Win32的進程都運行在一個
叫System VM中。其中,VxD中的“x”代表任意的設備。例如,VDD表示虛擬顯示設備,VDMAD表示虛
擬DMA設備。對于熟悉DOS的人而言,可以把VxD看做是32位的DOS。
Windows是怎么實現一個多任務的操作系統呢?原理很簡單,就是CPU把運算時間輪流地分給每個虛
擬機。這樣,在Windows 3.x里,Windows程序之間用的是合作多任務,虛擬機之間用的是優先級多任
務。而管理所有VxD和時間調試策略的程序就是虛擬機管理器(VMM)。虛擬機管理器是Windows的核
心,它控制著計算機的主存、CPU的執行時間和外圍設備功能。VMM結構圖如圖3.1所示。
圖3.1 VMM結構圖
. 虛擬機管理器
?

VMM是一個32位的保護模式程序。它的主要任務是建立和維護一個支持虛擬機的框架,并對每
個VM提供服務。例如,它要創建、運行和結束一個虛擬機。VMM是眾多的系統VxD程序之一,放在系統
目錄下的VMM32.VxD文件中。VMM是第一個被加載到內存的VxD程序。它創建系統虛擬機并初始化其他
的VxD程序,也為這些VxD程序提供許多服務。
VMM和VxD的操作模式和真正的程序不同。在大多數時候,它們是潛伏的。當應用程序在系統中運行
時,這些VxD程序沒有被激活。 當某些需要它們處理的中斷/錯誤/事件發生時,它們才被喚醒。
. 虛擬設備驅動程序
在DOS程序中,虛擬設備驅動程序能控制系統的一切資源。當它們在虛擬機中運行時,Windows需要
為每一個設備建立一種虛擬的設備來模擬DOS對硬件的操作。例如,在DOS程序中按下鍵盤時,這個事
件消息首先會通知VMM,VMM接到它感興趣的消息后,會向所有的VxD發送這個消息。當鍵盤VxD接收
到后,會把中斷發送給VMs。一個VxD程序通常控制真正的硬件設備,并對該設備在各個虛擬機之間的共
享進行管理。
盡管如此,并不是說每個VxD程序必須和一個硬件設備相聯。雖然VxD程序是用來虛擬硬件設備的,
但是我們也可以把VxD程序看做是在第0級別的DLL。如果需要編寫一個在第0級別才能工作的程序,就可
以編一個VxD程序來為你完成這個工作。這樣,由于此VxD程序并沒有虛擬任何設備,就可以把它僅僅看
做是你的程序的擴展。
CIH病毒就是一個VxD,所以能對硬件直接設置修改。
VxD是系統中權力最大的程序。由于它們可以對系統做任何事情,所以它們是極度危險的。一個惡意
的或錯誤的VxD程序可以毀掉整個系統。操作系統對于惡意的、錯誤的VxD程序沒有任何的保護措施。
VxD程序是Windows 3.1和Windows 9x特有的,在Windows NT下不能運行。現在Windows NT下的驅
動程序已經改為WDM,它比VxD更規范,標準對系統的控制也有更嚴格的限制。
Windows 95下有兩種VxD,靜態VxD和動態VxD。靜態VxD是那些從系統啟動就被加載,在系統關閉
之前一直存在于內存中的VxD程序。這種VxD是在Windows 3.x時產生的。動態VxD是在Windows 9x下才
有的。動態VxD程序可以在需要的時候,通過程序本身加載或卸載。這些程序大多數都是用來控制設置管
理器和輸入輸出監視器加載的即插即用設備的。
2. 虛擬機管理器
虛擬機管理器(VMM)是Windows 9x操作系統的真正內核。它建立并維護起所有的虛擬機,同時為其
他VxD程序提供許多重要的服務。VMM處在VM和VxD之間。所有在VM上運行的軟件和VxD之間通
過VMM接口連接起來。
VMM提供了一組服務例程,它們可以創建、撤銷、運行、同步以及改變所有VM的狀態。VMM還提供
了調試服務例程、內存管理及I/O管理和截取軟中斷服務。
?

第3章 Windows運行機理
3.1 內核分析(2)
① 內存的物理地址空間
VMM使用80386的保護模式管理內存。從認識CPU一章中,我們知道,在80386以后,系統能提
供4GB的32位的虛擬空間。VMM在使用空間上把它們分為4個區域,如圖3.2所示。
. 私有區
. 共享區
. 系統區
. DOS區
圖3.2 VMM對使用空間的劃分
私有區地址是從4MB到2GB。這是Win32應用程序運行的空間。每個Win32的進程都有它自己
的2GB(要減去4MB)的空間,被Win32應用系統用來存放自己的代碼和資源。這塊區域是私有的,因為
每個Win32程序映射到不同的物理空間上。當一個Win32程序訪問4MB空間內時,它其實訪問的是映射的
某物理空間。
共享區地址是從2GB到3GB。這個區域是被虛擬機內的所有應用程序共享的。系
統DLL(user32,kernel32,gid32)和Win16進程(由于Win16要求在共享空間運行)都駐存在這里。
系統區地址是從3GB到4GB的線性空間的頂端。這里是Win9x為第0級的超級進程VMM和VxD專門開
辟的區域,并且此空間也是共享的。
DOS區地址是從0到4MB的空間內,這個空間是專為DOS的應用程序留下的,另外,Win16應用程序
堆棧的一小部分也放在這里。
② 內存的服務程序
?

VMM中使用了虛擬存儲的技術,能夠克服物理內存的限制。盡管在物理上不存在,但理論上4GB的空
間是能被訪問的。通過從RAM和次級存儲器設備上交換(分頁)代碼和數據以及將代碼和數據交換
到RAM和次級存儲器設備上以實現虛擬技術。因為VxD駐留在32位的保護模式部分,所以它應該可以直
接訪問所有的內存空間,但內存的管理是通過VMM來完成的,所以它只能通過存儲器管理服務獲得的內
存空間。
Windows決定實際有效的虛擬存儲器的數量和有效的磁盤空間的數量。實際有效的虛擬存儲器的數量
基于系統物理上的總量,可以手工指定。
存儲器管理程序在外部程序需要時,會一直分配物理空間,直到物理存儲器已經用盡。然后,它會從
物理存儲器移動4KB的代碼或數據頁到磁盤上,以使附加的物理存儲器有效。Windows中是按4KB的大小
來對內存空間進行分頁的。這種分頁對程序來說是透明的。如果程序企圖訪問某部分已交換到磁盤上的數
據,則會產生一個頁錯中斷。然后存儲器管理程序將其頁換出存儲器,并恢復該程序所需要的那些頁。
下面列出了Windows存儲器管理服務。列出的服務構成了公共使用子集。
. 系統目標管理
Alloacte_Device_Cb_Area
. 設備虛擬V86頁管理
Assign_Device_V86_Pages
. 系統頁分配程序
HeapAllocate
HeapFree
. 系統頁分配程序
CopyPageTable
MapIntoV86
ModifyPage bits
PageAllocate
PageLock
PageUnlock
PageGetAllocInfo
PhyIntoV86
. 查看保護方式中的物理設備存儲器
MapehysToLiner
DataAccessServices
GetFristV86Page
. 對保護方式API的專用服務
實例數據管理
查看V86空間
中斷處理程序
線程調度程序
?

3. 虛擬設備
VxD的功能十分強大,它不但能“虛擬”某種設備,還能給別的VxD或應用程序提供服務。
VxD可以和VMM一起被靜態地裝入系統,也可以由應用程序主動地裝入系統。因為VxD就在第0級工
作,并且有極高的權限,所以VxD能訪問任何的硬件,不僅可以訪問任何的物理空間,還可以捕獲軟件中
斷和I/O端口以及其他程序對內存的訪問,就連硬件中斷也可以被它捕獲。
① VxD的組成
安裝一個VxD的過程有下面幾個部分,如圖3.3所示。
. 實模式的初始化代碼和數據在完成以下4部分后,被系統銷毀。
. 保護模式(PM)初始化代碼部分,完成后銷毀。
. 保護模式(PM)初始化代碼數據,完成后銷毀。
. PM代碼,包括設備過程、API和回調過程,以及服務例程。
. PM數據,包括設備描述符塊、服務表,以及全局數據。
② VxD的加載過程
Windows 9x支持靜態加載和動態加載兩種加載方式。靜態加載的VxD是在Windows初始化時被自動加
載的,只有當Windows結束運行后,它才會卸載。Windows 9x中可以通過兩種方法來加載靜態的VxD。
. 直接在SYSTEM.INI中加入如下一行代碼:
Device =VxD_NAME
. 可以在Windows 9x注冊表中的HKEY_LOCAL_MACHINE/
System/CurrentControlSet/Services/VxD/key/StaticVxD子鍵下加入如下的VxD的路徑和名字:
VxD_NAME=PATHNAME
這種動態加載的VxD不是和VMM一起在Windows啟動時一起裝入內存的,而由應用程序或另外
的VxD裝入,并且也可以通過VxD或其他應用程序動態地刪除,所以,動態的VxD就有很大的靈活性。
?

第3章 Windows運行機理
3.1 內核分析(3)
如果一個VxD只是為了某個應用提供某種服務,選擇動態加載就比較好,因為VxD能在需要時加載,
在用完后就立即卸載。這兩種加載方式所響應的VMM消息有一點不同,有的只能響應靜態VxD,有的則
只能響應動態VxD。但大多數消息對這兩種方式都能響應。
圖3.3 VxD安裝過程
③ DDB的結構
設備描述塊(The Device Descriptor Block)簡稱DDB,是VMM聯系VxD的句柄。DDB中包括
?

了VxD的信息和指向VxD主要的入口指針。當然,為了給其他的應用程序使用,也可以包括指向其他入口
的指針。表3.1是DDB的數據結構。
表3.1 DDB的數據結構
字段區域 描述
Name 8個字節的VxD名稱
Major Version VxD的主版號,與Windows的版本號無關
Minor Version VxD的從版號,與Windows的版本號無關
Device Control Procedure 設備控制過程的地址
Device ID Microsoft分配的惟一的ID號
Initialization Order 通常是Undefine_Init_Order 。如果要強制在某個指
定的VxD初始化之前或結束之后進行初始化,那就
在VMM.INC中找到相應的Init_Order加1或減1
Service Table 服務表的地址
V86 API Procedure V86 API函數的地址
PM API Procedure PM API函數的地址
VxD源程序中的標號是不區分大小寫的,大寫、小寫或者混合起來用,都可以。
下面對這些字段做些說明
. Name :VxD的名字,最多8個字符。它必須是大寫!在系統中的所有VxD程序里,它們的名
字不能重復,每個VxD的名字應該是惟一的。這個宏同時也會根據這個名字產生DDB的名字,產
生的辦法就是在這個名字的后面加上_DDB。
. MajorVer和MinorVer:VxD的主要的和次要的版本。
. CtrlProc:VxD程序的設備控制函數的名字。設備控制函數是一個接受和處理VxD程序的控制
消息的函數。你可以把設備控制函數看做Windows函數的等價物。
. DeviceID:VxD程序的16位惟一標識符,當且僅當VxD程序需要處理以下情況時,需要用到
這個ID:
VxD程序導出一些供其他VxD程序使用的VxD服務。因為20H中斷接口用設備ID來定位/區
分VxD程序,所以一個惟一的ID對你的VxD程序是必要的。
VxD程序要在初始化中斷2FH、1607H時通知實模式程序它的存在。
有一些實模式軟件(TSR)要用中斷2FH、1605H來加載VxD程序。
如果VxD程序不需要一個惟一的設備ID,則可以把這一項設為UNDEFINED_DEVICE_ID;如果需要
它,則可以向Microsoft申請一個。
. InitOrder:初始化的順序。簡單地說,就是加載的順序。VMM就按照這個次序來加載VxD程
序。每個VxD程序都有一個加載次序號,例如:
VMM_INIT_ORDER EQU 000000000H
DEBUG_INIT_ORDER EQU 000000000H
DEBUGCMD_INIT_ORDER EQU 000000000H
PERF_INIT_ORDER EQU 000900000H
APM_INIT_ORDER EQU 001000000H
可以看到,VMM, DEBUG和DEBUGCMD是首先加載的VxD程序,然后是PERF和APM。初始化
順序值越低的VxD程序越先被加載。如果VxD程序在初始化時需要用到其他VxD程序提供的服務,那
么必須把初始化順序的值設得比你所要調用的那個VxD程序的值大。這樣,當VxD程序加載時,所要
的VxD就已經在內存中為你準備好了。如果不想去管VxD的初始化順序,就把這個參數填寫
為UNDEFINED_INIT_ORDER 。



?

. V86Proc和PMProc:程序可以導出供V86和保護模式程序使用的API,這兩個參數就是用來
填寫這些API的地址。記住,VxD程序除了監控系統虛擬機外,還要監控一個或多個運行
在DOS或者保護模式下的虛擬機程序。VxD程序理所當然要為DOS和保護模式程序提供API支
持。如果你不導出這些API,則可以不填這兩個參數。
. RefData:這是輸入輸出監視器(IOS)要用到的參考數據。只有在一種情況下要用到這個參
數,即當在為IOS編寫一個層驅動程序時。否則,可以不填這個參數。
④ VxD的事件處理
當實模式初始化完成后,VMM將通過專門的消息方法來通知所有的VxD發生了什么。VxD的消息
處理就像Windows的窗口消息處理一樣,能通過如下一組切換函數:
switch (事件){
case 系統初始化事件
處理此消息代碼
case VM初始化
處理此消息代碼

case 其他

}
為了給VxD發送消息,VMM就會從VxD的DDB中取得設備控制函數的地址,在EAX中放置的是消息的
值,EBX中放入當前VM的句柄,接著調用對應的函數。
?

第3章 Windows運行機理
3.1 內核分析(4)
3.1.2 LE文件的格式
VxD采用線性可執行文件格式(LE)。這種文件格式是為OS/2 2.0版設計的。它同時包含16位和32位
代碼,這也是VxD程序的需要。回想VxD在Windows 3.x的時代,從DOS啟動Windows,Windows在把機
器轉到保護模式之前,需要在實模式下做一些初始化。實模式的16位代碼必須和32位代碼一起放在可執行
文件中。所以,LE文件格式成為理所當然的選擇。Windows NT驅動程序不必在實模式下初始化,所以它
們不必使用LE文件格式。它們用的是PE文件格式。
在LE文件中,代碼和數據被存放在幾類運行屬性不同的段中。以下是一些可用的段類。
. LCODE:頁面鎖定的代碼和數據段。這種段被鎖定在內存里。換句話說,它永遠不會被放在
硬盤上,所以一定要謹慎地使用這種段類,以免浪費寶貴的內存。但那些每時每刻都必須放在內
存中的代碼和數據應該放在這個段里。尤其是那些硬件中斷處理程序。
. PCODE:可調頁代碼段。VMM可以對這種段實行調頁處理,在這種段里的代碼不必時刻放
在內存里,當VMM需要物理內存的時候,它就會把這段放到硬盤上去。
. PDATA:可調頁數據段。
. ICODE:僅用于初始化段。這種段里的代碼僅僅用來進行VxD的初始化。當初始化完成
后,VMM就把這段從內存中釋放。
. DBOCODE:僅用于調試的代碼數據段。當你要調試VxD程序時,就要用到這種段里的代碼
和數據,例如,它包含要調試的消息的處理代碼。
. SCODE:靜態代碼和數據段。這種段時刻存在于內存中,即使VxD已經卸載,這種段對某些
動態的VxD程序也很有用。這些VxD程序需要在某一Windows進程里不停地加載/卸載,而又要記
錄上次的環境和狀態。
. RCODE:實模式初始化代碼數據段。這種段包含實模式初始化需要的16位代碼和數據。
. 16ICODE:16ICODE USE16保護模式初始化數據段。這是一個16位的段,它包含VxD要從
保護模式拷貝到V86模式的代碼。例如,如果要把一些V86的代碼拷貝到一個虛擬機上時,想拷貝
的代碼就要放在這里。如果你把它放在其他的段里,編譯程序就會產生錯誤的代碼,例如,它會
產生32位代碼而不是16位代碼。
. MCODE:鎖定的消息字串。這種段包含了由VMM消息宏幫助編譯的消息字串,這有助于構
造驅動程序的國際版本。
VxD程序并不意味著必須包含以上所有的段,可以選擇VxD程序需要的段。例如,如果VxD程序不進
行實模式初始化,那么就不必包含RCODE段。
大多數時候,要用到LCODE,PCODE和PDATA段。作為一個VxD程序編寫者,為代碼和數據選擇合
適的段取決于自己的判斷。總的來說,應該盡可能多地使用PCODE和PDATA。因為這樣,VMM就可以
LCODE
?

在需要的時候把段調入調出內存。另外,硬件中斷程序及其所用到的服務必須放在 段里。
注意,不能直接地使用這些段類,你要用這些段類來定義段,這些段的定義被存放在模塊定義文件
(.def)中。下面是一個標準的模塊定義文件:
VxD SthVxD DYNAMIC
DESCRIPTION
'SthVxD (C) Beijing Herosoft Computer Technology Ltd.1996-2002'
SEGMENTS
_LPTEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LTEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LDATA CLASS 'LCODE' PRELOAD NONDISCARDABLE
_TEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_DATA CLASS 'LCODE' PRELOAD NONDISCARDABLE
CONST CLASS 'LCODE' PRELOAD NONDISCARDABLE
_TLS CLASS 'LCODE' PRELOAD NONDISCARDABLE
_BSS CLASS 'LCODE' PRELOAD NONDISCARDABLE
_ITEXT CLASS 'ICODE' DISCARDABLE
_IDATA CLASS 'ICODE' DISCARDABLE
_PTEXT CLASS 'PCODE' NONDISCARDABLE
_PDATA CLASS 'PDATA' NONDISCARDABLE SHARED
_STEXT CLASS 'SCODE' RESIDENT
_SDATA CLASS 'SCODE' RESIDENT
_DBOSTART CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_DBOCODE CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_DBODATA CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_16ICODE CLASS '16ICODE' PRELOAD DISCARDABLE
_RCODE CLASS 'RCODE'
EXPORTS
SthVxD_DDB @1
第一個聲明定義了VxD的名稱,一個VxD的名稱必須是全部大寫的。
接下來是段的定義,段的定義包括三個部分:段的名稱、段類和要求的段的運行屬性。可以看到,很
多段都基于相同的段類,例如,_LPTEXT,_LTEXT,_LDATA都是基于LCODE段類,而且屬性也完全
一樣。這樣定義段有利于讓代碼更容易被理解。如,LCODE可以包含代碼和數據,對于一個程序員來
說,如果他能把數據放到_LDATA段里,把代碼放到_LTEXT 段里,代碼就會顯得很容易理解。最后,這
兩個段都會被編譯到最后的可執行程序的同一個段內。
一個VxD程序導出且僅導出一個標記:它的設備描述塊(DDB)。DDB實際上是一個結構,它包含
了VMM需要知道的所有的VxD信息。必須在模塊定義文件中導出DDB。
?

在大多數時候,可以把上面的.DEF文件用到新建的VxD項目中去。只要把.DEF文件里第一行和最后
一行的VxD名字改掉就可以了。在一個匯編的VxD項目中,段的定義是不必要的,段的定義主要用
于C的VxD項目編寫,但用在匯編里也是可以的。你會得到一大堆警告的信息,但是它能匯編成功。也可
以刪掉你項目里沒有用到的段定義,從而去掉這些討厭的警告信息。
vmm.inc包含了許多用于定義源文件中的段的宏:
_LTEXT VxD_LOCKED_CODE_SEG
_PTEXT VxD_PAGEABLE_CODE_SEG
_DBOCODE VxD_DEBUG_ONLY_CODE_SEG
_ITEXT VxD_INIT_CODE_SEG
_LDATA VxD_LOCKED_DATA_SEG
_IDATA VxD_IDATA_SEG
_PDATA VxD_PAGEABLE_DATA_SEG
_STEXT VxD_STATIC_CODE_SEG
_SDATA VxD_STATIC_DATA_SEG
_DBODATA VxD_DEBUG_ONLY_DATA_SEG
_16ICODE VxD_16BIT_INIT_SEG
_RCODE VxD_REAL_INIT_SEG
?

第3章 Windows運行機理
3.1 內核分析(7)
(2)用Win32應用程序里的 CreateFile API。你在調用CreateFile時,動態VxD要以下面的格式填
寫:
//./VxD完整路徑名
例如,如果要加載一個在當前目錄下名為SthVxD的動態VxD,則需要做如下的工作,一般可以直接
用C來編寫主功能,然后和匯編進行連接:
hCVxD = CreateFile(".//SthVxD", 0,0,0, CREATE_NEW,
FILE_FLAG_DELETE_ON_CLOSE, 0);
FILE_FLAG_DELETE_ON_CLOSE 這個標志用來說明該VxD在CreateFile返回的句柄關閉時被卸
載。
如果用CreateFile來加載一個動態VxD,那么這個動態VxD必須處理w32_DeviceIoControl 消息。當動
態VxD第一次被CreateFile函數加載的時候,WIN32向VxD發出這個消息。VxD響應這個消息,返回
時,eax中的值必須為零。當應用程序調用DeviceIoControl API來與一個動態VxD通信
時,w32_DeviceIoControl消息也被發送。
(3)當一個動態VxD在初始化時收到一個消息:
Sys_Dynamic_Device_Init
在結束時也收到一個控制消息:
Sys_Dynamic_Device_Exit
但動態VxD不會收到Sys_Critical_Init, Device_Init和Init_Complete控制消息,因為這些消息是在系統
虛擬機初始化時發送的。除了這三個消息,動態VxD能收到所有的控制消息,只要它還在內存里。它可以
做靜態VxD可以做的所有事情。簡單地說,動態VxD除了加載機制和接收到的初始化/結束消息跟靜
態VxD不同以外,它能做靜態VxD所能做的一切。
當VxD在內存里的時候,除了接收和初始化及結束相關的消息外,它還要收到許多別的控制消息。這
些消息有的是關于虛擬機管理器的,有的是關于各種事件的。例如,關于虛擬機的消息如下:
Create_VM
VM_Critical_Init
VM_Suspend
VM_Resume
Close_VM_Notify
Destroy_VM
選擇地響應你所感興趣的消息是你自己的責任。
(4)在VxD內創建函數
?

要在一個段里面定義函數,應該首先定義一個段,然后把函數放進去。例如,如果要把函數放到一個
可調頁段中,應該先定義一個可調頁段:
VxD_PAGEABLE_CODE_SEG
(你的函數寫在這里)
VxD_PAGEABLE_CODE_ENDS
可以在一個段里面插入多個的函數。作為一個VxD編寫者,必須決定每一個函數應該放到哪個段里面
去。如果函數必須時刻存在于內存中,如某些硬件中斷處理程序,就把它們放到鎖定頁面段里面,否則,
應該把它們放到可調頁段。
(5)要用BeginProc和EndProc 宏來定義函數:
BeginProc 函數名
EndProc 函數名
使用BeginProc 宏還可以加上一些參數,想了解這些細節,你可以看看Win95 DDK的文檔。大多數時
候,你只用填寫函數的名字就夠了。
因為BeginProc-EndProc 宏比proc-endp 指令的功能要強,所以你應該用BeginProc-EndProc宏來代
替proc-endp指令
3. VxD編程約定
① 寄存器的使用
VxD程序可以使用所有的寄存器,FS和GS。但是在改動段寄存器的時候一定要小心。尤其是,一定
不要改動CS和SS的內容,除非你對將發生的事情有絕對的把握。你可以使用DS和ES,但一定要記住在
返回時恢復它們的初值。有兩個特征位尤其重要:方向和中斷特征位。不要長時間地屏蔽中斷。還有,如
果你要改動方向特征位,不要忘了在返回之前恢復它的初值。
② 數傳遞約定
VxD服務函數有兩種調用約定:寄存器法和堆棧法。調用寄存器法服務函數時,通過各種寄存器來傳
遞服務函數的參數。并且,在調用完成后,檢查寄存器的值來看操作是否成功。不要總是以為在調用服務
函數后,主要寄存器的值還和以前一樣。當調用堆棧法服務函數時,你把要傳遞的參數壓棧,在eax得到
返回值。堆棧調用法的服務函數保存ebx,esi,edi和ebp的值。許多寄存器調用法服務函數都源
于Windows 3.x的時代。
在大多數時候,可以通過名字來區分這兩種服務函數。如果一個函數的名字以下劃線開頭,
如_HeapAllocate,它就是一個堆棧法的服務函數(除了少數從VWIN32.VxD導出的函數)。如果函數名
不是以下劃線開頭,它就是一個寄存器法的服務函數。
③ 調用VxD服務函數
可以通過VMMCall和VxDCall 宏來調用VMM和VxD服務。這兩個宏的語法是一樣的。當你要調
用VMM導出的VxD服務函數時,用VMMCall。當要用其他VxD程序導出的VxD服務函數時,用VxDCall。
VMMCall service ; 調用寄存器法服務函數
VMMCall _service, <argument list> ; 調用堆棧法服務函數
?

當調用堆棧法服務時,必須用角括號把你的參數列括起來。
VMMCall _HeapAllocate, <<size mybuffer>, HeapLockedIfDP>
_HeapAllocate是一個堆棧法服務函數。它有兩個參數,我們必須用角括號把它們括起來。由于第一
個參數是一個宏,這個宏不能正確解釋表達式,所以我們要再用一個角括號把它括起來。
4. VxD函數的調用方法
我們知道,VxD程序都有一個VxD的DDB列表,當VxD被加載時,DDB就會被裝到Windows 95的系統
內存里,Windows 95就是通過這個表把所有的VxD作為一個鏈表來進行管理的。Windows 95使用INT
20H來進行功能調用,凡是新VxD文件被裝入內存的時候,都會產生一個INT 20H,在其后會緊跟
著DDB的ID號碼和服務函數號碼。系統處理INT 20H時,就會去查找INT 20H的服務函數鏈表,當查找到
函數ID和地址相同,就替換掉程序本身的指令。
VxD程序,包括VMM在內,通常要導出一系列的被別的VxD程序調用的公共函數,這些函數被稱
為VxD服務。調用這些服務的機制和在第三層級別運行的應用程序有很大的不同:每個導出VxD服務
的VxD程序必須有一個惟一的ID,你可以從Microsoft得到一個這樣的ID。這個ID是一個包含了一個VxD惟
一的身份驗證的16位的數字,例如:
UNDEFINED_DEVICE_ID EQU 00000H
VMM_DEVICE_ID EQU 00001H
DEBUG_DEVICE_ID EQU 00002H
VPICD_DEVICE_ID EQU 00003H
VDMAD_DEVICE_ID EQU 00004H
VTD_DEVICE_ID EQU 00005H
?

第3章 Windows運行機理
3.1 內核分析(5)
每個宏都有與它相對應的結束宏,例如,如果要在源文件中定義一個_LTEXT段,應該寫成如下:
VxD_LOCKED_CODE_SEG
(把你的代碼寫在這里)
VxD_LOCKED_CODE_ENDS
我們可以用VC++ Dump工具提供的DUMPBIN工具來分析以下VxD的文件結構和組織機理。可以進
入MS_DOS輸入如下的命令行(在光碟上的第三章/cpu降溫/COOLCPU/BIN路徑下的STHVxD):
DUMPBIN /ALL STHVxD.VxD
就可以看到如下的信息:
Microsoft (R) COFF Binary File Dumper Version 6.00.8447
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.
Dump of file sthvxd.vxd
File Type: VXD
454C magic number
0 byte order
0 word order
0 executable format level
2 CPU type (**)
4 operating system (**)
0 module version
38000 module flags
4 number of memory pages
2 object number of entry point
0 offset of entry point
0 object number of stack
0 offset of stack
200 memory page size
2C bytes on last page
?

?61 fixup section size
0 fixup section checksum
6C loader section size
0 loader section checksum
C4 object table
3 object table entries
10C object map
0 iterated data map
0 resource table
0 resource table entries
11C resident names table
126 entry table
0 module directives table
0 module directives entries
130 fixup page table
144 fixup record table
191 imported modules name table
0 imported modules
191 imported procedures name table
0 page checksum table
1000 enumerated data pages
2 preload page count
162C non-resident name table
4E non-resident name table size
0 non-resident name checksum
0 automatic data object
0 debug information
0 debug information size
0 preload instance page count
0 demand instance page count
0 extra heap allocation
0 offset of Windows resources
0 size of Windows resources
ABC device id
400 DDK version
OBJECT HEADER #1
23C virtual size
0 virtual address
?

?2045 flags
Execute Read
Has preload pages
32-bit
1 map index
2 map size
444F434C reserved
OBJECT PAGE MAP #1
Logical Physical File Flags
Page Page Offset Flags
-------- -------- -------- --------
00000001 00000001 00001000 Valid
00000002 00000002 00001200 Valid
RAW DATA #1
00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010: 00 00 00 00 00 04 BC 0A 05 00 00 00 53 74 68 56 ............SthV
00000020: 58 44 20 20 00 00 00 80 00 00 00 00 00 00 00 00 XD ... ........
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000040: 00 00 00 00 00 00 00 00 00 00 00 00 76 65 72 50 ............verP
00000050: 50 00 00 00 31 76 73 52 32 76 73 52 33 76 73 52 P...1vsR2vsR3vsR
00000060: 8B 4C 24 08 85 C9 75 05 33 C0 C2 14 00 83 F9 FF .L$...u.3.......
00000070: 75 08 E8 69 00 00 00 C2 14 00 83 F9 03 76 08 B8 u..i.........v..
00000080: 32 00 00 00 C2 14 00 8B 44 24 14 8B 54 24 10 50 2.......D$..T$.P
00000090: 52 8B 44 24 14 8B 54 24 0C 50 52 FF 14 8D FC FF R.D$..T$.PR.....
000000A0: FF FF C2 14 00 CC CC CC CC CC CC CC CC CC CC CC ................
000000B0: A1 00 00 00 00 8B 4C 24 10 8B 49 18 85 C9 74 08 ......L$..I...t.
000000C0: 8B 09 89 0D 00 00 00 00 C2 10 00 CC CC CC CC CC ................
000000D0: E8 2A 01 00 00 B8 01 00 00 00 C3 CC CC CC CC CC .*..............
000000E0: B8 01 00 00 00 C3 CC CC CC CC CC CC CC CC CC CC ................
000000F0: 8B 44 24 04 83 F8 01 74 0A 83 F8 02 74 15 33 C0 .D$....t....t.3.
00000100: C2 0C 00 8B 44 24 08 50 E8 63 00 00 00 83 C4 04 ....D$.P.c......
00000110: C2 0C 00 8B 44 24 0C 8B 4C 24 08 50 51 E8 5E 00 ....D$..L$.PQ.^.
00000120: 00 00 83 C4 08 C2 0C 00 CC CC CC CC CC CC CC CC ................
00000130: 8B 44 24 04 83 F8 01 74 0A 83 F8 02 74 15 33 C0 .D$....t....t.3.
00000140: C2 0C 00 8B 44 24 08 50 E8 43 00 00 00 83 C4 04 ....D$.P.C......
?

00000150: C2 0C 00 8B 44 24 0C 8B 4C 24 08 50 51 E8 3E 00 ....D$..L$.PQ.>.
00000160: 00 00 83 C4 08 C2 0C 00 CC CC CC CC CC CC CC CC ................
00000170: B8 01 00 00 00 C3 CC CC CC CC CC CC CC CC CC CC ................
00000180: B8 02 00 00 00 C3 CC CC CC CC CC CC CC CC CC CC ................
00000190: B8 01 00 00 00 C3 CC CC CC CC CC CC CC CC CC CC ................
000001A0: B8 02 00 00 00 C3 CC CC 83 F8 1B 75 09 E8 00 00 ...........u....
000001B0: 00 00 83 F8 01 C3 83 F8 1C 75 09 E8 10 FF FF FF .........u......
000001C0: 83 F8 01 C3 83 F8 23 75 0E 56 52 53 51 55 E8 8D ......#u.VRSQU..
000001D0: FE FF FF 83 F8 01 C3 F8 C3 A1 00 00 00 00 85 C0 ................
000001E0: 75 02 F9 C3 FB F4 F9 C3 56 8D 35 00 00 00 00 CD u.......V.5.....
000001F0: 20 3A 00 01 00 5E B8 00 00 00 00 0F 93 C0 C3 56 :...^.........V
00000200: 8D 35 00 00 00 00 CD 20 2B 01 01 00 5E B8 00 00 .5..... +...^...
00000210: 00 00 0F 93 C0 C3 FF 75 18 FF 75 10 FF 75 1C E8 .......u..u..u..
00000220: CC FE FF FF 89 45 1C C3 FF 75 18 FF 75 10 FF 75 .....E...u..u..u
00000230: 1C E8 FA FE FF FF 89 45 1C C3 CC CC .......E....
OBJECT HEADER #2
B virtual size
0 virtual address
1005 flags
Execute Read
16:16 alias
3 map index
1 map size
444F4352 reserved
OBJECT PAGE MAP #2
Logical Physical File Flags
Page Page Offset Flags
-------- -------- -------- --------
00000001 00000003 00001400 Valid
RAW DATA #2
00020000: 33 DB 33 F6 66 33 D2 B8 00 00 C3 3.3.f3.....
OBJECT HEADER #3
2C virtual size
0 virtual address
2015 flags
?

?Execute Read
Discardable
32-bit
4 map index
1 map size
444F4349 reserved
OBJECT PAGE MAP #3
Logical Physical File Flags
Page Page Offset Flags
-------- -------- -------- --------
00000001 00000004 00001600 Valid
RAW DATA #3
00000000: 0D 0A 44 5F 45 5F 42 5F 55 5F 47 3D 3D 3D 3E 53 ..D_E_B_U_G===>S
00000010: 74 68 56 58 44 3C 3D 3D 3D 0D 0A CC CC CC CC CC thVXD<===.......
00000020: E8 00 00 00 00 B8 01 00 00 00 C3 CC ............
Summary
?

第3章 Windows運行機理
3.1 內核分析(6)
3.1.3 VxD的設計實現
VxD的設計并不是通常我們所講的調用API的Windows程序,而是通過對DDK的調用來工作。DDK可
以從微軟的網站上下載。在DDK中有很多VxD的例子,我們在設計時可以作為參照樣板。VxD的設計一般
要直接用匯編編程,并且要直接地操作硬件,所以設計比較困難。不過,用匯編寫VxD的框架結構、
用C來完成具體的工作實現就會大大地提高開發的效率,后面的例子中就是使用了這種方法。
1. 靜態VxD
在下列情況下,VMM加載一個靜態VxD:
(1)此VxD在注冊表中的如下位置有定義:
HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services/VxD/key/StaticVxD=VxD帶路徑
文件名
(2)此VxD在system.ini中的[386enh]行下有定義:
[386enh] section:
device=VxD帶路徑文件名
在開發的時候,建議從system.ini載入VxD程序,因為這樣如果VxD程序有錯而將導致Windows不能啟
動,可以在DOS下修改system.ini,而如果使用注冊表載入的辦法,就無法修改了。
當VMM加載靜態VxD程序時,VxD程序會按以下順序接收到3個系統控制消息。
(1)Sys_Critical_Init:VMM在轉入到保護模式后,開放中斷前發出這個控制消息。大多數VxD程序
不要用這個消息,除非VxD程序要接管一些其他VxD程序或者保護模式程序要用到的中斷。既然處理這個
消息的時候,這個中斷還沒有打開,就可以確定在你接管這個中斷的時候,此中斷不會被調用。VxD程序
為其他的VxD程序提供了一些VxD服務。
(2)Device_Init:控制消息時需要調用一些VxD服務,既然Sys_Critical_Init 控制消息
在Device_Init消息之前被發送,所以你應該在Sys_Critical_Init 消息發送時初始化你的程序。
如果要對這消息進行處理,則應該盡可能快地做完初始化工作,以免太長的執行時間導致硬中斷丟
失(記住,中斷還沒打開)。Device_Init VMM在開放中斷后發送此信息。大多數VxD程序都在得到這個消
息時初始化。因為中斷都開放了,所以耗時的操作也可以在這里執行,而不怕會導致硬中斷的丟失。你可
以在這時進行初始化(如果你需要的話)。
(3)Init_Complete:在所有的VxD程序處理完Device_Init 消息之后,VMM釋放初始化段
(ICODE和RCODE段類)之前,VMM發出這個控制消息。只有少數幾個VxD要處理這個消息。
VxD程序在成功地初始化后,必須將返回標志清零,反之,必須在返回之前把返回標志設為出錯信
息。如果VxD不需要初始化,就不必對這些消息進行處理。
?

當要結束靜態VxD的時候,VMM發送如下的控制消息。
(1)System_Exit2:當VxD程序收到這個消息,Windows 9x正在關閉系統,除了系統虛擬機外,所
有其他虛擬機都已經退出了。盡管如此,CPU仍然處于保護模式下,在系統虛擬機上執行實模式編碼也是
安全的。這時,Kernel32.dll也已經被卸載了。
(2)Sys_Critical_Exit2 :當所有的VxD完成對System_Exit2的響應處理并且中斷都被關閉后,VxD收
到這個消息。
許多VxD程序并不要響應這兩個消息,除非你要為系統做轉換到實模式的準備。要知道,當Windows
95關閉時,它進入到實模式。所以,如果VxD程序對實模式影像做了一些會導致它不穩定的操作,它就需
要在這時進行恢復。
你也許會感到奇怪:為什么這兩個消息后面都跟著個“2”?這是因為在VMM加載VxD程序的時候,它
是按照初始化順序值小的VxD先加載的順序加載的,這樣,VxD程序就可以使用那些在它們之前加載
的VxD程序提供的服務。例如,VxD2要用到VxD1中的服務,它就必須把它的初始化順序值定義得
比VxD1小。加載的順序是:
..... VxD1 => VxD2 => VxD3 .....
那么卸載的時候,理所當然地是初始化順序值大的VxD程序先被卸載,這樣它們仍然可以使用比它們
后加載的那些VxD程序提供的服務。如上面的例子,次序是:
.... VxD3 => VxD2 => VxD1.....
在上邊的例子中,如果VxD2在初始化時調用了VxD1中的某些服務,那么卸載時它可能也要再次用到
一些VxD1中的服務。System_Exit2和Sys_Critical_Exit2是按反初始化順序發送的。這表示,當VxD2接受
到這些消息時,VxD1還沒有被卸載,它仍可以調用VxD1的服務,而System_Exit和Sys_Critical_Exit消息
不是按照反初始化順序發送的。這意味著,你不能肯定你是否仍能調用在你之前加載的VxD提供的VxD服
務。
現在的VxD程序不應該使用這些消息,而應該使用以下兩種退出消息。
(1)Device_Reboot_Notify2 告訴VxD程序VMM正在準備重新啟動系統。這時候,不管是中斷還是開
放的Crit_Reboot_Notify2,都會告訴VxD程序VMM正在準備重新啟動系統,并把中斷關閉。
(2)Device_Reboot_Notify和Crit_Reboot_Notify 消息一樣,但它們并不是像“2”版本的消息那
樣,按反初始化順序發送。其他就和Device_Reboot_Notify2一樣了。
2. 動態VxD
動態VxD在Windows 9x里可以動態地被加載和卸載。這個特點在Windows 3.x下是沒有的。動
態VxD程序的主要作用是用來支持某些動態的硬件設備的重裝,比如即插即用設備。盡管如此,可以
從Win32程序中加載/卸載它,也可以把它看做是程序的一個到ring0的擴展。
上一節我們提到的例子是一個靜態的VxD,你可以把它轉換成一個動態的VxD,只要在.def文件
中VxD標記的后面加上關鍵字DYNAMIC:
VxD STHVxD DYNAMIC
這就是把一個靜態VxD轉換成一個動態的VxD所要做的一切。
?

一個動態的VxD可以按以下的方法被加載。
(1)把它放到Windows目錄下的/SYSTEM/IOSUBSYS目錄中。在這個目錄里的VxD會被輸入輸出監
視器(IOS)加載。這些VxD必須支持層設備驅動。所以用這種方法加載動態VxD并不是一個好辦法。
用VxD加載服務。VxDLDR是一個可以加載動態VxD的靜態VxD。你可以在其他VxD里面或者在16位
代碼里面調用它的服務。
?

第3章 Windows運行機理
3.1 內核分析(8)
可以看到,VMM的ID是1,VPICD的ID是3等。VMM用這些ID來找到導出所需VxD服務的VxD程序。
當一個VxD程序導出VxD服務時,它把所有服務的地址存在一個表里面。所以,你還需要通過服務分支表
里面服務的索引來找到你所要的服務。例如,如果你要調用第一個服務,GetVersion服務,就要指
定0(這個索引是從0開始的)。調用VxD服務實際上包括中斷20H,你的代碼產生一個中斷20h,并帶有
一個雙字的值,這個值包含了設備ID和服務索引。例如,如果你要調用一個VxD程序導出的VxD服務,假
設VxD程序設備ID是000DH,服務號碼是1,那么代碼應該是:
int 20h
dd 000D0001h
跟在中斷20H后的雙字的高字包含設備ID。低字是在服務列表中的索引。
當20H中斷執行時,VMM就得到了控制權,并馬上檢測跟著的雙字。然后它提出設備ID用來找
到VxD程序,用服務索引來定位在那個VxD程序中所要求的服務的地址。
可以看到,這個操作是很費時的。VMM必須浪費很多時間來定位VxD程序和所要服務的地址,所
以VMM作了個小小的弊 。當中斷20H操作成功后,VMM抓取鏈接。這就是說,VMM用直接的服務調用來
替代20H中斷和它后面的雙字。所以,上面的20H中斷代碼片斷就被改變成:
call dword ptr [VxD_Service_Address]
這個方法很不錯,因為int 20h+dword加一個雙字用6個字節,正好和call dword ptr結構相等。所以,
接下來的服務調用是快速而有效的。這個方法具有直接性、簡潔性。一方面,它減輕了VMM和VxD載入
器的工作量,因為它們不用定位VxD中所有的服務,那些沒有執行過的服務將會保持原樣。另一方面,一
旦一個靜態VxD程序導出的服務被調用,那么就不可能把這個靜態的VxD程序卸載了。由于VMM把調用鎖
定到VxD服務的實際地址上,如果提供這個服務的VxD程序從內存中被卸載了,其他VxD程序調用這個服
務時,就會很快地因為調用無效的內存地址而導致系統崩潰。沒有辦法來消除抓取的鏈接。這個問題的結
論是動態VxD不適合作為服務提供者。
3.1.4 【實例】:CPU降溫程序代碼分析
有人可能認為VxD很高深,其實不然。下面介紹一個簡單的CPU降溫的程序,來加深大家的理解。
1. 程序的組成
這個程序由兩個部分組成。
其一,VxD模塊。它是一個動態VxD,可以用以下的處理過程來分析CPU降溫的基本原理:
?

(1)被加載時,就對VMM注冊空閑的消息(idle)處理函數。
(2)當VMM空閑時,就會自動地調用VxD的注冊的消息函數。處理函數通過一條HLT指令使CPU暫
停,當CPU暫停時,很多器件就會停止工作,這樣就可以降溫。
(3)CPU接收到新指令時,中止HLT命令,即退出函數。
(4)當VMM空閑時,繼續調用(2)。這樣循環進行,直到程序退出。
其二,主程序模塊。它負責裝載和卸載VxD,并處理用戶的界面及響應用戶事件。基本的流程如下:
(1)如果裝載降溫VxD程序,則成功轉入(2),否則就退出系統。
(2)設置VxD的處理函數,即VxD的主處理函數。
(3)生成一個托盤(即在Windows 9x任務條右下角上的可控制的小圖標)以控制VxD的運行狀態。
2. 程序的編譯
我們現在就編譯程序,先看一下運行和效果。
① 編譯動態的VxD文件
在本書配套的光碟上,可以在 / COOLCPU /STHVxD目錄下找到該程序,我們可以看到有5個文件。
. CVxDctrl.asm:動態VxD的運行框架,這段必須用匯編編寫。
. SthVxD.c :VxD工作代碼(由匯編調用)。它是一個C的模塊,主要負責應用程序和VxD的
內核進行接口。通常VxD可以用匯編來編程,但這樣會使開發效率很低。為了更方便地開發,一
般用匯編生成框架,然后用C語言編寫程序的主要部件,這樣就會大大地提高開發的效率。
. SthVxD.def:VxD結構的定義文件。它是每個VxD必需的,是標準的一部分。
. Makefile:編譯的參數設置文件。
. SETPATH.BAT:設置編譯的路徑的文件。
當然,要編譯以上的文件,您還需要注意以下幾點
. 安裝VC++ 5.0以上的版本。
. 必須具備Windows 9x Device Driver Development Kit。可以從
http://download.microsoft.com/download/win98SE/Install/Gold/W98/EN-US/98DDK.EXE下
載Windows 98 DDK。不過,因為Windows 98 DDK的很多庫和函數都發生了改變,所以此程序不
能直接使用下載的DDK。我們可以用windows 95的DDK,它們在碟的COOLCPU的win95DDK目
錄中。Windows 95 DDK中包括Inc32和Lib,其中,Inc32目錄中包括了32位的頭文件,Lib目錄
中包括所有的庫文件上。
. 還有一點最重要的是,您的操作系統一定要是Windows 9x系列的,因為VxD只能用
在Windows 9x系列的操作系統中。
. 設置編譯器的文件和頭文件的查找路徑,需要修改SETPATH.BAT文件中的路徑設置,其原
始內容如下:
@ECHO OFF
SET
LIB= D:/win95DDK/LIB;D:/98DDK/LIB;C:/MSDEV/LIB;
?

D:/98DDK/lib/i386/free;%LIB%
SET PATH=D:/98DDK/BIN;D:/98DDK/bin/win98;%PATH%
SET INCLUDE=D:/WIN95/INC32;
D:/Microsoft Visual Studio/VC98/Include;D:/98DDK/ inc/win98;
@ECHO ON
D:/98DDK是DDK的安裝目錄,您可把這個目錄改成自己機器的DDK安裝的目錄。
D:/win95DDK是光盤中Windows 95的DDK的目錄。當您把此目錄復制到硬盤后,需要修改成對應
的目錄。
. 設置Makefile,來設置編譯路徑,在Makefile下可以找到如下的一行語句:
CFLAGS= -DWIN32 -DCON -Di386 -D_X86_ -D_NTWIN -W3 -Gs -D_DEBUG
-Zi -O2 -IC:/MSDEV/INCLUDE -ID:/WIN95DDK/INC32
?

第3章 Windows運行機理
3.1 內核分析(9)
把C:/MSDEV/INCLUDE這個路徑(其中,C:/MSDEV是VC++的頭文件.h文件的路徑)修改為您機器
安裝的VC++的路徑即可。
例如,如VC++安裝在C:/Program Files/Microsoft Visual Studio/VC98中,就可以將之改為:
C:/Program Files/Microsoft Visual Studio/VC98/INCLUDE
把D:/WIN95DDK/INC32修改為DDK的安裝路徑的頭文件的路徑。
例如,如果DDK的目錄為C:/WIN98DDK,就可以將之修改為E:/98DDK/inc/win98。
接下來,就可以編譯。可按如下步驟進行:
(1)進入MS DOS方式。
進入STHVxD文件的路徑,例如:
CD D:/COOLCPU/STHVxD
(2)運行nmake.exe程序,對整個程序進行編譯。當BIN目錄下生成SthVxD.VxD的文件時,該VxD就
編譯完成了。在編譯完成后,會出現一些警告,這是正常的,沒有什么問題。
注意:一定要把光碟上的程序復制到硬盤上才能進行編譯!
② 編譯主程序(CoolCpu)
VxD文件編譯好后,主程序就很容易編譯。只需打開VC++的open workspace文件
CoolCpu.dsp或CoolCPU.mak。
③ 運行程序
直接編譯,就可以看到在BIN目錄中生成了一個CoolCPU.EXE文件。當在編譯環境
中,BulidExecute系統將彈出一個寫有“can’t execute program”的信息提示框。這是為什么呢?
其實,這是CoolCPU.exe在當前的編譯目錄中找SthVxD.VxD的文件,因為當前路徑下沒有這
個VxD文件,所以就彈出錯誤的對話框。直接到BIN文件下運行CoolCPU.exe,就可以看見在Windows的任
務欄的右下角出現了一個小云雨的圖標。當單擊此圖標時,彈出菜單,“空閑時讓CPU節能”的小鉤被打上
時,表示允許CPU使用降溫功能。去掉小鉤時,表示不用此降溫功能。
3. 程序的分析
下面我們來分析一下這個程序。
首先看一下VxD的基本框架。從CVxDctrl.asm文件中,可以看到如下的程序結構。
PAGE 58,132
;*********************************************************
TITLE CONTROL - ControlDispatch for VxD in C
;********************************************************
?

;
.586p
;*********************************************************
; 包含頭
;*********************************************************
.xlist
include vmm.inc
include debug.inc
.list
;編譯成動態VxD,動態的VxD為1
SthVxD_DYNAMIC EQU 1
;VxD的ID號
CVxD_DEVICE_ID EQU 0ABCH
ifdef _VxD_SERVICES
;定義可以被其他VxD調用的接口函數
Create_CVxD_Service_Table = 1
;可以被其他VxD調用的接口函數表
Begin_Service_Table CVxD
CVxD_Service _CVxD_Get_Version, VxD_LOCKED_CODE
End_Service_Table CVxD
Endif
很多人可能對匯編不是很熟悉,但這不要緊。在這段匯編中用了很多的宏匯編語句,使整個代碼很像
高級語言。
在VxD的這段匯編的代碼中,很多東西是必須的,下面我們來分別介紹。
.586p
告訴編譯器要使用CPU特權指令的80586指令系統,還可以使用.386p或者.486p等。
include vmm.inc
每個VxD源代碼都必須包含imm.inc。它包含了代碼中宏的定義。可以根據需要包含其他的庫文件,
如Pci.inc(PCI設備)的宏。
SthVxD_DYNAMIC EQU 1
表示SthVxD_DYNAMIC等于1,相當于C語言中的#define語句的作用。
CVxD_DEVICE_ID EQU 0ABCH
?

每個VxD程序的16位惟一標識符,有了這個ID就可以導出一些供其他VxD程序使用的VxD服務。
如果VxD程序不需要一個惟一的設備ID,可以把這一項設為UNDEFINED_DEVICE_ID ,還可以由微
軟分配一個固定的ID號。也可以任意設置,只要以前沒有使用過,此處設置為0ABCH。
ifdef _VxD_SERVICES
;定義可以被其他VxD調用的接口函數
Create_CVxD_Service_Table = 1
;可以被其他VxD調用的接口函數表
Begin_Service_Table CVxD
CVxD_Service _CVxD_Get_Version, VxD_LOCKED_CODE
End_Service_Table CVxD
Endif
定義被其他函數調用的接口的申明,很多VxD中的函數可以給其他的VxD進行調用,有些VxD提供了
一此功能能讓別的VxD使用,就可以像動態連接庫一樣引出一些函數。
DECLARE_VIRTUAL_DEVICE SthVxD, 5, 0, CVxD_Control, CVxD_DEVICE_ID,/
UNDEFINED_INIT_ORDER, CVxD_V86, CVxD_PM
VxD_LOCKED_CODE_SEG
這是一個標準的VxD的說明部分,VMM通過VxD程序的設備描述塊(DDB)來獲取VxD的有關的信
息。一個設備描述塊是一個結構,它包含了許多關于VxD的重要信息,查閱DDB表可以知道。
SthVxD為VxD的名稱,5為主版本號,0為副版本號,CVxD_Control為指向VxD的消息處理函數的指
針,CVxD_DEVICE_ID為設備ID號。UNDEFINED_INIT_ORDER是初始化的序列,可以通過這個序列號
讓VMM來決定是裝入時初始化還是隨機地初始化或者是一定要在某事件前初始化,但一般的VxD是不必
要設置這個參數的,它一般用在系統的某些固定的設備的VxD上。CVxD_V86為V86的處理函
數,CVxD_PM為保護模式程序使用的API地址。
在上面這段代碼中,我們還可以看到VxD_LOCKED_CODE_SEG這個語句,它是什么呢?
其實,它是vmm.inc中的宏語句,表示代碼在內存中的一種存儲方式,請參考介紹LE的文件模式的一
節。
?

第3章 Windows運行機理
3.1 內核分析(10)
我們可以從SthVxD.c中看到CVxD_Dynamic_Init和CVxD_Dynamic_Exit這兩個函數的定義。
ifdef _VxD_SERVICES
extrn _CVxD_Get_Version:near
endif
extrn _CVxD_V86API@12:near

extrn _CVxD_PMAPI@12:near

extrn C EnableHlt: dword
BeginProc CVxD_Control
Control_Dispatch SYS_DYNAMIC_DEVICE_INIT,
CVxD_Dynamic_Init, sCall
Control_Dispatch SYS_DYNAMIC_DEVICE_EXIT,
CVxD_Dynamic_Exit, sCall
Control_Dispatch W32_DEVICEIOCONTROL,
CVxD_W32_DeviceIOControl,
sCall, <ebp, ecx, ebx, edx, esi>
clc
ret
EndProc CVxD_Contro
這是一段消息處理函數。
當VxD初始化時,發生SYS_DYNAMIC_DEVICE_INIT的消息而轉入CVxD_Dynamic_Init函數中進行
處理。
W32_DEVICEIOCONTROL是宏來定義的,設備控制程序CVxD_W32_DeviceIOControl,<ebp, ecx,
ebx, edx, esi>是調用函數時用來傳遞參數的寄存器的名字。CVxD_W32_DeviceIOControl是留給應用程
序的接口函數。當VxD的應用運行后,它和VxD進行數據交換就通過此函數來實現。因為它是用匯編編寫
的,所以,所有的參數都使用寄存器來傳遞。
. 接下來可以看見三個函數
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; 內核空閑(IDLE)時調用此函數
;;
?

;; 使用HLT指令可以降低CPU的溫度
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
BeginProc _IDLEHandleProc
mov eax,[EnableHlt]
test eax,eax
jnz NEXT ;查看EnableHlt是否為真,當EnableHlt為真時
;(即允許用降溫功能)轉入NEXT
;當EnableHlt為假時,清去零標志位,返回系統中。
stc
ret
NEXT:
sti ;必須打開中斷, 如果沒有打開中斷,CPU就不能響應中斷,
;就會死機。當中斷打開時,當運行HLT指令后,
;機器一直停止直到外部有一個中斷。
;例如敲鍵或移動鼠標時,當中斷處理寫成后,
;會從下一條指令開始執行,就不會死在HLT指令處。
hlt ;CPU停機,停機就是使得CPU不工作,
;當CPU不工作時,CPU的功耗就很小,所以能降低溫度
stc ;當有事件產生時,清去零標志位,
;返回系統中去處理其程序;
ret
EndProc _IDLEHandleProc
內核空閑(IDLE)時,調用_IDLEHandleProc函數,通過HLT指令達到降溫
的目的。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; 安裝內核空閑(IDLE)時調用的函數
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
BeginProc _InstallIDLEProc
push esi
lea esi,[_IDLEHandleProc]
VMMCall Call_When_Idle
pop esi
mov eax,0
setnc al
ret
?

EndProc _InstallIDLEProc
_InstallIDLEProc函數是在SthVxD.c的CVxD_Dynamic_Init(void)的函數中
被調用的,當它被裝入后,VMM空閑時,就自動地調用函數
_IDLEHandleProc。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; 取消安裝內核空閑(IDLE)時調用的函數
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
BeginProc _UnInstallIDLEProc
push esi
lea esi,[_IDLEHandleProc]
VMMCall Cancel_Call_When_Idle
pop esi
mov eax,0
setnc al
ret
EndProc _UnInstallIDLEProc
_UnInstallIDLEProc函數和_InstallIDLEProc函數一樣,也是在SthVxD.c中
被CVxD_Dynamic_Exit(void)函數調用的。它的作用是去掉VMM注冊的空閑函數。
;**************************************************
; V86模式調用的入口
;
; 把調用轉變成對C函數的調用, 以便于開發功能強大的處理能力
;
;**************************************************
BeginProc CVxD_V86
scall CVxD_V86API,
<[ebp].Client_EAX, [ebp].Client_EBX, [ebp].Client_ECX>
mov [ebp].Client_EAX,eax ; put return code
ret
EndProc CVxD_V86
;**************************************************
; 保護模式調用的入口
;
; 把調用轉變成對C函數的調用, 以便于開發功能強大的處理能力
;**************************************************
?

BeginProc CVxD_PM
scall CVxD_PMAPI,
<[ebp].Client_EAX, [ebp].Client_EBX, [ebp].Client_ECX>
mov [ebp].Client_EAX,eax
ret
EndProc CVxD_PM
VxD_LOCKED_CODE_ENDS
以上CVxD_V86和CVxD_PM兩個函數都在SthVxD.c中定義。它們是V86模式和保護模式調用的入
口,其實,它們的代碼分別為CVxD_V86API和CVxD_PMAPI。此處只有函數定義,沒有具體的實現功
能。
VxD不光可以給WIN32位程序調用,WIN16和DOS程序都可以調用VxD提供的功能,它們都通過中
斷2FH來調用,并設置相應的參數。保護模式與此也是一樣的,不過要用到VxD的號碼。
;*************************************************;
; 不是動態的VxD時,在Window啟動時會被實模式調用
;
;**************************************************
VxD_REAL_INIT_SEG
BeginProc CVxD_Real_Init
xor bx, bx
xor si, si
xor edx, edx
mov ax, Device_Load_Ok
ret
EndProc CVxD_Real_Init
VxD_REAL_INIT_ENDS
END CVxD_Real_Init
以上是一段實模式的初始化調用的函數,主要是用在Windows 9x啟動時。當Windows 9x啟動時,就
會調用到這個函數。這是一個實模式代碼,可以看到代碼段和寄存器是不同的。這不過是一個框架程序,
它會將對匯編的調用全部轉變成對C的調用。這樣就很方便開發程序。
?

第3章 Windows運行機理
3.1 內核分析(11)
在相應的SthVxD.c的代碼中,可以看見其實現方法:
int _stdcall CVxD_V86API(unsigned int function,
unsigned int parm1,
unsigned int parm2)
{
int retcode;
switch (function)
{
case CVxD_V86_FUNCTION1:
retcode = V86Func1(parm1);
break;
case CVxD_V86_FUNCTION2:
retcode = V86Func2(parm1, parm2);
break;
default:
retcode = FALSE;
break;
}
return (retcode);
}
function參數是傳入的功能號。
parm1和parm2是傳入的兩個參數。
程序會根據功能號轉入對應的段運行,很像GUI中的消息處理部分。在此
處,V86Func1(parm1)和V86Func2(parm1, parm2)沒有功能代碼,不過是向大家展示實現框架。
我們還可看到這段的說明,因為Windows 9x中為了省內存,段內的有些代碼和數據在系統啟動完以后
會釋放,也就是說,啟動完成后,這部分代碼就沒有了。
在匯編段中還定義了DeviceIOControl函數。DeviceIOControl這個函數只定義了一些必要傳遞的一批
參數,例如調用的服務號。具體的實現都是在應用程序中完成的,步驟如下:
?

應用程序->DeviceIoControl->內核->由匯編調用-> CVxD_W32_ DeviceIOControl
DWORD _stdcall CVxD_W32_DeviceIOControl( CRS * lpClient,
DWORD dwService,
DWORD dwDDB,
DWORD hDevice,
LPDIOC lpDIOCParms)
{
DWORD dwRetVal = 0;
// DIOC_OPEN is sent when VxD is loaded w/ CreateFile
// (this happens just after SYS_DYNAMIC_INIT)
if( dwService == DIOC_OPEN ){
//Out_Debug_String("SthVxD: WIN32 DEVIOCTL
//supported here!/n/r");
// Must return 0 to tell WIN32 that this VxD
//supports DEVIOCTL
dwRetVal = 0;
}
// DIOC_CLOSEHANDLE is sent when VxD is unloaded w/ CloseHandle
// (this happens just before SYS_DYNAMIC_EXIT)
else if( dwService == DIOC_CLOSEHANDLE ){
// Dispatch to cleanup proc
dwRetVal = CVxD_CleanUp();
}
else if( dwService > MAX_CVxD_W32_API )
{
// Returning a positive value will cause the
//WIN32 DeviceIOControl
// call to return FALSE, the error code can then
//be retrieved
// via the WIN32 GetLastError
dwRetVal = ERROR_NOT_SUPPORTED;
}
else {
//調用功能函數功能號從1開始
dwRetVal=(CVxD_W32_Proc[dwService-1])
(lpClient,dwDDB,hDevice,lpDIOCParms);
}
?

?return(dwRetVal);
}
通常,為了方便,一般的VxD的做法是把函數的指針放在某個結構中,然后通過功能號直接去調用這
個函數就行了。
//DeviceIoControl功能號表
DWORD (_stdcall *CVxD_W32_Proc[])(CRS *,DWORD,DWORD,LPDIOC)=
{
0, //1(未使用)
0, //2(未使用)
CVxD_W32_EnableHalt //3(開關降溫功能)
};
從CVxD_W32_Proc這個函數代碼可以看到,功能一、二是沒有用的,功能三是用來降溫的,功能三
調用CVxD_W32_EnableHalt函數,這個函數用來開關降溫功能。

//
// 開關降溫功能(功能號3)
//

DWORD _stdcall CVxD_W32_EnableHalt(CRS * lpClient,DWORD dwDDB,
DWORD hDevice, LPDIOC lpDIOCParms)
{
LPDWORD lpEnablePtr;
DWORD OldEnable;
OldEnable =EnableHlt;
lpEnablePtr=(LPDWORD)lpDIOCParms->lpvOutBuffer;
if(lpEnablePtr) EnableHlt=*lpEnablePtr;
return(OldEnable);
}
什么地方調用CVxD_W32_EnableHalt函數呢?我們可以從上面的CVxD_W32_DeviceIOControl函數
中看到以下的一段語句:
//調用功能函數功能號從1開始
dwRetVal=(CVxD_W32_Proc[dwService-1])(lpClient,dwDDB,hDevice,lpDIOCParms);
其中,dwService是傳入的服務號,lpClient,dwDDB,hDevice,lpDIOCParms是功能傳入的固定的
參數。
?

第3章 Windows運行機理
3.1 內核分析(12)
我們可從CpuCool.c程序中看到VxD的裝入內存、安裝功能和設置VxD功能號3的全過程。在代碼中都
有詳細的注釋。其實,VxD的裝入方法與一個通常的文件一樣,也是通過Win API的函數CreateFile來完
成,大家一定不要被Create這個詞誤解了,其實,CreateFile可以用來打開和創建新文件。
int APIENTRY WinMain( HANDLE hInstance,
HANDLE hPrevInstance,
LPSTR lpszCmdLine,int nCmdShow)
{
static char szAppName[]="CoolCPU";
char Buffer[64];
HMENU hPopupMenu;
WNDCLASS wndclass;
HWND hwnd;
MSG msg;
int Ret;
hResInstance=hInstance;
//是否是中文
LoadString(hResInstance,IDS_CODEPAGE,Text,sizeof(Text));
Ret=StrToInt(Text);
if(GetSystemMetrics(SM_DBCSENABLED) && GetACP()==(DWORD)Ret)
China=1;
else
China=0;
//取得操作系統的版本
WinNT=GetVersion();
WinNT=(WinNT&0x80000000)==0 ? 1:0;
//如果不是WinNT就打開VxD,因為VxD只能在WIN9X下工作
if(WinNT==0)
?

{//打開VxD
lstrcpy(SthVxDName,".//");
GetStartPath(Buffer,sizeof(Buffer));
//VxD只認短路徑
GetShortPathName(Buffer,&SthVxDName[4],
sizeof(SthVxDName));
lstrcat(SthVxDName,"SthVxD.VxD");
//嘗試打開默認的VxD
hCVxD = CreateFile(".//SthVxD", 0,0,0, CREATE_NEW,
FILE_FLAG_DELETE_ON_CLOSE, 0);
if(hCVxD==INVALID_HANDLE_VALUE)
{
//直接打開全路徑的VxD
hCVxD = CreateFile(SthVxDName, 0,0,0, CREATE_NEW,
FILE_FLAG_DELETE_ON_CLOSE, 0);
}
if(hCVxD==INVALID_HANDLE_VALUE)
{//直接打開默認的.VxD
hCVxD = CreateFile(".//SthVxD.VxD", 0,0,0, CREATE_NEW,
FILE_FLAG_DELETE_ON_CLOSE, 0);
}
//成功否
if(hCVxD!=INVALID_HANDLE_VALUE)
{
EnableHlt=TRUE;
//設置VxD功能號3
DeviceIoControl(hCVxD,3,(LPVOID)NULL,0,
(LPVOID)&EnableHlt,sizeof(EnableHlt),
&cbBytesReturned,NULL);
}
else {
if(China)
{
LoadString(hResInstance,IDS_MAYBEERROR,
Cap,sizeof(Cap));
LoadString(hResInstance,IDS_NOTLOADVxD,
?

Text,sizeof(Text));
MessageBox(NULL,Text,Cap,MB_OK);
}
else MessageBox(NULL,"Can't load STHVxD.VxD,STHVCD maybe failure !",
"Maybe Error",MB_OK);
}
}
hIcon=LoadIcon(hResInstance,MAKEINTRESOURCE(IDI_ICON));
if(China) hPopupMenu=LoadMenu(hResInstance,MAKEINTRESOURCE(IDR_CMENU));
else hPopupMenu=LoadMenu(hResInstance,MAKEINTRESOURCE(IDR_MENU));
hPopMenu=GetSubMenu(hPopupMenu,0);
if(!hPrevInstance)
{
wndclass.style =CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc =(WNDPROC)WndProc;
wndclass.cbClsExtra =0;
wndclass.cbWndExtra =0;
wndclass.hInstance =hInstance;
wndclass.hIcon =hIcon;
wndclass.hCursor =LoadCursor(NULL,IDC_ARROW);
wndclass.hbrBackground =(HBRUSH)COLOR_WINDOW;
wndclass.lpszMenuName =NULL;
wndclass.lpszClassName =szAppName;
RegisterClass(&wndclass);
}
MainWin=hwnd=CreateWindow( szAppName,"CoolCPU",
WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU|
WS_MINIMIZEBOX,
0,0,
240,160,
NULL,NULL,hResInstance,NULL);
ShowWindow(hwnd,SW_HIDE);
?

UpdateWindow(hwnd);
AddShellIcon();
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
DelShellIcon();
DestroyIcon(hIcon);
DestroyMenu(hPopupMenu);
//關閉降溫
if(WinNT==0)
{
if(hCVxD!=INVALID_HANDLE_VALUE)
{
EnableHlt=0;
//設置VxD功能號3
DeviceIoControl(hCVxD,3,(LPVOID)NULL,0,
(LPVOID)&EnableHlt, sizeof(EnableHlt),
&cbBytesReturned,NULL);
}
}
//關閉VxD
if( hCVxD != INVALID_HANDLE_VALUE )
CloseHandle(hCVxD);
return msg.wParam;
}
?

第3章 Windows運行機理
3.1 內核分析(13)
在程序的運行中,可以看見,當程序啟動的時候,并沒有出現窗口,而是在Windows的任務欄的右下
角出現了一個下雨一樣的小圖標,這叫做托盤方法。實現起來也是很簡單,很多資料中都介紹了,這里就
不贅述。
#define WM_ICONCALLBACK (WM_USER+0x1234)
///
//
// 添加任務條Icon
//
///
int AddShellIcon(void)
{
LPBYTE lpszTip;
NOTIFYICONDATA tnid;
BOOL res;
if(China)
{
LoadString(hResInstance,IDS_COOLCPUNAME,
Text,sizeof(Text));
lpszTip=Text;
}
else lpszTip="CoolCPU";
tnid.cbSize = sizeof(NOTIFYICONDATA);
tnid.hWnd = MainWin;
tnid.uID = 1;
tnid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
tnid.uCallbackMessage = WM_ICONCALLBACK;
tnid.hIcon = hIcon;
lstrcpyn(tnid.szTip,lpszTip,sizeof(tnid.szTip));
?

?res = Shell_NotifyIcon(NIM_ADD, &tnid);
return res;
}
///
//
// 刪除任務條Icon
//
///
int DelShellIcon(void)
{
NOTIFYICONDATA tnid;
BOOL res;
tnid.cbSize = sizeof(NOTIFYICONDATA);
tnid.hWnd = MainWin;
tnid.uID = 1;
res = Shell_NotifyIcon(NIM_DELETE, &tnid);
return res;
}
///
//
// 窗口處理函數
//
///
long APIENTRY WndProc( HWND hwnd,UINT message,UINT wParam,
LONG lParam)
{
POINT ptCurrent;
PAINTSTRUCT ps;
switch(message)
{
case WM_PAINT:
BeginPaint(hwnd,&ps);
EndPaint(hwnd,&ps);
return 0;
case WM_ICONCALLBACK: //任務條Icon回調消息
switch(lParam)
?

?{
case WM_LBUTTONDBLCLK:
case WM_LBUTTONDOWN:
case WM_RBUTTONDOWN:
GetCursorPos(&ptCurrent);
SetForegroundWindow(hwnd);
//顯示菜單
TrackPopupMenu( hPopMenu,
TPM_RIGHTBUTTON,
ptCurrent.x,
ptCurrent.y,
0,
hwnd,
NULL);
break;
}
return 0;
case WM_INITMENUPOPUP:
if(lParam==0)
{
if(WinNT==0 && hCVxD!=INVALID_HANDLE_VALUE)
{
if(EnableHlt)
CheckMenuItem((HMENU)wParam,
ID_COOLCPU,
MF_BYCOMMAND|MF_CHECKED);
else
CheckMenuItem((HMENU)wParam,
ID_COOLCPU,
MF_BYCOMMAND|MF_UNCHECKED);
}
else
EnableMenuItem((HMENU)wParam,ID_COOLCPU,
MF_BYCOMMAND|MF_GRAYED);
}
return 0;
case WM_COMMAND:
switch(wParam)
{
?

?case BN_CLICKED:
break;
case ID_EXIT:
PostMessage(hwnd,WM_CLOSE,0,0);
break;
//Cool Cpu
case ID_COOLCPU:
if(WinNT==0)
{
if(hCVxD!=INVALID_HANDLE_VALUE)
{
EnableHlt^=1;
DeviceIoControl(hCVxD,3,
(LPVOID)NULL,0,
(LPVOID)&EnableHlt,
sizeof(EnableHlt),
&cbBytesReturned,NULL);
}
}
break;
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_CLOSE:
break;
}
return (DefWindowProc(hwnd,message,wParam,lParam));
}
我們可以通過Windows的系統資源監視器看到,當降溫程序打開時,CPU的占用率會馬上提高,當降
溫程序關閉時,CPU的占用率又馬上恢復原值。這是因為系統資源監視器也是通過空閑時調用的方法實現
的,所以當降溫程序工作時,CPU就會暫停了,就好像是占用了很多的資源。
?

第3章 Windows運行機理
3.2 消息的運行方式(1)
3.2.1 認識消息
我們首先從16位的Windows來認識消息。在16位時代,Windows的整個內核是32位的、分時的、搶占
的。可以從Windows的內核模型得知,有兩種VM,一種是 SYSTEM VM,另一種是DOS的VM。一個系統
中可以運行很多的DOS窗口,因為在16位的時代,能運行DOS的程序是很重要的,所以在當
時,Windows的主要任務之一,就是能同時運行很多DOS窗口。Windows的內核實現上用了很多微內
核,而微內核的工作很多都是靠消息來完成的。
在系統內部我們可以看到Windows的消息內核原理,消息結構如圖3.4所示。
可以看到,所有的功能還是通過中斷來實現的,只不過是在保護模式內調用中斷。在Windows的16位
時代,大部分的工作都是基于各種中斷的基礎上,而且應用程序可以直接調用DOS中斷。其
實,Windows的內核和DOS是平等的,包括設備驅動,也是和DOS應用程序是同一級別的。這樣,這個
系統的VM中的DLL就直接調用中斷來進行管理,當調用中斷時,就會用VxD或.386文件,對中斷或IO進
行截取,來模擬直接操作硬件的工作。其實,它的驅動也是一個DLL,和USER.DLL、GDI.DLL是一樣
的。
圖3.4 消息結構
//一直等待消息,直到有消息發生時
while (GetMessage(&msg, NULL, 0, 0))
{
//翻譯消息
TranslateMessage(&msg);
?

?…
//分配消息到對應的窗口
DispatchMessage(&msg);
}
通過以上代碼可以看出,在Windows 16位時代中,實現消息調度的函數是GetMessage。還有一種函
數是PeekMessage,它會從消息中取出一條消息,但它和GetMessage不同的是,當消息隊列中有消息
時,它會返回函數;沒有消息時就會返回0。而GetMessage就會一直停止在這些函數上,直到有消息為
止。
到Windows 32位時,消息的運行機理就不相同了。從內核中可以看出,有一個Win32的VxD,
把DOS的搶占分時都放在這個VM中完成,系統VM就進一步和系統底層融合。然后在這個基礎上分出時間
片。這樣,每個應用程序就自己有自己的消息隊列。
所有的消息隊列看上去是放在USER32的模塊內,但每個應用程序自己有一個USER32,因為每個應
用程序在內存內都是從4000000B(也就是4MB的位置開始的),這樣,每
個GetMessage和PeekMessage都在處理事件。實際上,每個GetMessage就會成為一
個WaitsingleMessage,當有事件來后,就直接進行處理,也不用做什么調度。因為自己完成自己的消息處
理,每個程序都是獨立的,所以要用底層內核來實現頁面的切換。它某一程序切入時,其他程序就會被切
出。當切換出去時,整個消息隊列也就被切換出去了。所以,整個消息的處理就很簡單了。
Windows 32位時的消息機理如圖3.5所示。
圖3.5 消息的機理
?

第3章 Windows運行機理
3.2 消息的運行方式(2)
3.2.2 Windows系統中消息的運作方式
1. 消息循環
在Windows程序的經典設計程序中,可以看到如下程序:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
TCHAR szHello[MAX_LOADSTRING];
LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst,(LPCTSTR) IDD_ABOUTBOX,
hWnd, (DLGPROC)About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message,
wParam, lParam);
}
?

?break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Add any drawing code here...
RECT rt;
GetClientRect(hWnd, &rt);
DrawText(hdc, szHello, strlen(szHello),
&rt, DT_CENTER);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam,
lParam);
}
return 0;
}
首先有一個GetMessage,只要這個消息不為0,就可以一直循環,當有消息來時,就通過跳轉到消息
處理函數來完成相應的功能。這樣做有一個好處,例如,在DOS中,按鍵、鼠標消息都是放在鍵盤緩存區
和鼠標緩存區中的,現在就可以直接將輸入放入消息隊列中。消息的結構如下:
typedef struct tagMSG { // msg
HWND hwnd; //發送給的對應窗口句柄
UINT message; //消息的類型
WPARAM wParam; //消息傳送第一個32位參數,
LPARAM lParam; //消息傳送第二個32位參數
DWORD time; //發送消息的時間
POINT pt; //發送消息時鼠標所在的位置
} MSG;
從以上結構中可以看到,每個消息都對應著一個窗口。USER模塊是管理窗口的,一般每個窗口自己
有一個消息隊列。當鍵盤或鼠標有消息時,就會發給激活的窗口,當在程序設計中用SendMessage來發
送消息時,就會明確指定窗口句柄,當運行此函數后,就會把消息放到此窗口的消息隊列中。
所有的程序都通過系統消息隊列來調用USER的DLL來完成工作,所以,這個DLL就有機會輪循,來
查看什么程序有消息。如果某程序有消息,就會去調用這個程序的窗口函數,而這個窗口在生成時,必須
注冊在這個窗口類中。在窗口類中就有窗口的處理消息函數的地址指針。當程序有消息時,USER就調用
這個函數的地址,去完成消息處理。
?

在鍵盤或鼠標這類設備中,消息一般只是發給當前激活的窗口,當然其他窗口也可以得到消息,可以
通過程序直接發送。還有一些情況也可以得到消息,例如時鐘消息TIMER是底層驅動的,當TIMER產生一
個消息時,它會查找當前窗口中定義了時鐘消息的時間是否來到,當時間到了,就會在對應的窗口函數中
放入一時間消息事件。
其實,明白了消息的處理過程,消息也就很簡單了。消息不過是定義一個結構,定義一堆ID,在程序
運行中調用switch和case去完成相應的功能。
2. 消息處理函數
有兩種消息的發送函數,一種是立即發送消息,另一種是隊列調用。
LRESULT SendMessage(
HWND Hwnd
UINT uMsg,
WPARAM wParam,
LPARAM lParam );
LRESULT PostMessage(
HWND Hwnd
UINT uMsg,
WPARAM wParam,
LPARAM lParam );
這兩種函數的接口參數基本上是一樣的。
. HWnd:將要發送給消息的對應的窗口句柄。它實際指向消息發給誰。
. UMsg:消息的類型,說明被發送的消息是什么消息。
. WParam:第一個32位的參數。
. lParam:第二個32位的參數。
可不能小看這兩個參數,它們可是很有用的。
. SendMessage:當用它向一個窗口(也可以是本身窗口)發送消息時,它不會把消息放入消
息隊列中,而是直接發送給窗口。窗口接到消息后就立刻處理,處理完成后,把結果作為返回值
傳送回來。這樣的處理過程就像是操作函數一樣。
. PostMessage:當用它向一個窗口(也可以是本身窗口)發送消息時,它把消息放入消息隊
列中,自己什么也不干就會返回,到底消息什么時候處理,有沒有被處理它是不知道的。
?

第3章 Windows運行機理
3.2 消息的運行方式(3)
3.2.3 消息處理過程實例
我們已經對消息有了些了解,那到底消息是什么呢?其實,消息不過是定義了一個結構(在微軟中定
義的是MSG結構,自己也可以定義不同的結構),然后定義一堆ID號,例如:
#define WM_MSG01 0X0001
…..
#define WM_MSG** 0X*****
調到函數中,用swicth 和case語句對每一種ID進行相應的處理。
在Windows的編程中,有一個很經典的程序“Hello World”。我們也用這個最簡單的程序來說消息的處
理過程。這個程序可以直接在VC中用向導生成。首先,任何一個Windows程序都是從WinMain開始的。
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// TODO: Place code here.
MSG msg;
HACCEL hAccelTable;
// Initialize global strings
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_AA, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// Perform application initialization:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_AA);
?

?// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
大家可能對這段代碼已很熟悉了,在以前用API進行Windows編程時,幾乎所有的程序都會去套用這
個框架。
從一個WinMain中注冊一個窗口,其他再用GetMessage取得窗口的消息,翻譯后分給對應的窗口。
其實,很多程序可以完全不用注冊窗口。它只要做一些事件,當有事件來時,就處理相應的事件。例
如,以下就是一個Windows程序,其中沒有用到任何消息循環,只是在運行中彈出一個對話框:
//-------------------------------------------------
// HelloMsg.c -- Displays "Hello, Windows 98!" in a
//message box
// -----------------------------------------------
#include <windows.h>
int WINAPI WinMain ( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
MessageBox (NULL, TEXT ("Hello, Windows 98!"),
TEXT ("HelloMsg"), 0) ;
return 0 ;
}
可以看到,這個WIN32的程序就沒用到微軟的框架,它完全是自己做自己的事,直到被中止。
實際上,很多應用可以不用窗口,例如,Windows NT的服務程序就不需要窗口。
?

第3章 Windows運行機理
3.3 GDI的結構和組成(1)
3.3.1 GDI的組成
GDI有一些基本的函數。GDI內只有和HDC有關的幾個做圖的函數,更多的功能其實是在USER32內
實現的。所以我們說的GUI是GDI和USER接合起來的。
GDI在Windows內只是劃一些點、線。點線填充時,USER不但要管理窗口和字體等許多資源(大多
數GDI函數都和HDC有關),USER還要管理很多窗口,并且管理窗口的裁減和輸出。在每個程序中,它
所管理的屏幕就好像USER完全占用了所有的窗口,和其他程序在顯示屏上不沖突。在這個程序內
部,USER好像自己擁有一個顯示屏一樣。在HDC中是通過裁剪來對窗口進行管理的。
HDC是一個很大的結構,一般系統內沒有很多的HDC。在Windows 3.1中,只有5個系統的HDC,
在Windows 98下又擴充了幾個,它們組成了一個HDC的池。當系統要使用HDC的資源時,系統會隨機地
從這5個中選取其中一個沒有被占用的分配給用戶來使用,所以,當用戶使用HDC后,一定要釋放HDC資
源,要不就有可能導致系統的資源不足。這一點在Windows 3.1中很明顯,但Windows 95可以自己
用CreateDC動態地創建,當完成使用后,用DeleteDC函數來刪除HDC的系統。
下面是DC的數據結構,這個結構微軟是保密的,我是從開放源碼中獲得的,大家可以研究一下。
typedef struct tagDC
{
GDIOBJHDR header;
HDC hSelf; /* Handle to this DC */
const struct tagDC_FUNCS *funcs; /* DC function table */
PHYSDEV physDev; /* Physical device */
/*(driver - specific) */
INT saveLevel;
DWORD dwHookData;
FARPROC16 hookProc; /* the original SEGPTR */
DCHOOKPROC hookThunk; /* and the thunk to call it */
INT wndOrgX; /* Window origin */
INT wndOrgY;
INT wndExtX; /* Window extent */
INT wndExtY;
INT vportOrgX; /* Viewport origin */
INT vportOrgY;
?

?INT vportExtX; /* Viewport extent */
INT vportExtY;
int flags;
HRGN hClipRgn; /* Clip region (may be 0) */
HRGN hVisRgn; /* Visible region (must never be 0) */
HRGN hGCClipRgn; /* GC clip region(ClipRgn AND VisRgn) */
HPEN hPen;
HBRUSH hBrush;
HFONT hFont;
HBITMAP hBitmap;
HANDLE hDevice;
HPALETTE hPalette;
GdiFont gdiFont;
GdiPath path;
WORD ROPmode;
WORD polyFillMode;
WORD stretchBltMode;
WORD relAbsMode;
WORD backgroundMode;
COLORREF backgroundColor;
COLORREF textColor;
short brushOrgX;
short brushOrgY;
WORD textAlign; /* Text alignment from
SetTextAlign() */
short charExtra; /* Spacing from
SetTextCharacterExtra()*/
short breakTotalExtra; /* Total extra space
for justification */
short breakCount; /* Break char. count */
short breakExtra; /* breakTotalExtra breakCount */
short breakRem; /* breakTotalExtra % breakCount */
RECT totalExtent;
BYTE bitsPerPixel;
?

INT MapMode;
INT GraphicsMode; /* Graphics mode */
ABORTPROC pAbortProc; /* AbortProc for Printing */
ABORTPROC16 pAbortProc16;
INT CursPosX; /* Current position */
INT CursPosY;
INT ArcDirection;
/* World - to - window transformation */
XFORM xformWorld2Wnd;
/* World - to - viewport transformation */
XFORM xformWorld2Vport;
/* Inverse of the above transformation */
XFORM xformVport2World;
/* Is xformVport2World valid? */
BOOL vport2WorldValid;
} DC;
所以可以看到,以上這些結構和與之相關的函數就是一個“類”。在MFC中就是CDC類。CDC也就是把
所有的與DC有關的函數進行了封裝。
在Windows的應用程序中,當創建一個窗口時,如果用OwnDC屬性,就是使用窗口自己的DC。窗口
創建時,自己創建和管理自己的靜態DC,這樣就不使用系統的資源。
在Windows 95中,HDC的線、字體、刷子都是一種共享的屬性。也就是說,兩個應用程序中,當一
個程序改變了HDC的屬性,例如背景顏色時,另一個程序的背景顏色也會發生改變,這就是為什么使
用HDC的資源前,一定要保存原始的值,當用完后就立刻恢復的原因。
但在Windows NT中就不是這樣了。此時,每一個DC都是私有的,所以很多程序在Windows 95中能
正常運行,但在Windows NT中就不能正常運行了。
?

第3章 Windows運行機理
3.3 GDI的結構和組成(2)
3.3.2 GDI和DirectDraw的關系
屏幕上的顯示在內存中是以下這樣的結構。
當向顯示緩存區中寫入數據時,就會顯示相應的圖像。DirectDraw的作用是創建,其實就是取得緩存
區的地址,并且還能創建一個虛擬的緩存區內存。例如,A區域內存可以在主內存中創建一塊
叫offscreen的緩存區。
如果顯示卡的內存比較大,如圖3.6所示,有一塊區域是映像到屏幕上的可見區域,還有的顯存區域是
屏幕上看不見的,這個區域被稱為offscreen。也就是說,A區域為主顯存,B區域也可以稱為次顯存。B區
域實際上是被隱藏在后面的,就像DOS的游戲一樣,先在次顯存繪制好圖形,當需要顯示時,馬上就可以
切換過來。DirectDraw中有一個這種操作函數,這個命令如果能切換,就直接切換,如果不能直接地切
換,就直接通過顯示卡,從次緩存復制到主緩存,這種在顯卡內的復制要比軟件的memcpy命令快很多。
圖3.6 顯示內存圖
把兩個緩存區域結合起來用就可以做出高速的動畫。例如,游戲可以先在次顯存上繪制好下一幀畫,
一切換就能立刻顯示出來。這樣,畫面的速度就很快了。
如圖3.6所示,當向A地址寫入一個數據時,對應的屏幕上就會出現一個點。
如果需要快速地顯示圖像,就不能用GDI,而應直接使用DirectDraw。它的缺點就是你必須對顯示卡
有充分的了解。顯示卡可以分為很多種模式,如表3.2所示。
表3.2 顯示卡的模式
顏色數 內存位數 顏色位數分配 字節數
16色 4位 Index索引 1/2
256色 8位 Index索引 1
15位色 16位 5,5,5 2
16位 16位 5,6,5 2
24位 24位 8,8,8 3
32位 32位 8,8,8,8 4
?

當用GDI顯示一個圖像時,就不用管顯示卡是什么模式,只要設置好顏色,發送一個繪制命令即可。
如果一個圖是15位色,當把圖形數據直接復制到對應的顯存區域時,此時圖形就被顯示出來了。如果
用GDI來顯示圖形時,它會將相應的色彩進行轉換,把它轉換成顯示所支持的,這個過程需要用一點時
間。
DirectDraw只是提供了一種方法,直接地向顯存寫入數據。在寫數據進入顯存比較慢時,可能會出現
裂縫的圖像顯示。這是因為當上幀已顯示完成了,此時次顯存向主顯存復制數據。
當把一個24位的圖像用DirectDraw直接向顯存中寫入時是不正常的,但GDI就會沒問題。
要想在16位模式中顯示24位的圖形,就需要通過程序進行轉化。下面是轉化的程序。
//24 位 R G B (8 8 8) 16位 R G B(6 5 6)
void Convert24To16 (LPBYTE lpInDate,LPBYTE lpOutDate,
const int nSize)
{
int i ;
int nData;
BYTE R,G,B;
for (i = 0; i < nSize; i++){
nData = *((int *)lpInDate);
R = nData >> 3;
G = nData >> 10;
B = nData >> 19;
nData = B|(G<<5)|(B<<11);
*((int *)lpOutDate) = nData;
}
}
?

第3章 Windows運行機理
3.4 線程的機制(1)
3.4.1 線程的工作方式
線程是Windows 95的新特征,一個線程就是一個執行程序的事例。線程允許一個程序同時在多于一個
以上的地方運行,這有些像多個CPU,每一個CPU執行程序的一部分。在單處理器系統中(Window 95只
支持單處理器系統),只有同時處理時才出現線程。Windows 95系統中,線程之間切換CPU的間隔稱為
時間片(timeslicing)。因為硬件內部的計時器是以有規律的時間間隔通知操作系統的,所以操作系統可
以選擇不同的線程。另外,盡管16位的程序作為一個線程出現在系統線程表中,但只有Windows 32應用
程序中能產生附加的線程。
一個線程被切換有兩個原因,原因之一是本線程需要另一個線程先執行,此時,當前線程則把CPU讓
給另一個線程。另一個原因是當一個線程執行了足夠長的時間后,需要把線程給另一個程序。Windows
95線程調度使用的是這樣的一種算法,即把大部分時間給那些急需的線程。CPU時間間隔用硬件時鐘中
斷,操作系統內部計時器中斷處理調度決定另一個程序是否需要運行,如果運行,則切換到另一個線程
上。Windows 95的時間片是20毫秒,也就是說,一秒鐘內,理論上可在50個線程之間進行強制切換,但
如果所有的線程都主動放棄CPU或等待系統,則切換的頻率就會很高,每秒切換4、5千次也不奇怪。
每一個線程被分配到一個進程中,當操作系統產生一個新的進程時,也要設置一個初始線程。一個進
程中的所有線程共享該進程的資源(下面要用“資源”一詞來表示操作系統提供的內容),進程資源包括內
存文本、文本柄和當前目錄。
一般來講,進程不交換,也不使用其他進程的資源。然而,一個進程中的多線程可能在進程資源的使
用上發生沖突,這樣,資源共享可能是一個混合物。例如,程序有一段代碼改變了幾個全局變量的代碼序
列,如果一個線程正好在這個序列中間被切換掉,那么下一個線程將作用這些全局變量,而且與狀態不一
致。成功地執行多線程程序要求你標記出一個進程中的所有的資源,這些資源需要由同步機進行監視,保
證它們不會被不適宜的線程侵害。臨界段(CriticalSection)和其他的線程同步機在下面進行討論。
盡管線程共享進程資源,但每一個線程還有一定的資源提供自身,那么最重要的是棧嗎?
不,每一個線程本身沒有SS寄存器和相互依存,實際上,每一個線程在本身所在進程的地址空間內部
有一個地址空間區。每一線程被分配的棧區隱含值是1MB,這個容量要么在可執行文件的.DEF文件棧
中,要么在調用CreateThread產生線程規定一個非零棧區。Windows 95對每一個線程棧不使用MB,而是
用“guardpage(保護頁)”。
3.4.2 線程與GDI的沖突:死機的主要原因
很多人使用線程的時候,都喜歡在線程內畫圖。如果在線程內作畫,程序就會很容易出錯,而且還是
?

那種沒有任何響應和提示的錯誤問題。
例如,如下是一個文件復制的程序,這個程序由兩個線程組成,一個是復制文件的線程,另一個是顯
示文件復制進度的過程。當文件復制一部分后,進度條就向前移動一點。理論上,這個程序沒什么問題。
但是,這個程序有一個很大的隱患,即主程序也可能某一時刻要更新這個進度條。例如,進度被其他窗口
擋住后或者整個窗口放大縮小時,整個窗口就要刷新,這時,線程的那個部分也要刷新它,操作系統也要
刷新它。這樣,三個部分都要去刷新它,程序就很容易死鎖。程序運行界面如圖3.7所示。
圖3.7 程序運行界面圖
這時會什么響應也沒有了。這種問題在多線程中是很常見的。那怎么處理這個問題呢?
有一條原則,即程序中的線程一概不直接操作線程部分中的GDI。它只要發一個消息給主程序,讓主
程序來繪制圖形,就不會出現任何的問題了。
發送消息的方法就是用PostMessage的函數。但一定不能用SendMessage。因為用PostMessage可
以讓主程序去調度繪圖,而SendMesage會立即去繪制圖形。所以在線程中要避免畫圖,因為當作畫時,
程序會取得一個DC,內存中的DC表示的是一塊顯存。DC代表的是一個窗口,因為一個程序得到
此DC時,其他程序是不能再取得DC的。以后,如果繼續再取,就會進入死鎖的循環內。死鎖結構如
圖3.8所示。
?

第3章 Windows運行機理
3.4 線程的機制(2)
3.4.3 線程的內存泄漏的主要原因
在很多參考書上,都說不要用CreateThread 創建線程、并用CloseHandle來關閉這個線程,因為這樣
做會導致內存泄漏,而應該用_beginthread來創建線程,_endthread來銷毀線程。其實,真正的原因并非
如此。看如下一段代碼:
HANDLE CreateThread(
// 線程安全屬性
LPSECURITY_ATTRIBUTES lpThreadAttributes,
// 堆棧大小
DWORD dwStackSize,
// 線程函數
LPTHREAD_START_ROUTINE lpStartAddress,
//線程參數
LPVOID lpParameter,
// 線程創建屬性
DWORD dwCreationFlags,
// 線程ID
LPDWORD lpThreadId
);
線程中止運行后,線程對象仍然在系統中,必須通過CloseHandle函數來關閉該線程對
象。CloseHandle函數的原型是:
BOOL CloseHandle(
HANDLE hObject // 對象句柄
);
CloseHandle可以關閉多種類型的對象,比如文件對象等,這里使用這個函數來關閉線程對象。調用
時,hObject為待關閉的線程對象的句柄。
說用這種方法時內存在泄漏,其實不完全正確。那為什么會引起內存的泄漏呢?因為當線程的函數用
到了C的標準庫的時候,很容易導致沖突,所以在創建VC的工程時,系統提示是用單線程還是用多線程的
庫,因為在C的內部有很多的全局變量。例如,出錯號、文件句柄等全局變量。
因為在C的庫中有全局變量,這樣用C的庫時,如果程序中使用了標準的C的庫時,就很容易導致運行
不正常,會引起很多的沖突。所以,微軟和Borland都對C的庫進行了一些改進。但是這個改進的一個條件
?

就是,如果一個線程已經開始創建了,就應該創建一個結構來包含這些全局變量,接著把這些全局變量放
入線程的上下文中和這個線程相關起來。這樣,全局變量就會依賴于這個線程,不會引起沖突。
這樣做就會有一個問題,什么時候這個線程開始創建呢?標準的Windows的API是不知道的,因為它
是靜態的庫。這些庫都是放在VC的LIB的目錄內的,而線程函數是操作系統的函數。所以,VC和BC在創
建線程時,都會用_beginThread來創建線程,再用_endThread來結束線程。這樣,它們在創建線程的時
候,就會知道什么時候創建了線程,并把全局變量放入某一結構中,讓它和線程能關聯起來。這樣就不會
發生沖突了。
很顯然,要完成這個功能,首先需要分配結構表把全局變量包含起來。這個過程是在_beginThread時
做的,而釋放在_endTread內完成。
所以,當用_beginThread來創建,而用CloseHandle來關閉線程時,這時復制的全局結構就不會被釋
放了,這就有了內存的泄漏。這就是很多資料所說的內存泄漏問題的真正的原因。
其實,可以不用_beginThread和_endThread這一對函數。如果用CreateThread函數創建,
用CloseHandle關閉,那么,與C有關的庫就會用全局的,它們會引起沖突。所以,比較好的方法就是在
線程內不用標準的C的庫(可以使用Windows API的庫函數)。這樣就不會有什么問題,也就不會引起沖
突。例如,字符串的操作函數、文件操作等。
當某個程序創建一個線程后,會產生一個線程的句柄,線程的句柄主要用來控制整個線程的運行,例
如停止、掛起或設置線程的優先級等操作。一般來說,當線程啟用后,就會用線程的CloseHandle來關閉
線程。但在微軟的示例程序中,有一個例子創建以后,就馬上調用CloseHandle關閉線程的運行。這樣做
在Windows 98下沒什么問題,但在Windows NT下,內核就會出現錯誤。這是為什么呢?
這是因為雖然線程有關的結構已經釋放了,但線程還在運行中,所以程序就會出現錯誤。那怎么做才
能確保正常運行呢?
其實,要正常運行,可以讓線程完全結束以后,再調用CloseHandle來釋放資源。
怎樣知道線程完全結束呢?在Windows 的API中有一類等待線程的命令:
DWORD WaitForSingleObject(
HANDLE hHandle, // handle to object to wait for
DWORD dwMilliseconds // time-out interval in milliseconds
);
DWORD WaitForMultipleObjects(
DWORD nCount, // number of handles in the handle array
CONST HANDLE *lpHandles, // pointer to the object-handle array
BOOL fWaitAll, // wait flag
DWORD dwMilliseconds // time-out interval in milliseconds
);
可以用以上兩函數,等待線程的結束。如果線程結束,函數就會返回。否則就一直等待,直到指定的
時間結束。
還有一種線程根本不會退出,它一直運行著循環的線程。我們就要用中止線程的方法來結束線程的運
?

行,強制把它關閉。強制關閉后,再用CloseHandle來釋放結構。
3.4.4 進程管理
Win16中,一個正在運行的程序被稱為一個任務(task),16位的KERNEL把每一個Win16任務的信
息保持在一個叫任務數據庫(TDB)的段內,任務數據庫的選擇器被認為是一個HTASK,通過它可獲知
正在執行任務的API。
Windows 95中,針對32位程序做了什么改進呢?它把一個運行的程序稱為一個進程而不是一個任務,
每一個進程運行在自己的地址空間內。它們可以看到自己的內存和操作系統,而看不到其他的進程或其他
進程的空間。使進程相互之間保持分離的基本原因是防止有問題的進程影響其他進程。
在Win32程序中,給WinMain的hPrevInstance參數總是為0。不管其他程序是否運行,一般情況下,
一個進程自認為系統中只有該程序在運行。當然,如果你確實需要與另外的進程通信(或是去操作另一個
進程),也是很容易的,這在編寫代碼之前就要考慮到。
每一個Windows 95進程在系統中被分配一個單一值。這個值為進程ID,一個程序可以通
過GetCurrentProcessID函數獲取自己的進程ID。這個進程ID非常近似于一個Win16 HTASK。NT中的進
程ID分配給系統數據結構,因為典型的進程ID值是數字的,所以Windows 95中的進程ID的值比較高,并
且是隨機的。一個進程ID可以通過轉換獲取一個指示器,該指示器指向KERNEL32.DLL,用于跟蹤進程的
進程數據庫結構。
?

第3章 Windows運行機理
3.4 線程的機制(5)
e_lfanew是相對實際PE頭標的相對偏移量(或RVA)。要得到內存中一個指向PE頭標的指針,只需
將該域的值與映像的基相加:
//Ignoring typecasts and pointer conversion issues for clarity…
pNTHeader= dosHeader + dosHeader->e_lfanew;
其他字段的意義是和DOS頭有關的字節,這里沒有什么大的作用,就不做介紹了。
2. IMAGE_NT_HEADERS
主PE頭標是一個IMAGE_NT_HEADERS類型的結構,該類型在WINNT.H中定義。
在內存中,Windows中把IMAGE_NT_HEADERS結構作為它內存中的模塊數據庫。在Windows中,
每個被裝入的EXE或DLL都用一個IMAGE_NT_HEADERS結構來說明。其結構如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature表示此文件所表示的類型,其意義定義如下:
#define IMAGE_DOS_SIGNATURE 0x4D5A // MZ
#define IMAGE_OS2_SIGNATURE 0x4E45 // NE
#define IMAGE_OS2_SIGNATURE_LE 0x4C45 // LE
#define IMAGE_NT_SIGNATURE 0x50450000 // PE00
如果是PE格式,則Signature為PE/0/0(PE后跟兩個0)。
3. IMAGE_FILE_HEADER
PE頭標中緊隨PE的WORD記號的是一個IMAGE_FILE_HEADER類型的結構,如下所示:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
?

?DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
這個結構的域只包含了關于文件的最基本的信息。
Machine表示該文件運行所要求的CPU,有如下的CPU ID定義:
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c
// Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162
// MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166
// MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168
// MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169
// MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184
// Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0
// IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2
// SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4
// SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6
// SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0
// ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200
// Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266
// MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366
// MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466
?

// MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284
// ALPHA64
#define IMAGE_FILE_MACHINE_AXP64
//IMAGE_FILE_MACHINE_ALPHA64
NumberOfSection表示在EXE或OBJ中的節數。這個很重要,因為它直接表示節表數組的大小。
TimeDateStamp表示連接器生成該文件的時間。該值是指從1969年12月31日下午4點整開始至文件生
成時之間的秒數。
PointerToSymbolTable表示文件的COFF符號表的偏移量。該域只用在OBJ文件和帶有COFF調試信
息的PE文件中,此信息只在調試文件中有用。
NumberOfSymbols表示在COFF符號表中的符號數目,參見前一個域,此信息只在調試文件中有用。
SizeOfOptionalHeader表示緊跟該結構之后的一個可選頭標的大小。在可執行文件中,它是緊隨該結
構的image_file_header結構的大小。這個值必須有效。
Characteristics表示文件的信息化標記。一些重要的域描述如下:
// Relocation info stripped from file.
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001
// File is executable (i.e. no unresolved external references).
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002
// Line nunbers stripped from file.
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004
// Local symbols stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008
// Agressively trim working set
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010
// App can handle >2gb addresses
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020
// Bytes of machine word are reversed.
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080
// 32 bit word machine.
#define IMAGE_FILE_32BIT_MACHINE 0x0100
// Debugging info stripped from file in .DBG file
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200
// If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400
// If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800
// System File.
#define IMAGE_FILE_SYSTEM 0x1000
?

// File is a DLL.
#define IMAGE_FILE_DLL 0x2000
// File should only be run on a UP machine
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000
// Bytes of machine word are reversed.
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000
我們常見的意義如下。
. 0x0001:該文件中沒有重定位。
. 0x0002:文件是一個可執行的映像(即不是一個OBJ或LIB)。
. 0x2000:文件是一個動態連接庫,不是一個程序。
?

第3章 Windows運行機理
3.4 線程的機制(3)
當Windows 95進程工作時,不用跟蹤進程ID。實際上,大部分相關進程API函數期望一個HANDLE參
數,通常稱做hProcess。hProcess與某些事情(Win16任務數據庫)沒有直接的關聯,與進程ID不一樣,
可有多重獨特的hProcess值,但都屬于同一個進程。
KERNEL32對象句柄
句柄滲透著Win32 API。一個句柄就是當需做某件事情時,從操作系統返回給API函數的一個
魔數(Magic Value)。理論上講一句柄值對應用程序是無意義的,只有操作系統知道如何去解
釋它(幾乎所有Win16程序的句柄值可被解釋為選擇器值或指針)。
當用KERNEL32 API工作時,大部分句柄屬于調用KERNEL32的句柄。KERNEL32句柄有專
門屬性,比如可傳遞給對象WaitforSingleObject這樣的函數。KERNEL32的對象句柄包括進程
柄、線程柄、文件柄、Mutex柄等。
一個KERNEL32句柄只有在進程自身內部有效,企圖將一個進程柄用于另一個進程是沒有意
義的。盡管句柄在理論上是透明的,但對一應用程序而言,將一句柄轉換成有用的對象指針是可
能的。
Windows 95中最基本的進程函數是CreateProcess,這是模擬Win16 WinExec和LoadModule函數,且
這兩個函數仍存在于Windows 95中,但其內部有些改變。如果需要查詢或操作后來的進程,則應使
用CreateProcess,即可反饋給你一個hProcess HANLE。
因為WinExec和LoadModule沒有hProcess和HANDLE的概念,所以不能返回hProcess。實際上,這
兩個函數調用CreateProcess以后,立即關閉了CreateProcess返回的hProcess,這樣做的目的是防止為
那些聯系緊密且無必要的進程分配系統資源。
請記住,關閉一個處理并不意味著結束這個進程,相反你可通過特殊處理到該進程進行訪
問,當進程結束和所有的處理被關閉時,操作系統仔細地清除相關進程資源。
除了產生一進程獲取一個hProcess外,另一個方法是有效的進程ID去調用OpenProcess。
用hProcess可以做一些基本的進程查詢和操作。在進程控制的范圍,一個程序可以
用TerminateProcess中止另一個進程,用SetPriorityClass影響另一個進程的執行優先權。
學習一下Windows mirror KERNEL是很有趣的,在進程的任務區,每一個Win32進程有16位任務數據
庫(TDB),并把TDB連接到TDB鏈上。如果你用TOOLHELP瀏覽這個任務表,則會看到除了這個16位
任務外,每一個正在運行著的Win32程序也有一個TDB,TDB有8個字節的文件名,可重新調用。
除了TDB以外,對16位或32位進程而言,Windows中的所有TDB(包括Win32進程的TDB)還有一
個PSP。和Windows 3.x不一樣,Window 95 TDB中的PSP沒有必要跟著TDB立即進入內存,
?

在TDB和PSP之間的100h字節存放當前目錄區,這個區可有效地保存Window 95支持的足夠大的長文件名
和路徑名目錄,Windows 3.x中當前目錄存放在TDB內一個只有65字長的區域內。
3.4.5 同步機制
1. 進程與線程同步
同步的意思是一個程序保證在不適宜地被切換時,不會出問題,雖然Windows 3.1有多任務,但沒有
真正的同步基礎,因為這些多任務是協作多過調用API函數(如GetMessage和PeekMessage)。如果一
個程序調用了GetMessage或Peekmessage,則意思是說“現在我處在可中斷狀態”。
Win32程序沒有這樣的協作多任務。它們必須做好隨時被CPU切換掉的準備,一個真正的Win32程序
不會耗盡CPU時間等待某些事件發生,Win32 API有四個主要的同步對象:
. Event 事件
. Seqmaphore 信號器
. Mutexes 互斥
. Critical Section 臨界段
除Critical Section外,其余是系統全局對象,并且與不同進程及相同進程中的線程一起工作,這樣,
同步機也可以用于分離進程的同步活動(同一進程內部的線程除外)。
2. 事件(Event)
這是同步對象的一種類型,正如其名字的含義,在這個中心周圍是一些發生在另一個進程或線程中的
特殊活動。當你希望線程暫時掛起時,不會消耗CPU的工作周期。事件很類似于我們常用的消息的概念。
如果我們剖析消息的內核肯定會發現,它就是用事件來實現的。
程序可用CreateEvent或OpenEvent對事件獲得一個句柄:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
// pointer to security attributes
BOOL bManualReset, // flag for manual-reset event
BOOL bInitialState, // flag for initial state
LPCTSTR lpName // pointer to event-object name
);
HANDLE OpenEvent(
DWORD dwDesiredAccess, // access flag
BOOL bInheritHandle, // inherit flag
LPCTSTR lpName // pointer to event-object name
);
?

然后,該程序再調用 WaitForSingleObject,選定事件柄和暫停周期,那么線程就被掛起,一直到其他
線程給出事件有關信號后才被再次激活。其他線程指調用SetEvent或PulseEvent所需活動的線程,事件獲
得這個信號,被掛起的線程即被喚醒并繼續執行。
例如,當一個線程要使用另一個線程的排序結果時,你或許希望去使用一個事件。比較糟的方法是執
行這個線程并在結束時設置全局變量標志,另一個線程循環檢查這個標志是否已設置,這將浪費許
多CPU的時間。用事件(Event)做同樣的事情則很簡單,排序線程在結束時產生一個事件(Event),其
他線程調用WaitForSingleObject。這就使得線程被掛起,不浪費CPU周期,當排序線程完成排序時,調
用SetEvent喚醒另一個線程繼續執行,有效地利用了CPU。
除了WaitForSingleObject外,還有WaitForMultipleObject允許一個線程被掛起,一直到要么滿
足Event條件,要么有一個等待視窗信息時能恢復,其他掛起的函數一直等到掛起的被滿足或I/O操作已經
完成時才能,無疑這里體現了靈活性。
3. 信號器(Semaphores)
當你需限制訪問特殊資源或限制一段代碼到某些線程時,Semaphores非常有用。打一個比喻,就像
是餐廳用的餐桌一樣,假設這個餐廳有二十個餐桌,當你去時,二十個餐桌都有人在用餐,你就只好等二
十個餐桌中有人吃完后才能去用餐,否則你必須等待。在Win32編程中獲得Semaphores,就好像得到餐
桌的一次控制。
為了利用Semaphores,一個線程調用 CreatSemaphore去獲得一個HANDLE給Semaphores。該調用
包括同時有多少線程使用資源或代碼,如果其他線程在另一個進程中,可調用OpenSemaphore去獲得一
個可利用的HANDLE,當一個線程需要訪問共享資源時,要把資源傳遞給WaitForSingleObject,如果這
個Semaphore沒有被等待的所有線程請求,等待功能將簡單處理Semaphore的使用數,且線程繼續執
行。換句話說,如果Semaphore已經超出最大值,則調用等待功能的線程將被掛起。一個線程的含義就是
使用一個Semaphore來執行,并用ReleaseSemaphore來釋放資源。
?

第3章 Windows運行機理
3.4 線程的機制(4)
4. 互斥(Mutexes)
這是同步對象的第三種類型,Mutex(互斥)是“mutual exclusion”的縮略語。一個程序或一組程序希
望一次只有一個線程去訪問一個資源或一段代碼時可使用一次互斥。如果一個線程正在使用這個資源,則
另一個線程被排斥在同一資源之外。互斥的用法非常類似于信號器,產生、打開和釋放信號器函數都有與
互斥類似的內容。當一個線程有互斥要求時,可調用WaitForSingleObject/ WaitForMultipleObjects系列中
的函數。
用餐桌來比喻的話,就是整個餐廳只有一個餐桌,當有一個人在用餐時,另一個人只能等待用餐。
5. 臨界段(Critical Sections)
臨界段相當于一個微型的互斥,只能被同一進程中的線程使用。臨界段是為了防止多線程同時執行同
一段代碼。相對其他同步機而言,臨界段相對簡單和易用,一個臨界段可以被認為是僅在單一進程中有效
的輕量級互斥。為了使用臨界段,一個程序要么分配,要么聲明一個CRITICAL_SECTION類型的全局變
量。在臨界段首次使用之前,其場地需要通過調用InitiazeCriticalSection進行初始化,之后調
用EnterCriticalSection將一線程進入臨界段了。
臨界段使用起來很簡單,在Windows 95中,當沒有其他線程時,如果一個線程線程調
用EnterCriticalSection,則只需在CRITICAL_SECTION結構中調整和設置一些場地即可。只有已經存在臨
界段的另一個線程把EnterCriticalSection調入VMIN 32 VxD時,才能使該線程掛起。
6. WaitForSingleObject/ WaitForMultipleObjects函數
至此,已經概述了線程同步的四種基本方法,我想談論一下同步線程的其他方法。除了事情、信號器
和互斥外,WaitForSingleObject/ WaitForMultipleObjects系列函數可接受幾種其他的句柄,把一個進
程HANDLE傳到一個WaitForSingleObject/ WaitForMultipleObjects函數,則會引起調用線程掛起。如果這
個進程已經中止,則Wait函數立即返回。同樣,把一個線程的HANDLE傳到WaitForSingleObject/
WaitFor Multiple Objects,調用線程也將被掛起。
WaitForSingleObject/ WaitForMultipleObjects函數可以掛起的另一個HANDLE是這個文件的變更,之
間的變更可以限定一個給定的目錄及有選擇的子目錄。WaitForSingleObject/ WaitForMultipleObjects函數
的另外一個HANDLE是一個針對輸入裝置的HANDLE文件,一旦有未經使用的輸入進入輸入緩存,Wait函
數則返回,并告訴線程繼續執行。
3.5 PE結構分析
因為PE結構是一個很復雜的結構,所以下面我們在討論PE時把它分為PE頭標、表節、文件導入/導
出、資源分別介紹。如果你只對某部分內容感興趣,可以直接跳到此節閱讀。
3.5.1 PE頭標
PE 的意思就是 Portable Executable(可移植的執行體)。它是 Win32環境自身所帶的執行體文件格
?

式。它的一些特性繼承自 Unix的 Coff (common object file format)文件格式。“Portable Executable”(可
移植的執行體)意味著此文件格式是跨Win32平臺的:即使Windows運行在非Intel的CPU上,任
何win32平臺的PE裝載器都能識別和使用該文件格式。當然,移植到不同的CPU上的PE執行體必然得有
一些改變。所有Win32執行體(除了VxD和16位的DLL)都使用PE文件格式,包括NT的內核模式驅動程
序(Kernel Mode Drivers)。
我們在PE結構中最先看見的PE格式中的是PE結構的頭標。像所有其他微軟可執行文件格式一
樣,PE文件在一個已知(或容易找到的)位置上,有一系列域來定義該文件其余部分看起來像什
么。PE頭標包含了至關重要的一些信息,諸如代碼和數據區的位置和大小、該文件要用什么操作系統以
及初始的堆棧大小。我們在學習PE結構時最好用PEDUMP來DUMP一個EXE或DLL文件比較好學習點
(PEDUMP可以在X:Msvc/COMMON/TOOLS找到,X為VC的安裝目錄)。
1. DOS頭
與其他微軟的可執行格式相似的是,在PE頭標前面還有一個百多個字節的DOS頭。這個DOS區域是
一小段DOS程序。這一段程序只有幾行簡單的匯編程序,在Windows 3.1中可以自己定義。把一個很大
的DOS程序當成PE結構的頭也是可以的,例如說做一個從DOS下啟動的游戲,就可以把DOS啟動的內容
放在前面。到了Windows 9x中的PE結構,在VC 4.0以后,DOS頭就不可定義了。
現在,它的作用是如果此程序在DOS平臺運行時,它將打印出“該程序不能在DOS模式下運行”之類的
信息。這樣就能提示程序的用戶到Windows平臺去運行此程序。圖3.9是PE結構圖。
圖3.9 PE結構圖
PE文件的所有結構都能在WINNT.H文件中找到,其結構如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
?

?WORD e_minalloc; // Minimum extra
//paragraphs needed
WORD e_maxalloc; // Maximum extra
//paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information;
//e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
?

第3章 Windows運行機理
3.4 線程的機制(6)
4. IMAGE_OPTIONAL_HEADER
PE頭標的第三部分是一個IMAGE_OPTIONAL_HEADER類型結構。對于PE文件,這部分是必要的。
除了標準的IMAGE_FILE_HEADER外,COFF格式還允許單獨定義一個附加信息結構。
IMAGE_OPTIONAL_HEADER分為兩種,一種是32位的,一種是64位的,我們可以在WINNT.H中找
到對應的結構,其名分別為:
IMAGE_OPTIONAL_HEADER32各IMAGE_OPTIONAL_HEADER64。我們在這里只對32位進行介紹,
其結構如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
?

?WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Magic表示標志映像文件狀態的一個WORD記號。值定義如下:
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
. 0x0107:一個ROM映像。
. 0x010B:一個普通的可執行映像(大多數文件含此值)。
MajorLinkerVersion和MinorLinkerVersion表示生成該文件的連接器版本號。該數字以十進制形式顯
示,而不是十六進制,一個典型的連接器版本號是2.23。
SizeOfCode表示所有代碼段組合聚集在一起的尺寸大小,內存中整個PE映像體的尺寸。它是所有頭
和節經過節對齊處理后的大小。
SizeOfInitializedData表示由初始化的數據(不包括代碼段)組成的所有節的總尺寸。
SizeOfUninitializedData表示初始化的數據的大小。未初始化的數據通常被歸入稱為.bss的一節中。
AddressOfEntryPoint表示映像開始執行位置的地址。PE裝載器準備運行的PE文件的第一個指令
的RVA。若您要改變整個執行的流程,可以將該值指定到新的RVA,這樣,新RVA處的指令首先被執行。
BaseOfCode表示文件代碼節開始處的RVA。典型情況下,代碼節在PE頭標之后,并在數據節之前進
入內存。在微軟生成的EXE文件中,該RVA通常是0x1000。
BaseOfData表示文件的數據節開始處的RVA。典型情況下,數據節最后進入內存,排在PE頭標和代
碼節后面。
?

ImageBase表示當連接器創建一個可執行文件時,它假設該文件將被內存映射到內存中的一個指定位
置上。也就是PE文件的優先裝載程序的地址。因為在Windows操作系統中,總是把可執行程序安裝到虛
擬空間中去,每個虛擬空間在邏輯上都是相對獨立的,不相干的。此值就是表示程序裝在虛擬空間的什么
地方開始。
SectionAlignment表示內存中節對齊的粒度。例如,如果該值是4096 (1000h),那么每節的起始地址
必須是4096的倍數。若第一節從401000h開始且大小是10個字節,則下一節必定從402000h開始,即
使401000h和402000h之間還有很多空間沒被使用。
FileAlignment表示文件中節對齊的粒度。例如,如果該值是(200h),,那么每節的起始地址必須
是512的倍數。若第一節從文件偏移量200h開始且大小是10個字節,則下一節必定位于偏移量400h: 即使
偏移量512和1024之間還有很多空間沒被使用/定義。
MajorOperatingSystemVersion和MinorOperatingSystemVersion表示使用該可執行文件所要求的操作
系統最小版本。該域含義有點模棱兩可,因為subsystem域(后面的一些域)頁體現類似的目的。在大多
數Win32文件中,該域為版本1.0。
MajorImageVersion和MinorImageVersion表示一個用戶自定義域。該域允許你具有一個EXE或一
個DLL的不同版本。可用連接器的/VERSION開關來置該域的值,如LINK/VERSION:2.0 myobj.obj。
MajorSuvsystemVersion和MinorSubsystemVersion表示運行該可執行文件所要求的最小子系統版
本。該域的一個典型值是4.0(意為Windows 4.0,即Windows 95)。
Reserved1一般總為0。
SizeOfImage一般是裝載器不得不關心的映像部分的總尺寸。它是從映像基地址開始直到最后一節的
尾端這個范圍的長度。最后一節的尾端是被調整為最接近節對齊值的倍數的。
SizeOfHeaders表示PE頭標和節(對象)表的尺寸。這些節的生數據直接跟在所有頭標部分之后。
SizeOfHeaders =所有頭+節表的大小
也就等于文件尺寸減去文件中所有節的尺寸。
CheckSum總是值0。
Subsystem表示該可執行文件為它用戶接口而使用的子系統類型。WINNT.H定義了如下值:
// Unknown subsystem.
#define IMAGE_SUBSYSTEM_UNKNOWN 0
// Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1
// Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2
// Image runs in the Windows character subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3
// image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_OS2_CUI 5
// image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI 7
// image is a native Win9x driver.
?

#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8
// Image runs in the Windows CE subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9
?

第3章 Windows運行機理
3.5 PE結構分析(1)
3.5.2 表節
PE文件的真正內容劃分成塊,稱之為sections(節)。每節是一塊擁有共同屬性的數據,比如代碼/數
據、讀/寫、導入/導出等。我們可以把PE文件想像成一邏輯磁盤,PE header 是磁盤的boot扇區,
而sections就是各種文件,每種文件自然就有不同屬性,如只讀、系統、隱藏、文檔等。節的劃分是基于
各組數據的共同屬性,而不是邏輯概念。重要的不是數據/代碼是如何使用的,如果PE文件中的數據/代碼
擁有相同屬性,它們就能被歸入同一節中。
不必關心節中類似于“data”、“code”或其他的邏輯概念:如果數據和代碼擁有相同屬性,它們就可以
被歸入同一個節中。節名稱僅僅是個區別不同節的符號而已,自己也可以定義一些不同的名字的字。
節表位于PE頭標和映像節的生數據中間,節表包含了關于映像中的每節的信息。映像的節是以它們的
地址而不是其字母來排序的。
在此處是值得弄清楚一個節到底是什么的時候了。
然而與NE文件的段表又不同,一個PE節表并不為每個代碼或數據塊保存一個選擇器的值。取而代之
的是,節表的每一項存儲一個地址,該地址是文件的生數據被影射入內存所在位置的地址。盡管節類似
于32位段,但它們確實不是單獨的段。實際上,一個節簡單地對應一個進程的虛擬地址空間中的一片內存
區域。
PE文件不同于NE文件的另一個方面,體現在它們是如何管理支撐數據方面,你的應用程序不使用這
些支撐數據,但操作系統要用。可執行模塊用到的DLL列表和安置表的位置是支撐數據的兩個例子。
用PE文件就不同了。任何被認為是相關的代碼和數據被存儲在一個節中。因此,關于引入函數的信息
存儲在它自己的節中,它被作為模塊引出的函數表。對重定位數據也是如此。任何可能被程序或操作系統
需要的代碼或數據同樣也是獲得它們自己的節。
我先描述操作系統管理這些節所用的數據,在內存中,緊跟在PE頭標之后的是一
個IMAGE_SECTION_HEADER數組。這個數組中的元素個數在PE頭標中
(的IMAGE_NT_HEADER.FileHeader.NumberOfSection域)給出。
IMAGE_SECTION_HEADER的結構如下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
?

?DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
用PEDUMP程序可輸出節表和所有節的域和屬性。下面分別顯示了一個典型的EXE文件PEDUMP輸
出的節表以及一個OBJ文件的節表輸出。
Section Table
01 .text VirtSize: 00002C2A VirtAddr: 00001000
raw data offs: 00000400 raw data size: 00002E00
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 60000020
CODE MEM_EXECUTE MEM_READ
02 .rdata VirtSize: 0000038F VirtAddr: 00004000
raw data offs: 00003200 raw data size: 00000400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 40000040
INITIALIZED_DATA MEM_READ
03 .data VirtSize: 00001334 VirtAddr: 00005000
raw data offs: 00003600 raw data size: 00001000
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
04 .idata VirtSize: 000006E2 VirtAddr: 00007000
raw data offs: 00004600 raw data size: 00000800
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
?

?05 .rsrc VirtSize: 00000550 VirtAddr: 00008000
raw data offs: 00004E00 raw data size: 00000600
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 40000040
INITIALIZED_DATA MEM_READ
06 .reloc VirtSize: 0000041E VirtAddr: 00009000
raw data offs: 00005400 raw data size: 00000600
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42000040
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
每個IMAGE_SECTION_HEADER是關于EXE或OBJ文件中一節信息的一個完整數據庫,它具有如下
格式。
Name[IMAGE_SIZEOF_SHORT_NAME]:這是給本節命名的一個8字節長的ANSI名,多數節名以一
個小數點作為開始(例如:.text),你也可以在微軟C/C++編譯器中用#pragma data_seg和#pragma
code_seg來命名。重要的是,注意如果節名占滿了8個字節,則沒有NULL中止字節。如果你愛
用printf(),可使用“%.8s”,以避免拷貝名字串到另一緩沖區時誤用空白符中止了它。
Misc:根據是出現在EXE文件中還是在OBJ文件中,該域具有不同的含義。在一個EXE中,它保存代
碼或數據節的虛擬尺寸,這是調整到最接近文件對齊值倍數的尺寸,本結構中后面的SizeOfRawData域保
存這個對齊值。對于OBJ文件,該域指示本節的物理地址。第一節在地址0上開始。要找到其下一節的物
理地址,只需要將SizeOfRawData值與當前的此物理地址相加即可。
VirtualAddress節:的RVA(相對虛擬地址)。PE裝載器將節映射至內存時會讀取本值,因此如果域
值是1000h,而PE文件裝在地址400000h處,那么本節就被載到401000h。
SizeOfRawData:在EXE文件中,該域含本節被對齊到文件對齊尺寸后的尺寸。
PointerToRawData:這是基于文件的偏移量,用它可以找到本節的生數據所在位置。如果你自己內存
映射一個PE或COFF文件(而不是讓操作系統裝載它),則該域比VirtualAddress更為重要。那是因為在
這種情況下,你將有整個文件的一個完全線性映射,因此你將在該偏移處找到本節的數據,而不是
用VirtualAddress域指定的RVA。
PointerToRelocations:在OBJ文件中,這是一個該節重定位信息基于文件的偏移量。OBJ每節的重定
位信息直接跟在該節數據之后。在EXE文件中,這個域(和后一個域)無意義,并總被置為0。當連接器
創建EXE時,它已解決了大多數的地址分配和安排問題,只有基地址重定位和引入函數才在裝載時解決。
有關基地址和引入函數的信息存儲在基地址和引入函數節中。因此,對一個EXE,不需要在節的生數據之
后還要有每節重定位的數據。
?

第3章 Windows運行機理
3.4 線程的機制(7)
表示的意義如下。
. native=1:不需要子系統(例如,一個設備驅動器)
. WINDOWS_GUI=2:在Windows GUI子系統中運行
. WINDOWS_GUI=3:在Windows字符子系統中運行(一個控制臺應用程序)
. OS2_GUI=5:在OS/2字符子系統中運行(只對OS/2 1.x的應用程序)
. POSIX_CUI=7:在Posix字符子系統中運行
DllCharacteristics (在NT 3.5中標為obsolete)指示什么情況下一個DLL的初始化函數,例
如DllMain()要被調用的標志集合。該值看起來總被置為0,然而操作系統仍為4個事件調用了DLL初始
化函數。
被定義的值如下。
. 1:當DLL第一次被裝入一個進程的地址空間時調用;
. 2:當一個線程中止時調用;
. 4:當一個線程啟動時調用;
. 8:當DLL退出時調用。
SizeOfStakeReserve表示為初始線程棧保留的虛擬內存量。然而,這些內存不是都要交付的(見后一
個域)。該域默認為0x100000(1MB)。如果你對CreateThread()指定一個0作為棧的大小,結果線程
仍是得到一個域默認值相同的棧。
SizeOfStackCommit表示為初始線程棧首先交付的內存量。在微軟連接器中,該域默認值是0x1000字
節(1頁),而TLINK默認為0x2000字節(2頁)。
SizeOfHeapReserve表示為初始進程堆保留的虛擬內存量。該堆句柄可通過調用GetProcessHeap()來
獲得。這些內存也不是都要交付的(見下一個域)。
SizeOfHeapCommit表示在進程堆中初始交付的內存量。連接器在該域的默認值是0x1000字節。
Loaderflags(在NT 3.5中標記為obsolete)它們一般是與調試支持有關的域。
NumberOfRvaAndSizes表示在DataDiretory數組中項的數目。目前的工具總把該域的值置為16。
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]是一個IMAGE_DATA_DIRECTORY結
構數組。數組中前面的元素包含了該可執行文件重要部分的起始RVA和尺寸。數組尾端的元素目前還未用
到。數組的第一個元素總是引出函數表(如果有的話)的地址和尺寸。第二個數組項是引入函數表的地址
和尺寸,如此等等。對于一個完整的數組項的定義列表,在WINNT.H中
的IMAGE_DIRECTORY_ENTRY_xxx #defin’s中有如下的幾項:
// Export Directory
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
?

// Import Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
// Resource Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
// Exception Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
// Security Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
// Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
// Debug Directory
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
// Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7
// RVA of GP
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
// TLS Directory
#define IMAGE_DIRECTORY_ENTRY_TLS 9
// Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
// Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11
// Import Address Table
#define IMAGE_DIRECTORY_ENTRY_IAT 12
// Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13
// COM Runtime descriptor
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14
該數組的目的是允許裝載器可迅速地找到一個映像的特定節(例如引入函數表),而不必遍歷映像的
每一個節并逐一比較它們的名字。數組的大多數項描述了一個完整的節的數據。然
而,IMAGE_DIRECTORY_ENTRY_ DEBUG元素只含了.rdata節中一小部分字節。
?

第3章 Windows運行機理
3.5 PE結構分析(2)
PointerToLinenumbers:表示行號表的基于文件的偏移量。一個行號表把源文件行號和一個地址對應
起來,在該地址上可找到給定行產生的代碼。主要用于調試中。
NumberOfRelocations:表示本節重定向表中重定向的數目(PointerTorRelocations域已在前面列
出)。該域只用于OBJ文件。
WORD NumberOfLinenumbers:表示本節行號表中行號的數目(PointerRoLinenumbers域已在前面
列出)。
Characteristics:含標記以指示節屬性,比如節是否含有可執行代碼、初始化數據、未初始數據,是
否可寫、可讀等。對所有可能的節屬性的列表請見WINNT.H中的IMAGE_SCN_XXX_XXX #defines。
下面我們列出了一些常見的節名。
1. .text節
.text節又叫代碼節,它是編譯器或匯編器產生的所有通用碼。在.text節中,除了我用編譯器創建的和
從運行時間庫中用到的外,還有另外附加的代碼時,我感到很驚奇。在PE文件中,當你調用另一個模塊
中的函數(例如USER32.DLL的GetMessage())時,編譯器產生的CALL指令并不是把控制直接傳
給DLL中的該函數。取而代之的是,該調用指令把控制傳給也是在該.text節中的一個JMP DWORD
PTR[XXXXXXXX]指令。該JMP指令跳轉到以一個DWORD存于.idata節中的一個地址上。這個.idata節
的DWORD含該操作系統函數入口點的真正地址,如圖3.10所示。
圖3.10 .text節表
通過一個位置把所有對一個給定的DLL函數的調用進行歸結后,裝載器就沒有必要對每個調用DLL的
指令進行拼湊了。PE裝載器必須要做的只是把目標函數的正確地址放入.idata節中的該DWORD中就行
了。沒有任何的CALL指令需要拼湊。這與NE文件有明顯的不同,后者中每個段含有一個用在該段上的安
置表。如果該段調了某個DLL函數20次,則裝載器必須將該函數的地址拷貝到該段中20次。在PE方法
?

下,你不能用一個DLL函數的真正地址來初始化一個變量,你會以為如下句子:
FARPROC pfnGetMessage=GetMessage;
將會把GetMessage的地址放入變量pfnGetMessage中。在Win16中,這確實如此,但在Win32中則行
不通。在Win32中,變量pfnGetMessage結果存的是在.text節中轉換了的JMP DWORD
PTR[XXXXXXXX]的地址。如果你通過函數指針來調用,結果會像你期望的那樣出現。然而,如果要
在GetMessage()的開始處讀這些字節,則就不那么幸運了(除非你自己做一些附加的工作來跟
隨.idata的“指針”)。
2. .data節
正像.text是代碼的默認節一樣,.data節就是初始化了的數據所在的地方。初始化了的數據由全局變量
和靜態變量組成,他們在編譯時被初始化。它還包括字符串文字(例如,在一個C/C++程序中的字符
串“HELLO WORLD”)。連接器把來自于OBJ和LIB文件的所有.data節組合成EXE中的一個.data節。局部
變量是被定位在一個線程棧上的,并且在.data或.bss節中不占空間。
3. .bss節
.bss節是未初始化的靜態和全局變量存儲的地方。連接器把來自于OBJ和LIB文件的所有.bss節組合成
為EXE中的一個.bss節。在節表中,為.ss節所用的RawDataOffset域被置為0,表示這一節在文件中未占
任何空間。TLINK32不產生.bss節,代之的是它擴展DATA節的虛擬尺寸以說明未被初始化的數據。
4. .CRT節
.CRT節是另一個初始化了的數據節,它被微軟C/C++運行時間庫(因而稱為.CRT)所使用。該節中
的數據被用于這樣一些事情,如在Main或WinMain被執行之前調用靜態C++類的構造函數。
5. .rsrc節
.rsrc節包含了本模塊所用的資源。在NT出現后的較早時期,16位RC.EXE產生的.RES文件輸出格式
不能被微軟連接器所識別。CVTRES程序把這些.RES文件轉換成COFF格式的OBJ,并把數據放入OBJ內
部的.rsrc節中。連接器然后才能把資源OBJ當做另一個OBJ連接進來。這意味著連接器并不必知道關于資
源的任何特殊的東西。微軟較新的連接器似乎能夠直接處理.RES文件。
6. .idata節
.idata節包含模塊從其他DLL引入的函數(和數據)的信息。該節等價于NE文件的一個模塊訪問表。
不同的關鍵點在于:PE文件引入的每個函數特別地要在這一節列出。要在一個NE文件中找等價的信息,
你不得不深入到每段所用的生數據尾端處的重定位。
?

7. .edata節
.edata是被其他模塊使用的PE文件引出的函數和數據的一個列表。NE文件與此等價的是項表、駐留
名字表和非駐留名字表這三個表的結合。不像在Win16中那樣,幾乎沒有理由從一個EXE文件中輸出任何
東西,因此,通常只能在DLL文件中看到.edata節。例外的是Borland C++產生的EXE文件,它似乎總是
引出一個函數(_ _GetExceptDLLinfo),該函數為運行時間庫內部使用。
當使用微軟工具時,.edata節中的數據通過.EXP文件到PE文件中。另一方面,連接器本身不產生這些
信息,而是依靠庫管理器(LIB32)來掃描OBJ文件,并創建.EXP文件,然后連接器把該.EXP文件加到模
塊列表中以便連接。那些麻煩的.EXP文件確實正是具有一個不同擴展名的OBJ文件。通過用/S(顯示符
號表)選項來運行PEDUMP程序,可以看到從一個.EXP中引出的函數。
?

第3章 Windows運行機理
3.5 PE結構分析(3)
8. .reloc節
.reloc節容納了一個基址重定位的表。基址重定位是對指令或初始化過的變量值的一個調整;如果裝載
器不能把EXE或DLL文件裝到連接器假定它應該放置的地址上時,則該文件需要做這個調整。如果裝載器
能把映像裝到連接器預先確定的基地址上,則裝載器將忽略該節中的重定位信息。
如果你想要進行一個選擇,并且希望裝載器能夠總把映像裝到假定的基地址上,可使用/FIXED選項來
告訴連接器除去這個信息。盡管這樣可在可執行文件中節省空間,但它可能使該可執行文件不能在別
的Win32平臺上運行。例如,假設你建立了一個NT下的EXE文件,并把該EXE基址定到0x10000處。如果
你告訴連接器除去重定位信息,則該EXE將不能在95下運行,因為在95下,地址0x10000不是有效的
(在95中,最小的裝載地址是0x400000,即4MB)。
注意被編輯器產生的JMP和CALL指令用的是相對于其指令的偏移量,而不是在32位段中的實際偏移
量。假如映像需要裝載到與連接器所指定的基地址不同的位置上,這些指令也不需改變,因為他們用的是
相對地址。如果需要重定位的并沒有像你想像得那么多,通常只有使用了對某些數據的32位偏移量的指令
才需要重定位。例如,假如有如下的全局變量聲明:
int Addr;
int *ptr=& Addr;
如果連接器給映像指定的基址是0x10000,則變量Addr的地址結構是含像0x12004之類的值。在存指
針ptr的內存處,連接器將寫值0x12004,因為那是變量Addr的地址。如果裝載器(不管是什么原因)決定
在0x70000為基地址的地方裝載該文件,Addr的地址則將為0x72004。然而,這樣該預先初始化的ptr變量
的值就不正確了,因為Addr現在在內存中比原來高了0x60000字節。
這正是重定位信息發揮作用的地方。.reloc節實際上是記錄了映像中一些位置的一張表,在這些位置
上,連接器所假定的裝載地址和實際裝載地址之間的差別需要考慮。
3.5.3 PE文件引入
我們知道函數是如何對外部DLL文件調用的。它并不是直接調DLL的函數地址,而通過CALL指令轉向
該可執行文件中的.text節中其他地方上的一個JMP DWORD PTR[XXXXXXXX]。作為一種選擇,如果
在VC中用了_ _declspec(dllimprot),則函數調用變成CALL DWORD PTR[XXXXXXXX]。在這兩種情況
下,JMP或CALL要查的地址存于.idata節中。JMP或CALL指令把控制傳給該地址,該地址是所要求的目
的地址。
在被裝入內存之前,PE文件的.idata節包含了一些信息。這些信息對于裝載器確定目標函數的地址并
把它們拼入可執行的映像中,是必不可少的。在.idata節被裝入后,它包含了一個指針,該指針指
?

向EXE/DLL引入的函數。注意,在本節我所討論的所有數組和結構都被包含在.idata節中。
.idata節(從文件來說可以是idata節,如果內存映射就是import table,即引入表)用一
個IMAGE_IMPORT_DESCRIPTOR的數組作為開始。對于PE文件隱含連接的每個DLL,都有一
個IMAGE_IMPORT _DESCRIPTOR。對于該數組,沒有任何計數來指示該數組中結構的數目,數組的最
后一個元素是通過在最后域中填入NULL的一個IMAGE_IMPORT_DESCRIPTOR來表示的。一
個IMAGE_IMPORT_ DESCRIPTOR的格式如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
// 0 for terminating null import descriptor
DWORD Characteristics;
// RVA to original unbound IAT (PIMAGE_THUNK_DATA)
DWORD OriginalFirstThunk;
};
// 0 if not bound,
// -1 if bound, and real date/time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
//(new BIND) O.W. date/time stamp of DLL bound to (Old BIND)
DWORD TimeDateStamp;
// -1 if no forwarders
DWORD ForwarderChain;
DWORD Name;
// RVA to IAT (if bound this IAT has actual addresses)
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED
. Characteristics/OriginalFirstThunk:該域是相對的一個偏移量(RVA)。它指向一
個IMAGE_THUNK_DATA類型的數據。其中,每個IMAGE_THUNK_DATA DWORD對應一個被
該EXE/DLL引入的函數。
. TimeDataStamp:表示該時間/日期印記指示文件是什么時間建立的。該域一般為0。
. ForwarderChain:該域與傳遞相關聯,包括一個DLL把對它的一個函數的訪問傳遞給另一
個DLL。例如,在Windows中,KERNEL32.DLL把它的一些引出函數傳遞給NTDLL.DLL,一個應
用程序或許認為它調用了KERNEL32.DLL中的一個函數,但實際上它的調用進入到
了NTDLL.DLL中。該域把一個索引含到FirstThunk數組中,被該域索引過的函數將被傳遞給另一
個DLL。
. Name:這是相對一個用null作為結束符的ASCII字符串的一個RVA,該字符串含的是該引
入DLL文件的名字(例如,KERNEL32.DLL或者USER32.DLL)。
. PIMAGE_THUNK_DATA FirstThunk: 該域是相對一個PIMAGE_THUNK_DATA
?

DWORD數組的一個偏移量(RVA)。大多數情況下,該DWORD被解釋成指向一
個IMAGE_IMPORT_ BYNAME結構的一個指針。然而,也有可能用順序值引入一個函數。
一個IMAGE_IMPORT_DESCRIPTOR的重要部分是引入DLL的名字和兩個IMAGE_THUNK_DATA
DWORD數組。每個IMAGE_THUNK_ DATA DWORD對應一個引入函數。在EXE文件中,這兩個數組
(被Characteristics和FirstThunk域所指向)并行地運行,并且都是在尾端處以一個NULL指針項作為中
止。
為什么會有兩個并行的指向IMAGE_THUNK_DATA結構的指針數組呢?
第一個數組(被Characteristics指向的那一個)被單獨留下,并且絕不會被修改,它有時也被稱做提
示名稱表(hint-name table)。
第二個數組(被IMAGE_IMPORT_DESCRIPTOR的FirstThunk域所指向)被PE裝載器改寫,然后用
該引入函數的地址來改寫IMAGE_ THUNK_DATA DWORD的值。
?

第3章 Windows運行機理
3.5 PE結構分析(4)
對DLL函數的CALL調用要通過一個“JMP DWORD PTR[XXXX XXXX]”轉換,該轉換
的[XXXXXXXX]部分要根據FirstThunk數組中的某一項而定。因為被裝載器實際地改寫了的這
個IMAGE_THUNK_DATA數組,保存了所有引入函數的地址,因此它被稱為“引入地址表”(Import
Address Table)。圖3.11顯示了這兩個數組。
圖3.11 兩個指針數組
因為該引入地址表通常是在一個可寫的節中,因此可相對比較容易地截取一個EXE或DLL文件對另一
個DLL調用。可簡單地把恰當的引入地址表項指向希望截取的函數,這不需要修改調用者中的任何代碼。
這個功能是非常有用的。
在引入庫中的一個.idata節包含了替換要反查的DWORD。另一個.idata節有一個空間用于“提示序
數”,而引入函數名緊跟其后。這兩個域構成一個IMAGE_IMPORT_BY_NAME結構。當你稍后連接一個
用了該引入庫的PE文件時,給引入庫中的替換具有和被引入的函數相同的名字。連接器認為這個替換真
正就是引入函數,并且把對引入函數的調用安置到替換點上。在引入庫中的替換基本上可以看成是引入函
數。
除了提供一個引入函數替換的代碼部分外,引入庫提供了PE文件的.idata節(或稱為引入表)的部分
東西。這些部分來自于庫管理器放入引入庫中的各個.idata間的差別。它只不過是遵循為建立和組合節而
預先設置好的規則,并且每件事都自然而然地到了位。
每個IMAGE_THUNK_DATA DWORD對應一個引入函數。該DWORD的解釋根據該文件是否已被裝
入內存和該函數是否已通過名字或序數來引入了(通過名字更常用一些)而變化。
當一個函數是通過其序數值引入的時(少見的情形),EXE文件的IMAGE_THUNK_DATA
DWORD中置最高一個二進位為1(0X80000000)。例如,考慮GDI32.DLL數組中一個具有值
為0x80000112的IMAGE_ THUNK_DATA,該IMAGE_THUNK_DATA是引入來自于GDI32.DLL中的
第0x112個引出函數。用序數來引入的問題是:微軟不能在Windows NT、Windows 95和Win32之間
使Win32 API函數的引出序數保持不變。
?

如果一個函數用名字來引入,則它的IMAGE_THUNK_DATA DWORD含一個RVA,
該RVA是IMAGE_IMPORT_BY_NAME結構所要用到的。一個IMAGE_IMPORT_BY_NAME結構非常簡
單,看起來如下:
typedef struct _IMAGE_IMPORT_BY_NAME
{ WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint猜測是引入函數所用的引出序數之類的一個值。
BYTE[1]具有該引入函數名字的一個以NULL結尾的ASCII字符串。IMAGE_THUNK_DATA
DWORD的最終解釋是在PE文件被Win32裝載器裝入之后。Win32裝載器使用IMAGE_THUNK_DATA
DWORD中的原始信息來查閱引入函數(不管是用名字還是用序數引入的)的地址。裝載器然后用引入函
數的地址再改寫該IMAGE_THUNK_DATA DWORD。
有IMAGE_IMPORT_DESCRIPTOR和IMAGE_THUNK_DATA結構了,現在很容易就可構造關于一
個EXE或DLL使用的所有引入函數的報表。簡單地在該IMAGE_IMPORT_DESCRIPTOR數組(它的每一
元素對應一個引入的DLL)上反復進行如下操作就可達到目的:對每
個IMAGE_IMPORT_DESCRIPTOR,找出IMAGE_THUNK_DATA DWORD數組的位置并適當地解釋它
們。下面顯示了運行PEDUMP輸出的結果(無名字的函數是用序數來引入的)。
KERNEL32.dll
Hint/Name Table: 00007050
TimeDateStamp: 00000000
ForwarderChain: 00000000
First thunk RVA: 00007164
Ordn Name
665 lstrcpynA
23 CloseHandle
79 DeviceIoControl
48 CreateFileA
653 lstrcatA
293 GetShortPathNameA
662 lstrcpyA
331 GetVersion
156 GetACP
668 lstrlenA
251 GetModuleFileNameA
617 WideCharToMultiByte
277 GetProcAddress
598 VirtualAlloc
?

?359 HeapAlloc
365 HeapFree
630 WriteFile
601 VirtualFree
361 HeapCreate
363 HeapDestroy
297 GetStdHandle
238 GetFileType
534 SetHandleCount
264 GetOEMCP
162 GetCPInfo
253 GetModuleHandleA
226 GetEnvironmentStringsW
150 FreeEnvironmentStringsW
224 GetEnvironmentStrings
425 MultiByteToWideChar
149 FreeEnvironmentStringsA
587 UnhandledExceptionFilter
481 RtlUnwind
398 LoadLibraryA
210 GetCurrentProcess
577 TerminateProcess
106 ExitProcess
169 GetCommandLineA
295 GetStartupInfoA
USER32.dll
Hint/Name Table: 000070F8
TimeDateStamp: 00000000
ForwarderChain: 00000000
First thunk RVA: 0000720C
Ordn Name
374 LoadIconA
435 PostQuitMessage
9 BeginPaint
182 EndPaint
48 CheckMenuItem
176 EnableMenuItem
237 GetCursorPos
?

?501 SetForegroundWindow
573 TrackPopupMenu
128 DefWindowProcA
300 GetSystemMetrics
405 MessageBoxA
387 LoadStringA
382 LoadMenuA
296 GetSubMenu
370 LoadCursorA
445 RegisterClassA
85 CreateWindowExA
556 ShowWindow
591 UpdateWindow
277 GetMessageA
579 TranslateMessage
144 DispatchMessageA
136 DestroyIcon
137 DestroyMenu
433 PostMessageA
SHELL32.dll
Hint/Name Table: 000070F0
TimeDateStamp: 00000000
ForwarderChain: 00000000
First thunk RVA: 00007204
Ordn Name
101 Shell_NotifyIconA
這是本書中CoolCPU.exe的例子的導出。
?

第3章 Windows運行機理
3.5 PE結構分析(5)
3.5.4 PE文件引出
引入是引出的一個反過程,PE文件在.edata節中存儲它引出函數的信息。
DLL/EXE要引出一個函數給其他DLL/EXE使用,有兩種實現方法:通過函數名引出或者僅僅通過序數
引出。比如某個DLL要引出名為“TextOut”的函數,如果它以函數名引出,那么其他DLLs/EXEs若要調用
這個函數,必須通過函數名,就是TextOut。另外一個辦法就是通過序數引出。什么是序數呢?序數是惟
一指定DLL中某個函數的16位數字,在所指向的DLL里是獨一無二的。例如在上例中,DLL可以選擇通過
序數引出,假設是16,那么其他DLLs/EXEs若要調用這個函數必須以該值作為GetProcAddress調用參
數。這就是所謂的僅僅靠序數引出。
我們一般不提倡僅僅通過序數引出函數這種方法,這會帶來DLL維護上的問題。一旦DLL升級/修改,
程序員就無法改變函數的序數,否則調用該DLL的其他程序都將無法工作。
我們從前面知道,導出的數據位于.edata節中,在此節的開始處是一
個IMAGE_EXPORT_DIRECTORY結構。緊隨該結構的是由一個IMAGE_ EXPORT_DIRECTORY結構中
的域指向的數據。一個IMAGE_EXPORT_ DIRECTORY看起來如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
. Characteristics:該域似乎沒有被用到,并且總是被置為0。
. TimeDataStamp:該時間/日期印記指示該文件建立的時間。
. MajorVersion和MinorVersion:該域看起來也沒有用,并且被置為0。
. Name:具有該DLL名的一個ASCII字符串的RVA。
. Base:被本模塊引出的函數的起始引出序號。例如,如果文件用序數值20,21和22來引出其
?

函數,則本域的值是20。
. NumberOfFunctions:數組中元素個數。該值也是被本模塊引出的函數個數。這個值通常
和NumberOfNames域(見下一個描述)的相同,但它們也可以不同。
. NumberOfNames:在AddressOfFunctions數組中的元素個數。這個值包含了用名字來引出
的函數個數,它通常(但不總是)和引出函數的總數相匹配。
. AddressOfFunctions:該域是一個RVA,并且指向一個函數地址數組。該函數地址是本模塊
中每個引出函數的RVA。
. AddressOfNames:該域是一個RVA,并且指向一個字符串指針數組。該串含的是從這個模
塊中通過名字來引出的函數的名字。
. AddressOfNamesOrdinals:該域是一個RVA,并且指向一個WORD數組。這些WORD基本
上是從本模塊中所有通過名字來引出的函數的引出序數。然而不要忘記加上在Base域中給出的起
始引出序號。
引出一個函數所需要的是一個地址和一個引出序數。如果你用名字來引出函數,則應存在一個函數
名。你應該想到PE格式的設計者會把這三個項放入一個結構中,并且再具有這些結構的一個數組。否
則,你不得不在三個分離的數組中查詢各個部分。
被IMAGE_EXPORT_DIRECTORY指向的數組的最重要部分,是由AddressOfFunctions域所指向的數
組。它是一個DWORD數組,每個DWORD含一個引入函數的地址(RVA)。每個引出函數的引出序數對
應于數組中它的位置。例如(假定序數起始值為1),具有引出序數為1的函數的地址將在該數組的第一個
元素中,則引出序數是2的函數的地址存在該數組的第二個元素中,依次類推。
關于AddressOfFunctions數組,有以下兩點要注意:
第一,引出序數需要把一個IMAGE_EXPORT_DIRECTORY中的Base域的值作為基準值。如
果Base域值0,則AddressOfFunctions數組中的第一個DWORD對應引出序數10,第二項對應引
出序數11,并且依次類推。
第二,引出序數可能有空白。讓我們假定你明確地引出一個DLL中的兩個函數,用序數
值1和3。即使你只引出兩個函數,但AddressOfFunctions數組不得不含三個元素。在該數組中任
何不與一個引出函數相對應的項,其值都為0。
Win32 EXE和DLL更經常用名字而不是序數來引入函數。這正是另外兩個數組要發揮作用的地方,這
兩個數組由一個IMAGE_EXPORT_ DIRECTORY結構中的值指
向。AddressOfNames和AddressOfNames Ordinals數組是為了讓裝載器更快地找到與給定函數名相對應
的引出序數。這兩個數組都包含相同數目的元素(該數目由一個IMAGE_EXPORT_
DIRECTORY的NumberOfNames域給出)。該AddressOfFunctions數組是一個索引值的數組,該索引將
用在AddressOfFunctions數組中。
引出表的設計是為了方便PE裝載器工作。首先,模塊必須保存所有引出函數的地址以供PE裝載器查
詢。模塊將這些信息保存在Address OfFunctions域指向的數組中,而數組元素數目存放
在NumberOfFunctions域中。 因此,如果模塊引出40個函數,則AddressOfFunctions指向的數組必定
有40個元素,而NumberOfFunctions值為40。現在如果有一些函數是通過名字引出的,那么模塊必定也在
?

文件中保留了這些信息。這些名字的RVAs存放在一數組中以供PE裝載器查詢。該數組
由AddressOfNames指向,NumberOfNames包含名字數目。
考慮一下PE裝載器的工作機制,它知道函數名,并想以此獲取這些函數的地址。至今為止,模塊已有
兩個部分:名字數組和地址數組,但兩者之間還沒有聯系的紐帶。因此,我們還需要一些聯系函數名及其
地址的內容。PE參考指出使用到地址數組的索引作為聯接,因此,PE裝載器在名字數組中找到匹配名字
的同時,它也獲取了指向地址表中對應元素的索引。 而這些索引保存在由AddressOfNameOrdinals域指
向的另一個數組(最后一個)中。由于該數組起了聯系名字和地址的作用,所以其元素數目必定和名字數組
相同。比如,每個名字有且僅有一個相關地址,反過來則不一定: 每個地址可以有好幾個名字來對應。因
此我們給同一個地址取“別名”。為了起到連接作用,名字數組和索引數組必須并行地成對使用,譬如,索
引數組的第一個元素必定含有第一個名字的索引,以此類推。
?

第3章 Windows運行機理
3.5 PE結構分析(6)
下面顯示了對WS2_32.DLL引出節的PEDUMP輸出。
Name: WS2_32.dll
Characteristics: 00000000
TimeDateStamp: 3A1B81FA
Version: 0.00
Ordinal base: 00000001
# of functions: 000001F4
# of Names: 0000006D
Entry Pt Ordn Name
0000CC51 1 accept
00001E77 2 bind
000013B6 3 closesocket
0000C453 4 connect
0000C553 5 getpeername
0000C5FA 6 getsockname
00001ABC 7 getsockopt
00001E2E 8 htonl
000012B0 9 htons
00007FFE 10 ioctlsocket
……
0000DB55 116 WSACleanup
00001BF5 151 __WSAFDIsSet
0000E180 500 WEP
我們知道導出有兩種方法,一種是代名導出,一種是序號導出,那它們到底是一個怎樣的過程呢?
當用名字導出時,導出過程如下。
(1)定位到PE header。
(2)從數據目錄讀取引出表的虛擬地址。
(3)定位引出表獲取名字數目(NumberOfNames)。
(4)并行遍歷AddressOfNames和AddressOfNameOrdinals指向的數組匹配名字。如果
在AddressOfNames 指向的數組中找到匹配名字,則從AddressOfNameOrdinals 指向的數組中提取索引
值。例如,若發現匹配名字的RVA存放在AddressOfNames 數組的第54個元素,那就提
?

取AddressOfNameOrdinals數組的第54個元素作為索引值。如果遍歷完NumberOfNames 個元素,則說
明當前模塊沒有所要的名字。
(5)從AddressOfNameOrdinals 數組提取的數值作為AddressOfFunctions 數組的索引。也就是說,
如果值是5,就必須讀取AddressOfFunctions 數組的第5個元素,此值就是所要函數的RVA。
序號導出方法的過程如下。
(1)定位到PE header。
(2)從數據目錄讀取引出表的虛擬地址。
(3)定位引出表獲取nBase值。
(4)減掉nBase值得到指向AddressOfFunctions 數組的索引。
(5)將該值與NumberOfFunctions作比較,大于等于后者則序數無效。
通過上面的索引就可以獲取AddressOfFunctions數組中的RVA了。
可以看出,從序數獲取函數地址比函數名快捷容易。不需要遍歷AddressOfNames 和
AddressOfNameOrdinals 這兩個數組。然而,綜合性能必須與模塊維護的簡易程度作一平衡。
總之,如果想通過名字獲取函數地址,則需要遍歷AddressOfNames 和 AddressOfNameOrdinals 這
兩個數組。如果使用函數序數,減掉nBase值后就可直接索引AddressOfFunctions 數組。
3.5.5 PE文件資源
在PE文件中尋找資源比較復雜,當尋找它們時,需要遍歷一個復雜的層次結構才能找到。
資源目錄結構很像磁盤的目錄。它有一個主目錄(根目錄),主目錄含有子目錄,子目錄還可有它自
己的子目錄。在這些子目錄中,可以找到文件。資源目錄的數據結構格式如下:
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
. Characteristics:理論上講,這個域應該保存該資源的標記,但看起來它總是為0。
. TimeDataStamp:這個時間/日期印記描述該資源創建時間。
. MajorVersiont和MinorVersion:理論上講,這些域保存該資源的版本號。但這些域似乎總被
置為0。
. NumberOfNameEntries:使用名字并且跟在本結構之后的數組元素(稍后描述)的數目。
. NumberOfIdEntries:使用整數ID并且跟在結構和任何有命名的項之后的數組元素的數目。
. IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]:該域形式上并不
是IMAGE_RESOURCE_DIRECTORY_ENTRY結構的部分,而是緊跟其后的一
?

個IMAGE_RESOURCE_DIRECTORY_ ENTRY結構數組。該數組中元素個數
是NumberOfNameEntries和NumberOfIdEntries域之和。含有名稱標志符(而不是整數ID)的目
錄項元素位于數組的最前面。
在資源中,一個目錄項既可指向一個子目錄(即另一個IMAGE_
RESOURCE_DIRECTORY_ENTRY),也可指向一個IMAGE_RESOURCE_ DIRECTORY_ENTRY。當
目錄是指向一個目錄數據時,它將描述在文件中什么地方可以找到資源的生數據。
一般情況下,在你到達一個給定資源的IMAGE_RESOURCE_DATA_ ENTRY之前,至少有三個目錄
層。最頂層目錄(只有一個)總位于資源節(.rsrc)的開始處。最頂層目錄的子目錄對應于文件中能找到
的資源的各種類型。
例如,如果一個PE文件包括對話框、字符串表和菜單,則這三個子目錄將會是一個對話目錄、一個字
符串表目錄和一個菜單目錄。每一個這樣的“類型”子目錄將輪流具有“ID”子目錄。對一種給定資源類型的
一個實例,將有一個ID子目錄。圖3.12中我們以一個更易理解的可視形式顯示了資源目錄層次結構。
?

第3章 Windows運行機理
3.5 PE結構分析(7)
下面顯示的是用PEDUMP來DUMP出的CPUCOOL.EXE中的資源的輸出。見交叉狀結構的第二層,
可以看到其中有圖表、菜單、對話、字符串表、組圖標和版本資源。在第三層上,有一個圖標、一個圖標
組、一個菜單以及其他等資源。
圖3.12 資源目錄層次結構
ResDir (0) Named:00 ID:04 TimeDate:00000000 Vers:0.00 Char:0
ResDir (ICON) Named:00 ID:01 TimeDate:00000000
Vers:0.00 Char:0
ResDir (1) Named:00 ID:01 TimeDate:00000000
Vers:0.00 Char:0
ID: 00000804 DataEntryOffs: 00000110
Offset: 172B0 Size: 002E8 CodePage: 0
ResDir (MENU) Named:00 ID:02 TimeDate:00000000
Vers:0.00 Char:0
ResDir (68) Named:00 ID:01 TimeDate:00000000
Vers:0.00 Char:0
ID: 00000804 DataEntryOffs: 00000120
Offset: 175B0 Size: 00054 CodePage: 0
ResDir (69) Named:00 ID:01 TimeDate:00000000
Vers:0.00 Char:0
ID: 00000804 DataEntryOffs: 00000130
Offset: 17608 Size: 00036 CodePage: 0
ResDir (STRING) Named:00 ID:01 TimeDate:00000000
?

Vers:0.00 Char:0
ResDir (1) Named:00 ID:01 TimeDate:00000000
Vers:0.00 Char:0
ID: 00000804 DataEntryOffs: 00000140
Offset: 17640 Size: 00068 CodePage: 0
ResDir (GROUP_ICON) Named:00 ID:01 TimeDate:00000000
Vers:0.00 Char:0
ResDir (66) Named:00 ID:01 TimeDate:00000000
Vers:0.00 Char:0
ID: 00000804 DataEntryOffs: 00000150
Offset: 17598 Size: 00014 CodePage: 0
每個資源目錄項是一個IMAGE_RESOURCE_DIRECTORY_ENTRY類型的結構。每
個IMAGE_RESOURCE_DIRECTORY_ENTRY具有如下格式:
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
. Name:該域包含的既可以是一個整數ID,也可是指向含一個字符串名字的結構的指針。如
果高位(0x80000000)是0,則該域被解釋為一個整數ID。如果高位非零,則低的31個二進制數
是相對于一個IMAGE_RESOURCE_DIR_STRING_U結構的偏移量(相對資源節的開始處)。這
個結構含一個WORD字符計數,后跟一個具有資源名稱的單一碼字符串。使得即使是為非單一
碼Win32而設計的PE文件,也在這里使用單一碼。要把該單一碼字符串轉換為一個ANSIC字符
串,請見WideCharToMultiByte()函數。
. OffsetToData:該域既可是相對于另一個資源目錄的一個偏移量,也可是指向關于一個特定
?

資源實例的信息的一個指針。如果高位(0x80000000)被置為1,則該目錄項對應一個子目錄,
低31個二進制數是一個相對于另一個IMAGE_RESOURCE_DIRECTORY結構的偏移量(相對于
資源的開始處)。如果高位置為0,則低31位是一個相對于一
個IMAGE_RESOURCE_DATA_ENTRY結構的偏移量(相對于該資源
節)。IMAGE_RESOURCE_DATA_ENTRY結構包含了資源的生數據的位置、它的尺寸和它的代
碼頁。
?


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/454306.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/454306.shtml
英文地址,請注明出處:http://en.pswp.cn/news/454306.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

nodejs里的module.exports和exports

引 在node.js中我們可以使用module.exports和exports導出模塊&#xff0c;設置導出函數、數組、變量等等 為什么可以用這兩個模塊&#xff1f; 或者直接問&#xff0c;node.js的模塊功能是怎么實現的。 這樣得益于javascript是函數性的語言&#xff0c;并支持閉包。 js的閉包 直…

c語言貪吃蛇最簡單代碼_C語言指針,這可能是史上最干最全的講解啦(附代碼)!!!...

點擊上方“大魚機器人”&#xff0c;選擇“置頂/星標公眾號”福利干貨&#xff0c;第一時間送達&#xff01;指針對于C來說太重要。然而&#xff0c;想要全面理解指針&#xff0c;除了要對C語言有熟練的掌握外&#xff0c;還要有計算機硬件以及操作系統等方方面面的基本知識。所…

SpringSecurity深度解析與實踐(3)

這里寫自定義目錄標題 引言SpringSecurity之授權授權介紹java權限集成 登錄失敗三次用戶上鎖 引言 SpringSecurity深度解析與實踐&#xff08;2&#xff09;的網址 SpringSecurity之授權 授權介紹 Spring Security 中的授權分為兩種類型&#xff1a; 基于角色的授權&#…

簡單解釋什么是 依賴注入 和 控制反轉

簡單解釋什么是 依賴注入 和 控制反轉2017-07-09 關于 依賴注入 與 控制反轉 的概念有些人覺得很難理解&#xff0c;最近在給別人講這個概念的時候梳理了一個比較好理解的解釋&#xff0c;而且我認為非技術人員也應該能聽的懂&#xff0c;因此分享給大家&#xff0c;希望下次你…

python pip install指定國內源鏡像

有時候安裝一些依賴包&#xff0c;網不好&#xff0c;直接超時&#xff0c;或者這個包就是死都下不下來的時候&#xff0c;可以指定國內源鏡像。 pip install -i 國內鏡像地址 包名 清華&#xff1a;https://pypi.tuna.tsinghua.edu.cn/simple 阿里云&#xff1a;http://mirr…

機器學習之單標簽多分類及多標簽多分類

單標簽二分類算法 Logistic算法 單標簽多分類算法 Softmax算法 One-Versus-One&#xff08;ovo&#xff09;&#xff1a;一對一 One-Versus-All / One-Versus-the-Rest&#xff08;ova/ovr&#xff09;&#xff1a; 一對多 ovo和ovr的區別 Error Correcting Output code…

ionic3 隱藏子頁面tabs

看了幾天ionic3 問題還挺多的&#xff0c;今天想把所有子頁面tabs 給去掉&#xff0c;整了半天&#xff0c;發現app.Module 是可以配置的 修改 IonicModule.forRoot(MyApp&#xff09; imports: [BrowserModule,// IonicModule.forRoot(MyApp),HttpModule,IonicModule.forRoot(…

cas單點登錄-jdbc認證(三)

前言 本節的內容為JDBC認證&#xff0c;查找數據庫進行驗證&#xff0c;其中包括&#xff1a; 密碼加密策略&#xff08;無密碼&#xff0c;簡單加密&#xff0c;加鹽處理&#xff09;認證策略&#xff08;jdbc&#xff09;一、業務需求 不同的公司&#xff0c;需求業務需求或者…

get clone 出現 fatal: the remote end hung up unexpectedly5 MiB | 892.00 KiB/s 報錯信息

fatal: the remote end hung up unexpectedly5 MiB | 892.00 KiB/s 解決方案 &#xff08;親測有效&#xff09; 解決方案如下&#xff1a; git clone時加上 --depth1&#xff0c;比如&#xff1a; git clone https://gitee.com/songyitian/tctm.git --depth 1depth用于指定…

mybatis foreach map_重學Mybatis(六)-------輸入映射(含面試題)

博主將會針對Java面試題寫一組文章&#xff0c;包括J2ee&#xff0c;SQL&#xff0c;主流Web框架&#xff0c;中間件等面試過程中面試官經常問的問題&#xff0c;歡迎大家關注。一起學習&#xff0c;一起成長&#xff0c;文章底部有面試題。入參映射關鍵字說明圖中paramenterTy…

php輸出多余的空格或者空行

1&#xff0c;文件是否有bom。可以通過腳步檢測&#xff0c;或者利用notepa打開&#xff0c;查看編碼格式。 2. <?php echo something; ?> 或許是你的php標簽外&#xff0c;有空格或者空行。一般的項目都是用框架&#xff0c;包含很多的文件&#xff0c;如果一個個文…

執行git命令時出現fatal: ‘origin‘ does not appear to be a git repository錯誤

執行git命令時出現fatal: ‘origin’ does not appear to be a git repository錯誤 在執行git pull origin master時出現&#xff1a;   fatal: ‘origin’ does not appear to be a git repository   致命提示:“origin”看起來不是一個git存儲庫   fatal: Could not r…

蔣濤作序盛贊Leo新作為“程序員職場實用百科全書”——《程序員羊皮卷》連載(1)

《程序員羊皮卷》當當購買地址&#xff1a;http://product.dangdang.com/product.aspx?product_id20691986 互動購買地址&#xff1a;http://www.china-pub.com/196049 程序員行業從外面看起來有很多絢麗的光環&#xff0c;這里有無數以程序致富的天才&#xff0c;世界首富比…

matlab ones函數_Matlab中相見恨晚的命令(持續更新)

知乎上有個“有哪些讓人相見恨晚的Matlab命令”的話題&#xff0c;很多答主提供的命令確實很實用&#xff0c;為了更方便大家的學習&#xff0c;我就知乎上的答案和我自己想到的都綜合整理成了一篇文章&#xff0c;把我覺得很實用的指令整理出來。知乎原答案鏈接dbstop if erro…

機器學習之特征工程

特征工程-概念 特征工程是一個面向十分廣的概念&#xff0c;只要是在處理數據就可以認為是在做特征工程。個人理解&#xff0c;真正意義上的特征工程還是數據降維和數據升維的過程。 而前期對數據的處理過程&#xff1a; 需要哪些數據&#xff1f;數據如何存儲&#xff1f;數…

ArcGIS AO開發高亮顯示某些要素

參考代碼1 ifeaturecursor pcur ifeatureclass.search(iqueryfilter pfilter); pfilter.whereclause strAddress; //輸入查詢條件&#xff0c;也就是你寸地址的字段名didian ifeature pfeat pcur.nextfeature();// 如果pCur多個要素&#xff0c;則可以考慮將其合并并一起高亮…

Oracle傳輸表空間介紹

傳輸表空間通過拷貝數據文件的方式&#xff0c;實現可跨平臺的數據遷移&#xff0c;效率遠超expdp/impdp, exp/imp等工具。還可以應用跨平臺&數據庫版本遷移表數據、歸檔歷史數據和實現表空間級時間點數據恢復等場景。轉載于:https://www.cnblogs.com/ilifeilong/p/7712654…

git push到GitHub的時候遇到! [rejected] master -> master (non-fast-forward)的問題

git push到GitHub的時候遇到! [rejected] master -> master (non-fast-forward)的問題 解決方法&#xff1a; 1、git pull origin master --allow-unrelated-histories //把遠程倉庫和本地同步&#xff0c;消除差異 2、重新add和commit相應文件 3、git push origin maste…

程序員考核的五大死因(上)

程序員作為企業開發力量的最核心資產&#xff0c;無疑得到公司從上至下的一致關注。開發是個智力密集型產業&#xff0c;程序開發的特點是&#xff0c;付出相同時間的情況下&#xff0c;兩個開發者之間的產能會相差十幾甚至幾十倍。軟件開發人員向來以“不容易考核、工作不容易…

du -sh 如何找到最大的文件夾_小白必看!手把手教你如何在linux上安裝redis數據庫...

首先我們要清楚redis是什么&#xff1f;redis是一種非關系型數據庫&#xff0c;它與MySQL的這種關系型數據庫不同&#xff0c;MySQL是將數據存儲在磁盤中&#xff0c;而redis是儲存在內存中。一般很多公司都是使用MySQLredis兩種數據存儲方式&#xff0c;這樣可以提高性能&…