# Chapter 3 Shared Assemblies and Strongly Named Assemblies

In this chapter, I’ll concentrate on creating assemblies that can be accessed by multiple applications. The assemblies that ship with the Microsoft .NET Framework are an excellent example of globally deployed assemblies, because all managed applications use types defined by Microsoft in the .NET Framework Class Library (FCL).

# Two Kinds of Assemblies, Two Kinds of Deployment

The CLR supports two kinds of assemblies: weakly named assemblies and strongly named assemblies.

Weakly named assemblies and strongly named assemblies are structurally identical—that is, they use the same portable executable (PE) file format, PE32(+) header, CLR header, metadata, manifest tables, and Intermediate Language (IL) that we examined in Chapter 1, “The CLR’s Execution Model,” and Chapter 2. And you use the same tools, such as the C# compiler and AL.exe, to build both kinds of assemblies. The real difference between weakly named and strongly named assemblies is that a strongly named assembly is signed with a publisher’s public/private key pair that uniquely identifies the assembly’s publisher. This key pair allows the assembly to be uniquely identified, secured, and versioned, and it allows the assembly to be deployed anywhere on the user’s machine or even on the Internet. This ability to uniquely identify an assembly allows the CLR to enforce certain known-to-be-safe policies when an application tries to bind to a strongly named assembly. This chapter is dedicated to explaining what strongly named assemblies are and what policies the CLR applies to them.

An assembly can be deployed in two ways: privately or globally. A privately deployed assembly is an assembly that is deployed in the application’s base directory or one of its subdirectories. A weakly named assembly can be deployed only privately. I talked about privately deployed assemblies in Chapter 2. A globally deployed assembly is an assembly that is deployed into some well-known location that the CLR looks in when it’s searching for the assembly. A strongly named assembly can be deployed privately or globally. I’ll explain how to create and deploy strongly named assemblies in this chapter. Table 3-1 summarizes the kinds of assemblies and the ways that they can be deployed.

Untitled

💡 小结:CLR 支持两种程序集:弱命名程序集(weekly namely assembly)和强命名程序集(strong named assembly)。两种程序集的结构完全相同,两者真正的区别在于,强命名程序集使用发布者的公钥 / 私钥进行了签名。这一对密钥允许对程序集进行唯一性的标识、保护和版本控制,并允许程序集部署到用户机器的任何地方,甚至可以部署到 Internet 上。程序集又可采用两种放暑部署:私有或全局。弱命名程序集只能以私有方式部署。全局部署的程序集是指部署到一些公认位置的程序集。强命名程序集既可私有部署,也可全局部署。

# Giving an Assembly a Strong Name

If multiple applications are going to access an assembly, the assembly must be placed in a well-known directory, and the CLR must know to look in this directory automatically when a reference to the assembly is detected. However, we have a problem: Two (or more) companies could produce assemblies that have the same file name. Then, if both of these assemblies get copied into the same well-known directory, the last one installed wins, and all of the applications that were using the old assembly no longer function as desired. (This is exactly why DLL hell exists today in Windows, in which shared DLLs are all just copied into the System32 directory.)

Obviously, differentiating assemblies simply by using a file name isn’t good enough. The CLR needs to support some mechanism that allows assemblies to be uniquely identified. This is what the term strongly named assembly refers to. A strongly named assembly consists of four attributes that uniquely identify the assembly: a file name (without an extension), a version number, a culture identity, and a public key. Because public keys are very large numbers, we frequently use a small hash value derived from a public key. This hash value is called a public key token. The following assembly identity strings (sometimes called an assembly display name) identify four completely different assembly files.

"MyTypes, Version=1.0.8123.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
"MyTypes, Version=1.0.8123.0, Culture="en-US", PublicKeyToken=b77a5c561934e089"
"MyTypes, Version=2.0.1234.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
"MyTypes, Version=1.0.8123.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

There must be a way to distinguish this company’s assembly from another company’s assembly that happens to have the same attributes. For several reasons, Microsoft chose to use standard public/ private key cryptographic technologies instead of any other unique identification technique such as GUIDs, URLs, or URNs. Specifically, cryptographic techniques provide a way to check the integrity of the assembly’s bits as they are installed on a machine, and they also allow permissions to be granted on a per-publisher basis. I’ll discuss these techniques later in this chapter. So a company that wants to uniquely mark its assemblies must create a public/private key pair. Then the public key can be associated with the assembly. No two companies should have the same public/private key pair, and this distinction is what allows two companies to create assemblies that have the same name, version, and culture without causing any conflict.

A strongly named assembly has a file name, an assembly version, and a culture. In addition, a strongly named assembly is signed with the publisher’s private key.

The first step in creating a strongly named assembly is to obtain a key by using the Strong Name utility, SN.exe, that ships with the .NET Framework SDK and Microsoft Visual Studio. This utility offers a whole slew of features depending on the command-line switch you specify. Note that all SN.exe’s command-line switches are case-sensitive. To generate a public/private key pair, you run SN.exe as follows.

SN –k MyCompany.snk

This line tells SN.exe to create a file called MyCompany.snk . This file will contain the public and private key numbers persisted in a binary format.

Public key numbers are very big. If you want to, after creating the file that contains the public and private key, you can use the SN.exe utility again to see the actual public key. To do this, you must execute the SN.exe utility twice. First, you invoke SN.exe with the –p switch to create a file that contains only the public key ( MyCompany.PublicKey ).

SN –p MyCompany.snk MyCompany.PublicKey sha256

Then, you invoke SN.exe, passing it the –tp switch and the file that contains just the public key.

SN –tp MyCompany.PublicKey

The SN.exe utility doesn’t offer any way for you to display the private key.

The size of public keys makes them difficult to work with. To make things easier for the developer (and for end users too), public key tokens were created. A public key token is a 64-bit hash of the public key. SN.exe’s –tp switch shows the public key token that corresponds to the complete public key at the end of its output.

Now that you know how to create a public/private key pair, creating a strongly named assembly is simple. When you compile your assembly, you use the /keyfile : compiler switch.

csc /keyfile:MyCompany.snk Program.cs

When the C# compiler sees this switch, the compiler opens the specified file (MyCompany.snk), signs the assembly with the private key, and embeds the public key in the manifest. Note that you sign only the assembly file that contains the manifest; the assembly’s other files can’t be signed explicitly.

Here’s what it means to sign a file: When you build a strongly named assembly, the assembly’s FileDef manifest metadata table includes the list of all the files that make up the assembly. As each file’s name is added to the manifest, the file’s contents are hashed, and this hash value is stored along with the file’s name in the FileDef table. You can override the default hash algorithm used with AL.exe’s /algid switch or apply the assembly-level System.Reflection.AssemblyAlgorithmIdAttribute custom attribute in one of the assembly’s source code files. By default, a SHA-1 algorithm is used.

After the PE file containing the manifest is built, the PE file’s entire contents (except for any Authenticode Signature, the assembly’s strong name data, and the PE header checksum) are hashed, as shown in Figure 3-1. This hash value is signed with the publisher’s private key, and the resulting RSA digital signature is stored in a reserved section (not included in the hash) within the PE file. The CLR header of the PE file is updated to reflect where the digital signature is embedded within the file.

Untitled

The publisher’s public key is also embedded into the AssemblyDef manifest metadata table in this PE file. The combination of the file name, the assembly version, the culture, and the public key gives this assembly a strong name, which is guaranteed to be unique. There is no way that two companies could each produce an assembly named OurLibrary with the same public/private keys unless the companies share this key pair with each other.

As described in Chapter 2, when you compile your source code, the compiler detects the types and members that your code references. You must specify the referenced assemblies to the compiler. For the C# compiler, you use the /reference compiler switch. Part of the compiler’s job is to emit an AssemblyRef metadata table inside the resulting managed module. Each entry in the AssemblyRef metadata table indicates the referenced assembly’s name (without path and extension), version number, culture, and public key information.

💡 小结:要由多个应用程序访问的程序集必须放到公认的目录。另外,检测到对程序集的引用时,CLR 必须能自动检查该目录。但加入两个相同文件名的程序集都复制到相同的公认目录,那么最后一个安装的就是 “老大”,造成正在使用旧程序集的所有应用程序都无法正常工作(这正是 Windows”DLL hell“的由来,因为共享 DLL 全部复制到 System32 目录)。所谓的” 强命名程序集 “就是为了解决上述问题 CLR 对程序集进行唯一性标识的机制。强命名程序集具有 4 个重要特性,它们共同对程序集进行唯一性标识:文件名(不计扩展名)、版本号、语言文化和公钥。由于公钥数字很大所以经常使用从公钥派生的小哈希值,称为公钥标记(public key token),这种标记可以在 AssemblyRef 元数据信息中看到(该表指名了被引用程序集的名称、版本号、语言文化和公钥信息)。弱命名程序集是没有公钥标记的。值得注意的是,CLR 在做出安全或信任决策时,永远都不会使用公钥标记,因为几个公钥可能在哈希处理之后得到同一个公钥标记。而 AssemblyDef 的记录项总是存储完整公钥,而不是公钥标记,这是为了保证文件没有被篡改,本章后面将解时强命名程序集如何放篡改。” 对文件进行签名 “的准确含义是:生成强命名程序集时,程序集的 FileDef 清单元数据表列出构成程序集的所有文件,每将一个文件名添加到清单,都对文件内容进行哈希处理。哈希值和文件名一道存储到 FileDef 表中。哈希值用发布者的私钥进行签名,得到的 RSA 数字签名存储到 PE 文件的一个保留区域。PE 文件的 CLR 头进行更新,反映数字签名在文件中的嵌入位置。

# The Global Assembly Cache

If an assembly is to be accessed by multiple applications, the assembly must be placed into a well-known directory, and the CLR must know to look in this directory automatically when a reference to the assembly is detected. This well-known location is called the global assembly cache (GAC). The exact location of the GAC is an implementation detail that is subject to change with different versions of the .NET Framework. However, you can typically find it in the following directory:

%SystemRoot%\Microsoft.NET\Assembly

The GAC directory is structured. It contains many subdirectories, and an algorithm is used to generate the names of these subdirectories. You should never manually copy assembly files into the GAC; instead, you should use tools to accomplish this task. These tools know the GAC’s internal structure and how to generate the proper subdirectory names.

While developing and testing, you can use the most common tool for installing a strongly named assembly into the GAC, which is GACUtil.exe. Running this tool without any command-line arguments yields the following usage.

Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.17929
Copyright (c) Microsoft Corporation. All rights reserved.
Usage: Gacutil <command> [ <options> ]
Commands:
 /i <assembly_path> [ /r <...> ] [ /f ]
 Installs an assembly to the global assembly cache.
 /il <assembly_path_list_file> [ /r <...> ] [ /f ]
 Installs one or more assemblies to the global assembly cache.
 /u <assembly_display_name> [ /r <...> ]
 Uninstalls an assembly from the global assembly cache.
 /ul <assembly_display_name_list_file> [ /r <...> ]
 Uninstalls one or more assemblies from the global assembly cache.
 /l [ <assembly_name> ]
 List the global assembly cache filtered by <assembly_name>
 /lr [ <assembly_name> ]
 List the global assembly cache with all traced references.
 /cdl
 Deletes the contents of the download cache
 /ldl
 Lists the contents of the download cache
 /?
 Displays a detailed help screen
 Options:
 /r <reference_scheme> <reference_id> <description>
 Specifies a traced reference to install (/i, /il) or uninstall (/u, /ul).
 /f
 Forces reinstall of an assembly.
 /nologo
 Suppresses display of the logo banner
 /silent
 Suppresses display of all output

💡 Note: GAC 默认只能由 Windows Administrators 用户组的成员操作。如果执行 GACUtil.exe 的用户不是改组的成员,GACUtil.exe 将无法安装或卸载程序集。如果将强命名程序集打包到.cab 文件中,或者以其他方式进行压缩,程序集的文件首先必须解压成临时文件,然后才能使用 GACUtil.exe 将程序集文件安装到 GAC 中。安装好程序集的文件之后,临时文件可以删除。

Using GACUtil.exe’s /i switch is very convenient for developer testing. However, if you use GACUtil.exe to deploy an assembly in a production environment, it’s recommended that you use GACUtil.exe’s /r switch in addition to specifying the /i or /u switch to install or uninstall the assembly. The /r switch integrates the assembly with the Windows install and uninstall engine. Basically, it tells the system which application requires the assembly and then ties the application and the assembly together.

The GACUtil.exe tool doesn’t ship with the end-user .NET Framework redistributable package. If your application includes some assemblies that you want deployed into the GAC, you should use the Windows Installer (MSI), because MSI is the only tool that is guaranteed to be on end-user machines and capable of installing assemblies into the GAC.

💡 Note: 在 GAC 中全局部署是对程序集进行注册的一种形式,虽然这个过程对 Windows 注册表没有半点影响。将程序集安装到 GAC 破坏了我们想要达成的一个基本目标,即:简单地安装、备份、还原、移动和卸载应用程序。所以,建议尽量进行私有而不是全局部署。

What is the purpose of “registering” an assembly in the GAC? Well, say two companies each produce an OurLibrary assembly consisting of one file: OurLibrary.dll. Obviously, both of these files can’t go in the same directory because the last one installed would overwrite the first one, surely breaking some application. When you install an assembly into the GAC, dedicated subdirectories are created under the %SystemRoot%\Microsoft.NET\Assembly directory, and the assembly files are copied into one of these subdirectories.

💡 小结:由多个应用程序访问的程序集必须放到公认的目录,而且 CLR 在检测到对该程序集的引用时,必须知道检查该目录。这个公认位置就是全局程序集缓存(Global Assembly Cache,GAC)。GAC 目录是结构化的,其中包含许多子目录,子目录名称用算法生成。永远不要将程序集文件手动复制到 GAC 目录;相反,要用工具完成这项任务,最常用的工具是 GACUtil.exe,工具知道 GAC 的内部结构,并知道如何生成正确的子目录名。这种方式保证了即使两家公司都生成了一个同名程序集,由于公私钥不同,最后将在 % SystemRoot%\Microsoft.NET\Assembly 下生成不同的子目录,因此不会产生名称冲突覆盖问题。此外,弱命名程序集不能放到 GAC。

# Building an Assembly That References a Strongly Named Assembly

Whenever you build an assembly, the assembly will have references to other strongly named assemblies. This is true because System.Object is defined in MSCorLib.dll, which is strongly named. However, it’s likely that an assembly will reference types in other strongly named assemblies published either by Microsoft, a third party, or your own organization. In Chapter 2, I showed you how to use CSC.exe’s /reference compiler switch to specify the assembly file names you want to reference. If the file name is a full path, CSC.exe loads the specified file and uses its metadata information to build the assembly. As mentioned in Chapter 2, if you specify a file name without a path, CSC.exe attempts to find the assembly by looking in the following directories (in order of their presentation here):

  1. Working directory.
  2. The directory that contains the CSC.exe file itself. This directory also contains the CLR DLLs.
  3. Any directories specified using the /lib compiler switch.
  4. Any directories specified using the LIB environment variable.

So if you’re building an assembly that references Microsoft’s System.Drawing.dll, you can specify the /reference:System.Drawing.dll switch when invoking CSC.exe. The compiler will examine the directories shown earlier and will find the System.Drawing.dll file in the directory that contains the CSC.exe file itself, which is the same directory that contains the DLLs for the version of the CLR the compiler is tied to. Even though this is the directory where the assembly is found at compile time, this isn’t the directory where the assembly will be loaded from at run time.

You see, when you install the .NET Framework, two copies of Microsoft’s assembly files are actually installed. One set is installed into the compiler/CLR directory, and another set is installed into a GAC subdirectory. The files in the compiler/CLR directory exist so that you can easily build your assembly, whereas the copies in the GAC exist so that they can be loaded at run time.

The reason that CSC.exe doesn’t look in the GAC for referenced assemblies is that you’d have to know the path to the assembly file and the structure of the GAC is undocumented. Alternatively, CSC.exe could allow you to specify a still long but slightly nicer-looking string, such as “System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a.” Both of these solutions were deemed worse than having the assembly files installed twice on the user’s hard drive.

In addition, the assemblies in the compiler/CLR directory are machine agnostic. That is, these assemblies contain only metadata in them. Because the IL code is not required at build time, this directory does not have to contain x86, x64, and ARM versions of an assembly. The assemblies in the GAC contain metadata and IL code because the code is needed only at run time. And, because the code can be fine-tuned for a specific CPU architecture, the GAC allows multiple copies of an assembly to reside within it; each copy is located under a different subdirectory for each CPU architecture.

💡 小结:第二章介绍过如何使用 CSC.exe 的 /reference 编译器开关指定想引用的程序集文件名。如果文件名是完整路径,CSC.exe 会加载指定文件,并根据它的元数据生成程序集。如果指定不含路径的文件名,CSC.exe 则会依次在工作目录、CSC.exe 所在目录、使用 /lib 编译器开关指定的任何目录、使用 LIB 环境变量指定的任何目录。安装.NET Framework 时,实际会安装 Microsoft 的程序集文件的两套拷贝。一套安装到编译器 / CLR 目录,另一套安装到 GAC 的子目录。编译器 / CLR 目录中的文件方便你生成程序集,而 GAC 中的拷贝则方便在运行时加载。编译器 / CLR 目录中的程序集只包含元数据(由于不需要 IL 代码,所以于 CPU 框架无关,故称这些程序集不依赖机器)。GAC 中程序集才同时包含元数据和 IL 代码,因为仅在运行时才需要代码。另外,由于代码可针对特定 CPU 架构进行优化,所以 GAC 允许存在一个程序集的多个拷贝,每个 CPU 框架都有一个专门的子目录来容纳这些拷贝。CSC.exe 之所以不在 GAC 中查找引用的程序集,是因为你必须知道程序集路径,而 GAC 的结构有没有正式公开。

# Strongly Named Assemblies Are Tamper-Resistant

Signing an assembly with a private key and embedding the signature and public key within an assembly allows the CLR to verify that the assembly has not been modified or corrupted. When an assembly is installed into the GAC, the system hashes the contents of the file containing the manifest and compares the hash value with the RSA digital signature value embedded within the PE file (after unsigning it with the public key). If the values are identical, the file’s contents haven’t been tampered with. In addition, the system hashes the contents of the assembly’s other files and compares the hash values with the hash values stored in the manifest file’s FileDef table. If any of the hash values don’t match, at least one of the assembly’s files has been tampered with, and the assembly will fail to install into the GAC.

When an application needs to bind to an assembly, the CLR uses the referenced assembly’s properties (name, version, culture, and public key) to locate the assembly in the GAC. If the referenced assembly can be found, its containing subdirectory is returned, and the file holding the manifest is loaded. Finding the assembly this way assures the caller that the assembly loaded at run time came from the same publisher that built the assembly the code was compiled against. This assurance is possible because the public key token in the referencing assembly’s AssemblyRef table corresponds to the public key in the referenced assembly’s AssemblyDef table. If the referenced assembly isn’t in the GAC, the CLR looks in the application’s base directory and then in any of the private paths identified in the application’s configuration file; then, if the application was installed using MSI, the CLR asks MSI to locate the assembly. If the assembly can’t be found in any of these locations, the bind fails, and a System.IO.FileNotFoundException is thrown.

When strongly named assembly files are loaded from a location other than the GAC (via the application’s base directory or via a codeBase element in a configuration file), the CLR compares hash values when the assembly is loaded. In other words, a hash of the file is performed every time an application executes and loads the assembly. This performance hit is a tradeoff for being certain that the assembly file’s content hasn’t been tampered with. When the CLR detects mismatched hash values at run time, it throws a System.IO.FileLoadException.

💡 小结:用私钥对程序集进行签名,并将公钥和签名嵌入程序集,CLR 就可以验证程序集未被修改或破坏。程序集安装到 GAC 时,系统会对包含清单的那个文件的内容进行哈希处理,将哈希值与 PE 文件中嵌入的 RSA 数字签名进行比较,也会与清单文件的 FileDef 表中存储的哈希值进行比较。任何一个哈希值不匹配,表明程序集至少有一个文件被篡改,程序集将无法安装到 GAC。应用程序需要绑定到程序集时,CLR 根据被引用程序集的属性(名称、版本、语言文化和公钥)在 GAC 中定位该程序集。找到被引用的,就返回包含它的子目录,并加载清单所在的文件。如果被引用程序集不在 GAC 中,CLR 会查找应用程序的基目录,然后查找应用程序配置文件中标注的任何私有路径。需要注意的是,入宫强命名程序集文件从 GAC 之外的位置加载,CLR 会在程序集加载后比较哈希值。也就是说,每次应用程序执行并加载程序集时,都会对文件进行哈希处理,以牺牲性能为代价,保证程序集文件内容没有被篡改。而将强命名程序集安装到 GAC 时,系统仅会在安装时执行一次检查操作。除此之外,为了增强性能,如果强命名程序集被完全信任,并加载到完全信任的 AppDomain 中,CLR 将不检查该程序集是否被篡改。

# Delayed Signing

When you’re ready to package your strongly named assembly, you’ll have to use the secure private key to sign it. However, while developing and testing your assembly, gaining access to the secure private key can be a hassle. For this reason, the .NET Framework supports delayed signing, sometimes referred to as partial signing. Delayed signing allows you to build an assembly by using only your company’s public key; the private key isn’t necessary. Using the public key allows assemblies that reference your assembly to embed the correct public key value in their AssemblyRef metadata entries. It also allows the assembly to be placed in the GAC appropriately. If you don’t sign the file with your company’s private key, you lose all of the tampering protection afforded to you because the assembly’s files won’t be hashed, and a digital signature won’t be embedded in the file. This loss of protection shouldn’t be a problem, however, because you use delayed signing only while developing your own assembly, not when you’re ready to package and deploy the assembly.

Basically, you get your company’s public key value in a file and pass the file name to whatever utility you use to build the assembly. (As I have shown earlier in this chapter, you can use SN.exe’s –p switch to extract a public key from a file that contains a public/private key pair.) You must also tell the tool that you want the assembly to be delay signed, meaning that you’re not supplying a private key. For the C# compiler, you do this by specifying the /delaysign compiler switch. In Visual Studio, you display the properties for your project, click the Signing tab, and then select the Delay Sign Only check box. If you’re using AL.exe, you can specify the /delay[sign] commandline switch.

When the compiler or AL.exe detects that you’re delay signing an assembly, it will emit the assembly’s AssemblyDef manifest entry, which will contain the assembly’s public key. Again, the presence of the public key allows the assembly to be placed in the GAC. It also allows you to build other assemblies that reference this assembly; the referencing assemblies will have the correct public key in their AssemblyRef metadata table entries. When creating the resulting assembly, space is left in the resulting PE file for the RSA digital signature. (The utility can determine how much space is necessary from the size of the public key.) Note that the file’s contents won’t be hashed at this time either.

At this point, the resulting assembly doesn’t have a valid signature. Attempting to install the assembly into the GAC will fail because a hash of the file’s contents hasn’t been done—the file appears to have been tampered with. On every machine on which the assembly needs to be installed into the GAC, you must prevent the system from verifying the integrity of the assembly’s files. To do this, you use the SN.exe utility, specifying the –Vr command-line switch. Executing SN.exe with this switch also tells the CLR to skip checking hash values for any of the assembly’s files when loaded at run time. Internally, SN’s –Vr switch adds the assembly’s identity under the following registry subkey:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\StrongName\Verification

When you’re finished developing and testing the assembly, you need to officially sign it so that you can package and deploy it. To sign the assembly, use the SN.exe utility again, this time with the –R switch and the name of the file that contains the actual private key. The –R switch causes SN.exe to hash the file’s contents, sign it with the private key, and embed the RSA digital signature in the file where the space for it had previously been reserved. After this step, you can deploy the fully signed assembly. On the developing and testing machines, don’t forget to turn verification of this assembly back on by using SN.exe’s –Vu or –Vx command-line switch.

csc /keyfile:MyCompany.PublicKey /delaysign MyAssembly.cs
SN.exe –Vr MyAssembly.dll
SN.exe -Ra MyAssembly.dll MyCompany.PrivateKey
SN.exe –Vu MyAssembly.dll

💡 小结:准备打包自己的强命名程序集时,必须使用受严密保护的私钥对它进行签名。然而,在开发和测试程序集时,访问这些受严密保护的私钥可能有点碍事儿。有鉴于此,.NET Framework 提供了对延迟签名(delayed signing)的支持,该技术也称为部分签名(partial signing)。延迟签名允许只用公司的公钥生成程序集,暂时不用私钥。编译器或 AL.exe 一旦检测到要对程序集进行延迟签名,就会生成程序集的 AssemblyDef 清单记录项,其中将包含程序集的公钥。公钥使程序集能正确存储到 GAC。创建程序集时,会在生成的 PE 文件中为 RSA 数字签名预留空间(实际程序根据公钥大小判断需预留多大空间)。要注意的时,文件内容不会再这个时候进行哈希处理,由于程序集没有有效签名,因此在需要安装到 GAC 的每台机器上,都必须禁止系统验证程序集文件的完整性。结束程序集的开发和测试之后,要正式对其进行签名,以便打包和部署它。

# Privately Deploying Strongly Named Assemblies

Installing assemblies into the GAC offers several benefits. The GAC enables many applications to share assemblies, reducing physical memory usage on the whole. In addition, it’s easy to deploy a new version of the assembly into the GAC and have all applications use the new version via a publisher policy (described later in this chapter). The GAC also provides side-by-side management for an assembly’s different versions. However, the GAC is usually secured so that only an administrator can install an assembly into it. Also, installing into the GAC breaks the simple copy deployment story.

Although strongly named assemblies can be installed into the GAC, they certainly don’t have to be. In fact, it’s recommended that you deploy assemblies into the GAC only if the assembly is intended to be shared by many applications. If an assembly isn’t intended to be shared, it should be deployed privately. Deploying privately preserves the simple copy install deployment story and better isolates the application and its assemblies. Also, the GAC isn’t intended to be the new C:\Windows\System32 dumping ground for common files. The reason is because new versions of assemblies don’t overwrite each other; they are installed side by side, eating up disk space.

In addition to deploying a strongly named assembly in the GAC or privately, a strongly named assembly can be deployed to some arbitrary directory that a small set of applications know about. For example, you might be producing three applications, all of which want to share a strongly named assembly. Upon installation, you can create four directories: one for each application and an additional directory for the assembly you want shared. When you install each application into its directory, also install an XML configuration file, and have the shared assembly’s codeBase element indicate the path of the shared assembly. Now at run time, the CLR will know to look in the strongly named assembly’s directory for the shared assembly. For the record, this technique is rarely used and is somewhat discouraged because no single application controls when the assembly’s files should be uninstalled.

💡 Note:配置文件的 codeBase 元素实际标记了一个 URL。这个 URL 可引用用户机器上的人户目录,也可引用 Web 地址。如果引用 Web 地址,CLR 会自动下载文件,并把它存储到用户的下载缓存(% UserProfile%\Local Settings\Application Data\Assembly 下的子目录)。将来引用时,CLR 将下载文件的时间戳与 URL 处的文件的时间戳进行对比。如果 URL 处的文件具有较新的时间戳,CLR 下载新版本并加载。否则,CLR 加载现有文件,不重复下载(从而增强性能)。

💡 小结:GAC 使程序集能被多个应用程序共享,减少了总体物理内存消耗。另外,很容易将程序集的新版本部署到 GAC,让所有应用程序都通过发布者策略使用新版本。GAC 还实现了对程序集多个版本的并行管理。但 GAC 通常受到严密保护,只有管理员才能在其中安装程序集,并且这种方式违反了 “简单复制部署” 的目标。强命名程序集并非必须安装到 GAC,私有部署达成了 “简单复制部署” 目标,而且能更好地隔离应用程序及其程序集。另外,不要将 GAC 想象成新的 C:\Windows\System32 垃圾堆机场。这是新版本程序集不会相互覆盖,它们并行安装,每个安装都占用磁盘空间。

# How the Runtime Resolves Type References

At the beginning of Chapter 2, we saw the following source code.

public sealed class Program {
 public static void Main() {
 System.Console.WriteLine("Hi");
 }
}

This code is compiled and built into an assembly, say Program.exe. When you run this application, the CLR loads and initializes. Then the CLR reads the assembly’s CLR header, looking for the MethodDefToken that identifies the application’s entry point method (Main). From the MethodDef metadata table, the offset within the file for the method’s IL code is located and JIT-compiled into native code, which includes having the code verified for type safety. The native code then starts executing. Following is the IL code for the Main method. To obtain this output, I ran ILDasm.exe, chose the View menu’s Show Bytes menu item, and then double-clicked the Main method in the tree view.

.method public hidebysig static void Main() cil managed
// SIG: 00 00 01
{
 .entrypoint
 // Method begins at RVA 0x2050
 // Code size 11 (0xb)
 .maxstack 8
 IL_0000: /* 72 | (70)000001 */
 ldstr "Hi"
 IL_0005: /* 28 | (0A)000003 */
 call void [mscorlib]System.Console::WriteLine(string)
 IL_000a: /* 2A | */
 ret
} // end of method Program::Main

When JIT-compiling this code, the CLR detects all references to types and members and loads their defining assemblies (if not already loaded). As you can see, the preceding IL code has a reference to System.Console.WriteLine . Specifically, the IL call instruction references metadata token 0A000003. This token identifies entry 3 in the MemberRef metadata table (table 0A). The CLR looks up this MemberRef entry and sees that one of its fields refers to an entry in a TypeRef table (the System.Console type). From the TypeRef entry, the CLR is directed to an AssemblyRef entry: “mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089”. At this point, the CLR knows which assembly it needs. Now the CLR must locate the assembly in order to load it.

When resolving a referenced type, the CLR can find the type in one of three places:

■ Same file Access to a type that is in the same file is determined at compile time (sometimes referred to as early bound). The type is loaded out of the file directly, and execution continues.

■ Different file, same assembly The runtime ensures that the file being referenced is, in fact, in the assembly’s ModuleRef table of the current assembly’s manifest. The runtime then looks in the directory where the assembly’s manifest file was loaded. The file is loaded, its hash value is checked to ensure the file’s integrity, the type’s member is found, and execution continues.

■ Different file, different assembly When a referenced type is in a different assembly’s file, the runtime loads the file that contains the referenced assembly’s manifest. If this file doesn’t contain the type, the appropriate file is loaded. The type’s member is found, and execution continues.

💡 Note:ModuleDef,ModuleRef 和 FileDef 元数据表在引用文件时使用了文件名和扩展名。但 AssemblyRef 元数据表只使用文件名,无扩展名。

Untitled

There is one more twist to this story: to the CLR, all assemblies are identified by name, version, culture, and public key. However, the GAC identifies assemblies by using name, version, culture, public key, and CPU architecture. When searching the GAC for an assembly, the CLR figures out what type of process the application is currently running in: 32-bit x86 (possibly using the WoW64 technology), 64-bit x64, or 32-bit ARM. Then, when searching the GAC for an assembly, the CLR first searches for a CPU architecture–specific version of the assembly. If it does not find a matching assembly, it then searches for a CPU-agnostic version of the assembly.

💡 Note:CLR 提供了将类型(类、结构体、枚举、接口或委托)从一个程序集移动到另一个程序集的功能。CLR 提供了名为 System.Runtime.CompilerServices.TypeForwardedToAttribute 特性,可将它应用于原始程序集。要向该特性的构造器传递一个 System.Type 类型的参数,之处应用程序要使用的新类型。CLR 绑定器(binder)会利用这个信息。由于 TypeForwardedToAttribute 的构造器获取的是 Type,所以包含该特性的程序集要依赖于现在定义类型的程序集。为了使用这个功能,还要向新程序集中的类型应用名为 System.Runtime.CompilerServices.TypeForwardedFromAttribute 的特性,向该特性的构造器传递一个字符串来之处定义类型的旧程序集的全名。由于 TypeForwardedFromAttribute 的构造器获取的是 String,所以包含该特性的程序集不依赖于过去定义类型的程序集。

💡 小结:” 运行时 “解析引用类型时,CLR 可能在以下三个地方找到类型:1. 相同文件。编译时便能发现对相同文件中的类型的访问,这称为早期绑定(early binding)。类型直接从文件中加载,执行继续。2. 不同文件,相同程序集。” 运行时 “确保被引用的文件在当前程序集元数据的 FileDef 表中,检查加载程序集清单文件的目录,加载被引用的文件,检查哈希值以确保文件完整性。发现类型的成员,执行继续。3. 不同文件,不同程序集。如果引用的类型在其他程序集中,” 运行时 “会加载被引用程序集的清单文件。如果需要的类型不在该文件中,就继续加载包含了该类型的文件。发现类型的成员,执行继续。还要注意,对于 CLR,所有程序集都根据名称、版本、语言文化和公钥来识别。但 GAC 根据名称、版本、语言文化、公钥和 CPU 架构来识别。在 GAC 中搜索程序集时,CLR 首先搜索程序集的 CPU 架构专用版本。如果没有找到符合要求的,就搜索不区分 CPU 的版本。

# Advanced Administrative Control (Configuration)

Having discussed only the probing element’s privatePath attribute in Chapter 2, I’m going to discuss the other XML configuration file elements in this section. Following is an XML configuration file.

<?xml version="1.0"?>
<configuration>
 <runtime>
 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
 <probing privatePath="AuxFiles;bin\subdir" />
 <dependentAssembly>
 <assemblyIdentity name="SomeClassLibrary"
 publicKeyToken="32ab4ba45e0a69a1" culture="neutral"/>
 <bindingRedirect
 oldVersion="1.0.0.0" newVersion="2.0.0.0" />
 <codeBase version="2.0.0.0"
 href="http://www.Wintellect.com/SomeClassLibrary.dll" />
 </dependentAssembly>
 <dependentAssembly>
 <assemblyIdentity name="TypeLib"
 publicKeyToken="1f2e74e897abbcfe" culture="neutral"/>
 <bindingRedirect
 oldVersion="3.0.0.0-3.5.0.0" newVersion="4.0.0.0" />
 <publisherPolicy apply="no" />
 </dependentAssembly>
 </assemblyBinding>
 </runtime>
</configuration>

This XML file gives a wealth of information to the CLR. Here’s what it says:

probing element

Look in the application base directory’s AuxFiles and bin\subdir subdirectories when trying to find a weakly named assembly. For strongly named assemblies, the CLR looks in the GAC or in the URL specified by the codeBase element. The CLR looks in the application’s private paths for a strongly named assembly only if no codeBase element is specified.

First dependentAssembly, assemblyIdentity, and bindingRedirect elements

When attempting to locate version 1.0.0.0 of the culture-neutral SomeClassLibrary assembly published by the organization that controls the 32ab4ba45e0a69a1 public key token, locate version 2.0.0.0 of the same assembly instead.

codeBase element

When attempting to locate version 2.0.0.0 of the culture-neutral SomeClassLibrary assembly published by the organization that controls the 32ab4ba45e0a69a1 public key token, try to find it at the following URL: www.Wintellect.com/SomeClassLibrary.dll. Although I didn’t mention it in Chapter 2, a codeBase element can also be used with weakly named assemblies. In this case, the assembly’s version number is ignored and should be omitted from the XML’s codeBase element. Also, the codeBase URL must refer to a directory under the application’s base directory.

Second dependentAssembly, assemblyIdentity, and bindingRedirect elements

When attempting to locate version 3.0.0.0 through version 3.5.0.0 inclusive of the culture-neutral TypeLib assembly published by the organization that controls the 1f2e74e897abbcfe public key token, locate version 4.0.0.0 of the same assembly instead.

publisherPolicy element

If the organization that produces the TypeLib assembly has deployed a publisher policy file (described in the next section), the CLR should ignore this file.

When compiling a method, the CLR determines the types and members being referenced. Using this information, the runtime determines, by looking in the referencing assembly’s AssemblyRef table, the assembly that was originally referenced when the calling assembly was built. The CLR then looks up the assembly/version in the application’s configuration file and applies any version number redirections; the CLR is now looking for this assembly/version.

Using these configuration files, an administrator can really control what assembly the CLR decides to load. If an application is experiencing a bug, the administrator can contact the publisher of the errant assembly. The publisher can send the administrator a new assembly that the administrator can install. By default, the CLR won’t load this new assembly because the already-built assemblies don’t reference the new version. However, the administrator can modify the application’s XML configuration file to instruct the CLR to load the new assembly.

If the administrator wants all applications on the machine to pick up the new assembly, the administrator can modify the machine’s Machine.config file instead, and the CLR will load the new assembly whenever an application refers to the old assembly.

If the new assembly doesn’t fix the original bug, the administrator can delete the binding redirection lines from the configuration file, and the application will behave as it did before. It’s important to note that the system allows the use of an assembly that doesn’t exactly match the assembly version recorded in the metadata. This extra flexibility is very handy.

Publisher Policy Control

In the scenario described in the previous section, the publisher of an assembly simply sent a new version of the assembly to the administrator, who installed the assembly and manually edited the application’s or machine’s XML configuration files. In general, when a publisher fixes a bug in an assembly, the publisher would like an easy way to package and distribute the new assembly to all of the users. But the publisher also needs a way to tell each user’s CLR to use the new assembly version instead of the old assembly version. Sure, each user could modify his or her application’s or machine’s XML configuration file, but this is terribly inconvenient and error prone. What the publisher needs is a way to create policy information that is installed on the user’s computer when the new assembly is installed. In this section, I’ll show how an assembly’s publisher can create this policy information.

Let’s say that you’re a publisher of an assembly and that you’ve just created a new version of your assembly that fixes some bugs. When you package your new assembly to send out to all of your users, you should also create an XML configuration file. This configuration file looks just like the configuration files we’ve been talking about. Here’s an example file (called SomeClassLibrary.config) for the SomeClassLibrary.dll assembly.

<configuration>
 <runtime>
 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
 <dependentAssembly>
 <assemblyIdentity name="SomeClassLibrary"
 publicKeyToken="32ab4ba45e0a69a1" culture="neutral"/>
 <bindingRedirect
 oldVersion="1.0.0.0" newVersion="2.0.0.0" />
 <codeBase version="2.0.0.0"
 href="http://www.Wintellect.com/SomeClassLibrary.dll"/>
 </dependentAssembly>
 </assemblyBinding>
 </runtime>
</configuration>

Of course, publishers can set policies only for the assemblies that they themselves create. In addition, the elements shown here are the only elements that can be specified in a publisher policy configuration file; you can’t specify the probing or publisherPolicy elements, for example.

This configuration file tells the CLR to load version 2.0.0.0 of the SomeClassLibrary assembly whenever version 1.0.0.0 of the assembly is referenced. Now you, the publisher, can create an assembly that contains this publisher policy configuration file. You create the publisher policy assembly by running AL.exe as follows.

AL.exe /out:Policy.1.0.SomeClassLibrary.dll
 /version:1.0.0.0
 /keyfile:MyCompany.snk
 /linkresource:SomeClassLibrary.config

Let me explain the meaning of AL.exe’s command-line switches:

/out

This switch tells AL.exe to create a new PE file, called Policy.1.0.SomeClassLibrary.dll, which contains nothing but a manifest. The name of this assembly is very important. The first part of the name, Policy, tells the CLR that this assembly contains publisher policy information. The second and third parts of the name, 1.0, tell the CLR that this publisher policy assembly is for any version of the SomeClassLibrary assembly that has a major and minor version of 1.0. Publisher policies apply to the major and minor version numbers of an assembly only; you can’t create a publisher policy that is specific to individual builds or revisions of an assembly. The fourth part of the name, SomeClassLibrary, indicates the name of the assembly that this publisher policy corresponds to. The fifth and last part of the name, dll, is simply the extension given to the resulting assembly file.

/version

This switch identifies the version of the publisher policy assembly; this version number has nothing to do with the SomeClassLibrary assembly itself. You see, publisher policy assemblies can also be versioned. Today, the publisher might create a publisher policy redirecting version 1.0.0.0 of SomeClassLibrary to version 2.0.0.0. In the future, the publisher might want to direct version 1.0.0.0 of SomeClassLibrary to version 2.5.0.0. The CLR uses this version number so that it knows to pick up the latest version of the publisher policy assembly.

/keyfile

This switch causes AL.exe to sign the publisher policy assembly by using the publisher’s public/private key pair. This key pair must also match the key pair used for all versions of the SomeClassLibrary assembly. After all, this is how the CLR knows that the same publisher created both the SomeClassLibrary assembly and this publisher policy file.

/linkresource

This switch tells AL.exe that the XML configuration file is to be considered a separate file of the assembly. The resulting assembly consists of two files, both of which must be packaged and deployed to the users along with the new version of the SomeClassLibrary assembly. By the way, you can’t use AL.exe’s /embedresource switch to embed the XML configuration file into the assembly file, making a single file assembly, because the CLR requires the XML file to be contained in its own separate file.

After this publisher policy assembly is built, it can be packaged together with the new SomeClassLibrary.dll assembly file and deployed to users. The publisher policy assembly must be installed into the GAC. Although the SomeClassLibrary assembly can also be installed into the GAC, it doesn’t have to be. It could be deployed into an application’s base directory or some other directory identified by a codeBase URL.

I want to make one last point about publisher policy. Say that a publisher distributes a publisher policy assembly, and for some reason, the new assembly introduces more bugs than it fixes. If this happens, the administrator would like to tell the CLR to ignore the publisher policy assembly. To have the runtime do this, the administrator can edit the application’s configuration file and add the following publisherPolicy element.

<publisherPolicy apply="no"/>

This element can be placed as a child element of the element in the application’s configuration file so that it applies to all assemblies, or as a child element of the element in the application’s configuration file to have it apply to a specific assembly. When the CLR processes the application’s configuration file, it will see that the GAC shouldn’t be examined for the publisher policy assembly. So the CLR will continue to operate using the older version of the assembly. Note, however, that the CLR will still examine and apply any policy specified in the Machine.config file.

💡 小结:系统允许使用和元数据记录的不完全匹配的程序集版本,这种控制由应用程序的 XML 配置文件完成。而发布者策略程序集则是为了解决用户手动修改应用程序或机器的 XML 配置文件不方便和容易出错的问题。将由发布者来创建策略信息。只有部署程序集更新或 Service Pack 时才应创建发布者策略程序集。执行应用程序的全新安装不应安装发布者策略程序集。创建发布者策略程序集,发布者相当于肯定了程序集不同版本的兼容性。如果新版本程序集不兼容某个老版本,就不应创建发布者策略程序集。通常,如果需要生成程序集的 bug 修复版本,就应该提供发布者策略程序集。作为发布者,应主动测试新版本程序集的向后兼容性。相反,如果要在程序集中增添新功能,就应该把它视为与之前版本没有关联的程序集,不要随带发布者策略程序集。另外,也不必测试这类程序集的向后兼容性。