三年前寫的《.net之美》的第六章,現在書名改為了《.net專題解析》。
本書是一本講解.net技術的書籍,目标讀者群也是在.net架構(.net framework)下進行開發的程式員,是以我們無法回避的問題就是:什麼是.net架構?它包含了哪些内容?為開發程式提供了哪些支援?很多朋友對這類個問題的第一反應可能是.net架構所提供的龐大類庫及編寫代碼所采用的c#語言,實際上遠不止這些。
要描述.net架構,自然會遇到與其相關的一系列專業的技術術語和縮寫,相信大家已經見到過許多了,比如:cli、cil、cts、cls、clr、jit、bcl、fcl、module、assembly 等,足以讓很多人一頭霧水、望而卻步。筆者不會像字典一樣按首字母排序對術語進行逐一解釋,因為這樣還是難以了解。我們還是從大家最熟悉的東西開始吧!
設想一下:編寫下面這樣一個最簡單的顯示“hello, world!”的控制台程式,并将該程式運作起來需要哪幾個步驟呢?
這些步驟包括:打開visual studio,建立一個c#控制台應用程式項目(在這裡将它命名為consoleapp),編寫代碼,編譯程式然後運作。雖然這樣的程式誰都會寫,但是再多進行一下思考就會發現,盡管是一個很小的程式,但已經引入了.net架構的幾個重要方面。
如果建立一個vb.net類型的項目,實作和上面c#項目完全一樣的功能,那麼編譯後生成的檔案有什麼差別?
編寫控制台應用程式,将字元輸出到螢幕,需要調用console.writeline()方法。這個console類型從何而來呢?
生成的檔案在系統中是如何運作起來的?其機制和使用傳統vc++生成的可執行檔案是否相同?
其實,上面每一個問題的答案都包含.net架構所提供的支援,這裡将它分為三個部分:
對于編譯後生成的檔案格式和内容,.net中存在着諸多規範。符合這些規範的程式語言,也叫做面向.net的語言。編譯後生成的檔案都可以在.net運作時下執行,這就是大家所熟知的.net多語言支援。
在開發階段,.net提供了一個龐大的類庫,支援開發者快速開發各種應用程式,也支援程式語言設計者開發其語言編譯器。
在程式執行階段,.net提供了一個程式運作時的環境,這個運作時環境幫助我們管理記憶體、實時編譯程式、進行安全檢查、執行垃圾回收等。
接下來就針對上述内容開始為大家詳細講述。
首先要了解的就是c#程式源碼在編譯之後會得到什麼樣的一個檔案。大家知道,過去使用vc++生成的可執行檔案,經過預編譯、編譯、彙編、連結幾個步驟後,最終生成的可執行檔案中就已經包含了處理器的本地代碼(native code),支援它運作的隻是作業系統和本地的機器指令集。那麼采用c#編譯器生成的檔案又是什麼呢?現在需要引入程式集這個概念:在.net架構下,類似c#這樣的進階語言經過編譯後生成的結果檔案被稱做程式集,其字尾名是.dll(類庫)或.exe(可執行程式)。在引入這個概念之前,前面(上一節)提到程式集時,都是用“檔案”這個詞來描述的。
程式集的定義隻是給編譯後生成的檔案一個稍微正式一點的名稱,對于解釋“它是由什麼構成的”這個問題并沒有太大的幫助。為了進一步了解程式集,我們再來做一個試驗,使用vb.net建立一個控制台應用程式項目(consoleappvb),并生成一個程式集,代碼功能和上面用c#建立的項目是一樣的的。
現在,需要一個工具來檢視這個程式集的内容,并且與c#項目生成的程式集進行對比。還好,微軟已經提供了一個利器——il dasm(il disassembler,il反彙程式設計式)來幫助開發者檢視程式集的資訊。如果安裝了visual studio,il dasm将會随同visual studio一起安裝。依次選擇開始菜單→ microsoft visual studio 2010 → microsoft windows sdk tools →il 反彙程式設計式(il dasm)可以啟動il dasm。
打開il dasm後選擇vb.net項目生成的consoleappvb.exe,可以看到如圖6-1所示的界面。

圖6-1 il dasm 運作界面
這部分内容很多,會在下一章“程式集”中進行專門講述,,這裡暫且略過。展開圖6-1中的consoleappvb.program類型,在main()方法上輕按兩下,會彈出另外一個視窗,顯示圖6-2中的代碼,看上去有點像彙編語言。在這裡可以看到熟悉的string text變量聲明及“hello, world !”。
圖6-2 方法體的cil語言描述(vb.net)
接下來再打開c#項目生成的consoleapp.exe,進行同樣的操作,在打開main()方法後會發現其中的代碼與圖6-2中幾乎完全一樣,如圖6-3所示
圖6-3方法體的cil語言描述(c#)
至此,可以得到一個初步的推斷:不管是vb.net還是是c#,編譯之後的程式集都能夠用il dasm打開,是以它們生成的程式集的格式都是相同的;當程式所實作的功能相同時,程式集所包含的cil代碼也是類似的。
現在對上面程式集中所包含的類似彙編的語言做一下介紹,即是本節标題中的cil(common intermediate language,公共中間語言)。cil最初是随着.net由微軟一起釋出的,是以之前也叫做msil(microsoft intermediate language),後來進行了标準化,之後便被稱做cil。在一些書或文章中,cil也會簡寫為il,其實都是指同樣的東西。為了避免混淆,本書統一用cil這個縮寫。
我們可以将上面的過程用圖6-4來表示出來。
圖6-4 源程式編譯為了程式集
接下來再深入地分析一下,公共中間語言這個術語到底包含了哪幾層含義。
公共。因為不論是c#語言也好,vb.net語言也好,c++/cli語言也好,甚至是重新開發的一套以自己的名字縮寫命名的語言,隻要它期望運作的目标平台是.net,在經過相應的編譯器編譯之後,所生成的程式集就是由cil語言代碼描述的。
中間。這個詞也是大有深意,為什麼不叫公共機器語言(common machine language),或者公共本地語言(common native language)?因為這種語言隻是比我們使用的進階語言,比如c#低級一點,并不是cpu可以直接執行的本地機器語言。這種語言還需要.net運作時(.net runtime)環境的支援,在執行之前,進行一個被稱為just-in-time(即時)的二次編譯過程,才能轉變成計算機可以識别的指令。關于.net運作時,以及詳細過程後面再介紹,現在隻要知道,這個檔案所包含的cil代碼并非機器可以直接執行的指令代碼。
語言。cil不過是一種程式語言,隻不過相對于c#來說,它是一種更低級語言。從圖6-2 的代碼截圖中,已經可以看到,cil是一種基于堆棧的語言,同時,它提供了class、interface、繼承、多态等諸多面向對象的語言特性,是以它又是完全面向對象的語言。如果願意,甚至可以直接編寫cil代碼,并且使用cil的編譯工具il asm(il assembler,il彙程式設計式)來對它進行編譯。隻不過,和大多數低級語言一樣,這種方式會使開發效率會變得很低。這裡注意差別一下il asm和il dasm,它們的拼寫是不同的。
為了加深一下印象,我們來做一個試驗:編寫一段簡單的cil代碼,并且使用il asm工具對其進行編譯,得到和前面一樣的consoleapp.exe程式。
1)打開記事本程式,輸入下面的代碼,然後将其儲存在d:\consoleapp.il。
2)打開visual studio 2010指令行工具,輸入:
3)成功後會看到consoleapp.exe程式,它的執行結果和上面用c#編寫的完全一樣。
由于程式集是由cil語言所描述的,是以cil也叫做程式集語言(assembly language)。又因為.net程式集需要由.net運作時加載才能運作,可以視其為由.net運作時進行管理的,是以cil代碼也叫做托管代碼(managed code)。相對的,不需要.net運作時就可以執行的代碼就叫做非托管代碼(unmanaged code)。
好了,已經知道了cil的存在,從現在開始,最好在頭腦裡建立起兩個模型或兩種視角:一種是基于c#或其他進階語言的源程式的視角,一種是基于cil中間語言的程式集視角。c#源程式在被編譯為程式集以後,就獨立于c#,是以程式集可以由其他種類的語言所調用;同時,因為程式集并沒有包含本地機器的指令,是以它與具體的機器類型也分隔開了,可以被裝有.net架構的任何機器運作。
我們先來看一個有意思的現象:再次打開前面建立的c#控制台項目(consoleapp),然後在解決方案面闆下打開“引用”檔案夾,如果用的是visual studio 2010,并且面向的目标架構是.net 4.0版本,那麼将會看到如圖6-5所示的這些引用。
圖6-5 解決方案中的“引用”檔案夾
在建立項目時并沒有做任何額外的操作,那麼這些引用顯然是在建立項目時自動添加的。為了友善初學者,這裡稍微解釋一下:要使用(實際上筆者覺得consume這個詞表達的更貼切)其他開發者所設計的類型,就需要在項目中将該類型所在的程式集引用進來。現在看到的這些程式集引用,都是微軟認為很常用的,幾乎是每個項目都會使用到的,是以在建立項目時自動添加了進來,免得開發者再手動進行添加。
但是在這裡這些引用不利于我們了解一些内容,是以我們把這些引用全部删除掉,如圖6-6所示,然後再次編譯程式。
圖6-6 删除掉所有的項目引用
可能有人會認為,在删掉這些引用之後,編譯器将會毫不客氣地提示編譯錯誤:未能找到類型或命名空間“system”(是否缺少using指令或程式集引用?)。可實際上,當編譯并運作上面的代碼時,程式會正确無誤地執行。這是因為我們已經删掉了所有引用的程式集,隻定義了一個program類型,并沒有定義console類型,是以此時要面對的第一個問題就是:console類型從哪裡來?
visual studio提供了一個快捷的辦法使我們可以快速檢視類型:将光标定位在console上,然後按下鍵盤上的f12,就可以看到console的類型定義。在console類型定義的最上方,可以看到它所在的程式集位址:c:\program files\reference assemblies\microsoft\framework\.netframework\v4.0\mscorlib.dll。
可以看到console類型來自于mscorlib.dll這個程式集。從上面的實驗可以看出,不管我們是否引用mscorlib.dll程式集,它總是會自動引用進來。這個程式集中所包含的類庫,即是本節标題中的bcl(base class library,基類庫)。從名字就可以看出來,這個類庫包含的都是些最基本的類型,其本身已經與cil語言融為一提了,為cil語言提供基礎的程式設計支援,以至于該類庫已經成為了cli标準的一部分(後面會介紹,是以也可以說bcl中的類型就是cil語言的類型,所有面向cil的語言都能夠使用它們。我們可以使用對象浏覽器(visual studio菜單→視圖→對象浏覽器)來檢視mscorlib.dll程式集中都包含了哪些命名空間和類型,如圖6-7所示。
圖6-7 mscorlib.dll中包含的命名空間
可以看到該程式集下包含的主要是system命名空間,稍微細心一點的讀者會發現,在建立項目的時候,還包含了system.dll程式集,并且其中所包含的類型與mscorlib中的類型十分相似。
圖6-8 system 程式集
圖6-9 system.dll中包含的命名空間
這又是怎麼回事呢?實際上,隻要點開system命名空間就會發現,mscorlib.dll的system命名空間下面定義的類型和system.dll的system命名空間下面定義的類型完全不同,它們之間并沒有沖突之處。
現在就明白了:bcl提供了像console這樣的類型來支援開發者編寫類似控制台這樣的程式。
既然已經思考了這麼多,不妨再深入一下,思考這樣一個問題:寫下的這條語句string text = “hello, world !”,其中的string從哪裡來?從直覺來看,string在visual studio中以深藍色呈現,屬于c#的關鍵字,那麼它應該是c#提供的内置類型。可是,當我們将光标移動到string上并按下f12時,轉到string的定義時,看到的卻是下面這樣的内容:
注意最上方的程式集位址,再次看到了mscorlib.dll,并且string類型與console類型一樣,同位于system命名空間下。由此可見,c#的關鍵字string,不過是bcl中system.string類型的一個别名而已。類似地,vb.net中的string關鍵字也是bcl中的system.string類型的别名。是以,在.net架構中,語言從本質上來說沒有太大的差別,更多的差別是在文法方面。從上面的例子也可以看出,c#和vb.net的很多語言能力并不是自己的,而是從cil“借”過來的這樣做也保證了在不同語言中相應類型的行為是一緻的。
表6-1列出了幾個典型的,不同語言關鍵字與cil類型的對應關系。筆者覺得了解重于記憶,是以這裡隻列出了幾個。要了解其他基礎類型時,隻要将光标移動到類型上,然後再按下f12鍵就可以了。
表6-1不同語言關鍵字與cil類型的對應關系
cil 類型
c# 關鍵字
vb.net關鍵字
system.byte
byte
sytem.int16
short
system.int64
int
integer
從表6-1可以看出,.net同時也對語言開發者提供支援.如你需要設計一款語言,那麼在開發編譯器時将語言的關鍵字映射為cil中的類型就可以了,也就是說,對自己語言中的一些特殊符号(關鍵字)進行映射處理,就好像c#中的關鍵字int和string一樣。
大家可能聽說過這樣一種特殊的類型——基元類型(primitive type)。實際上,講到這裡大家應該已經明白了,那些由編譯器直接支援,将語言本身的關鍵字類型轉換為cil類型的,就叫做基元類型。顯然,上面的byte、int、string都是基元類型。而c#中并沒有一個關鍵字去映射console,是以我們認為console隻是普通的類類型(class type)。
作為一名.net程式員,每天都要打交道的就是fcl了(framework class library,架構類庫)。在上一節中介紹了bcl,它是fcl的一個子集。bcl中包含了與編譯器及cil語言關系緊密的核心類型,以及常見開發任務中都會使用到的類型。而fcl包含的内容極多,僅服務于一種應用場景的子類庫就足夠寫一本書了,這裡僅簡單對它進行介紹。
從功能上來看,可以将fcl架構類庫劃分成以下幾層。
最内一層,由bcl的大部分組成,主要作用是對.net架構、.net運作時及cil語言本身進行支援,例如基元類型、集合類型、線程處理、應用程式域、運作時、安全性、互操作等。
中間一層,包含了對作業系統功能的封裝,例如檔案系統、網絡連接配接、圖形圖像、xml操作等。
最外一層,包含各種類型的應用程式,例如windows forms、asp.net、wpf、wcf、wf等。
假設要開發一套新的語言,這種語言和c#或vb.net一樣,在編譯後也能夠生成cil代碼,也可以在.net環境下運作,那麼首先需要什麼呢?
根據6.2節所講述的内容我們知道,要開發的新語言相當于cil的進階語言版本,是以實際上要做什麼并不是由新語言決定的,而是由cil來決定的。是以,需要一套cil的定義、規則或标準。這套規則定義了我們的語言可以做什麼,不可以做什麼,具有哪些特性。這套規則就稱作cts(common type system,公共類型系統)。任何滿足了這套規則的進階語言就可以稱為面向.net架構的語言。c#和vb.net不過是微軟自己開發的一套符合了cts的語言,實際上還有很多的組織或團體,也開發出了這樣的語言,比如delphi.net、fortran等。
那麼cts具體包括哪些内容呢?在回答這個問題之前我們需要弄清楚一個概念。還是通過一段c#代碼來說明,先看下面幾行代碼:
對于以上代碼,通常是這麼描述的:定義了一個book類,并且建立了兩個book類的執行個體item1、item2。實際上這隻包含了兩層含義如表6-2所示。
表6-2 類、類的執行個體
類
book
類的執行個體
item1,item2
再思考一下就會發現,還有一個更高的層面,那就是book這個類的類型,我們稱之為類類型(class type),是以上表可以改成如表6-3所示。
表6-3 類類型、類、類的執行個體
類類型
class
類似的,還有枚舉類型(enum type)、結構類型((struct type)等。現在大家應該明白這裡要表達的意思了,cts規定了可以在語言中定義諸如類、結構、委托等類型,這些規則定義了語言中更高層次的内容。是以,在c#這個具體的語言實作中,我們才可以去定義類類型(class type)或者結構類型(struct type)等。
同樣,可以在book類中定義一個字段name并提供一個方法showname()。實際上,這些也是cts定義的,它規範了類型中可以包含字段(filed)、屬性(property)、方法(method)、事件(event)等。
除了定義各種類型外,cts還規定了各種通路性,比如private、public、family(c#中為protected)、assembly(c#中為internal)、family and assembly(c#中沒有提供實作)、family or assembly(c#中為protected internal)。
cts還定義了一些限制,例如,所有類型都隐式地繼承自system.object類型,所有類型都隻能繼承自一個基類。從cts的名稱和公共類型系統可以看出,不僅c#語言要滿足這些限制,所有面向.net的語言都需要滿足這些限制。衆所周知,傳統c++是可以繼承自多個基類的。為了讓熟悉c++語言的開發者也能在.net架構上開發應用程式,微軟推出了面向.net的c++/cli語言(也叫托管c++),它就是符合cts的c++改版語言,為了滿足cts規範,它被限制為了隻能繼承自一個基類。
關于上面内容有兩點需要特别說明:
1)c#并沒有提供family and assembly的實作,c#中也沒有全局方法(global method)。換言之,c#隻實作了cts 的一部分功能。,也就是說,cts規範了語言能夠實作的所有能力,但是符合cts規範的具體語言實作不一定要實作cts規範所定義的全部功能。
2)c++/cli又被限制為隻能繼承自一個基類,換言之,c++中的部分功能被删除了。,就是說,任何語言要符合cts,其中與cts不相容的部分功能都要被舍棄。
顯然,由于cil是.net運作時所能了解的語言,是以它實作了cts的全部功能。雖然它是一種低級語言,但是實際上,它所具有的功能更加完整。c#語言和cil的關系,可以用圖6-10進行表示。
圖6-10 c#和cil的關系
既然已經了解了cts是一套語言的規則定義,就可以開發一套語言來符合cts了。假設這個語言叫做n#,它所實作的cts非常有限,僅實作了其中很少的一部分功能,它與cts和c#語言的關系可能如圖6-11所示。
圖6-11 c#、n#和cil的關系
那麼現在就有一個問題:由c#編寫的程式集,能夠引用由n#編寫的程式集嗎?答案顯然是不能,,雖然c#和n#同屬于cts旗下,但是它們并沒有共通之處。是以,雖然單獨的n#或c#程式可以完美地在.net架構下運作,但是它們之間卻無法互相引用。如果使用n#開發項目的開發者本來就不希望其他語言類型的項目來引用他的項目倒也罷了,但是,如果n#項目期望其他語言類型的項目能夠對它進行引用,就需要n#中公開的類型和功能滿足c#語言的特性,即它們需要有共通之處。注意,這句話中有一個詞很重要,就是“公開的”(public)。n#中不公開的部分(private、internal、protected)是不受影響的,可以使用獨有的語言特性,因為這些不公開的部分本來就不允許外部進行通路。是以, 如果n#想要被c#所了解和引用,它公開的部分就要滿足c#的一些規範,此時,它與cts和c#語言的關系就會變成如圖6-12所示。
圖6-12 c#、n#、cil的關系
如果世界上僅有c#和n#兩種語言就好辦了,把它們共同的語言特性提取出來,然後要求所有公開的類型都滿足這些語言特性,這樣c#和n#程式集就可以互相引用了。可問題是:語言類型有上百種之多,并且.net的設計目标是實作一個開放的平台,不僅現有的語言經過簡單修改就可以運作在.net架構上,後續開發的新語言也可以,而新語言此時并不存在,如何提取出它的語言特性?是以又需要一套規範和标準來定義一些常見的、大多數語言都共有的語言特性。對于未來的新語言,隻要它公開的部分能夠滿足這些規範,就能夠被其他語言的程式集所使用。這個規範就叫做cls (common language specification,公共語言規範)。很明顯,cls是cts的一個子集。現在引入了cls,圖6-12的關系圖就可以改成如圖6-13所示。
圖6-13 語言、cls、cil的關系
如果利用c#開發的一個程式集的公開部分僅采用了cls中的特性,那麼這個程式集就叫做cls相容程式集(clscompliant assembly)。顯然,對于上面提到的fcl架構類庫,其中的類型都符合cls,僅有極個别類型的成員不符合cls,這就保證了所有面向.net的語言都可以使用架構類庫中的類型。
現在,讀者又會有一個疑問:上面幾段文字中反複出現了一個詞———“語言特性”(language features),滿足cls就是要求語言特性要一緻,那麼什麼叫做語言特性?這裡給出幾個具體的語言特性:是否區分大小寫,辨別符的命名規則如何,可以使用的基本類型有哪些,構造函數的調用方式(是否會調用基類構造函數),支援的通路修飾符等。
那麼我們如何檢驗程式集是否符合cls呢?.net為我們提供了一個特性clscompliant,便于在編譯時檢查程式集是否符合cls。我們來看下面一個例子:
可以注意到,在clstest類的前面為程式集加上了一個clscompliant特性,表明這個程式集是cls相容的。但是,有三處并不滿足這個要求,是以編譯器給出了警告資訊。這三處是:
不能以大小寫來區分成員,是以字段name和方法name()不符合cls。
方法的傳回類型和參數類型必須是cls相容的,uint和sbyte類型并非cls相容,是以getvalue()和setvalue()方法不符合cls。
辨別符的命名不能以下劃線“_”開頭,是以屬性_myproperty不符合cls。
還會注意到,編譯器給出的隻是警告資訊,而非錯誤資訊,是以可以無視編譯器的警告,不過這個程式集隻能由其他c#語言編寫的程式集所使用。
前面提到過:程式集包含了cil語言代碼,而cil語言代碼是無法直接運作的,需要經過.net運作時進行即時編譯才能轉換為計算機可以直接執行的機器指令。那麼這個過程是如何進行的呢?
接下來我們要了解的就是.net架構的核心部分:clr(common language runtime),公共語言運作時),有時也會稱做.net運作時(.net runtime)。在了解clr之前,需要先進一步學習一下程式集,因為下一節會對程式集進行專門的講述,這裡僅簡單介紹一下程式集中對于了解clr有幫助的概念。
從直覺上來看,前面以.exe為字尾的控制台應用程式就是一個直接的可執行檔案,因為在輕按兩下它後,它确實會運作起來。這裡的情況和面向對象中的繼承有一點像:一台轎車首先是一部機動車、一隻貓首先是一個動物,而一個.net程式集首先是一個windows可執行程式。
那麼什麼樣格式的檔案才是一個windows可執行檔案?這個格式被稱做pe/coff(microsoft windows portable executable/common object file format),windows可移植可執行/通用對象檔案格式。windows作業系統能夠加載并運作.dll和.exe是因為它能夠了解pe/coff檔案的格式。顯然,所有在windows作業系統上運作的程式都需要符合這個格式,當然也包括.net程式集在内。在這一級,程式的控制權還屬于作業系統,pe/coff頭包含了供作業系統檢視和利用的資訊。此時,程式集可以表示成如圖6-14所示。
圖6-14 程式集結構1
在前面提到過,程式集中包含的cil語言代碼并不是計算機可以直接執行的,還需要進行即時編譯,那麼在對cil語言代碼進行編譯前,需要先将編譯的環境運作起來,是以pe/coff頭之後的就是clr頭了。clr頭最重要的作用之一就是告訴作業系統這個pe/coff檔案是一個.net程式集,差別于其他類型的可執行程式。
圖6-15 程式集結構2
在clr頭之後就是大家相對熟悉一些的内容了。首先,程式集包含一個清單(manifest),這個清單相當于一個目錄,描述了程式集本身的資訊,例如程式集辨別(名稱、版本、文化)、程式集包含的資源(resources)、組成程式集的檔案等。
圖6-16 程式集結構3
清單之後就是中繼資料了。如果說清單描述了程式集自身的資訊,那麼中繼資料則描述了程式集所包含的内容。這些内容包括:程式集包含的子產品(會在第7章介紹)、類型、類型的成員、類型和類型成員的可見性等。注意,中繼資料并不包含類型的實作,有點類似于c++中的.h頭檔案。在.net中,檢視中繼資料的過程就叫做反射(reflection)。
圖6-17 程式集結構4
接下來就是已經轉換為cil的程式代碼了,也就是中繼資料中類型的實作,包括方法體、字段等,類似于c++中的.cpp檔案。
圖6-18 程式集結構
注意,圖6-18中還多添加了一個資源檔案,例如.jpg圖檔。從這幅圖可以看出,程式集是自解釋型的(self-description),不再需要任何額外的東西,例如系統資料庫,就可以完整地知道程式集的一切資訊。
至此對程式集的簡單介紹就先到這裡,接下來看一下程式集是如何被執行的。
6.6.2 運作程式集
現在已經了解過了程式集,并且知道程式集中包含的cil代碼并不能直接運作,還需要clr的支援。概括來說,clr是一個軟體層或代理,它管理了.net程式集的執行,主要包括:管理應用程式域、加載和運作程式集、安全檢查、将cil代碼即時編譯為機器代碼、異常處理、對象析構和垃圾回收等。相對于編譯時(compile time),這些過程發生在程式運作的過程中,是以,将這個軟體層命名為了運作時,實際上它本身與時間是沒有太大關系的。有一些朋友在初學.net的時候,糾結在了runtime這個詞上,總以為和時間有什麼關系,總是不能很好地了解clr。筆者認為重要的是了解clr是做什麼的,而不用過于關注它的名稱。
實際上,clr還有一種叫法,即ves(virtual execution system,虛拟執行系統)。從上一段的說明來看,這個命名應該更能描述clr的作用,也不容易引起混淆,但是可能為了和cil、cts、cls等術語保持一緻性,最後将其命名為了clr。在這裡,我們知道clr不過是一個.net程式集的運作環境而已,有點類似于java虛拟機。ves這個術語來自于cli,會在6.7節進行講述。
可以用圖6-19來描述clr的主要作用。
圖6-19 clr的主要作用
前面已經概要地了解了clr的作用,接下來開始更進一步的學習。首先遇到的問題就是:clr以什麼樣的形式位于什麼位置?
由于clr本身用于管理托管代碼,是以它是由非托管代碼編寫的,并不是一個包含了托管代碼的程式集,也不能使用il dasm進行檢視。它位于c:\%systemroot%\microsoft.net\framework\版本号下,視安裝的機器不同有兩個版本,一個是工作站版本的mscorwks.dll,一個是伺服器版本的mscorsvr.dll。wks和svr分别代表work station和server。
接下來再看一下clr是如何運作起來的。雖然從windows server 2003開始,.net架構已經預裝在作業系統中,但是它還沒有內建為作業系統的一部分。當作業系統嘗試打開一個托管程式集(.exe)時,它首先會檢查pe頭,根據pe頭來建立合适的程序。
接下來會進一步檢查是否存在clr頭,如果存在,就會立即載入mscoree.dll。這個庫檔案是.net架構的核心元件之一,注意它也不是一個程式集。mscoree.dll位于c:\%systemroot%\system32\系統檔案夾下所有安裝了.net架構的計算機都會有這個檔案。大家可能注意到了,這個庫安裝在system32系統檔案夾下,而沒有像其他的核心元件或類庫那樣按照版本号存放在c:\%systemroot%\microsoft.net\framework\檔案夾下。這裡又存在一個“雞生蛋問題”:根據不同的程式集資訊會加載不同版本的clr,是以加載clr的元件就應該隻有一個,不能再根據clr的版本去決定加載clr的元件的版本。
mscoree.dll是一個很細的軟體層。加載了mscoree.dll之後,會調用其中的_corexemain()函數,該函數會加載合适版本的clr。在clr運作之後,程式的執行權就交給了clr。clr會找到程式的入口點,通常是main()方法,然後執行它。這裡又包含了以下過程:
加載類型。在執行main()方法之前,首先要找到擁有main()方法的類型并且加載這個類型。clr中一個名為class loader(類加載程式)的元件負責這項工作。它會從gac、配置檔案、程式集中繼資料中尋找這個類型,然後将它的類型資訊加載到記憶體中的資料結構中。在class loader找到并加載完這個類型之後,它的類型資訊會被緩存起來,這樣就無需再次進行相同的過程。在加載這個類以後,還會為它的每個方法插入一個存根(stub)。
驗證。在clr中,還存在一個驗證程式(verifier),該驗證程式的工作是在運作時確定代碼是類型安全的。它主要校驗兩個方面,一個是中繼資料是正确的,一個是cil代碼必須是類型安全的,類型的簽名必須正确。
即時編譯。這一步就是将托管的cil代碼編譯為可以執行的機器代碼的過程,由clr的即時編譯器(jit complier)完成。即時編譯隻有在方法的第一次調用時發生。回想一下,類型加載程式會為每個方法插入一個存根。在調用方法時,clr會檢查方法的存根,如果存根為空,則執行jit編譯過程,并将該方法被編譯後的本地機器代碼位址寫入到方法存根中。當第二次對同一方法進行調用時,會再次檢查這個存根,如果發現其儲存了本地機器代碼的位址,則直接跳轉到本地機器代碼進行執行,無需再次進行jit編譯。
可以看出,采用這種架構的一個好處就是,.net程式集可以運作在任何平台上,不管是windows、unix,還是其他作業系統,隻要這個平台擁有針對于該作業系統的.net架構就可以運作.net程式集。
cli是一個國際标準,由ecma和iso進行了标準化,全稱為common language infrastructure(公共語言基礎)。它隻是一個概念和彙總,實際上本章的每一小節都是這個标準的一部分。cli包括:cil、cts、cls、ves、中繼資料、基礎架構。
看到這裡很多人會感覺到有點奇怪,為什麼cli和.net架構包含的内容如此雷同?它們之間是什麼關系?簡單來說,cli是一個标準,而.net架構是這個标準的具體實作。在cli中,并沒有clr的概念,隻有ves,而clr就是.net架構中ves的具體實作。既然cli隻是一個标準,而.net架構是它在windows平台上的具體實作,那麼是不是就隻有.net架構這一個cli的實作?顯然不是,mono project就是cli标準的另一個實作。mono project的目标就是将.net架構多平台化,使其可以運作在各種平台上,包括mac os、linux等。
本章系統的學習地介紹了一下.net架構的底層知識,幾乎包含了常見的所有術語,例如程式集、cil、cts、cls、clr等,同時也介紹了它們之間是如何互相協作共同建構起整個.net平台的。相信經過本章的學習,大家會對.net架構有一個更好的全局性認識。
感謝閱讀,希望這篇文章能給你帶來幫助。