Technologies: Assemblies | MSIL | MSIL Disassembler
Explore .NET Assemblies
Assembly Binding Revealed
By Jeff Niblack
This article digs down into .NET assemblies, metadata, the assembly manifest, and more. Having a thorough understanding of these concepts will help you keep your sanity when deploying .NET-based applications.
An assembly is a logical .dll or .exe with a few extras to help the .NET run time understand how to use and work with it. Among other things, these extras include:
- Version, name, culture, and any security requirements.
- Other files, if any, that make up the assembly.
- Imported and exported types, methods, events, properties, etc.
In general, this information is the metadata that describes the assembly, and is always physically included with the assembly that it describes. Create a simple program by entering the following in your favorite code editor, and save it as hw.vb:
Public Module Hello
Then compile it by entering the following at a command prompt:
This simple application exposes a single type, Hello, that provides a single method, Main. Main uses the Console.WriteLine method of the System namespace to write out "Hello World!" to the command line when the application is executed.
As the assembly is compiled, the compiler emits information about the assembly, and stores it in a collection of tables. These tables are stored in a raw format that's not the easiest to read or understand. Fortunately, you can use the MSIL Disassembler tool, Ildasm.exe, that ships with the .NET Framework to view this information in an easy-to-understand format. To examine the assembly that the VB .NET compiler creates, enter:
ildasm /adv hw.exe
Including the /adv flag turns on a few advanced features of Ildasm, including MetaInfo (you'll use this feature later). Although there's a tremendous amount of information provided by Ildasm, I'll focus on the more important aspects. To view the manifest, as shown in FIGURE 1, double-click Manifest.
FIGURE 1: Manifest for hw.exe.
The first block listed is .assembly extern mscorlib. This means that hw.exe references an external assembly named mscorlib. mscorlib.dll is an assembly that's automatically added to every .NET assembly, and includes the base types, some of the System namespaces, and a few other things. (You can use Ildasm to disassemble mscorlib.dll and learn more about its features.) The .publickeytoken value indicates that mscorlib is a pubic assembly, and the value is the key used to verify its authenticity. The .ver value is the version of mscorlib that was used during compilation. With future releases of .NET, mscorlib.dll will be updated. Knowing which version of mscorlib was used to compile your assembly could help in isolating problems associated with changes to mscorlib.
The second block is specific to VB .NET assemblies. Once again, hw.exe is referencing an external assembly named Microsoft.VisualBasic. This assembly reference is comparable to using the VB run-time environment in previous versions of VB. Comparing the.ver value with the current version of the Microsoft.VisualBasic assembly being used is helpful when isolating changes in future releases.
The third block describes the hw assembly. Notice that the extern keyword isn't used because this block defines the assembly that's included in this file. Version information wasn't included for this simple application, so the compiler automatically set the version to 0:0:0:0. The .hash algorithm value is used by the Common Language Runtime (CLR) to determine if the assembly was tampered with after it was compiled. When the CLR runs hw.exe, it generates a hash value based on the current file. If this run-time hash value is different from the hash value stored in the assembly, the CLR will generate a run-time error.
Following the third block are two values: .module and MVID. .module is the actual name of the file that was output by the compiler. Even if the file is physically renamed, say to hw.junk, the manifest will still list hw.exe as the.module. Comparing the compiled name and the current physical name is one method to ensure that your application is using the correct file.
MVID is another important value to use when verifying assemblies. Each time an assembly is compiled, a GUID is generated to uniquely identify the compilation. This GUID is stored as the MVID for the assembly. Even when the assembly code and version information do not change from one compile to another, MVID is always updated with a new GUID. Comparing MVID values of two assembly files that are supposed to be the same will verify if the files are from different compilations or exact copies.
The remaining information in the manifest, address locations, and offsets for where the auxiliary manifest and metadata tables begin is beyond the scope of the current discussion.
Another useful feature of Ildasm is the View | MetaInfo | Show! menu option available from the main Ildasm window. MetaInfo is useful in providing a more detailed view of the metadata associated with an assembly. As shown in FIGURE 2, the ScopeName and MVID values provide the name of the assembly and the compile-time GUID that's associated with the assembly, respectively.
FIGURE 2: MetaInfo for hw.exe.
The remaining MetaInfo output lists detailed information from various manifest and metadata tables. The first significant entry is TypeDef #1, as shown in FIGURE 3. The TypeDef table contains one entry for each type defined in the assembly. For this simple application, there's only one type: Hello. As indicated, Hello is a public type - and because custom types are based on System.Object, Hello extends the System.Object namespace.
FIGURE 3: TypeDef #1 for hw.exe.
Within each TypeDef, any methods associated with the type are listed. The Hello type provides one method: Main. The [ENTRYPOINT] attribute indicates that Main is the entry point for our type. .NET assemblies are permitted to have one and only one entry point. Generally, this is Main, WinMain, or DllMain. As indicated by the Flags value, Main is a public method with a return type of Void, shown by the ReturnType value. Main also doesn't require any arguments, as indicated by the No arguments declaration. The remaining information is valuable for the run time, but not too interesting for the current discussion.
The TypeRef table contains an entry for each type referenced in our assembly, as shown in FIGURE 4. The first one with a token of 0x01000001 is the System.Object type that was referenced in TypeDef #1. Along with the TypeRefName value, the ResolutionScope is provided. The ResolutionScope is a token value that is used to define where the System.Object type reference is resolved. Generally, this points to an entry in the AssemblyRef, TypeRef, or ModuleRef tables. For TypeRef #1 (token 0x01000001), it's resolved by the AssemblyRef #1 entry (token 0x23000001), which is mscorlib, as indicated by the name value shown in FIGURE 5.
FIGURE 4: TypeRef table for hw.exe.
FIGURE 5: AssemblyRef table for hw.exe.
TypeRef #2, also resolved by mscorlib, is the reference to the System.Console type. The application uses the WriteLine method, which is listed in the associated MemberRef table, provided by the System.Console type.
TypeRef's 3 and 4 are always included with VB .NET assemblies. TypeRef #3 is the type reference for Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute and has a ResolutionScope of 0x23000002. If you look at the AssemblyRef table, you'll see that 0x23000002 is the Microsoft.VisualBasic assembly.
Looking beyond the TypeRef and AssemblyRef tables, the last table used by this assembly is UserString. This table lists the user-defined strings used by the assembly.
In the end, this simple Hello World VB .NET application includes:
- One defined type (Hello) with one method (Main)
- Four external types, provided by two external assemblies (mscorlib and Microsoft.VisualBasic)
- One user-defined string (Hello World!)
Up to this point, a simple, single file assembly has been used as our guinea pig. However, assemblies can be made up of one or more files. Although multiple files are used, the assembly is still versioned, deployed, managed, and referenced as a single unit. Supporting multi-file assemblies might sound like a bad idea on the surface because of the increased chance of deployment issues (i.e., not including all files when installing an assembly). However, by decoupling the logical representation from the physical implementation, you can have increased control over how your application loads resources. For example, because .NET uses a "lazy" approach to loading assemblies, when a type is referenced, the file that resolves that reference is loaded. The other files in an assembly remain unloaded until a type they provide is referenced. In the Internet space, this allows you to build an assembly made up of small modules that are only downloaded when referenced. If a user never requires a type, the associated file is never downloaded.
There are several deployment options when creating a multi-file assembly. The first one includes a single .exe file that references a module (.mod) file. First, create a file named hwmod1.vb, and enter the code shown in FIGURE 6.
Public Class Test
Public Sub Hello(strLang As String)
If strLang = "SP" Then
FIGURE 6: Source for hwmod1.mod.
This code provides a Test class with one method, Hello, that accepts a string argument that determines what language to use when saying Hello. If "SP" is provided, Hello returns "Hola" (Spanish for Hello). If any other string is provided, Hello returns "Hello". Compile this as a module by entering:
vbc /out:hwmod1.mod /t:module hwmod1.vb
Next, create a file named hwexe1.vb, and enter the code shown in FIGURE 7.
Public Module Hello
Dim t As New Test()
FIGURE 7: Source for hwexe1.exe.
This code exposes a single type, Hello, which supports a single method, Main. Within Main, the Test class is referenced, and the Spanish and English versions of Hello are returned. If compiled like this:
vbc /out:hwexe1.exe /t:exe hwexe1.vb
the compiler can't resolve the reference to the Test type, so a reference needs to be added to the hwmod1.mod module to the assembly. Enter the following to compile the hwexe1.vb code:
vbc /out:hwexe1.exe /t:exe /addmodule:hwmod1.mod hwexe1.vb
Notice that the /addmodule option allows the assembly to resolve the reference to the module, but the module still remains a separate file from the assembly, and must be included during deployment. Now that the assembly and its associated module are compiled, use Ildasm again to look into the details:
ildasm /adv hwexe1.exe
Then open the MetaInfo window. The MetaInfo describing hwexe1.exe is very similar to the hw.exe MetaInfo viewed previously; however, there are a few new definitions. TypeRef #2, shown in FIGURE 8, is similar to the previous TypeRef entries, but in this case, the name is Test and there's a ResolutionScope of 0x1a000001. Notice that Test also has two MemberRef entries; the default constructor (.ctor) and Hello. Test is resolved from the ModuleRef table, using the 0x1a000001 token, to point to hwmod1.mod.
FIGURE 8: TypeRef #2 for hwexe1.exe.
FIGURE 9 shows the File and ExportedType tables where ModuleRef #1 is resolved to File #1 with a token of 0x26000001. The ExportedType table entry lists Test as the exported type that's implemented by token 0x26000001 (File #1 or hwmod1.mod).
FIGURE 9: File and ExportedType tables for hwexe1.exe.
ildasm /adv hwmod1.mod
Then open the MetaInfo window for the module. While the MetaInfo output looks similar to the previous examples, there are two important things to note. First, no entry point is defined; second, there's no Assembly table entry for this module because it was compiled as a module rather than an .exe or .dll file. These are the two major differences in the metadata that distinguish a module from an .exe or .dll file.
Now create a multi-file assembly, and reference a namespace that's implemented in an external .dll file. First, create a file named hwmod2.vb, and enter the code shown in FIGURE 10.
Public Class Test
Public Sub Hello(strLang As String)
If strLang = "SP" Then
FIGURE 10: Source for hwmod2.dll.
This code is exactly the same as in hwmod1.vb, except that now there's a defined namespace, HW, which encapsulates the Test class. Compile this as a library by entering:
vbc /out:hwmod2.dll /t:library hwmod2.vb
Next, create a file named hwexe2.vb, and enter the code shown in FIGURE 11.
Public Module Hello
Dim t As New HW.Test()
FIGURE 11: Source for hwexe2.exe.
This code is exactly the same as in hwexe1.vb, except that now the HW namespace is used to resolve the reference to the Test type. With the addition of an external reference, the following reference (/r) directive needs to be included when compiling hwexe2:
vbc /out:hwexe2.exe /t:exe /r:hwmod2.dll hwexe2.vb
With the .dll and .exe compiled, look at hwexe2.exe using Ildasm by entering:
ildasm /adv hwexe2.exe
Then open the MetaInfo window. Once again the MetaInfo information is similar to previous examples. Looking at TypeRef #2 in FIGURE 12, you can see that HW.Test is referenced with a ResolutionScope of 0x23000003. This token is resolved in the AssemblyRef table where hwmod2 is listed as an external assembly.
FIGURE 12: TypeRef #2 for hwexe2.exe.
ildasm /adv hwmod2.dll
Then open the MetaInfo window for the library. As was the case for the module created earlier, there's no entry point defined. However, there's an Assembly table entry for this .dll. The table in FIGURE 13 summarizes the major metadata differences.
Assembly Table Entry
Yes - one and only one!
FIGURE 13: Major assembly metadata differences.
So far, the MSIL Disassembler tool has been used to walk through the metadata emitted by the compiler. You can use this information to further understand how third-party assemblies, as well as your own, are composed, and how the various files, types, methods, etc. are related. This provides invaluable information when developing with and deploying assemblies. This view, however, is static.
Run-time Assembly Binding
Another tool to determine the run-time binding of an assembly is the assembly manager (Fusion.dll). Within the .NET Framework, it is loaded each time an assembly is loaded. The assembly manager is responsible for binding the various external references with the called assembly. To view this binding process, the Assembly Binding Log Viewer (FusLogVw.exe) tool is shipped with the .NET Framework. This tool provides insight into how an assembly was composed at run time. It's invaluable when determining why assembly loads fail, or to verify the exact assemblies being referenced at run time.
By default, FusLogVw only logs assembly bind failures. Generally, these are related to version mismatches or assemblies deployed in the wrong location. Typically, these failures show up as TypeLoadExceptions in your application; however, you can have FusLogVw log successful assembly binds as well. This is a great learning tool to see how the run time handles assembly binding. It's also the only way to verify if an assembly is being loaded from a particular location. To log all assembly binds, you will need to add a new DWORD registry value at HKLM\Software\Microsoft\Fusion\ForceLog. Setting ForceLog to 1 will log all assembly binds. Setting the value to 0 will log only assembly bind failures - the default behavior.
Add the ForceLog registry value, and set its value to 1. Then run hwexe2.exe, the previous sample application. To view the assembly binding log, enter:
FIGURE 14 shows the viewer (you might have more assembly bind information than shown). FusLogVw has three locations in which it will look for log files:
- Default looks in per-user directories on disk in the wininet cache. All application types are logged in this location except for ASP.NET applications.
- ASP.NET looks in a per-application cache directory, and only contains ASP.NET application logs.
- Custom is controlled by the HKLM\Software\Microsoft\Fusion\LogPath value. By default, this value doesn't exist. However, you can set LogPath to a valid directory name where all logs will be stored. In general, it's best to not set this value because the run time automatically manages and cleans out the Default location. Whereas, if you use the Custom location, you're responsible for managing and cleaning up the directory.
FIGURE 14: Assembly Binding Log Viewer.
As shown in FIGURE 14, hwexe2.exe requires three different assembly binds: mscorlib, Microsoft.VisualBasic, and hwmod2. Select the log entry for the hwexe2.exe and mscorlib bind, and click the View Log button, or simply double-click the mscorlib log entry. FIGURE 15 shows the log for this binding. As noted in the log, this is a successful bind. The first entry of the pre-bind state information area shows the name, version, culture, and PublicKeyToken value (i.e., the assembly identification) that is requested for the bind. All the possible bind paths and configuration values associated with them are also listed. In the case of mscorlib, these values are generally Null, and the run time is allowed to load the mscorlib available in the cache (Global Assembly Cache [GAC]). The last line in the log verifies that mscorlib is loaded from the GAC.
FIGURE 15: Assembly binding log for hwexe2.exe and mscorlib.dll.
The bind log for hwexe2.exe and Microsoft.VisualBasic is very similar to mscorlib. Generally, the run time is allowed to manage the location of the Microsoft.VisualBasic assembly, and it's typically found in the GAC.
Select and view the bind log for hwexe2.exe and hwmod2, as shown in FIGURE 16. As indicated, the operation is successful. Likewise, as in the previous example, the pre-bind state information provides the specifics on the expected identity of the target assembly. In addition, the bind paths are all set to Null because custom configuration options were not provided for this assembly. The last section of the log shows the process, or probing, followed to load the hwmod2 assembly. An attempt to locate the default configuration file for the application, hwexe2.exe.config, failed. Therefore, Fusion.dll determined there is no custom configuration policy associated with binding to hwmod2. Since this wasn't present, the default location for hwmod2 is tried first. This is the same directory as the application with .dll appended to the assembly name. As shown in the log, hwmod2.dll is located successfully. The next step is for Fusion.dll to begin the initialization process for hwmod2.dll, as indicated by the last entry in the log.
FIGURE 16: Successful assembly binding log for hwexe2.exe and hwmod2.dll.
What happens when an assembly bind fails? To illustrate this, rename hwmod2.dll to hwmod2.junk so that when hwexe2.exe runs, it won't be able to locate hwmod2. Then run hwexe2.exe once again. When the bind failure occurs, the run time presents the Just-In Time Debugging dialog box. Just select No to close the dialog box. Next, refresh the FusLogVw window. The first thing you'll notice is that the mscorlib and hwmod2 log entries have updated Date/Time values, while the Microsoft.VisualBasic log entry still has the Date/Time from the previous run. This is because when hwexe2.exe is executed, the Microsoft.VisualBasic assembly wasn't loaded before the hwmod2 bind failed. Therefore, there's no need to bind with Microsoft.VisualBasic.
and view the bind log for hwexe2.exe and hwmod2 (see FIGURE 17). As indicated,
the bind operation failed because it could not locate the file. Focusing on the
last section of the log, notice that after the attempt to locate the custom
configuration policy file failed, the probing process begins by looking for
hwmod2.dll in the same directory as the application. The next location
attempted is hwmod2/hwmod2.dll off of the application's root directory. The
third and forth attempts to locate hwmod2 search for hwmod2.exe in the
application's root, followed by the hwmod2 directory off of the application's
root directory. Note that even though the
FIGURE 17: Failed assembly binding log for hwexe2.exe and hwmod2.dll.
From this simple example, you can see how the Assembly Binding Log Viewer tool can be helpful when determining what assemblies are being loaded, and the probing process used to locate the actual file to resolve the bind request.
The MSIL Disassembler tool was used in this article to dig into the manifest and metadata details of an assembly to determine how it was constructed at compile time. The Assembly Binding Log Viewer was also used to determine how an assembly was constructed at run time. Having both of these tools in your developer handbag and knowing when and how to use them will save countless hours when deploying and debugging your .NET applications.
To learn more about the assembly manifest and metadata, along with lots of other useful information about assemblies, refer to the Common Language Infrastructure (CLI) and the IL Assembly Language Programmer's Reference documents. Usually, these documents are found on your .NET Framework install drive at C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Tool Developers Guide\docs\StartDocs.htm. These documents, along with the tools used in this article, ship with the .NET Framework, and provide detailed explanations of all aspects of assemblies.
The files referenced in this article are available for download.
Jeff Niblack, chief information officer for Interactive Gaming & Wagering (http://www.interactive-gaming.com), has over 18 years experience in the IT field. Jeff has dealt with various aspects of application, network, and database design and development. In addition, Jeff has written articles for numerous online and print publications, and regularly speaks at technical conferences, including TechEd, Microsoft ASP.NET Connections, and ASP.NET and XML Web Services Solutions Conference, on a variety of .NET topics. You can reach Jeff at mailto:[email protected].
Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.