別再掉進DLL地獄的陷阱裡(DLL Hell)~.NET解決之道

資策會數位教育研究所講師 王芳芳

 

Introduction

DLL 陷阱是一個惡夢, 是一種相當奇怪的問題。

        相信很多讀者都有這樣的經驗,如果你的軟體今天原本運作順暢,當你安裝某個新軟體之後,突然間電腦就無法運作了。這絕對不是你的硬體有問題,也不是應用程式的問題,而是作業系統設計上的缺失,這樣的問題層出不窮,這通常是因為新的應用程式版本覆蓋掉共享的程式庫(DLL),而且往往修改了一些現存應用程式所必需的「bug」,這個缺失有了一個名字叫做DLL Hell (DLL地獄)。開發人員與系統管理者(以及使用者)面臨最大的挑戰就是版本更新的問題 ,他們花很多時間在 Windows 登錄檔 (Regedit) 上試著解決其問題而吃盡苦頭 。

.在Microsoft .NET的世界裡,軟體元件再也不需要登錄(Registry)了! NET Framework包含了一些功能,可以實際消除「DLL Hell」的問題,一項稱之為「side-by-side」開發模式的新功能。

DLL & DLL Hell

為什麼要使用 DLL (Dynamic Linking Library) - 動態連結檔 ?

        微軟當初為Windows設計動態連結檔主要是擷取它的兩項優點:一是動態連結、一是資源共享。資源共享的例子相當顯而易見,例如之前曾經提過Windows有三個核心的動態連結檔:Kernel主要是負責系統和應用程式的記憶體、行程和執行緒等等的管理工作;User主要負責使用者介面和訊息的傳遞;GDI則負責系統的任何圖形繪製、顯示等工作。而這些動態連結檔所提供的任何函數都可以在必要的時候,讓每一個Windows環境底下的執行檔使用。因為DLL具備節省記憶體的特性,因此自從Windows 3.1版以來,它就逐漸成為Windows程式設計的主流

 

˙動態連結檔可以資源共享

        許多大型軟體廠商的眾多軟體產品可能都會有許多可以共用的模組,如果每一套軟體各自擁有一份這些可以共用的模組,不僅會造成磁碟空間的浪費,還會讓維護這些模組的工作變的既複雜、又凌亂。最好的方法就是僅保持一份程式碼,然後透過共享的方式讓其他自家的應用程式也可以存取其中共用的模組。共用模組的方法之一就是將模組製作成動態連結檔,然後透過軟體的安裝程式複製到電腦,那麼只要安裝了其中一套軟體之後,其他自家的產品就可以互相共用這一套動態連結檔。

        假設有一函數庫X供三個應用程式A、B、C使用,如果函數庫為目的碼或原始程式碼,則程式編譯之後,函數庫X將會各自成為執行檔A、B、C的一部份,而將來如果應用程式A、B、C同時執行,函數庫X也會各自佔用一份記憶體,顯然這是比較浪費記憶體的方式。

        如果函數庫為DLL形式,則編譯之後,函數庫並不會成為執行檔的一部分,而將來如果應用程式A、B、C同時被執行,則系統只會載入一份函數庫讓程式A、程式B、程式C共用,如圖。

 


Figure: 程式與DLL的共用架構圖
 

 

˙動態連結檔節省記憶體空間

        動態連結檔的資源共享可以節省磁碟空間,而動態載入的連結方式則可以節省記憶體空間。動態連結檔採用動態載入的連結方式,動態載入讓程式檔在需要相關的函數或資源的時候,才載入放置在動態連結檔裡面的函數或資源,這種方式將可以有效地使用記憶體。不論是節省磁碟空間或記憶體空間,都是希望利用動態連結檔所提供的共享函數與系統資源的方式,降低整個Windows環境對於硬體設備的需求。
 

DLL的問題 - DLL Hell

 


˙動態連結檔到底出了什麼問題?

        其實DLL的優點(程式碼共用、節省記憶體),正是其缺點的起源。原本是立意良好的DLL,有一天會變成DLL Hell,恐怕是當初DLL的設計者所始料未及的。

        而之所以會出現DLL Hell,也是因為動態連結檔可以與其他程式共用函數、共享資源所引起,可謂「成也共用、敗也共用」。此話怎講呢?為了要讓其他程式共用動態連結檔所提供的函數或資源,動態連結檔的設計者必須相當謹慎地、縝密地考慮到功能的一致性、回溯相容等細節問題,否則一旦程式所使用動態連結檔沒有提供所預期的功能,那麼使用者就會為此而掉入地獄了。

        但是要完全考慮到一致性或回溯相容,實在是困難重重,就算真的要做到,也會讓利用動態連結檔的軟體廠商付出相當的成本;但,有必要付出這些成本嗎?想想現今的電腦執行環境,與當初微軟設計動態連結檔的時候已經有相當、相當大的變化。現在的硬體比起當初已經便宜太多、太多了,個人電腦的記憶體都是從64MB起跳,配備128MB記憶體的電腦更是比比皆是,而硬碟容量更是以GB計算。在如此的硬體環境之下,Windows程式設計師還需要這麼刻苦地考慮共用的問題嗎?而且動態連結檔的動態載入,其實已經替Windows系統節省了不少系統資源,因此微軟也重新調整動態連結檔的設計理念,而且也針對作業系統進行改善,希望不要再有任何使用者掉入因為共用動態連結檔而起的地獄深淵。

˙數種DLL Hell 的狀況


        讓我們想一想,如果某一副程式或物件類別有90%符合我們的需求,卻有10%不符合,怎麼辦呢?對副程式來說,大概只有修改「原始程式碼」一途。

        假設程式A會使用物件X,在程式A安裝到系統時,會同時安裝物件X,假設另一個程式B也會使用到物件X,那麼程式B直接複製到硬碟中即可正常運作,因為物件X已經存在於系統中,這聽起來很好,因為程式A與程式B可以共用物件X。然而對程式A來說,原本在安裝後,執行得好好的,卻可能在未知的一天變成無法執行,這就是所謂的DLL Hell。以下為描述DLL Hell的兩種狀態。


狀況 1. 動態連結檔沒有善盡回溯相容的責任

         如果程式A使用的是1.0版的物件X,而程式B使用的是 2.0 版的物件X(通常是因為程式B開發的時間較晚,使用較新的版本),結果會怎樣呢?結果在程式B被安裝到系統時,物件X 2.0版也必須安裝到系統中,此時系統中 1.0 版的物件X將會被 2.0 版所取代。

        在大部分的情況下,物件X 2.0版相容於1.0版,所以程式A依然可以正常運作,但有時候卻會出現 2.0 版及 1.0 版不相容的情況,此時程式A便無法正常執行了。此種DLL Hell的起因則是的設計者,原因在於動態連結檔沒有善盡回溯相容的原則。試著想想A.exe 需要 X.dll 所提供的功能,但是在新版的 X.dl l裡面,功能竟然被取消了,這時候也極可能發生DLL Hell。

        但是誠如之前所討論,有時往往很難保證百分之百的回溯相容,而且目前個人電腦的硬體配備已經不再像以前簡陋,因此微軟也提出了所謂Side-By-Side的動態連結檔,讓程式都能擁有自己專屬的動態連結檔,進而減少共用動態連結檔以避免這種DLL Hell的發生。


狀況 2. 動態連結檔善盡回溯相容的責任, 但動態連結檔本身出現bug

        另一種情況,物件X的提供者確實考慮到版本相容的問題,而根據物件的規格來看,新舊版也的確相容,但程式A使用新版的物件X就是有問題,畢竟程式A並沒有與新版的物件X一起運作過,誰知道會發生什麼情況?
 

 

Client

Server DLL (X.DLL)

Server Codes

程式 A 用 X 1.0

(X 1.0) (原始版本)

Public SetValue(ByVal Value As Integer)

        If Value < 0 Then

           Value = 0

        End If

        m_Value = Value

End Property

程式 B 用 X 2.0

 (X 2.0) (更新版本)

Public SetValue(ByVal Value As Integer)

        'Fix the bug

        If Value < 0 Then

           Err.Raise Number:=APP_ERROR, Description:="Negative Value " 

        End If

        m_Value = Value

End Property

 


        如上表所發生的不幸的事, 縱使 X 2.0 的開發人員小心翼翼透過 VB6.0 的二進位相容模式控制DLL版本,且所有的內部GUID值與方法和參數都完全相同,由於X 1. 0 之中有一個名為 Value 的屬性名稱, 當此一屬性設為負數時, 該屬性就會變成零, 但卻不會出現錯誤訊息。這個做法是錯誤的, X 2.0版將此臭從解決了- 若將Value屬性設為負數, 則會拋出錯誤。

        當程式 B 以 X 2.0 散佈時, 這支程式B 當然也可以正常運作。不過, 如果將 X 2.0 安裝在系統之中, 程式A 會當掉。之前程式將Value 屬性設為 -1 不會有問題, 但現在會出現執行時期的錯誤。同時程式 A的開發人員並未在這設計錯誤檢視的機制。

        在許多真實的案例中, 要善盡二進位相容模式控制DLL版是非常困難的, 亙何況即使是二進位相容模式控制DLL版本還是有可能造成DLL Hell。

 

 

.NET 如何解決DLL Hell 的問題

自我描述的Assembly(Self-describing Assembly)

        Assembly(組合)是簡化部署與版本管理的關鍵。Assembly是部署與版本管理的基本單元,其中包含一群資源(Resource)與型別(Type),以及它們內含的Metadata,同時也包括此組合在建造時所依賴的其他組合的版本資訊。有了獨立完整的組態資訊,同一組合的不同版本也可安裝在同一部機器上,搭配共通語言執行環境具備根據各組合的組態資訊,載入正確版本的依賴組合(Dependent Assemblies)的能力,安裝與解除安裝的過程,就如同複製檔案與刪除檔案一樣單純,以往因先後安裝彼此覆蓋而產生的所謂“DLL Hell”的版本失控的現象不復存在。



Figure . Self-Describing Assembly 可包括多個檔案

        Metadata(定義)是Assembly能自我描述的關鍵。Metadata是編輯器在產生執行碼時,伴隨產生的定義性資訊,舉凡元件所使用的型別、屬性、方法、事件甚至輔助與備註等資訊都可包含在內,而且保證與執行碼的一致性,完全取代並且超越了傳統分離式的IDL(Interface Definition Language) 檔案與型別庫(Type Library)所扮演的功能,同時元件服務要求與執行所需的資訊皆動態來自Metadata 。.asseembly directive 作為辨識 assembly 本身 , .assembly extern directives 定義此 assembly 所依賴的其他 Assemblies。



Figure . 可使用 IL Disassembler (Ildasm)呈現 DLL 的 metadata

簡化的部署與版本管理

 

˙Application-Private Assemblies (Isolated Assembly)
        Application-Private Assemblies (or 被隔離的 assembly) 只能被一個應用程式所使用- 它不能被其他的應用程式所共用。隔離 assembly 讓程式開發者有著對應用程式絕對的控制權,這也是 .NET應用程式的預設方式。

        開發好的Application-Private Assemblies 要在另一個 .NET 環境進行安裝時,手續只有一個,就是Copy And Paste。只要把編譯好的程式,無論是EXE執行檔、DLL元件、ASP.NET的.aspx網頁或Web Service的.asmx檔,全部都是以複製/貼上的方式部署在和應用程式相同的目錄,這些檔案複製完成後,不需額外註冊或設定。.NET程式執行時,如果需要額外的元件,首先會自本身執行檔下的同一目錄開始尋找,因此,每套應用程式預設都是使用本身同一目錄下的元件,不同應用程式間不會相互干擾,也消除了DLL Hell 的困擾。(註: The CLR finds these assemblies through a process called probing. Probing is simply a mapping of the assembly name to the name of the file that contains the manifest.)

˙Shared Assemblies - Side by side execution (並排執行)
        然而,應用程式共享 assembly 還是有其必要性, 因為讓每個應用程式都有自己一份 copy (如 System.Windowns.Forms, System.Web or a common Web Forms control )是件很奇怪的事 。

        為了解決DLL Hell的問題,.NET增加了一種新的技術,稱為Side by side execution,意思是應用程式可以擁有各自版本的DLL,例如程式 A使用版本1.0的物件、而程式B使用版本2.0的物件,1.0版與2.0版的物件可以同時在系統執行。

        透過Side by side execution的技術,應用程式只要安裝成功之後,就不用擔心DLL更新版本,或規格的改變,因為就算DLL改朝了,應該程式也不用換代。以下簡述 Side by side execution的過程圖 及應用步驟:


Figure . Process for implementing strong names

step 1. create a key pair using the Strong Name tool (Sn.exe) .

Sn –k MyKey.snk
嚴格名稱(Strong names)是一種在.NET 架構下可減少 .NET DLL 陷阱的功能

(註: The author of an assembly generates a key pair, signs the file containing the manifest with the private key, and makes the public key available to callers by the Strong Name tool. The key pair is passed to the compiler using the custom Assembly attribute )


Step 2. sign an assembly with a strong name and version number:

In Assemblyinfo.vb,
<assembly:AssemblyKeyFileAttribute("MyKey.snk")>
<assembly:AssemblyVersion("1.0.0.1")

Step 3. Install Assembly to Global assembly cache (GAC)
gacutil /i:myassembly.dll

(註: See the .NET Framework SDK documentation for a full description of the options supported by gacutil.)


Figure : Global Assembly Cache

Step 4. 應用程式與 Assembly 的 版本繫結 (Version Policy)

        .NET 提供有組態設定機制可以控制Assembly 的繫結, 故在程式中可以載入相關 Assembly的升級版本。組態設定機制是由XML設定檔來負責,透過這種安全機制可以控制程式的安全、版本以及遠端的功能。每一個應用程式可以有XML程式設定檔來指定應用程式所要繫結Assembly的不同版本

程式設定檔的檔案名稱為程式名稱加上 .config 副檔名, 如 Myapp.exe.config。

<bindingRedirect> 的標記用來指是 .NET 載入更新版的 Assembly 。

例如以下範例設定檔會載入MarineCtrl 版本 5.0.0.1 而非版本 5.0.0.0

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly
<assemblyIdentity name="MarineCtrl" publicKeyToken="9335a2124541cfb9" />
<bindingRedirect oldVersion="5.0.0.0" newVersion="5.0.0.1" />
</dependentAssembly>
</assemblyBinding>

Summary

        The .NET Framework NET Assembly 自我描述與版本管理功能讓 zero-impact 的部署安裝成為可能,同時也終結了DLL Hell 。
        Application-Private Assemblies (or 被隔離的 assembly) 只能被一個應用程式所使用 - 它不會被其他的應用程式所影響。 隔離的 assembly 讓程式開發者對應用程式有著絕對的控制權,開發好的Application-Private Assemblies只要部署在和應用程式同一目錄即可。

        透過Side by side execution的技術,應用程式只要安裝成功之後,就不用擔心DLL更新版本,或規格的改變, 它允許 一個 assembly 的多個版本在一個機器上同時被安裝並執行, 而且每一個應用程式都可以要求和不同的 Assembly 版本繫結。

        The .NET Framework 紀錄應用程式版本資訊,並在執行應用程式時使用此資訊載入應用程式所需依賴的正確版本的 Assemblies。