在使用DELPHI開發軟體的過程中,我們就像草原上一群快樂牛羊,無憂無慮地享受著Object Pascal語言為我們帶來的陽光和各種VCL控制提供的豐富的水草。抬頭望無邊無際蔚藍的天空,低頭品嚐大地上茂密的青草,誰會去想宇宙有多大,比分子和原子更小的東西是什麼?那是哲學家的事。而哲學家此時正坐在高高的山頂上,仰望宇宙星雲變換,凝視地上小蟲的爬行,驀然回頭,對我們這群吃草的牛羊點頭微笑。隨手扯起一根小草,輕輕地含在嘴裡,閉上眼睛細細品嚐,不知道這根青草在哲學家的嘴裡是什麼味道?只是,他的臉上一直帶著滿意的微笑。
認識和了解DELPHI微觀的原子世界,可以使我們徹底理解DELPHI的宏觀應用程式結構,從而在更廣闊的思想空間中開發我們的軟體。這就好像,牛頓發現了宏觀物體的運動,卻因為搞不清物體為什麼會這樣運動而苦惱,相反,愛因斯坦卻在基本粒子規律和宏觀物體運動之間體驗著相對論的快樂生活!
第一節TObject原子
TObject是什麼?
是Object Pascal語言體系結構的基本核心,也是各種VCL控制項的起源。我們可以認為,TObject是構成DELPHI應用的原子之一,當然,他們又是由基本Pascal語法元素等更細微的粒子所構成。
說TObject是DELPHI程式的原子,是因為TObject是DELPHI編譯器內部支援的。所有的物件類別都是從TObject派生的,即使你並未指定TObject為祖先類別。 TObject被定義在System單元,它是系統的一部分。在System.pas單元的開頭,有這樣的註解文字:
{ PRedefined constants, types, procedures, }
{ and functions (such as True, Integer, or }
{ Writeln) do not have actual declarations.}
{ Instead they are built into the compiler }
{ and are treated as if they were declared }
{ at the beginning of the System unit. }
它的意思說,這一單元包含預先定義的常數、類型、過程和函數(諸如:Ture、Integer或Writeln),它們並沒有實際的聲明,而是編譯器內建的,並在編譯的開始就被認為是已經聲明的定義。你可以將Classes.pas或Windows.pas等其他原始程式檔案加入你的專案檔案中進行編譯和偵錯其原始程式碼,但你絕對無法將System.pas原始程式檔案加入到你的專案檔案中進行編譯! DELPHI將報告重複定義System的編譯錯誤!
因此,TObject是編譯器內部提供的定義,對於我們使用DELPHI開發程式的人來說,TObject是原子性的東西。
TObject在System單元中的定義是這樣的:
TObject = class
constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): HResult; virtual;
procedure AfterConstruction; virtual;
procedure BeforeDestruction; virtual;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;
下面,我們將逐步敲開TObject原子的大門,看看裡面到底是什麼結構。
我們知道,TObject是所有物件的基本類,那麼,一個物件到底是什麼?
DELPHI中的任何物件都是指針,這個指針指明該物件在記憶體中所佔據的一塊空間!雖然,物件是一個指針,但我們引用物件的成員時卻不用寫成這樣的程式碼MyObject^.GetName,而只能寫成MyObject.GetName,這是Object Pascal語言擴充的語法,是由編譯器支援的。使用C++ Builder的朋友就很清楚物件與指標的關係,因為在C++ Builder的物件都要定義為指標。物件指標指向的地方就是物件儲存資料的物件空間,我們來分析一下物件指標所指向的記憶體空間的資料結構。
物件空間的頭4個位元組是指向該物件類別的虛擬方法位址表(VMT – Vritual Method Table)。接下來的空間就是儲存物件本身成員資料的空間,並依照從該物件最原始祖先類別的資料成員到該物件類別的資料成員的總順序,以及每一層類別中資料成員的定義順序儲存。
類別的虛擬方法位址表(VMT)保存從該類別的原始祖先類別派生到該類別的所有類別的虛擬方法的過程位址。類別的虛擬方法,就是用保留字vritual宣告的方法,虛方法是實現物件多態性的基本機制。雖然,以保留字dynamic聲明的動態方法也可實現物件的多態性,但這樣的方法不保存在虛擬方法位址表(VMT)中,它只是Object Pascal提供的另一種可節約類別儲存空間的多態實作機制,但卻是以犧牲呼叫速度為代價的。
即使,我們自己並未定義任何類別的虛擬方法,但該類別的物件仍然存在指向虛擬方法位址表的指針,只是位址項目的長度為零。可是,那些在TObject中定義的那些虛擬方法,如Destroy、FreeInstance等等,又儲存在哪裡呢?原來,他們的方法位址儲存在相對VMT指針負方向偏移的空間中。其實,在VMT表的負方向偏移76個位元組的資料空間是物件類別的系統資料結構,這些資料結構是與編譯器相關的,並且在將來的DELPHI版本中有可能被改變。
因此,你可以認為,VMT是一個從負偏移位址空間開始的資料結構,負偏移資料區是VMT的系統資料區,VMT的正偏移資料是使用者資料區(自訂的虛擬方法位址表)。 TObject中定義的有關類別資訊或物件運行時刻資訊的函數和過程,一般都與VMT的系統資料有關。
一個VMT資料就代表一個類,其實VMT就是類!在Object Pascal中我們用TObject、TComponent等等識別碼來表示類,它們在DELPHI的內部實作為各自的VMT資料。而用class of保留字定義的類別的類型,實際上就是指向相關VMT資料的指標。
對我們的應用程式來說,VMT數據是靜態的數據,當編譯器編譯完成我們的應用程式之後,這些數據資訊已經確定並已初始化。我們編寫的程序語句可訪問VMT相關的信息,獲得諸如對象的尺寸、類名或運行時刻的屬性資料等等信息,或者調用虛擬方法或讀取方法的名稱與地址等等操作。
當一個物件產生時,系統會為該物件分配一塊記憶體空間,並將該物件與相關的類別連結起來,於是,在為物件分配的資料空間中的頭4個位元組,就成為指向類別VMT數據的指針。
我們再來看看對像是如何誕生和滅亡的。看著我三歲的兒子在草地上活蹦亂跳,正是由於親眼目睹生命的誕生過程,我才能真真體會到生命的意義和偉大。也只有那些經歷過死別的人,才會更理解、珍惜生命。那麼,就讓我們來理解一下對象的產生與消亡的過程吧!
我們都知道,用下面的語句可以建構一個最簡單物件:
AnObject := TObject.Create;
編譯器將其編譯實現為:
以TObject對應的VMT為依據,呼叫TObject的Create建構子。而在Create建構函式呼叫了系統的ClassCreate過程,系統的ClassCreate過程又透過儲存在類別VMT呼叫NewInstance虛擬方法。呼叫NewInstance方法的目的是要建立物件的實例空間,因為我們沒有重載該方法,所以,它就是TObject類別的NewInstance。 TObjec類別的NewInstance方法將根據編譯器在VMT表中初始化的物件實例尺寸(InstanceSize),呼叫GetMem過程為該物件分配內存,然後調用InitInstance方法將分配的空間初始化。 InitInstance方法先將物件空間的頭4個位元組初始化為指向物件類別對應VMT的指針,然後將其餘的空間清除。建立物件實例之後,也呼叫了一個虛擬方法AfterConstruction。最後,將物件實例資料的位址指標儲存到AnObject變數中,這樣,AnObject物件就誕生了。
同樣,用下面的語句可以消滅一個物件:
AnObject.Destroy;
TObject的析構函數Destroy被宣告為虛方法,它也是系統固有的虛方法之一。 Destory方法先呼叫了BeforeDestruction虛方法,然後呼叫系統的ClassDestroy過程。 ClassDestory過程又透過類別VMT呼叫FreeInstance虛方法,由FreeInstance方法呼叫FreeMem過程釋放物件的記憶體空間。就這樣,一個物件就在系統中消失。
對象的析構過程比對象的構造過程簡單,就好像生命的誕生是一個漫長的孕育過程,而死亡卻相對的短暫,這似乎是一種必然的規律。
在物件的建構和析構過程中,呼叫了NewInstance和FreeInstance兩個虛函數,來建立和釋放物件實例的記憶體空間。之所以將這兩個函數宣告為虛函數,是為了能讓使用者在編寫需要使用者自己管理記憶體的特殊物件類別時(如在一些特殊的工業控製程式中),有擴充的空間。
而將AfterConstruction和BeforeDestruction宣告為虛函數,也是為了將來派生的類別在產生物件之後,有機會讓新誕生的物件呼吸第一口新鮮空氣,而在物件消亡之前可以允許物件完成善後事宜,這都是合情合理的事。其實,TForm物件和TDataModule物件的OnCreate事件和OnDestroy事件,就是在TForm和TDataModule重載的這兩個虛擬函數過程分別觸發的。
此外,TObjec也提供了一個Free方法,它不是虛方法,它是為了那些搞不清物件是否為空(nil)的情況下能安全釋放物件而專門提供的。其實,搞不清楚物件是否為空,本身就有程序邏輯不清晰的問題。不過,任何人都不是完美的,都可能犯錯,使用Free能避免偶然的錯誤也是件好事。然而,編寫正確的程式不能一味依靠這樣的解決方法,還是應該以保證程式的邏輯正確性為程式設計的第一個目標!
有興趣的朋友可以閱讀System單元的原始程式碼,其中,大量的程式碼是用組合語言書寫的。細心的朋友可以發現,TObject的建構子Create和析構函數Destory竟然沒有寫任何程式碼,其實,在調試狀態下通過Debug的CPU窗口,可清楚反映出Create和Destory的彙編程式碼。因為,締造DELPHI的大師門不想將過多複雜的東西提供給用戶,他們希望用戶在簡單的概念上編寫應用程序,將複雜的工作隱藏在系統的內部由他們承擔。所以,在發布System.pas單元時特別將這兩個函數的程式碼去掉,讓使用者認為TObject是萬物之源,用戶衍生的類別完全從虛無開始,這本身並沒有錯。雖然,閱讀DELPHI的這些最本質的程式碼需要少量的組合語言知識,但閱讀這樣的程式碼,可以讓我們更深刻認識DELPHI世界的起源和發展的基本規律。即使看不太懂,能起碼了解一些基本東西,對我們寫DELPHI程式也是大有幫助。
第二節TClass原子
在System.pas單元中,TClass是這樣定義的:
TClass = class of TObject;
它的意思是說,TClass是TObject的類別。因為TObject本身就是一個類,所以TClass就是所謂的類別的類別。
從概念上來說,TClass是類別的類型,即,類別之類。但是,我們知道DELPHI的一個類,代表一項VMT資料。因此,類別之類可以認為是為VMT資料項定義的類型,其實,它就是一個指向VMT資料的指標類型!
在以前的傳統C++語言中,是不能定義類別的型別的。物件一旦編譯就固定下來,類別的結構資訊已經轉換為絕對的機器碼,在記憶體中將不存在完整的類別資訊。一些較高級的物件導向語言才能支援對類別資訊的動態存取和調用,但往往需要一套複雜的內部解釋機制和較多的系統資源。而DELPHI的Object Pascal語言吸收了一些高階物件導向語言的優秀特徵,又保留可將程式直接編譯成機器碼的傳統優點,比較完美地解決了高階功能與程式效率的問題。
正是由於DELPHI在應用程式中保留了完整的類信息,才能提供諸如as和is等在運行時刻轉換和判別類的高級面向對像功能,而類的VMT數據在其中起了關鍵性的核心作用。有興趣的朋友可以閱讀System單元的AsClass和IsClass兩個彙編過程,他們是as和is操作符的實作程式碼,以加深對類別和VMT資料的理解。
DELPHI的原子世界(2)
關鍵字:Delphi控制項雜項
第二節TClass原子
在System.pas單元中,TClass是這樣定義的:
TClass = class of TObject;
它的意思是說,TClass是TObject的類別。因為TObject本身就是一個類,所以TClass就是所謂的類別的類別。
從概念上來說,TClass是類別的類型,即,類別之類。但是,我們知道DELPHI的一個類,代表一項VMT資料。因此,類之類可以認為是為VMT資料項定義的類型,其實,它就是一個指向VMT資料的指標類型!
在以前的傳統C++語言中,是不能定義類別的型別的。物件一旦編譯就固定下來,類別的結構資訊已經轉化為絕對的機器碼,在記憶體中將不存在完整的類別資訊。一些較高級的物件導向語言才能支援對類別資訊的動態存取和調用,但往往需要一套複雜的內部解釋機制和較多的系統資源。而DELPHI的Object Pascal語言吸收了一些高階物件導向語言的優秀特徵,又保留可將程式直接編譯成機器碼的傳統優點,比較完美地解決了高階功能與程式效率的問題。
正是由於DELPHI在應用程式中保留了完整的類信息,才能提供諸如as和is等在運行時刻轉換和判別類的高級面向對像功能,而類的VMT數據在其中起了關鍵性的核心作用。有興趣的朋友可以閱讀System單元的AsClass和IsClass兩個彙編過程,他們是as和is操作符的實作程式碼,以加深對類別和VMT資料的理解。
有了`類的類型,就可以將類別當作變數來使用。可以將類別的變數理解為一種特殊的對象,你可以像訪問對像那樣存取類別變數的方法。例如:我們來看看下面的程式片段:
type
TSampleClass = class of TSampleObject;
TSampleObject = class( TObject )
public
constructor Create;
destructor Destroy; override;
class function GetSampleObjectCount:Integer;
procedure GetObjectIndex:Integer;
end;
var
aSampleClass : TSampleClass;
aClass : TClass;
在這段程式碼中,我們定義了一個類別TSampleObject及其相關的類別類型TSampleClass,還包含兩個類別變數aSampleClass和aClass。此外,我們也為TSampleObject類別定義了建構子、析構函式、一個類別方法GetSampleObjectCount和一個物件方法GetObjectIndex。
首先,我們來理解一下類別變數aSampleClass和aClass的意思。
顯然,你可以將TSampleObject和TObject當作常數值,並可將它們賦值給aClass變量,就好像將123常數值賦值給整數變數i一樣。所以,類別類型、類別和類別變數的關係就是型別、常數和變數的關係,只不過是在類別的這個層次而不是物件層次上的關係。當然,直接將TObject賦值給aSampleClass是不合法的,因為aSampleClass是TObject派生類別TSampleObject的類別變量,而TObject並不包含所有與TSampleClass類型相容的定義。相反,將TSampleObject賦值給aClass變數是合法的,因為TSampleObject是TObject的衍生類,是和TClass類型相容的。這與物件變數的賦值和類型匹配關係完全相似。
然後,我們再來看看什麼是類別方法。
所謂類別方法,就是指在類別的層次上呼叫的方法,如上面所定義的GetSampleObjectCount方法,它是用保留字class宣告的方法。類別方法是不同於在物件層次上呼叫的物件方法的,物件方法已經為我們所熟悉,而類別方法總是在存取和控制所有類別物件的共同特性和集中管理物件這一層次上使用的。在TObject的定義中,我們可以發現大量的類別方法,如ClassName、ClassInfo和NewInstance等等。其中,NewInstance也被定義為virtual的,即虛的類別方法。這意味著你可以在衍生的子類別中重新編寫NewInstance的實作方法,以便用特殊的方式建構該類別的物件實例。
在類別方法中你也可使用self這個標識符,不過其所代表的意義與物件方法中的self是不同的。類別方法中的self表示的是自身的類,即指向VMT的指針,而物件方法中的self表示的是物件本身,即指向物件資料空間的指針。雖然,類別方法只能在類別層次上使用,但你仍可透過一個物件去呼叫類別方法。例如,可以透過語句aObject.ClassName來呼叫物件TObject的類別方法ClassName,因為物件指標所指向的物件資料空間中的頭4個位元組又是指向類別VMT的指標。相反,你不可能在類別層次上呼叫物件方法,象TObject.Free的語句一定是非法的。
值得注意的是,建構子是類別方法,而析構函數是物件方法!
什麼?建構子是類別方法,析構函式是物件方法!有沒有搞錯?
你看看,當你建立物件時分明使用的是類似下面的語句:
aObject := TObject.Create;
分明就是呼叫類別TObject的Create方法。而刪除物件時卻用的下面的語句:
aObject.Destroy;
即使使用Free方法釋放對象,也是間接呼叫了對象的Destroy方法。
原因很簡單,在建構物件之前,物件還不存在,只存在類,創建物件只能用類別方法。相反,刪除對像一定是刪除已經存在的對象,是對像被釋放,而不是類別被釋放。
最後,順便討論一下虛構造函數的問題。
在傳統的C++語言中,可以實現虛析構函數,但實現虛構造函數卻是一個難題。因為,在傳統的C++語言中,沒有類別的型別。全域物件的實例是在編譯時就存在於全域資料空間中,函數的局部物件也是編譯時就在堆疊空間中映射的實例,即使是動態建立的對象,也是用new運算子按固定的類別結構在堆空間中分配的實例,而建構函數只是一個對已產生的物件實例進行初始化的物件方法而已。傳統C++語言沒有真正的類別方法,即使可以定義所謂靜態的基於類別的方法,其最終也被實現為一種特殊的全域函數,更不用說虛擬的類別方法,虛擬方法只能針對特定的物件實例有效。因此,傳統的C++語言認為,在具體的物件實例產生之前,卻要根據即將產生的物件建構物件本身,這是不可能的。的確不可能,因為這會在邏輯上產生自相矛盾的悖論!
然而,正是由於在DELPHI中有動態的類別的類型信息,有真正虛擬的類別方法,以及建構函數是基於類別實現的等等這些關鍵概念,才可實現虛擬的建構函數。對像是由類產生的,對象就好像成長中的嬰兒,而類就是它的母親,嬰兒自己的確不知道自己將來會成為什麼樣的人,可是母親們卻用各自的教育方法培養出不同的人,道理是相通的。
正是在TComponent類別的定義中,建構子Create被定義為虛擬的,才能讓不同類型的控制項實作各自的建構方法。這就是TClass創造的類之類概念的偉大,也是DELPHI的偉大。
....................................
第三章WIN32的時空觀
我的老父親看著地上玩玩具的小孫子,然後對我說:「這孩子和小時的你一樣,喜歡把東西拆開,看過究竟才罷手」。想想我小時侯,常將玩具車、小鬧鐘、音樂盒,等等,拆得一塌糊塗,常常被母親訓斥。
我第一次理解電腦的基本原理,與我拆開的音樂盒有關。那是在讀高中時的一本漫畫書上,一位白鬍子老頭在講解智能機的理論,一位留八字胡的叔叔在說電腦和音樂盒。他們說,電腦的中央處理器就是音樂盒中用來發音的那一排音樂簧片,電腦程式就是音樂盒中那個小圓筒上密布的凸點,小圓筒的轉動相當於中央處理器的指令指標的自然移動,而小圓筒上代表音樂的凸點控制音樂簧片振動發音相當於中央處理器執行程式的指令。音樂盒發出美妙的旋律,是按工匠早已刻在小圓筒上的音樂譜演奏的,計算機完成複雜的處理,是根據程式設計師預先編制好的程序實現的。上大學之後,我才知道那個白鬍子老頭就是科學巨匠圖靈,他的有限自動機理論推動了整個信息革命的發展,而那個留八字胡的叔叔就是計算機之父馮.諾依曼,馮氏電腦體系結構至今仍是電腦的主要體系機構。音樂盒沒白拆,母親可以寬心。
有深入淺出的理解,才能有高深而簡潔的創造。
這一章我們將討論Windows的32位元作業系統中與我們程式設計有關的基本概念,建立WIN32中正確的時空觀。希望閱讀完本章之後,我們能更深入地理解程式、進程和線程,理解執行檔、動態連接庫和運行包的原理,看清全域資料、局部資料和參數在記憶體中的真相。
第一節理解進程
由於歷史的原因,Windows是起源於DOS。而在DOS時代,我們一直只有程式的概念,而沒有進程的概念。那時侯,只有作業系統的正規軍,如UNIX和VMS等等,才有進程的概念,而且多進程就意味著小型主機、終端和多用戶,也意味著金錢。我絕大多數的時間只能使用相對廉價的微電腦和DOS系統,只是在學操作系統這門課程時才開始接觸進程和小型機。
在Windows 3.X之後,Microsoft才在圖形介面的作業系統站住腳跟,而我也是在這時開始正式面對多任務和進程的概念。以前在DOS下,同一時間只能執行一個程序,而在Windows下同一時間可執行多個程序,這就是多任務。在DOS下運行一個程序的同時,不能執行相同的程序,而在Windows下,同一程序可以同時有兩個以上的副本在運行,每一個運行的程序副本就是一個進程。更確切地說,任何程式的一次運行都會產生一個任務,而每個任務就是一個行程。
當將程式和流程放到一起理解時,可以認為程式一詞說的是靜態的東西,一個典型的程式是由一個EXE檔案或一個EXE檔案加上若干DLL檔案所組成的靜態程式碼和資料。而進程是程式的一次運行,是在記憶體中動態運行的程式碼和動態變化的資料。當靜態的程式要求運行時,作業系統將為本次運行提供一定的記憶體空間,把靜態的程式碼和資料調入這些記憶體空間,將程式的程式碼和資料進行重定位映射之後,就在該空間內執行程序,這樣就產生了動態的進程。
同一個程式同時運行的兩個副本,意味著在系統記憶體中有兩個進程空間,只不過它們的程式功能是一樣的,但處於不同的動態變化的狀態。
從進程運行的時間上來說,各進程是同時執行的,專業術語稱為並行執行或並行執行。但這主要是作業系統給我們的表面感覺,實際上各進程是分時執行的,也就是各進程輪流佔用CPU的時間來執行進程的程式指令。對一個CPU來說,同一時間只有一個行程的指令在執行。作業系統是調度進程運行的幕後操縱者,它不斷保存和切換各進程在CPU中執行的當前狀態,使得每一個被調度的進程都認為自己是完整和連續地運行著。由於進程分時調度的速度非常快,所以給我們的感覺就是進程都是同時運作的。其實,真正意義上的同時運作只有在多CPU的硬體環境中才有。稍後在講述線程一節時,我們將發現,真正推動進程運轉的是線程,進程更重要的是提供了進程空間。
從進程佔據的空間來說,各進程空間是相對獨立的,每一個進程在自己獨立的空間中運作。一個程式既包含程式碼空間又包含資料空間,程式碼和資料都要佔據進程空間。 Windows為每一進程所需的資料空間分配實際的內存,而對代碼空間一般都採用共享手段,將一個程序的一份代碼映射給該程序的多個進程。這意味著,如果一個程式有100K的程式碼並需要100K的資料空間,也就是總共需要200K的進程空間,則第一次執行程式時作業系統將分配200K的進程空間,而執行程式的第二個進程時,作業系統只分配100K的資料空間,而程式碼空間則共享前一個行程的空間。
上面所說的是Windows作業系統中行程的基本時空觀,其實Windows的16位元和32位元作業系統在行程的時空觀上有很大的差異。
從時間上來說,16位元的Windows作業系統,如Windows 3.x等,行程管理是非常簡單的,它其實只是一個多工管理作業系統。而且,作業系統對任務的調度是被動的,如果一個任務不自己放棄對訊息的處理,作業系統就必須等待。由於16位元Windows系統在管理進程方面的缺陷,一個行程運行時,完全佔有CPU的資源。在那個年代,為了16位Windows可以有機會調度別的任務,微軟公司大力讚揚開發Windows應用程式的開發者是心胸寬闊的程式設計師,以使得他們樂意多編寫幾行恩賜給作業系統的程式碼。相反,WIN32的作業系統,如Windows 95和NT等,才是具備了真正的多進程和多任務作業系統的能力。 WIN32中的進程完全由作業系統調度,一旦進程運行的時間片結束,不管進程是否還在處理數據,作業系統將主動切換到下一進程。嚴格地說,16位元的Windows作業系統不能算是完整的作業系統,而32位元的WIN32作業系統才是真正意義上的作業系統。當然,微軟公司不會說WIN32彌補了16位元Windows的缺陷,而是宣稱WIN32實現了一種稱為「搶佔式多任務」的先進技術,這是商業手段。
從空間上看,16位元的Windows作業系統中的進程空間雖然相對獨立,但進程之間可已很容易地互相存取對方的資料空間。因為,這些進程實際上是在相同的物理空間中的不同的資料段而已,而且不當的位址操作很容易造成錯誤的空間讀寫,並使作業系統崩潰。然而,在WIN32作業系統中,各進程空間完全是獨立的。 WIN32為每一個進程提供一個可達4G的虛擬的,並且是連續的位址空間。所謂連續的位址空間,是指每個行程都擁有從$00000000到$FFFFFFFF的位址空間,而不是向16位元Windows的分段式空間。在WIN32中,你完全不必擔心自己的讀寫操作會無意地影響到其他進程空間中的數據,也不用擔心別的進程會來騷擾你的工作。同時,WIN32為你的進程提供的連續的4G虛擬空間,是作業系統在硬體的支援下將實體記憶體映射給你的,你雖然擁有如此廣闊的虛擬空間,但係統絕不會浪費一個字節的實體記憶體。
第二節進程空間
當我們用DELPHI編寫WIN32的應用程式時,很少去關心進程在運行時的內部世界。因為WIN32為我們的進程提供了4G的連續虛擬進程空間,可能目前世界上最龐大的應用程式也只用到了其中的部分空間。似乎進程空間是無限的,但4G的進程空間是虛擬的,而你機器的實際記憶體可能與此相差甚遠。雖然,進程擁有如此廣闊的空間,但有些複雜演算法的程式還是會因為堆疊溢位而無法運行,特別是含有大量遞歸演算法的程式。
因此,深入地認識和了解這4G的進程空間的結構,以及它與物理記憶體的關係等等,將有助於我們更清楚地認識WIN32的時空世界,從而可在實際的開發工作中運用正確的世界觀和方法論解決各種難題。
下面,我們將透過簡單的實驗,來了解WIN32的進程空間的內在世界。這可能需要一些對CUP暫存器和組合語言的知識,但我盡量用簡單的語言來說明。
當啟動DELPHI時,將自動產生一個Project1的項目,我們就拿它開刀。在Project1.dpr原程式的任意位置設一斷點,例如,就在begin一句處設一斷點。然後運行程序,當程序運行到斷點時會自動停下來。這時,我們就可以開啟偵錯工具中的CPU視窗來觀察進程空間的內部結構了。
目前的指令指標暫存器Eip是停在$0043E4B8,從程式指令所在位址的最高兩位16進位數都是零,可以看出目前的程式處在4G進程空間相當底端的位址位置,其佔據$00000000到$FFFFFFFF的相當少的位址空間。
在CPU視窗中的指令框中,你可以向上查看進程空間中的內容。當查看小於$00400000的空間內容時,你會發現小於$00400000的內容出現一串串的問號“????”,那是因為該地址空間還未映射到實際物理空間的緣故。如果在這時,你查看全域變數HInstance的16進位值就會發現它也是$00400000。雖然HInstance反映的是進程實例的句柄,其實,它就是程式被載入到記憶體中的起始位址值,在16位元Windows中也是如此。因此,我們可以認為進程的程式是從$00400000開始載入的,也就是從4G虛擬空間中的4M以後的空間開始是程式載入的空間。
從$00400000往後,到$0044D000之前,主要是程式碼和全域資料的位址空間。在CPU視窗中的堆疊框中,可以查看到目前堆疊的位址。同樣,你會發現目前堆疊的位址空間是從$0067B000到$00680000的,長度為$5000。其實,進程最小的堆疊空間大小就是$5000,它是根據編譯DELPHI程式時在ProjectOptions中Linker頁中設定的Min stack size值,加上$1000而得到的。堆疊是由高階位址向底端成長的,當程式運行的堆疊不夠時,系統將自動向地端位址方向增加堆疊空間的大小,這個過程將把更多的實際記憶體映射到進程空間。可在編譯DELPHI程式時,透過設定ProjectOptions中Linker頁中Max stack size的值,控制可增加的最大堆疊空間。特別是在含有深層的子程序呼叫關係或運用遞歸演算法的程式中,一定要合理地設定Max stack size的值。因為,呼叫子程式是需要耗用堆疊空間,而堆疊耗盡之後,系統就會拋出「Stack overflow」的錯誤。
似乎,從堆疊空間之後的進程空間就應該是自由的空間了吧。其實不然,WIN32的相關資料說,$80000000之後的2G空間是系統使用的空間。看來,進程能夠真正擁有的只有2G空間。其實,進程能真正擁有的空間連2G都不夠,因為從$00000000到$00400000的這4M空間也是禁區。
但不管怎樣,我們的流程可以使用的位址還是非常廣闊的。特別是堆疊空間之後到$80000000之間,是進程空間的主戰場。進程從系統分配的記憶體空間將被映射到這塊空間,進程加載的動態連接庫將被映射到這塊空間,新建線程的線程堆疊空間也將映射到這塊空間,幾乎所有涉及分配記憶體的操作都將映射到這塊空間。請注意,這裡所說的映射,意味著實際內存與這塊虛擬空間的對應,沒有映射為實際內存的進程空間是無法使用的,就像調試時CPU窗口指令框中的那一串串的“ ????」。