目錄
一 “單檔案程式集”與“多檔案程式集”
二 “普通程式集”與“強式名稱程式集”
三 “私用組件”與“共用組件”
在學習程式集時,總是發現程式集被冠以各種頭銜。程式集按檔案數量可分為:單檔案程式集和多檔案程式集;按是否簽名,可分為:普通程式集和強命名程式集;按部署方式,可分為:私用組件和共用組件。下面開始分別介紹。
一 “單檔案程式集”與“多檔案程式集”
程式集可以由多個模組組成。在大多數情況下,程式集只由一個模組構成。這種情況下,程式集就一個檔案,因此被稱為單檔案程式集。多檔案程式集是一組檔案集合,包括主模組、輔助模組以及資源。大多數程式集是單檔案程式集。多檔案程式集的存在主要用來支援兩個情況:第一種情況是用現購現付的方法實現程式集的下載,以便用戶端下載一個程式集時,通過垂滴方式可以下載所需的模組;第二種情況是實現“本地化”。顯示了單檔案程式集於多檔案程式集的構成:
多檔案程式集的特點如下:
- 可用單獨的檔案對程式集進行劃分,允許檔案“增量”方式下載。
- 可以使某個檔案成為程式集的一部分,如Excel檔案.。
- 程式集的各個模組可以用不同程式設計語言實現。
多檔案程式集包括主模組、輔助模組以及資源。主模組包含了程式集的清單。輔助模組的副檔名多為.netmodule(CLR沒有強制要求),它也包含IL和中繼資料,同時還有一個模組層級別的清單,該清單只記錄該模組外部參考的程式集。資源中則可能包含表徵圖或本地化資源。組成一個多檔案程式集的模組並沒有相互串連成一個大檔案,相反,它們只是依靠在主模組中記錄的資訊邏輯地聯絡在一起。
一旦CLR載入包含了清單的那個檔案,就可以確定在程式集的其它檔案中,具體是哪一些檔案包含應用程式引用的類型和資源。每次載入一個程式集CLR和OS都要花費一定時間來尋找、載入並初始化程式集。所以,需要載入的檔案數量越少,效能越好。
VS沒有提供多檔案程式集模板,我們需要使用命令列建立多檔案程式集,具體用法請參考MSDN的 csc.exe 手冊。
VS支援引用多檔案程式集,只需要引用主模組,VS會自動複製相關的模組到項目。在使用多檔案程式集中的類型時,會按需載入模組。
我們可以以各種方式組合多檔案程式集,但從最佳實務角度講,應該遵循以下的組合規則:
- 總是在獨立的程式集中儲存特定的地區設定資源,而不是作為程式集中的一個內嵌資源使用它們。
- 避免帶有不包含IL模組的多檔案類型程式集。
- 最小化應用程式集的代碼。關注可視化布局,而將商務邏輯封裝在其他類庫程式集中。
- 確保一個類庫的所有組件有同樣的生命週期,並且總是有相同的版本號碼和安全憑證。如果你預見到可能有偏差,則可將程式集分割成兩個類庫。
二 “普通程式集”與“強式名稱程式集”
CLR支援“普通程式集”和“強式名稱程式集”,兩者在結構上完全一致,其區別在於,強式名稱程式集使用發行者的公開金鑰/私密金鑰對進行了簽名,它唯一地標識了程式集的發行者。這一對密鑰允許對程式集進行唯一的標識、保護和版本控制。由於程式集被唯一性地標識,所以當一個應用程式試圖綁定到一個強式名稱程式集時,CLR可以應用一些已知安全的策略。(建議使用強式名稱程式集,因為:第一,不同的普通程式集可能具有相同的名稱,會造成混淆;第二,如果 CLR 能唯一地標識了一個程式集,就可以應用更多的版本控制和向後相容策略。)
(一) 為普通程式集分配強式名稱
一個強式名稱程式集的唯一標識由四部分組成:
- 檔案名稱(不計副檔名)
- 版本號碼([AssemblyVersion] 特性)
- 語言文化標識([AssemblyCulture] 特性)
- 公開金鑰([AssemblyKeyFile] 特性:顯式指定時,VS會產生警告來通知你使用正規途徑建立密鑰檔案。)
顯示了一個普通程式集的這四部分的值:
.NET使用標準的公開金鑰/私密金鑰加密技術(唯一且抗篡改),不僅能在程式集安裝到一台機器上時檢查其位元據的完整性,還允許每個發行者能授予一套不同的許可權。因為公開金鑰/私密金鑰對不會重複,所以即使程式集具有相同名稱、版本、文化也不會造成衝突。
1 建立密鑰
使用VS建立密鑰:
當勾選使用密碼保護時,產生.pfx的密鑰檔案(用密碼保護的密鑰檔案);而不使用密碼保護時產生.snk的密鑰檔案,檔案中都包含了二進位形式的公開金鑰/私密金鑰對。
2 簽名
編譯時間,編譯器將基於公開金鑰和私密金鑰資料產生的數位簽章,並把它嵌入到程式集中,如所示:
簽名的確切含義是:產生一個強式名稱程式集時,程式集的FileDef清單中繼資料表列出了構成程式集的所有檔案,每次將一個檔案的名稱添加到清單中,檔案的內容都會進行雜湊處理,得到的雜湊值會和檔案名稱一道儲存在FlieDef表中。(可以在代碼中使用[assembly:AssemblyAlgorithmId]特性來改變預設的SHA-1演算法。)
編譯器知道密鑰檔案的位置後,就會在編譯過程中使用.publickey標記把密鑰值記錄到資訊清單中。產生了包含清單的PE檔案後,會對PE檔案的完整內容進行雜湊處理(產生一個基於整個程式集的散列值,只要程式集內容有一丁點的修改,這個散列值都會改變),此時使用的演算法始終是SHA-1,而且不能改變。這個雜湊值使用發行者的私密金鑰進行簽名(散列值結合私密金鑰組成數位簽章),最終得到的RSA數位簽章會儲存到PE檔案的一個保留地區中(進行雜湊處理時會忽略這個地區)。PE檔案的CLR頭會進行更新,反映出數位簽章在檔案中的嵌入位置(數位簽章被嵌入到了CLR頭)。發行者公開金鑰也嵌入這個PE檔案的AssemblyDef清單中繼資料表中。檔案名稱、程式集版本號碼、語言文化、公開金鑰的組合為這個程式集賦予了一個強式名稱,它保證了唯一性。
公開金鑰會寫入資訊清單,簽名可以使用公開金鑰來驗證,私密金鑰不儲存在程式集中,這樣就可以確保除了私密金鑰的主人外,沒有人可以修改該程式集。
3 查看結果
再次查看構成程式集資訊:
資訊清單:
從可看出2個公開金鑰不太像。這是因為,公開金鑰太大了,為了簡化,設計了公開金鑰標記(public key tocken)。公開金鑰標記是公開金鑰的64位雜湊值(最後8個位元組)。AssemblyRef儲存的實際上是簡化了的公開金鑰標記而不是公開金鑰。我們看到的雖然是公開金鑰標記,但是,CLR在做出安全或信任決策時,永遠都不會使用公開金鑰標記,因為不同公開金鑰可能會得到相同的公開金鑰標記。
查看AssemblyRef:
查看AssemblyDef:
(二) 延遲簽名
延遲簽名也稱部分簽名。延遲簽名允許你只用公開金鑰來產生一個程式集。在全域程式集屬性AssemblyDelaySign設定為true時,簽名就不會儲存在程式集中,但保留了足夠的空間,以便以後添加,但是,不使用密鑰,就不能測試程式集,在全域組件快取(GAC)中安裝它,也無法實現防篡改保護。如果為了保護群組織私密金鑰的安全,可以使用臨時密鑰進行測試,以後再用真正的密鑰代替這個臨時密鑰。延遲簽名的更多用處在於配合混淆器使用。程式集在完全簽名之後,不能再對它運行混淆器,否則雜湊值就不正確了。所以,想混淆一個組件檔,或者進行其它形式的“產生後”操作,就應該使用延遲簽名。
在VS中只要勾選“僅延遲簽名”即可:
如所示,延遲簽名後該項目將不會運行,也不能進行調試。可以使用sn.exe,跳過驗證過程,操作流程如下:
(1) 建立密鑰:sn -k key.snk
(2) 在VS中勾選“僅延遲簽名”並編譯器集,或者執行命令列:csc /keyfile:key.PublicKey /delaysign myCustom.cs
(3) 關閉簽名的驗證功能,以便安裝到GAC:sn -Vr myCustom.dll
(4) 部署到GAC
(5) 發布前,用私密金鑰重新簽名:sn -R myCustom.dll key.PrivateKey
(6) 重新啟用對程式集的驗證:sn -Vu myCustom.dll
延遲簽名只能在開發過程中關閉,不經過驗證是不能進行發布程式的,因為這個程式集可能被懷有惡意的程式集替代。
(三) 防篡改
完全的強式名稱程式集可以防篡改。強簽名程式集可以私人部署,也可以共用部署,兩種部署方式都提供了檢查機制來防止篡改。
1 共用部署的檢查機制
將一個強式名稱程式集安裝到GAC時,系統會執行一次檢查,核對含有清單的那個檔案沒有被篡改,系統對包含清單的檔案的內容進行雜湊處理,並將雜湊值與PE檔案中嵌入的RSA數位簽章進行比較(在公開金鑰解除了對它的簽名之後)。如果兩個值完全一致,表明檔案的內容未被篡改,可保證你拿到的公開金鑰與發行者的私密金鑰是完全對應的。除此之外,系統還會對程式集的其它檔案的內容進行雜湊處理,並將雜湊值與資訊清單檔的FileDef表中儲存的雜湊值進行比較。任何一個雜湊值不匹配,表明程式集至少有一個檔案被篡改,程式集將無法安裝到GAC。這個檢查只在安裝時執行一次。除此之外,為了增強效能,如果程式集被完全信任,並載入到一個完全信任AppDomain,CLR不會檢查強式名稱的程式集是否被篡改。
應用程式需要綁定一個程式集時,CLR根據所引用程式集的屬性(名稱、版本、語言文化、公開金鑰)在GAC中定位程式集。如果能夠找到引用的程式集,就返回包含它的那個子目錄,並載入容納清單的檔案。以這種方式尋找程式集,可以保證運行時載入的程式集和最初編譯時間所產生的程式集來自同一個發行者。之所以能做出這樣的保證,是因為進行引用的程式集的AssemblyRef表中的公開金鑰標記與被引用的程式集的AssemblyDef表中的公開金鑰是匹配的。如果被引用的程式集不在GAC中,CLR會尋找應用程式的基目錄,然後尋找應用程式設定檔中標註的任何私人路徑。然後,如果應用程式是用MSI安裝的,CLR會要求MSI定位程式集。如果在任何位置都找不到程式集,綁定操作會失敗,並拋出FileNotFoundException異常。
2 私人部署的檢查機制
如果程式集是從GAC之外的位置載入的,CLR會在程式集載入時比較雜湊值,應用程式每次執行並載入程式集時,都會校正程式集的清單,並對組件檔進行一次雜湊處理,以犧牲一定的效能為代價,保證組件檔的內容沒有被篡改。CLR在運行時檢測到不匹配的雜湊值,會跑出FileLoadException異常。
三 “私用組件”與“共用組件”
程式集可以採用兩種方式部署:私人或共用。私用組件部署到應用程式的目錄中。普通程式集只能以私人方式部署。共用組件是指部署到一次已知位置的程式集。強式名稱程式集既可以部署為私人,也可以部署為共用。
(一) 私用組件
私用組件要求放置在用戶端應用程式目錄或者其子目錄下。
1 私用組件標識
私用組件的全名稱包括易記名稱和數字版本號碼,兩者都被記錄在程式集的清單中,易記名稱就是模組(包含資訊清單的)名字減去副檔名。鑒於私用組件的獨立性,CLR在解析它的位置時並不忙於使用它的版本號碼。
2 探測過程
.NET使用一種“探測”技術解析私用組件的位置。探測是一種把外部程式集請求映射到被請求的二進位檔案位置的過程。一個載入請求可以是顯式的或者隱式的。隱式的載入請求發送在CLR查詢清單的.assembly extern標記來解析程式集的位置的時候;明確式載入請求發送在以編程方式調用System.Reflection.Assembly的Load()或者LoadFrom()方法時。
不管隱式還是顯式,CLR獲得程式集的易記名稱後,便開始探測應用程式目錄下的組件檔(*.dll)。如果找不到,CLR會嘗試尋找該目錄下具有程式集易記名稱的子目錄。如果找不到檔案,它就嘗試去尋找具有相同名稱的可執行程式集(*.exe),在次將之前的目錄再尋找一遍。如果還是找不到,CLR會拋出FileNotFound異常。CLR只會載入探測過程中第一個找到的程式集。附屬組件遵循類似的規則,只是CLR會在應用程式基底目錄下的一個子目錄中尋找它,子目錄的名稱與語言文化的名稱相符。
通過使用設定檔可以影響CLR的探測過程。設定檔必須擁有和應用程式相同的名稱,副檔名為.config,並位於與程式集相同的目錄。如果想讓CLR探測程式的MyCustom子目錄,可以如下設定設定檔:
如果我們程式集應用了“en-US”語言文化,和上例中的設定檔,那麼CLR尋找程式集do.dll和附屬組件language.dll的探測順序如下:
為了節省探測過程時間,在設定檔中,最好指定一個或者多個culture元素,對CLR尋找附屬組件的探測過程進行限制。
3 私人部署強式名稱程式集
可以通過設定檔將強式名稱程式集部署到一個已知路徑下面,如所示:
(二) 共用組件
共用組件的一個副本可以供一台機器上的多個應用程式使用,所以,如果要建立一個機器層級的類庫,那麼需要把它部署為共用組件。共用組件安裝在全域組件快取(GAC)中。GAC在Windows目錄下的名為Assembly的目錄。只有*.dll檔案且程式集是強式名稱的才可以部署為共用組件。
1 GAC
預設情況下GAC只能由系統管理員許可權的使用者操作。GAC目錄是結構化的,其中包含很多子目錄,並用一個演算法來產生這些子目錄的名稱。因為不清楚GAC的內部結構,所以,安裝程式集時不能手動複製程式到GAC目錄,必須通過拖拽或使用工具來完成這項任務,以便產生正確的子目錄。
2 在GAC中安裝和移除共用組件
安裝的最簡單方法就開啟Windows資源管理員,把程式集直接拖到GAC中(不要複製、黏貼),以便本地測試使用。
最好使用gacutil.exe來管理GAC,運行時會自動顯示協助:
3 使用共用組件
在VS的添加共用組件的引用時,不能通過瀏覽GAC添加,只能在GAC外部其它目錄下引用程式集。因為共用組件是強式名稱的,VS會假定這個程式集已經部署到GAC,因此,在調試和發布時,不會對該程式集進行複製。可以在所示選項中進行修改:
4 動態重新導向到共用組件的特定版本
在設定檔中進行如下設定可以使CLR載入一個不同於程式清單中的特定版本的共用組件:
5 發行者策略程式集
設定檔可以忽略清單中的版本,使用戶端綁定指定版本的程式集。如果使用這種方法,我們必須保證每個用戶端上的設定檔都必須被正確配置,顯然每次修改起來很麻煩。發行者策略允許程式集的發行者在安裝程式集到GAC的同時,把設定檔的二進位版本(作為程式集看待,發行者策略程式集)也安裝帶GAC中。這樣,用戶端就不需要設定檔了。CLR會讀取當前用戶端程式的清單,嘗試在GAC中尋找被請求的版本。如果CLR找到一個發行者策略程式集,它會讀取其中的配置資訊,在GAC層級執行請求重新導向。
建立發行者策略的步驟如下:
(1) 建立發行者策略檔案
發行者策略檔案是一個把現有版本或某個版本範圍重新導向到新版本的XML檔案,如前面”動態重新導向到共用組件的特定版本”的設定檔就可以作為發行者策略檔案。
(2) 建立發行者策略程式集
(3) 將發行者程式集添加到GAC
將發行者策略添加到GAC之後,可以刪除用戶端設定檔,發行者策略將會生效。
通過設定檔可以關閉發行者策略: