Visual Studio安裝程序會把Visual Studio的共享庫放在一個稱為"並列緩存(side-by-side cache)"的地方,那怎樣才能有效地利用它呢?
在文章開頭,先看一個示例。在命令行中,創建一個C++源文件,輸入例1中的代碼。(雖然此處使用的是C++/CLI語法,但不管你是用C++/CLI、托管C++、或本地C++,都不影響要講解的主題。)
例1:lib.cpp
using namespace System;
public ref class Test
{
public:
void CallMe()
{
Console::WriteLine("called me");
}
};
將其編譯為一個托管庫程序集:
cl /clr /LD lib.cpp
在此要多留意,我們是使用了混合模式(/clr)來編譯此代碼,當然了,如果適當修改,也能以舊式托管C++語法(/clr:oldsyntax)來編譯。
下一步,創建一個調用此庫的C#程序(例2),當然也可以使用Visual Basic.NET,不過C#更好一點。再與庫一起編譯:
例2:
using System;
class App
{
static void Main()
{
Test test = new Test();
test.CallMe();
}
}
csc app.cs /r:lib.dll
運行此程序,會拋出一個異常:
Unhandled Exception:
System.IO.FileNotFoundException:
The specified module could not be found.
(Exception from HRESULT: 0x8007007E)
at App.Main()
怎麼會這樣呢?打開程序所在的目錄,庫也在那啊。HRESULT的高位字為0x8007,其代表FACILITY_WIN32,也就是說,這是一個Win32錯誤;低位字以十進制表示為126,在winerror.h中列明其代表ERROR_MOD_NOT_FOUND。如果LoadLibrary不能查找到某個模塊,才會返回這個錯誤結果,因此,現在非常清楚了,這個錯誤表示不能查找到一個非托管的DLL。
為找出庫所使用的模塊列表,可在ILDASM中加載它,並查看MANIFEST。如果庫是通過平台調用加載DLL的,那這些DLL會作為.module條目列出,然而,對這個庫來說,你將會發現,它只用到了托管程序集mscorlib與Microsoft.VisualC,兩者都在.NET全局程序集緩存(GAC)中。另有一種可能性,在程序集中,還存在著非托管代碼,由它調用了非托管庫(例如,那些使用托管C++ It Just Works的代碼)。
為調查清楚,從ILDASM的View菜單中選項Headers,這將會列出庫中的PE文件頭。向下滾動直至找到導入表(IAT),會得到一份所有從非托管庫引入的方法列表。因為庫是以混合模式編譯的,因此庫用到了C運行時庫(CRT),從導入表中也確認了這點--它列出了msvcr80.dll及msvcm80.dll,前者是CRT的DLL多線程版本,後者是一個包含了一些CRT托管版本的混合模式庫。這下非常清楚了,錯誤產生的原因是Windows找不到這兩個庫、或其一。
最後,查看%systemroot%\system32目錄下是否有這些庫--但它們不會在那的,此時,你可能會指責Visual Studio安裝程序沒有把最新版本的CRT安裝在自己的電腦上,但實際上,安裝程序已經安裝了這些CRT庫--只是不在你原先期待的地方。
並列緩存
Visual Studio安裝程序會把Visual Studio的共享庫放在一個稱為"並列緩存(side-by-side cache)"的地方,目錄位於%systemroot%\WinSxS,且只有SYSTEM及Administrators組成員有寫訪問權限,其他用戶只有讀取和運行權限。並列緩存中包含了"程序集"--不是托管程序集,而是非托管的等價物。
在WinSxS目錄下,每個程序集都會有一個目錄,另外,還有兩個目錄分別是Manifests和Policies,其中包含了版本的相關信息。以下兩個目錄與CRT有關:
x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0.50727.42_x-ww_0de06acd
x86_Microsoft.VC80.DebugCRT_1fc8b3b9a1e18e3b_8.0.50727.42_x-ww_f75eb16c
顯而易見,一個是發布版(Release Build),而另一個是調試版(Debug Build),但重點是,版本號與一個公有密鑰權標也是目錄名的一部分。如果你查看前一個目錄的內容,可看到有msvcm80.dll、 msvcp80.dll、及msvcr80.dll,它們是被稱為"Microsoft.VC80.CRT"非托管程序集8.0.50727.42版本的內容。一個非托管程序集可包含一個或多個文件,而這些文件也可為包含本地代碼或COM對象的DLL。一個非托管程序集通常被作為一個單獨的單元部署,且其中的所有文件由一個被稱為"清單(manifest)"的XML文件來描述。
清單文件存儲在Manifest目錄中,且與程序集同名,但是後綴名為 .manifest。這個文件列出了程序集中的所有文件;此外,還有一個文件的文件名也與程序集同名,但是後綴名為 .cat,這是一個已簽名的安全編目文件,其包含了程序集中文件的hash值,正是因為它已簽名,所以可以防止被篡改,且Windows也能利用這些hash值來檢查程序集的任一部分是否在部署後已被篡改。
客戶清單文件
一個客戶文件(程序或庫)能依賴於程序集中的某個文件來構建,但客戶文件只會依賴於程序集的某個特定版本來構建,Windows也只會加載所需的特定版本。為標出所需共享程序集的版本,一個可執行文件(程序或庫)也必須有一個清單文件(manifest)。鏈接器在可執行文件生成時,會為其創建一個包含清單信息的文件,因此,如果回過頭來看一下前面生成的庫的目錄,會找到一個名為"lib.dll.manifest"的文件,例3是其的內容。
例3:
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
<dependency>
<dependentAssembly>
<assemblyIdentity type='win32' name='Microsoft.VC80.CRT'
version='8.0.50608.0' processorArchitecture='x86'
publicKeyToken='1fc8b3b9a1e18e3b' />
</dependentAssembly>
</dependency>
</assembly>
正如大家所看見的,它說明托管程序集lib.dll依賴於Microsoft.VC80.CRT共享並列程序集中的某些文件。盡管這個文件位於庫lib.dll的同一目錄中,但Windows明顯不會用到它,而這與MSDN中寫明的有點背道而馳:
(MSDN):你可在應用程序二進制可執行頭文件中包含應用程序清單文件……,作為備選方案,也可把一個單獨的清單文件放在應用程序可執行文件的同一目錄中,操作系統會首先從文件系統中加載此清單文件,並檢查可執行文件的資源節。文件系統的版本具有優先權。
然而,完全不是這麼回事。對庫而言,Windows會忽略清單文件,盡管如此,文檔還是給出了怎樣解決這個問題的一個線索,清單文件一定要以資源ID為2的非托管資源RT_MANIFEST形式綁定到可執行文件。
在此有兩種方法:第一種方法是創建一個包含對清單文件引用的資源腳本文件:
#include <winuser.h>
2 RT_MANIFEST lib.dll.manifest
它會在以後通過Windows資源編譯器rc.exe編譯為一個資源,並通過鏈接器限定為一個非托管資源。這種方法的問題之處在於,是鏈接器創建了這個清單文件,因此必須運行兩次鏈接器:一次是為生成清單文件,一次是把資源鏈接到最終生成文件。例4是一個范例makefile,演示了如何進行操作:
例4:
# The main target
all: app.exe
# A C# process that depends upon a Managed C++ library
app.exe : app.cs lib.dll
csc app.cs /r:lib.dll
# This is the second invocation of the linker, so the object file and
# manifest will already exist, so they do not need to be rebuilt.
lib.dll : lib.cpp lib.res lib.obj
link /DLL /manifest:no /machine:x86 lib.res lib.obj
lib.res : lib.rc
rc lib.rc
# Create a temporary resource script that binds the manifest file to the DLL
lib.rc : lib.dll.manifest
type <<$@
#include <winuser.h>
2 RT_MANIFEST lib.dll.manifest
<<KEEP
# Create the object file, and invoke the linker to create the manifest file
lib.dll.manifest lib.obj : lib.cpp
cl /LD /clr lib.cpp
另一個方法是使用mt.exe未公開的隱藏選項把資源綁定到最終生成文件上,這也是Visual Studio 2005創建加載的C++庫(托管混合模式或非托管模式)時所使用的方法。兩個隱藏選項分別為/manifest和/outputresource:前者用於指定清單文件名,而後者用於指出將要修改的PE文件及清單資源的資源ID。一般而言,對庫來說,資源ID應為2;對程序來說,應為1。請看以下示例:
mt /manifest lib.dll.manifest
/outputresource:lib.dll;#2
注意此處的區別:/manifest選項後跟的參數是用空格分隔的;而/outputresource選項後跟的參數是用冒號分隔的。明顯看得出,這兩個選項是由不同的程序員開發的。
一旦你把清單文章綁定到庫,Windows就可以判斷需加載程序集的正確版本。如果在作出這些修改之後,運行前面的C#程序,也會發現程序運行正常。
如果你生成了一個混合模式(/clr)或純媒介語言模式(/clr:pure)的托管C++程序,來使用這個混合模式的庫,鏈接器也創建了一個相應的程序清單文件,當此程序運行時,Windows會查找資源ID為1的清單文件,或查找名為manifest的相應文件。因為混合模式或純媒介語言模式程序都用到了CRT,意味著將會在清單文件中提及CRT程序集,所以,在這個特例中,庫不需要清單文件。然而,你不應該依賴這個機制,因為在本例中,程序使用同一個非托管程序集作為庫是一個偶然情況。
版本重定向
回過頭來再看一下為庫創建的清單文件,注意程序集所需的版本號給定為8.0.50608.0,再次提醒,Visual Studio 2005安裝的程序集是8.0.50727.42,這個叫策略版本重定向。在並列緩存的同級Policies目錄中,可找到下面這個文件夾:
x86_policy.8.0.Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_x-ww_77c24773
注意,除了版本部分,它有著程序集的全名。此文件夾中分別包含了一個策略及安全編目文件,文件名基於將要重定向至的版本號:
8.0.50727.42.policy
這是一個XML文件(見例5)。這個策略文件是針對版本8.0.50727.42的,其也是Visual Studio安裝程序所安裝的版本。它在<bindingRedirect>中指明了所有將要被重定向至本版本的版本號,例5中表明,對版本號8.0.41204.256至8.0.50608.0程序集的所有請求,都會被重定向至8.0.50727.42這個版本。與Fusion(混淆: .NET中的程序集加載技術)不同的是,對並列共享程序集的版本重定向只能是那些生成或修訂的版本值之間的變化,不能用於主、副版本值的變化。
例5:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!-- Copyright (r) 1981-2001 Microsoft Corporation -->
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type="win32-policy" name="policy.8.0.Microsoft.VC80.CRT"
version="8.0.50727.42" processorArchitecture="x86"
publicKeyToken="1fc8b3b9a1e18e3b"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.VC80.CRT"
processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"/>
<bindingRedirect oldVersion="8.0.41204.256-8.0.50608.0"
newVersion="8.0.50727.42"/>
</dependentAssembly>
</dependency>
</assembly>
那就又帶出了一個問題:那為什麼需要重定向呢?為什麼鏈接器不在清單文件中直接指定由安裝程序安裝的程序集版本呢?原因在於,鏈接器是從導入靜態庫中獲得所需的程序集版本。這又引出了另外一個問題:為什麼鏈接器要為DLL的不同版本使用導入庫,而不是安裝的那個?原因是,這些安裝的都是重要的庫。
目前為止的討論都是針對托管C++編譯器(C++/CLI及舊式語法),然而,即便本地C++開發技巧再高,也有可能被這些新"特性"所影響。如果你的代碼使用了某個共享的Visual Studio本地庫(MFC、ATL或CRT),那麼,必須有一個單獨的.manifest清單文件,要麼綁定至可執行文件,要麼只綁定至一個 .exe文件。
結論
以前Microsoft C++編譯器及鏈接器的各個版本所生成的庫,都能被Windows加載並運行,但Visual Studio 2005中的版本14,生成的庫卻無法運行。
此處有兩個解決方法:第一種方法是運行鏈接器兩次,一次是生成清單文件,其能編譯進非托管資源,接著一次是把這個清單綁定至PE文件。這也是本文所推薦的方法,因為如果在構造一個具有"強名稱"的程序集,在第二次調用時,就能提供密鑰文件或容器的名稱。
另一個方法是,使用mt.exe未公開的選項來修改程序集,然而,如果使用鏈接器來生成一個"強名稱"的程序集,mt.exe的動作會使強名稱簽名無效,且程序集也不會加載。