65.9K
CodeProject 正在变化。 阅读更多。
Home

揭秘 .NET 全局程序集缓存

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (120投票s)

2003年6月17日

22分钟阅读

viewsIcon

1087690

对全局程序集缓存工作原理的解释。

引言

我第一次接触 .NET 全局程序集缓存(以下简称 GAC)是在 .NET Framework Beta 1 发布一周后,由 Develop Mentor 举办的 Guerrilla .NET 课程上。这个在整个 .NET 拼图(是的,.NET 对我来说很长时间以来都是一个谜)中神奇而神秘的部分立刻吸引了我。普通的(Private)程序集在其主程序(客户端可执行文件)的文件夹或其子文件夹中和平共存,而高度社交的(Shared)程序集,包括实现框架类库的所有重要的 .NET System 程序集,则驻留在 GAC 这个宏伟的住所中。

撰写本文的主要目的是与开发社区分享我关于 GAC 的发现。本文中提供的信息是原创的,是我个人研究的直接成果。据我所知,这些细节尚未在任何 .NET 文章或书籍中提及。

由于本文中多次提及 Fusion,因此有必要澄清此名称与 GAC 的关系。Fusion 是微软内部项目名称,旨在消除共享 DLL 代码的问题。GAC 最初被称为 Fusion 缓存,目前在 Fusion.dll 中实现。

致谢

我谨感谢微软 .NET 团队的 Vince Henderson(软件设计工程师/测试主管)、Sameer Bhangar(软件设计工程师/测试)和 Alan Shi(软件设计工程师),在我研究本文材料时耐心解答我的问题,并在审阅初稿后提供了宝贵的建议。

免责声明

考虑到本文某些部分涵盖了底层细节,因此在这些信息之前已放置了适当的警示说明。请务必注意这些警告,如果您忽视这些警示说明,风险自负。

基础知识

为了那些确实不了解 GAC 是什么的人,我提供了这个从在线文档中摘录的简单描述:

安装了公共语言运行时的每台计算机都拥有一个机器范围的代码缓存,称为全局程序集缓存。此全局程序集缓存存储专门指定用于在该计算机上由多个应用程序共享的 .NET 程序集。

通常,在 .NET 中 Windows 注册表被认为是旧版机制,强烈推荐 XCOPY 部署,这反过来意味着构建程序主可执行文件私有的程序集。然而,对 .NET System 程序集以及其他用户创建的共享程序集的需求是明确存在的,而 GAC 正是满足了这一需求。有关此主题的正式产品文档,请参阅.NET Framework 开发人员指南:全局程序集缓存

微软知识库文章 Q315682 HOW TO: Install an Assembly into the Global Assembly Cache in Visual Studio .NET 引导初学者完成将 .NET 程序集安装到 GAC 所需的步骤。如果还不够明显,这里需要注意的一个关键点是,一旦您的应用程序将程序集安装到 GAC 中,它就会失去 XCOPY 部署的优势,因为这些程序集(除了 .NET System 库)必须明确安装在 GAC 中。

在介绍基础知识时,理解与 .NET 程序集相关的**标识**概念非常重要。程序集的标识包括其简单的文本名称、版本号、如果程序集包含本地化资源则可选的区域性,以及用于保证名称唯一性并保护名称免受不必要重用(名称欺骗)的可选公钥。具有简单标识的程序集仅包含强制的简单文本名称和版本号组件。由于 GAC 是共享程序集的机器范围存储库,如果这些程序集仅具有简单标识,则很容易出现名称冲突问题,因为它们由不同的供应商甚至您组织内的开发人员独立开发。因此,强制要求安装在 GAC 中的每个共享程序集都必须具有**强名称**。在线文档中该术语的正式定义是:

强名称由程序集的标识(其简单文本名称、版本号和区域性信息(如果提供))以及公钥和数字签名组成。它是使用相应的私钥从程序集文件(包含程序集清单的文件,该清单又包含构成程序集的所有文件的名称和哈希值)生成的。

有关强名称程序集的详细信息,请参阅强名称程序集创建和使用强名称程序集。第一个 URL 中特别值得注意的是描述强名称所满足要求的三个要点,以及描述为什么强名称程序集只能引用其他强名称程序集的最后一节。

常见误解

一个常见的误解是强名称程序集必须始终安装在 GAC 中。强名称程序集可以放在 GAC 中,但绝非必须放在 GAC 中。如果您想确保应用程序没有系统范围的影响,并确保可以进行 XCOPY 部署,则最好将强名称程序集放在应用程序目录下。

另一个常见的误解是,程序集必须安装在 GAC 中才能供 COM Interop 或非托管代码访问。这两种情况都不强制将程序集安装在 GAC 中,作为一般准则,您只有在必须与同一机器上的其他应用程序共享程序集时才应将其安装在 GAC 中。

与普遍看法相反,无法在 Visual Studio.NET 项目中直接引用 GAC 中的程序集。简单来说,Visual Studio.NET 2002 和 2003 的“添加引用”对话框的“**.NET**”选项卡中列出的程序集并非从 GAC 中枚举,因为该对话框是基于路径的。请参阅 Microsoft 知识库文章INFO: How to Display an Assembly in the Add Reference Dialog Box的“**更多信息**”部分,以了解 .NET 开发人员面临的这一常见问题的解决方法。

GAC 的公开和非公开面貌

为了让 GAC 发挥作用,我们需要能够与其交互。我了解以下五种可用于此类交互的接口。

  1. Windows 安装程序 2.0
  2. 命令行工具 GACUtil.exe
  3. SHFusion.dll 中实现的 Windows Shell 命名空间扩展
  4. .NET Framework 配置管理工具
  5. 通过 API 以编程方式访问 GAC

以下各节将详细探讨这些内容。GAC 本身是作为目录层次结构实现的。当使用这些接口与 GAC 交互时,我们与 GAC 的实现细节是隔离的(顺便说一下,这是一件好事)。

<%windir%> 目录继承的默认 ACL 允许本地 Administrators 组和 SYSTEM 用户对 GAC 拥有 Full Control。本地 Users 组对 GAC 拥有 Read & ExecuteList Folder ContentsRead 权限。当您尝试使用本文概述的技术与 GAC 交互时,这些权限将发挥作用。

1. Windows 安装程序 2.0

Windows 安装程序包的开发者可以使用 Microsoft Windows Installer 2.0 将程序集安装到 GAC。这是安装此类共享程序集的首选方式,并且应该是仅限于在非开发机器上安装共享程序集的方式。使用 Windows Installer 2.0 将程序集安装到 GAC 的主要优点是:

  1. 它支持基于 GAC 中程序集的安装、修复和删除的精确引用计数。
  2. 它支持 GAC 中程序集的按需安装。如果 GAC 中缺少某个程序集,并且用户启动了需要该程序集的应用程序,MSI 将自动安装/重新安装该程序集到 GAC。
  3. GAC 中程序集安装、修复和删除失败时进行回滚。程序集以单元形式添加和从 GAC 中删除;也就是说,构成程序集的文件总是同时安装或删除。这是因为安装具有事务模型,它直到脚本结束才真正提交到 GAC,以便回滚可以移除 GAC 程序集。

以下是 .NET 和 Visual Studio.NET 文档中关于 Windows Installer 2.0 的一些入门参考资料:

2. 命令行工具 GACUtil.exe

这个命令行工具允许您将程序集安装到 GAC 中,从 GAC 中移除它们,并列出 GAC 的内容。此工具的在线文档可在全局程序集缓存工具 (Gacutil.exe)处获取。

要在 GAC 中安装一个名为 MyAssembly 的程序集,您可以使用以下命令:

gacutil /i MyAssembly.dll

要从 GAC 中移除此程序集,您可以使用以下命令:

gacutil /u MyAssembly

要查看 GAC 的内容,您可以使用以下命令:

gacutil /l

请注意,在卸载时,我们只使用程序集的简单文本名称,因为对于安装在 GAC 中的强名称程序集,其简单文本名称与 DLL 名称匹配。

使用 /r 选项(gacutil /ir MyAssembly.dll gacutil /ur MyAssembly)将确保对程序集的引用被追踪。当使用 gacutil /lr 列出共享程序集时,也会显示每个共享程序集的追踪引用。在 .NET Framework 1.1 中,/r 必须单独列出,例如 gacutil /l /r

您应避免使用 GACUtil 将程序集安装到 GAC,原因如下:

  1. 除非使用 /r 选项,否则无法在 GAC 中跟踪安装引用。
  2. GACUtil.exe 是 .NET Framework SDK 的一部分,因此不能保证在所有目标机器上都存在。此外,它未被标记为可再分发,因此您需要检查许可,以确保可以将其与应用程序的安装镜像打包。
  3. 使用 GACUtil.exe 安装不会在 GAC 中程序集丢失时启用自动修复(自动修复仅适用于 Windows Installer 2.0)。

3. 在 SHFusion.dll 中实现的 Windows Shell 命名空间扩展

由于一图胜千言,我在此附上 GAC 在 Windows 资源管理器中启用 Shell 扩展 SHFusion.dll 时的视图。

此 Shell 扩展的完整文档可在程序集缓存查看器 (SHFusion.dll)处获取。

当选择(点击)c:\windows\assembly 目录时,右侧窗格会显示 GAC 中安装的每个程序集,以及类型、版本、区域性和公钥令牌等附加信息。类型显示为**本机映像**的程序集,本质上意味着该程序集有 NGEN.exe 生成的本机映像可用。请注意,对于每个此类程序集,都有一个对应的 MSIL 程序集,例如 System.Windows.FormsSystem.Xml。这是必需的,因为元数据不包含在本机映像中。用于生成本机映像的源 IL 映像不需要存在于 GAC 中,尽管它们通常存在。

从右键菜单中选择“属性”将显示以下属性窗口:

大部分信息是不言而喻的,但有几个属性可能需要进一步澄清。

参考文献

这并不是一个非常有用的字段,在 .NET Framework 的下一个版本中很可能会被移除。基本问题是 GAC 实现只从 MSI 获得关于它是否引用了 GAC 中程序集的 Yes/No 答案,但没有确切的 MSI 持有该程序集多少引用的计数(例如,MSI 可能有 10 个引用,但它仍然只显示一个引用)。此外,GAC 实现知道它自己的跟踪引用(gacutil /ir 选项)。因此,References 字段显示的数量实际上是 跟踪引用数 + 1(如果 MSI 持有任何引用)。

要查看引用信息,gacutil /lr 更有用。尽管它不显示 MSI 引用的详细信息,但提供的信息比单纯的数字更多。

代码库

此属性显示程序集的原始路径。它仅用于信息目的,不能总是依赖其可用性。当程序集通过 MSI 安装时,该共享程序集没有可用的代码库,因为比特流是从 MSI 包中传输的,因此该字段将显示为空白。基本上,只有当共享程序集使用文件路径安装时(例如 gacutil /i <程序集的完整路径名>),代码库才可用。

当您考虑到两个不同的程序集的显示名称可能相同(例如 Microsoft 的 System 和 Widgets 的 System)时,此属性的值应该清楚。强名称使这成为一种真正的可能性,而原始路径可以帮助用户清楚地知道指的是哪个程序集,而不同的公钥令牌则不能。同样重要的是要指出,没有像 Windows 文件保护或 Windows Installer 2.0 支持的自动修复选项那样从这个原始路径恢复/还原文件的机制。

禁用程序集缓存查看器

警告:以下步骤涉及修改 Windows 注册表。如果您不正确使用注册表编辑器,可能会导致严重问题,甚至可能需要重新安装操作系统。作者和微软均不能保证您能够解决因不正确使用注册表编辑器而导致的问题。请自行承担使用注册表编辑器的风险。

如果您想禁用程序集缓存查看器,并在 Windows 资源管理器中以其“裸露”的形式查看 GAC,您可以将 HKLM\Software\Microsoft\Fusion\DisableCacheViewer [DWORD] 设置为 1。

4. .NET Framework 配置管理工具

Microsoft .NET Framework 配置 MMC 管理单元可通过 开始 | 控制面板 | 管理工具 访问。当您第一次点击 我的电脑 下的“程序集”节点时,将显示以下屏幕:

查看程序集缓存中的程序集列表

选择此任务后,右侧窗格会显示已安装的程序集列表,与程序集缓存查看器提供的视图类似。

“**操作**”下拉菜单包括“**复制**”、“**删除**”、“**属性**”和“**帮助**”菜单项,所有这些都一目了然。属性窗口只显示“**常规**”选项卡。与“**程序集缓存查看器**”显示的属性窗口相比,“**引用**”属性缺失,而多了一个“**缓存类型**”属性。

“视图”下拉菜单包含“添加/移除列”、“帮助主题”和“刷新程序集列表”菜单项。“帮助主题”菜单项切换到首次单击“程序集缓存节点”时显示的视图。

将程序集添加到程序集缓存

选择此任务后,将显示以下对话框,该对话框允许将程序集安装到 GAC 中。

5. 通过 API 编程访问 GAC

注意:请勿在您的应用程序中使用这些 API 执行程序集绑定或测试程序集的存在或其他运行时、开发或设计时操作。只有管理工具和安装程序才应使用这些 API。如果您使用 GAC,这会直接使您的应用程序面临程序集绑定脆弱性,或者可能导致您的应用程序在未来版本的 .NET Framework 上运行不正常。

尽管前四个与 GAC 交互的选项很有用,但在某些情况下,我们可能被迫通过自己的代码以编程方式与 GAC 交互。实现 GAC 的开发人员已经考虑到了这一点,并且有一个完整的 API 可用于此目的。

Microsoft 知识库文章 Q317540 全局程序集缓存 (GAC) API 未在 .NET Framework 软件开发工具包 (SDK) 文档中记录正式记录了此 API,因此我没有理由在此部分重复信息。遵循知识库文章开头的警示说明(已在本文中转载)是明智的。

迁移 GAC

GAC 的默认位置是在安装了 Windows 操作系统的硬盘上的 <%windir% >\assembly 文件夹下。在 .NET Framework 设置/安装期间,此位置不可配置。一旦 .NET Framework 完全安装,就可以将 GAC 重新定位到不同的位置。将 GAC 移动到不同位置的步骤如下:

警告:以下步骤涉及修改 Windows 注册表。如果您不正确使用注册表编辑器,可能会导致严重问题,甚至可能需要重新安装操作系统。作者和微软均不能保证您能够解决因不正确使用注册表编辑器而导致的问题。请自行承担使用注册表编辑器的风险。

  1. HKLM\Software\Microsoft\Fusion 下的注册表项 CacheLocation (REG_SZ) 设置为 GAC 需要定位的文件夹的路径名。.NET 将在注册表项中指定的 CacheLocation 下创建一个 assembly 子文件夹,因此您不应在指定的路径名中包含 assembly
  2. 将当前 GAC 的内容(很可能是默认位置 c:\<%windir%>\Assembly)XCOPY 到这个新位置。您也可以使用资源管理器 Shell 将当前 GAC 位置下的 assembly 子文件夹复制到 CacheLocation 注册表项中指定的新位置。

例如,如果您想将机器上的 GAC 从默认的 c:\windows\assembly 移动到 d:\dotnetgac,您应该:

  • HKLM\Software\Microsoft\Fusion\CacheLocation 设置为 d:\dotnetgac
  • XCOPY c:\windows\assembly 到 d:\dotnetgac\assembly

关于 GAC 重新定位过程的一些值得注意的点如下:

  • 步骤 1 和步骤 2 的执行顺序无关紧要。
  • 除非您迁移或重新安装它们,否则在更改注册表项后,即使是基本的 .NET Framework 程序集也将无法找到。
  • .NET 不会为新的缓存位置设置任何权限。<%windir%> 下的默认位置继承 ACL,因此只有管理员才对该文件夹拥有 **修改** 权限。在重新定位 GAC 后,您需要手动在新位置设置适当的 ACL,以防止所有用户篡改 GAC。

当我第一次尝试将 GAC 移动到另一个位置时,我注意到 Shell 命名空间扩展 SHFusion.dll 在 Windows 资源管理器中为原始位置和新位置都显示了抽象视图。经过进一步调查,我意识到 SHFusion.dll 使用 <CacheLocation>\Assembly 目录下的隐藏文件 Desktop.ini 来确定如何显示该文件夹的内容。

一旦学习了这项技术最初的兴奋劲过去后,人们会问的合乎逻辑的问题是,为什么有人会想要在现实生活中这样做?最初,我曾想过利用这个功能将 GAC 安装在文件共享(例如网络访问存储设备)上,并让其他需要相同共享程序集的机器引用该位置。这样,GAC 就不会占用集群/组中每台机器的硬盘空间。此外,我将能够从一台机器安装共享程序集,并让这些程序集可供该集群/组中所有其他机器上运行的 .NET 应用程序使用。

鉴于我对 .NET 安全性的相当不错的理解(并且没有太多时间来尝试这种配置),我很快得出结论,这种配置是不切实际的,因为系统程序集本身将在 LocalIntranet 权限集下执行,导致不可预测的行为。我也确信微软产品支持会认为这是一种不受支持的配置。尽管如此,我确实觉得这将是一个不错的选择,因为我看到 GAC 的大小随着时间的推移急剧膨胀,而服务器集群的中心位置将有利于磁盘空间和管理。

因此,目前我能想到的唯一好理由是,如果最初安装 GAC 的硬盘分区空间紧张,可以将 GAC 重新定位到不同的 Windows 驱动器(C: D: 等)。考虑到 .NET Framework 和第三方共享程序集的并行安装所占用的额外磁盘空间,这当然是可能的。

Application Center 2000 对 .NET GAC 的复制支持

Application Center 2000 (AC2000) 复制功能可确保集群中服务器上的指定文件和目录保持同步。如果此类文件或目录被意外(或恶意)删除/覆盖,AC2000 复制将确保原始文件立即复制到该服务器上(并添加一个事件日志条目以指示其执行的操作)。

在 AC2000 SP2 发布之前,GAC 程序集复制支持作为单独的下载提供给运行 Windows 2000 操作系统的服务器上安装的 AC2000。Microsoft 知识库文章 Q396250 INFO: Application Center 2000 GAC Replication Support for Windows 2000 包含了有关此临时支持包的所有详细信息。随着 AC2000 SP2 的发布,临时版本的 GAC 程序集复制不再相关(除非 Microsoft 支持服务授予某种例外,允许服务器安装继续运行 AC2000 SP1)。在运行 Windows 2000 操作系统的服务器上安装 AC2000 SP2,如果此前已安装临时版本的 GAC 程序集复制,它将自动替换为最终版本。AC2000 SP2 是第一个支持在运行 Windows Server 2003 的服务器上使用 Application Center 的服务包,并且在这些服务器上安装 SP2 将始终安装最终版本的 GAC 程序集复制功能。

探索 GAC 的当前实现

警告:本节描述的 GAC 详细信息仅与 Microsoft .NET Framework 1.0 和 1.1 版本中观察到的实现相关。未来发布的 .NET Framework 可能会导致当前实现的更改甚至彻底修改。强烈建议不要依赖此处描述的实现细节,并且应自行承担风险。

此外,使用 MS DOS 命令更改或删除这些内部文件夹或其中文件的内容可能会导致不可预测的行为或其他严重问题,可能需要您在受影响的机器上重新安装 .NET Framework。使用此类命令风险自负。

如前一节所述,GAC 的默认位置在 <%windir% >\assembly 文件夹下。以下屏幕截图显示了一个 MS DOS 命令窗口,其中已在提示符下执行了各种命令。

Dir 命令显示了四个独立的目录,在此简要描述。

GAC:GAC 中安装的所有 MSIL 程序集的容器。

NativeImages1_v1.0.3705:为 .NET Framework 1.0 生成的本机映像。

Temp 和 Tmp:这些是当前实现使用的临时暂存目录。

Dir /AH 命令显示 SHFusion.dll 使用的隐藏文件 Desktop.ini,而 type desktop.ini 命令显示该文件的内容。我使用 CLSID 在注册表编辑器中搜索 HKCR 节点,发现它引用了一个在 SHFusion.dll 中实现的 COM 组件。有趣的是,MSCorEE.dll 列在 InprocServer32 键下,尽管该对象在 SHFusion.dll 中实现,并且 SHFusion.dll 列在 Server 键下。这样做的原因是 MSCorEE.dll 是一个垫片,它知道机器上的多个并行运行时。由于 SHFusion.dll 位于版本化的运行时目录中,因此 MSCorEE.dll 被用作 InProcServer32,以便它可以在运行时加载正确版本的 SHFusion.dll

进一步探索 GAC 文件夹会发现以 GAC 中安装的每个程序集命名的文件夹。

逐步深入这个文件夹层次结构,揭示了 GAC 的组织方式以及它如何实现并行安装。

在每个共享程序集的文件夹中,都有一个子文件夹,其命名使用了程序集的版本和公钥令牌组件,例如 1.0.3300.0__b77a5c561934e089。在该子文件夹中,存放着实际安装到 GAC 中的程序集。

请注意,与每个程序集文件并存的 __AssemblyInfo__.ini 文件。它存储了程序集属性,例如 URLMVIDDisplayName 等。对于安装了本机映像的程序集,还会包含一个 CustomString 属性。不言而喻,该文件的格式和存储信息在未来的运行时版本中可能会发生变化。

URL 与 SHFusion.dll 显示的属性对话框中作为 CodeBase 属性显示的原始路径相同。

DisplayNameVersionCulturePublicKeyToken 是唯一标识程序集的组件。

MVIDCustomString 由加载器用于确定在运行时使用特定的预 JIT 程序集(使用 NGEN.exe 安装)是否有效。

如果目录意外删除会怎样?

由于 GAC 程序集没有 Windows 文件保护,因此重申本文前面部分已涵盖的内容非常重要。如果意外删除了 GAC 文件夹层次结构中的某个文件夹,则只有使用 MSI 安装的程序集在 GAC 中找不到时才会在运行时重新安装。由于 .NET Framework 是使用 MSI 安装的,因此所有 System.* 程序集都被认为可以安全地免受意外或恶意篡改(前提是 .NET 运行时 MSI 可访问)。

摘要

在本文中,我们详细探讨了 .NET GAC。我真诚地希望读者在此过程中学到了新知识,并在日常使用 .NET 时会发现其中一些信息很有用。

我始终乐于接受改进建议,因此将不胜感激对本文所呈现材料的建设性批评。

历史

  • 初稿,2003 年 6 月 17 日。
  • 第二次修订,2004 年 1 月 15 日。
© . All rights reserved.