Units of Deployment, Execution, and Reuse


Once a program and its set of types are written and compiled, the resulting assembly is distributed for use directly by users (EXE) or indirectly by programs (DLL), which depend on its exported library types and functions. The CLR's logical unit of deployment, execution, and reuse in this case is, as noted above, called an assembly. An assembly contains one-to-many smaller independent physical units called modules. Modules are files that are logically part of their containing assembly. Modules can contain managed metadata and code but can also be ordinary files, such as localized resources, plain-text, or opaque binary, for example. The vast majority of managed applications employ single-file assemblies (those with one module), although the ability to create multi-file assemblies is a powerful (and underutilized) capability. Figure 4-1 demonstrates this general architecture at a high level.

image from book
Figure 4-1: An assembly with three modules—two PE files and one opaque resource file.

For example, many users will start with a C# or set of C# files, for example printxml.cs:

 using System; using System.Xml; class Program {     static void Main(string[] args)     {         XmlDocument xml = new XmlDocument();         xml.Load(args[0]);         XmlTextWriter writer = new XmlTextWriter(Console.Out);         writer.Formatting = Formatting.Indented;         xml.WriteTo(writer);     } } 

This example makes use of some types located in the .NET Framework's mscorlib.dll and System.Xml.dll assemblies. Thus, the developer must supply dependency information during compile time:

 csc.exe /out:printxml.exe /target:exe     /reference:c:\windows\microsoft.net\framework\v2.0.50725\system.xml.dll     printxml.cs 

Many Framework assemblies are automatically included for you by the C# compiler (e.g., mscorlib.dll), so some dependencies will be resolved without manually specifying reference locations. The above example has the effect of generating a printxml.exe assembly containing references the assembly System .Xml.dll; the runtime will resolve these references at runtime in order to run System.Xml.dll code. Printxml.exe contains the executable code — in the form of IL — listed above and contains a native bootstrapper to invoke the CLR at load time, which will JIT-compile Main and execute it. No types are exported for reuse because none are marked public.

Note

Most developers interact with their language's compiler through their IDE, that is, Visual Studio, in which case they must Add References to code dependencies. Visual Studio in turn uses the (new in 2.0) MSBuild infrastructure to invoke your compiler with the correct switches and information.

An assembly always has a single primary module that contains an assembly manifest specifying information such as its strong name, what other modules make up the assembly (if any), external code dependencies, public types that the assembly exports for use by other code, and a cryptographic hash of the assembly itself (to avoid the runtime loading a tampered or corrupted image).

This design allows for a single packaging mechanism regardless of your program's use (Web, client, reusable library, etc.) and is rich with metadata so that the execution engine can perform security verification (among other analyses) on your code while loading it into memory. This is a key difference between an assembly and an unmanaged, opaque DLL or EXE created outside of the CLR's type system: assemblies are entirely self-describing. Programs can use this self-describing metadata to examine the contents of the assembly, what libraries it depends on, and any security requirements it might demand, and can even walk the precise coding instructions it contains before it decides to, for example, execute its code. The CLR does just that whenever it loads and runs such managed code.

Most programs don't utilize the full capabilities of the modules and assemblies architecture depicted above. When you work with C#, VB, or C++/CLI, for example, unless you take explicit action (e.g., the /target:module switch in C#), you will be working with a single module assembly. Although options do exist to create individual modules and combine them — we take a quick look at the Assembly Linker (al.exe) tool later in this chapter — the standard tools in the Framework and SDK steer you toward single-file assemblies. One example of a common multi-file assembly mechanism can be seen when working with localized resources. The C# compiler permits you to embed resources inside your assembly, as discussed further later on.

We will also see later on the various methods you can use to distribute your assembly, including XCopy and ClickOnce deployment, in addition to sharing machine-wide assemblies with the GAC. For the time being, however, we'll start by taking a look at precisely what modules, assemblies, and manifests contain, and will go on to examine the way assemblies are located and loaded by the runtime.

Inside Assembly Metadata

As noted in the above paragraphs, assemblies utilize the standard PE Win32 code and object file format. Using this format enables transparent loading by the OS without any specific knowledge of the CLR. To the OS, a managed EXE contains only a small bit of code, called the boostrapper, which knows how to load the CLR and start execution. Moreover, IL is just text to Windows. This bootstrapper loads mscoree.dll and hands your code over to "the shim," which takes care of the rest. We discuss the EXE bootstrapping process later in this chapter.

Note

Although we don't cover it here, it is possible to crack open and parse PE files by hand. Going one step further, you can parse the CLR-specific sections, too. The Windows.h and .NET Framework SDK's CorHdr.h header files define a set of data structures to assist you in doing so. The CLI standard also describes semantic details for each component of the file format.

If an assembly is meant to be executed instead of being dynamically loaded (that is, it is an EXE rather than a DLL), the only discernable difference in the metadata is the presence or absence respectively of (1) a method annotated with an .entrypoint declaration, and (2) the CLR bootstrapping stub. If these exist, there can only be a single .entrypoint for the entire assembly, and it must reside in the assembly's primary module (the one with the manifest).

Aside from entrypoint information, assemblies contain the metadata for your program or library. This consists of a large number of definition sections, telling the CLR about things like the internal and external types, methods, fields, referenced libraries, strings, and blobs (large bits of data) contained in your code, and so forth. The logical contents of an assembly are summarized in Figure 4-2.

image from book
Figure 4-2: Physical structure of an assembly PE file.

A program's metadata is what fully describes all of the abstractions your code provides and makes use of. All of the XxxDefs shown above are simply logical representations of CTS abstractions. The runtime actually constructs the physical data structures for these from the assembly metadata at load time. XxxRefs are simply fully qualified references to abstractions defined elsewhere, used by the CLR to resolve dependent components. In IL, these are often represented by XxxSpecs , which are simple textual references to other information. For example, [mscorlib]System.String is the TypeSpec for the System.String class defined in mscorlib.dll.

Assembly Identity

An assembly's identity is like its unique key. It gets used when the CLR must resolve references to other assemblies, for example when loading dependencies.

The identity of an assembly based on its display name, which is generated from an assembly's name, version, culture, public key token, and (new in 2.0) its processor architecture. The other bits of metadata you can supply — for example, company, product name — have no bearing on identity at all. For example, mscorlib.dll's identity on 32-bit machines is:

 mscorlib, Version=2.0.0.0, Culture=neutral,     PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL 

If we were to take a static dependency on mscorlib.dll, for example, it would be recorded in our assembly manifest as follows:

 .assembly extern mscorlib {     .publickeytoken = (B7 7A 5C 56 19 34 E0 89)     .ver 2:0:0:0 } 

The runtime can then ensure when we run our program that the correct version (2.0.0.0) of mscorlib.dll is loaded and, using the key information, verify that the binary hasn't been tampered with. If our program required a specific culture or processor architecture, you would see that in the reference information too.

Assemblies are considered to have the same identity by most of the runtime system if their names are equivalent. The loader actually considers two assemblies in a process identity-equivalent if and only if the path that an assembly has been loaded from is the same. What this means is that multiple assemblies with the same "identity" (as defined above) can actually be loaded into the same AppDomain — and be considered not equal — if they are being loaded from separate paths. We discuss details around assembly loading later in this chapter.

Assembly names may be represented in managed code by the System.Reflection.AssemblyName type. For example, we could construct a fully qualified reference to mscorlib.dll as follows:

 AssemblyName mscorlib = new AssemblyName(); mscorlib.Name = "mscorlib"; mscorlib.Version = new Version(2, 0, 0, 0); mscorlib.CultureInfo = CultureInfo.InvariantCulture; mscorlib.SetPublicKeyToken(new byte[] {     0xb7, 0x7a, 0x5c, 056, 0x19, 0x34, 0xe0, 0x89 }); mscorlib.ProcessorArchitecture = ProcessorArchitecture.X86; 

You can then supply this to various APIs that deal with resolving or recording assembly information.

Manifests and Metadata

Let's take a quick look at what ildasm.exe says about manifests and metadata for a sample program. Consider the following simple snippet of code, which we've compiled down to program.exe using the C# compiler:

 using System; class Program {     static void Main(string[] args)     {         Console.WriteLine("Hello, World!");     } } 

If you were to run ildasm /metadata /out=program.il program.exe it would generate a dump of all of the IL the assembly contains. The /metadata switch tells it to create a table of the CLR data structures alongside the default manifest and IL output. Most of this information is derived from the IL and metadata, not stored verbatim in the assembly itself. This program's manifest (in text form) looks something like this:

 .assembly extern mscorlib {     .publickeytoken = (B7 7A 5C 56 19 34 E0 89)     .ver 2:0:0:0 } .assembly program {     .custom instance void          [mscorlib]System.Runtime.CompilerServices.          CompilationRelaxationsAttribute::.ctor(int32) =          ( 01 00 08 00 00 00 00 00 )     .hash algorithm 0x00008004     .ver 0:0:0:0 } .module program.exe // MVID: {} .imagebase 0x00400000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003       // WINDOWS_CUI .corflags 0x00000001    //  ILONLY // Image base: 0x04080000 

Notice that the manifest has an external reference to mscorlib.dll, binding specifically to version 2.0.0.0 and using the same public key we used above (b77a5c561934e089). It also has a section defining the program assembly's metadata, for which I've chosen not to generate a strong name, hence the lack of, for example, key and version information. Notice also that the C# compiler has stuck a Compilation RelaxationsAttribute on my assembly by default; many compilers do similar things, and this is (sometimes) subject to change over time.

Dumping the full contents of its metadata reveals the following information:

 =========================================================== ScopeName : program.exe MVID      : {} =========================================================== TypeDef #1 (02000002) ------------------------------------------------------- TypDefName: Program  (02000002)     Flags     : [NotPublic] [AutoLayout] [Class]                 [AnsiClass] [BeforeFieldInit]  (00100000)     Extends   : 01000001 [TypeRef] System.Object     Method #1 (06000001) [ENTRYPOINT] -------------------------------------------------------         MethodName: Main (06000001)         Flags     : [Private] [Static] [HideBySig] [ReuseSlot]  (00000091)         RVA       : 0x00002050         ImplFlags : [IL] [Managed]  (00000000)         CallCnvntn: [DEFAULT]         ReturnType: Void         1 Arguments             Argument #1:  SZArray String         1 Parameters             (1) ParamToken : (08000001) Name : args flags: [none] (00000000)     Method #2 (06000002)     -------------------------------------------------------         MethodName: .ctor (06000002)         Flags     : [Public] [HideBySig] [ReuseSlot] [SpecialName]                     [RTSpecialName] [.ctor]  (00001886)         RVA       : 0x0000205e         ImplFlags : [IL] [Managed]  (00000000)         CallCnvntn: [DEFAULT]         hasThis         ReturnType: Void         No arguments. TypeRef #1 (01000001) ------------------------------------------------------- Token:             0x01000001 ResolutionScope:   0x23000001 TypeRefName:       System.Object     MemberRef #1 (0a000003)     -------------------------------------------------------         Member: (0a000003) .ctor:         CallCnvntn: [DEFAULT]         hasThis         ReturnType: Void         No arguments. TypeRef #2 (01000002) ------------------------------------------------------- Token:             0x01000002 ResolutionScope:   0x23000001 TypeRefName:       System.Runtime.CompilerServices.                    CompilationRelaxationsAttribute     MemberRef #1 (0a000001)     -------------------------------------------------------         Member: (0a000001) .ctor:         CallCnvntn: [DEFAULT]         hasThis         ReturnType: Void         1 Arguments             Argument #1:  I4 TypeRef #3 (01000003) ------------------------------------------------------- Token:             0x01000003 ResolutionScope:   0x23000001 TypeRefName:       System.Console     MemberRef #1 (0a000002) -------------------------------------------------------         Member: (0a000002) WriteLine:         CallCnvntn: [DEFAULT]         ReturnType: Void         1 Arguments             Argument #1:  String Assembly -------------------------------------------------------     Token: 0x20000001     Name : program     Public Key :     Hash Algorithm : 0x00008004     Version: 0.0.0.0     Major Version: 0x00000000     Minor Version: 0x00000000     Build Number: 0x00000000     Revision Number: 0x00000000     Locale: <null>     Flags : [none] (00000000)     CustomAttribute #1 (0c000001)     -------------------------------------------------------         CustomAttribute Type: 0a000001         CustomAttributeName: System.Runtime.CompilerServices.             CompilationRelaxationsAttribute :: instance void .ctor(int32)         Length: 8         Value : 01 00 08 00 00 00 00 00         ctor args: (8) AssemblyRef #1 (23000001) -------------------------------------------------------     Token: 0x23000001     Public Key or Token: b7 7a 5c 56 19 34 e0 89     Name: mscorlib     Version: 2.0.0.0     Major Version: 0x00000002     Minor Version: 0x00000000     Build Number: 0x00000000     Revision Number: 0x00000000     Locale: <null>     HashValue Blob:     Flags: [none] (00000000) User Strings ------------------------------------------------------- 70000001 : (13) L" Hello, World!" 

Most of this information is self-evident. Our program defines a single type Program, represented by the sole TypeDef section in the text. Program contains two MethodDef sections, one for the entrypoint Main and the other for the default constructor (.ctor) that the C# compiler generated automatically. After the definition sections appears a set of TypeRef and MethodRef entries for each type and method used in the program, all of which are from mscorlib in this example. Toward the end, reference details about our strong binding to mscorlib are represented in an AssemblyRef.

Lastly, any strings used are pooled in the string table and assigned a token, in this case L" Hello, World!" (L is simply the Cstyle prefix for representing UTF-8 strings). Any ldstr instructions in our program will reference the string table via these tokens. For example, while you might have seen ldstr "Hello, World!" in your source IL, the actual binary IL is doing a ldstr 70000001. If you turn on the "Show token values" option under the View menu in ildasm.exe, you can see this very clearly.

Strong Naming

Assemblies always have at least a simple name, often just the primary module's filename (this is the default for most compilers, for example C# and VB). This name can be further augmented with version, culture, and a public key and digital signature, among other informational attributes. The specific combination mentioned makes up an assembly's strong name. Assemblies with a strong name can only depend on other strongly named assemblies. Furthermore, assigning a strong name to an assembly permits you to sign it and for other assemblies to depend securely on that version, taking advantage of the CLR's secure binding support to guarantee integrity at load time.

Note

In reality, if somebody were able to tamper with the contents of a file on your machine, you're probably already at risk. But this additional integrity check adds a layer of defense. It's a great defense-in-depth strategy, especially for code that gets distributed over a network, for example.

The benefits of strong naming and signing are not nearly as useful for application code as those distributed over the Internet or intranet, since in most circumstances EXEs will be launched via their filename using a shortcut that doesn't recognize what a strong name is. But for libraries or well-factored applications, strong-name signing can make sense. In fact, strong naming is required in order to share assemblies across an entire machine using the GAC. You can choose to forego individual parts of the process, for example by just assigning a name and version, which still affords many of the benefits — for example version-specific binding — without having to worry about key management. We discuss the GAC later on; for now, we will continue exploring strong names, in particular how to assign them.

You can pursue two general approaches to assigning a strong name: a postcompilation tool that modifies the assembly's metadata (AssemblyDef) to contain strong-naming information, or you can build it into your code using a set of attributes. The former is often preferable since strong naming is part of the packaging and deployment process; weaving this information into the actual program itself is often messy, especially given that it's typically an administrator or setup author who will need to generate the signing information. Usually assigning a strong name and signing are done at the same time, but as we'll see below this coupling isn't necessary.

Generating a Private/Public Key Pair

Before signing an assembly, you need a public and private key. The 128-byte public key is then stored in your assembly's manifest (PublicKey section) for all to access. The private key is then used to compute a cryptographic SHA hash of the manifest, which is then stored in the assembly for verification purposes. This ensures the loader will notice any tampering with the manifest. To guarantee integrity, clearly you must take great care to protect your private key used to generate the hash; otherwise, a malicious person could tamper with the contents and resign the file. One way to protect this information is to use a password-protected source control system.

To generate the keys, you can use the sn.exe tool, located in your .NET Framework SDK's bin directory. In two steps, you can generate a new public/private key pair file and extract the public key into its own file, for example:

 sn.exe -k MyKeyPair.snk sn.exe -p MyKeyPair.snk MyPublicKey.snk 

This first generates a new key-pair (public and private key) and stores it in a file mypair.snk. Executing sn.exe -p mypair.snk public.snk will take the contents of mypair.snk, extract the public key from it, and store it in public.snk. The original mypair.snk file should be secured. The sn.exe tool has additional capabilities beyond this. For details on its usage, please refer to the .NET Framework SDK documentation.

Signing Your Assembly

The easiest way to sign your code is to modify your project settings in Visual Studio so that signing is performed at compile time. Just go to the Signing tab in the Project Settings configuration section. There is a "Sign the assembly" checkbox and a textbox into which you can enter your public/private key filename. Alternatively, if you're using an automated build system like MSBuild or NAnt, or just compiling straight from the command line, you can pass the /keyfile:<keyfile> switch to the C# compiler. This signs the resulting assembly using the pair file specified.

For example, to compile a program.cs file and sign it with a key, this command would do the trick:

 csc.exe /out:program.exe /keyfile:MyKeyPair.snk program.cs 

If you prefer instead to couple your signing information with your code — which is advised against for reasons outlined above — you can use the assembly-level attribute System.Reflection.AssemblyKey FileAttribute. It takes a single argument: the filename of the key pair to use for signing. Most compilers — for example, C#, VB, C++/CLI — recognize this attribute and respond by performing the signing with the specified pair during compilation. For example, adding this line to your code (assuming that you've generated the key file as stated above) will do the trick:

 [assembly: System.Reflection.AssemblyKeyFileAttribute("MyKeyPair.snk")] class Program { /* ... */ } 

You can also specify other assembly-wide metadata to be stored in the output manifest. This is done by annotating your assembly with one or many of the various System.Reflection.Assembly XxxAttribute types. Those that impact an assembly's strong name identity are AssemblyCulture Attribute and AssemblyVersionAttribute. Many Visual Studio project templates will auto-generate a Properties\AssemblyInfo.cs file for you when you start a new project that contains these attributes auto-populated with some useful default values. You'll likely want to alter the defaults, but it's a good starting point.

Taking a look at a signed assembly's manifest reveals what the magic above actually does:

 .assembly Program {   .publickey = /* long sequence of hexadecimal values representing our key */   .hash algorithm 0x00008004   .ver 1:2:33:2355 } 

What is not shown here is the lengthy cryptographic hash that also gets stored in metadata. The hash is used along with the public key to verify integrity at load time.

Delay Signing

The above process requires both the public and private key to be available on the machine performing the build. Because of security concerns around distributing the private key (for example, on each developer's machine), a feature called delay signing permits you to do signing as a postbuild step. You still assign the public key to the assembly, but the actual generation of the cryptographic hash does not happen immediately.

To use delay signing, the above example would change to:

 [assembly: System.Reflection.AssemblyKeyFileAttribute("MyPublicKey.snk")] [assembly: System.Reflection.AssemblyDelaySign(true)] class Program { /* ... */ } 

Notice that the AssemblyKeyFileAttribute refers only to the public key — not the entire pair. It's safe to embed such information in your program. But as with ordinary signing, most compilers offer options to perform delay signing, too. For example, C# offers the /delaysign command-line switch. After compilation of a delay signed assembly, it won't be immediately usable. If you try, you'll get an exception that notes "Strong name validation failed. (Exception from HRESULT: 0x8013141A)."

To complete the signing process, you must run sn.exe again on the assembly:

 sn.exe -R program.exe MyKeyPair.snk 

The -R switch is used to complete signing and the full pair is supplied. Note that if you'd like to use the assembly for testing purposes on your local machine, you can disable signature verification on the assembly by running sn.exe -Vr on it.

Embedded and Linked Resources

Resources are any noncode, nonmetadata data that make up part of an assembly. The most common use in managed code is to encode localizable text. But you can also encode both nonlocalized and localized binary data (e.g., bitmaps, icons, and so forth). These resources are typically then loaded and used somehow by an application. We discuss resources, and the APIs in the Framework that assist in working with them, in Chapter 8. Although resources are not discussed until later in this book, discussing the mechanisms for packaging resources while we're already discussing assemblies makes sense.

There are two models for packaging resources available:

  • Resources are embedded inside a PE file, or

  • Resources live in separate modules (files) and are referenced in the assembly's manifest.

The first model is the default behavior that most project systems (e.g., Visual Studio) will use for resources. This is specified with the C# compiler's /embedresource switch. It makes distributing your resources simpler because they are embedded right inside your assembly itself. And it makes working with them more performant because the assembly's contents and the resources are mapped into memory simultaneously as part of the same PE file. Although, if you don't intend to use some (or all) of the resource content each time you load your assembly, you will have to pay the performance penalty of loading and holding all of that data in memory.

The second model is appropriate either when you have large data, for example so that you don't bloat your assembly, or when your resource usage is volatile. That is, the resources that your program accesses vary greatly each time it executes. This is specified with the C# compiler's /linkresource switch. Chapter 8 discusses how loading linked resources differs from loading embedded resources.

Sometimes a mixture of models will be used. For instance, when localizing you might want to package your default language resources embedded in the PE file (to make using them fast) and package your alternative languages as linked files on disk. That way, your default language users pay very little for loading the default language, and the less common case of an alternative language user pays a slightly higher cost to load the linked resources.

Shared Assemblies (Global Assembly Cache)

The Global Assembly Cache (GAC) is a shared repository for machine-wide assemblies. The GAC stores its contents in a well-known directory and enables applications to share a set of common assemblies without duplication. This practice also enables machine-wide policy to redirect binding requests to alternative versions. Applications may also load and bind to assemblies using their strong names without worrying about precise location.

The GAC is useful under several circumstances. Using strong-name binding in combination with the GAC ensures that applications on a machine are utilizing a common set of bits. This is important for a number of reasons:

  • You can store multiple versions of the same assembly on a machine and execute them side by side. They are differentiated by their version number in the strong name.

  • Storing the same assemblies in multiple locations on a machine uses additional unneeded storage. Keeping them in one location reduces this cost, and enables you to NGen one image per assembly to be stored centrally. NGen is discussed in detail later in this chapter.

  • Servicing assemblies on a machine becomes simpler because you only have to update one location (the GAC) rather than searching for multiple instances of an assembly stored on a machine.

The GAC can be useful, but is not a panacea. For shared libraries shared among a number of applications on a single machine, it is an appropriate solution. But registering application assemblies, for example, is usually not worth the trouble.

Managing the GAC (gacutil.exe)

The gacutil.exe tool is used to interact with the GAC, providing facilities to inspect and/or manipulate GAC contents without having to understand the intricate details of how files get laid out on the disk. A complete description of this tool's capabilities is beyond the scope of this book. Please refer to the .NET Framework SDK documentation for details.

Friend Assemblies

An assembly A can make another assembly B its friend, meaning that the normal rules of visibility are changed such that B can use A's internal types and members. This can be useful to avoid marking types public, for example, just so they can be used by another assembly which is part of your same application. This avoids exporting a large set of types that were never meant for (or documented as being) generally consumable APIs.

For example, say you have two assemblies, MyApp.exe and MyLib.dll. If you wanted to use your library types in your application, this would typically require that you mark them public. However, using friend assemblies, you can mark them internal and achieve the same thing. For example, MyLib.dll might do something like this:

 using System; using System.Runtime.CompilerServices; [assembly:InternalsVisibleTo("MyApp")] namespace MyCompany {     internal class Foo     {         internal static void Bar() { /*...*/ }     } } 

Notice the System.Runtime.CompilerServices.InternalsVisibleToAttribute assembly-level attribute in this library. If we then compiled our application MyApp.exe with references to (say) MyCompany .Foo.Bar, the program would be permitted to see and use the internals.

Referencing the Friend Assembly

Notice that InternalsVisibleToAttribute takes a string representing the name of the assembly to which you wish to expose your internals. This is used by the compiler to make the decision whether or not to allow another assembly to see your internals, which happens after your assembly has been generated and at the time that the friend assembly is being compiled. Any assembly that matches the string name you supplied will be permitted to see such internals. If you specify only the assembly name in the friend declaration — as I show above — anybody with a matching assembly name could come along and use your internals.

So, clearly friend declarations are spoofable. Remember: visibility is not a security mechanism. You can, however, specify other information such as the public key of the friend assembly. In fact, you can specify any other components of the strong name except for version information. Version is disallowed because it could easily lead to versioning nightmares; since you are declaring that another assembly is dependent on yours, it's difficult to know up front that the dependent's version number will not change over time. And furthermore, strongly named assemblies may only expose their internals to other strongly named assemblies.




Professional. NET Framework 2.0
Professional .NET Framework 2.0 (Programmer to Programmer)
ISBN: 0764571354
EAN: 2147483647
Year: N/A
Pages: 116
Authors: Joe Duffy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net