编程与调试 -- Assembly Manifest 通俗简易手册

Assembly Manifest 通俗简易手册 恩,为了大家都能很方便的理解,我将尽量简单通俗地进行描述。

[现象]

对这个问题的研究是起源于这么一个现象:当你用 VC++2005(或者其它 .NET)写程序后,在自己的计算机上能毫无问题地运行, 但是当把此 exe 文件拷贝到别人电脑上时,便不能运行了,大致的错误提示如下: 应用程序配置不正确,请重新安装程序……或者是 MSVCR80D.dll 没有找到什么的(我记得不是很清楚,不过大致是这样的)

[分析]

看到这样的提示,当然不会傻到重装咯。第一反应应该是什么配置有问题、或者是缺少了什么依赖的库文件; 于是我就根据以前 Windows 缺少库文件的经验,把所有库文件(××.DLL)统统一股脑地复制到当前文件夹下来, 满心欢喜以为可以运行了,以运行……@#¥@#%¥……还是挂了。

[探索]

于是开始网上搜索,我 Google,我摆渡;渐渐我发现,这一切都和一个叫做 ***.manifest 类型的文件发生关系, 那么到底什么是 .manifest 文件呢?他有什么用,以前为什么没有?

后来,经过艰苦努力,终于得知,原来这一切都是 Windows 的 Assembly Manifest 搞的鬼。 这个东东的作用就是为了解决 以前 windows 上的“Dll 地狱” 问题才产生的新的 DLL 管理解决方案。 大家知道,Dll 是动态加载共享库,同一个 Dll 可能被多个程序所使用,而所谓“Dll 地狱”就是当不通程序依赖的 Dll 相同, 但版本不同时,由于系统不能分辨到底哪个是哪个,所以加载错了 Dll 版本,然后就挂了。 于是盖茨就吸取了教训,搞了一个程序集清单的东东,每个程序都要有一个清单, 这个清单存再和自己应用程序同名的 .manifest 文件中,里面列出其所需要的所有依赖, 这儿所列出的依赖可不是简单地靠文件明来区分的,而是根据一种叫做“强文件名”的东西区分的,那么什么是强文件明呢? 我们来看一下这个 .manifest 文件便知道了。

<?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>

我们发现原来这是一个 XML 格式的文件,其中 <dependency> 这一部分指明了其依赖于一个名字叫做 Microsoft.VC80.CRT 的库。 但是我们发现, <assemblyIdentity> 属性里面还有其它的东东,分别是 type 系统类型,version 版本号,processorArchitecture 平台环境,publicKeyToken 公匙(一般用来标示一个公司)…… 把他们加在一起便成了“强文件名”了,有了这种“强文件名”,我们就可以根据其区分不同的版本、不同的平台…… 总之,有了这种强文件名,系统中可以有多个不同版本的相同的库共存而不会发生冲突。

[深入]

恩,那么现在,我们就来具体了解一下这一套机制。

首先是强弱文件名的问题。正如上面提到的那样,为了区分不同版本或不同厂商生成的相同的程序集, 必须用一个 Assembly Manifest 程序清单来列出我这个程序集的强文件名--慢着,到这里你可能会问: 刚才不是说 Assembly Manifest 程序清单是列出其所依赖的程序集的强文件名呢,怎么这里变成了当前文件的强文件明了呢? 其实,Assembly Manifest 程序清单有两部分功能, 上面这个实例之所以标注了其所依赖的文件的强文件名是因为其是客户端的 Assembly Manifest, 在服务端有另外一个 Manifest 来标注。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<noInheritable></noInheritable>
<assemblyIdentity type="win32" name="Microsoft.VC80.CRT" version="8.0.50727.42" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
<file name="msvcr80.dll" hash="2a0d797a8c5eac76e54e98db9682e0938c614b45" hashalg="SHA1">
    <asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
        <dsig:Transforms>
            <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity">
            </dsig:Transform>
        </dsig:Transforms>
        <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">
        </dsig:DigestMethod>
        <dsig:DigestValue>phRUExlAeZ8BwmlD8VlO5udAnRE=</dsig:DigestValue>
    </asmv2:hash>
</file>
<file name="msvcp80.dll" hash="cc4ca55fb6aa6b7bb8577ab4b649ab77e42f8f91" hashalg="SHA1">
    <asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
        <dsig:Transforms>
            <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity">
            </dsig:Transform>
        </dsig:Transforms>
        <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">
        </dsig:DigestMethod>
        <dsig:DigestValue>7AY1JqoUvK3u/6bYWbOagGgAFbc=</dsig:DigestValue>
    </asmv2:hash>
</file>
<file name="msvcm80.dll" hash="55e8e87bbde00d1d96cc119ccd94e0c02c9a2768" hashalg="SHA1">
    <asmv2:hash xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
        <dsig:Transforms>
            <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity">
            </dsig:Transform>
        </dsig:Transforms>
        <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">
        </dsig:DigestMethod>
        <dsig:DigestValue>hWq8zazTsMeKVxWFBa6bnv4hEOw=</dsig:DigestValue>
    </asmv2:hash>
</file>
</assembly>

这个便是从 WINDOWS\WinSxS\Manifests 目录下取出来的一个 manifest 文件,再这个文件夹下有一陀子这种 XML 格式的 manifest 文件, 其是服务端的程序清单。WinSxs 是 windows XP 以上版本提供的 非托管并行缓存(side-by-side catche) 里面安装了各种版本的经过强文件名签名的系统库,而上面这个文件 <assemblyIdentity> 正是标注了系统中 Microsoft.VC80.CRT 的一个版本的强文件名签名, 如果其和客户端。.manifest 清单里面 <dependentAssembly> 所列出的依赖项对上的话,就会被加载。 刚才说的 side-by-side 是指各种不同的版本并行运行。 上面这个服务端 manifest 文件中 <file> 标签具体指明了当前强文件名签名的到底是哪一个文件, 其中还有这个文件的 Hash 签名,以确保文件的完整性。

好了,有了这一套机制,就可以非常非常安全地进行库文件关联了,但是、但是貌似还有一个一直困扰我们的问题: 这套机制安全是安全了,但是却失去了以前良好的前后版本兼容性, 即如果你的系统库发生了升级,那么服务端的版本号发生了变化,那岂不是所有服务端程序都不能使用了吗? 其实,windows 还使用一个 policy 的策略文件来确认映射关系。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!-- Copyright ? 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>

这便是在 WINDOWS\WinSxS\Policies 目录下的一个 Policy 文件,其中 <bindingRedirect> 标签便指定了所有 8.0.41204.256-8.0.50608.0 变本的客户需求映射到 8.0.50727.42 这个我现在系统中安装的比较新的版本的库。 当然我们也能对别的字段进行映射,这样便能很好解决系统升级带来的问题。

[应用]

经过以上的讲解,大家对整个依赖查找过程都有了一个整体的认识,那么在实际中问题就好解决了。 让我们回到实际问题中,我之前说了,把一个程序编译连接成可执行程序后,在别人的电脑上发现找不到其所依赖的库了, 那么怎么办呢?聪明的你自然想到把其所依赖的库相应的版本拷贝到目标计算机上面,可是…… 当你在拼命寻找那个可执行文件的 assembly manifests 文件的时候,却突然发现找不到了, 在执行目录下面明明只有一个 exe 文件嘛。是不是没有生成呢?显然不会, 原来是资源连接器把那个 assembly manifests 文件连接到了可执行文件里面了; 不信,你可以用你的 vc++ 打开一个可执行文件看看,在其资源项里面就有一个叫做 RT_MANIFEST 的项目。 这个里面就是二进制标示的 manifests 文件。那么根据这里面提供的要求, 将相应版本的依赖文件(一般就是 CRT 运行库)拷贝到系统目录 Windows\WinSxS\, 记住一般会是连带着一个特殊命名的目录一起拷贝到那个文件夹下, 比如 CRT 的运行库就是 WinSxS\x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0.50608.0_x-ww_b7acac55 有这样一个目录, 其标注了此库的版本号以及签名等信息,以防止多个版本重名时不能复制到同一 WinSxS 目录下。

这样就搞定了么?如果是以前,那么一切都解决了,系统会在这个目录下面找到这个运行库, 可是现在单单这样可不行,系统可是要找到这个运行库的 assembly manifests 文件, 并且对比强文件名之后才能加载,所以所以千万别忘了把相应的 manifests 文件拷贝到 \WinSxS\Manifests 目录下面。

当然,这样在目标的系统文件夹下面打动干戈,自然有些过于暴动了,还好,Windows 还为我们提供了一种私有查找方式。 这种方式会在前面的位置找不到合适库的时候在本地文件夹下面找。 所以你只要把之前的库以及那个 manifests 文件一起拷贝到你的应用程序的路径下面,就可以使用啦。

根据 MSDN 的说明,在本地查找并加载遵循一下规则:

在应用程序本地文件夹中查找名为 <assemblyName>.manifest 的清单文件。 在此示例中,加载程序试图在 appl.exe 所在的文件夹中查找 Microsoft.VC80.CRT.manifest。 如果找到该清单,加载程序将从应用程序文件夹中加载 CRT DLL。如果未找到 CRT DLL,加载将失败。

尝试在 appl.exe 本地文件夹中打开文件夹 <assemblyName> ,如果存在此文件夹, 则从中加载清单文件 <assemblyName>.manifest 。 如果找到该清单,加载程序将从 <assemblyName> 文件夹中加载 CRT DLL。如果未找到 CRT DLL,加载将失败。

最后,我想补充的一点是,在你的 VC++ 安装目录下面的“Microsoft Visual Studio 8\VC\redist”目录下, 有着所有的提供发布的已经配备相应 .manifest 的库文件。 所以你想要发布一个程序最简单最安全的做法(不用担心用户电脑是否包含你所需要的库) 就是把这个目录下面的相应的库的文件夹和你的可执行文件放在一起发布。 比如在 X86 平台下如果你的可执行文件用到了 CRT 库(废话么), 那么就拷贝 Microsoft Visual Studio 8\VC\redist\x86\Microsoft.VC80.CRT 这个文件夹到你的程序所在的目录, 一起发布,就万事大吉啦!


参考资料快照
参考资料快照

本文短链接:
If you have any questions or feedback, please reach out .