《CLR via C#》读书笔记 第2章 生成、打包、部署和管
首先了解几个命令:
-
路径:%SystemRoot%\Microsoft.NET\Framework(64)\version
- csc.exe 通过在命令提示符处键入 C# 编译器的可执行文件名称 (csc.exe ),可调用该编译器。
-
路径:%ProgramFiles%\Microsoft SDKs\Windows\version\bin
- ildasm.exe IL反汇编程序,它是 IL 汇编程序 (ilasm.exe) 的配套工具。 ildasm.exe可利用包含中间语言(IL)代码的可移植可执行(PE)文件,并创建适合输入到ilasm.exe的文本文件(*.il文件)。
-
al.exe
程序集链接器从一个或多个文件(这些文件可以是模块或资源文件)生成一个具有程序集清单的文件。 模块是不含程序集清单的中间语言 (IL) 文件。(*只能打包没有程序集清单的文件)
将类型生成到模块中(csc应用)
- 创建Progress.cs文件
public sealed class Program { public static void Main() { System.Console.WriteLine("Hi"); } }
- 执行csc命令,生成可执行文件
csc.exe /out:Progress.exe /t:exe /r:MSCorLib.dll Progress.cs
至此C#编译器生成Progress.exe文件,它是标准PE(可移植执行体,Portable Executable)文件。
解释一下几个参数:(更多指令含义查看csc -help)
- /out:<文件> 指定输出文件夹(默认值:包含主类文件或第一个文件的基名称)
- /t(arget):exe生成控制台可执行文件(默认)
- /r(eference):MSCorLib 从指定程序集文件引用元数据
响应文件:是包含一组编译器命令行开关的文本文件,编译器打开响应文件,并使用其中包含的所有开发,感觉就是这些开关直接在命令行上传递给CSC.exe。使用步骤如下:
- 创建响应文件 MyProject.rsp
/out:MyProject.exe /target:exe
- 执行命令行,生成可执行文件
csc.exe @MyProject.rsp Progress.cs
.NET Framework安装时会在%SystemRoot%\Microsoft.NET\Framework(64)\version下安装默认全局CSC.rsp文件。由于全局CSC.rsp引用列出了所有程序集,所以我们不用使用/reference开关显示引用这些程序集。
元数据概述
托管PE文件由4部分构成:PE32(+)头、CLR头、元数据、以及IL
-
PE32(+)头:Windows要求的标准信息
-
CLR头:小的信息块,是需要CLR的模块(托管模块)特有的,这个头包含模块生成时面向CLR的major(主)和minor(次)版本号;一些标志(flag);一个MethodDef token,该token指定了模块的入口方法;一个可选的强名称数字签名,最后还包含模块内部的一些元数据表的大小和偏移。
-
元数据:由几个表构成的二进制数据块。有三种表,分别是定义表(definition table)、引用表(reference table)和清单表(manifest table)。
定义表
在编译器编译源代码时,代码中定义任何东西都可能导致在上面列出的某个表中创建一条记录,而编译器还会检测代码所引用的类型、字段、方法等,并创建相应的元数据引用记录,下面总结了常用的引用元数据表。
引用表
引用表续 元数据清单表
首先看一下定义表和引用表,查看PE文件中的元数据使用到了ILDasm.exe(IL反汇编器),打开ILDasm.exe、将上一节生成的可执行文件拖进去。要查看元数据可以使用“视图”|“元信息”|“显示”菜单项(或者Ctrl+M)下面摘抄部分信息,从下面的信息我们可以看到TypeDef中由我们定义的Progress以及两个方法Main、.ctor。而TypeRef中由我们引用的模块比如System.Object等
```
===========================================================
ScopeName : Progress.exe
MVID : {E0D3E63C-F15E-4D4B-A808-A932764E9CC9}
===========================================================
Global functions
-------------------------------------------------------
Global fields
-------------------------------------------------------
Global MemberRefs
-------------------------------------------------------
TypeDef #1 (02000002)
-------------------------------------------------------
TypDefName: Progress (02000002)
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] [BeforeFieldInit] (00100101)
Extends : 01000001 [TypeRef] System.Object
Method #1 (06000001) [ENTRYPOINT]
-------------------------------------------------------
MethodName: Main (06000001)
Flags : [Public] [Static] [HideBySig] [ReuseSlot] (00000096)
RVA : 0x00002050
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void
No arguments.
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 (0a000004)
-------------------------------------------------------
Member: (0a000004) .ctor:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.
(...省略部分信息...)
Assembly
-------------------------------------------------------
Token: 0x20000001
Name : Progress
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)
CustomAttribute #2 (0c000002)
-------------------------------------------------------
CustomAttribute Type: 0a000002
CustomAttributeName: System.Runtime.CompilerServices.RuntimeCompatibilityAttribute :: instance void .ctor()
Length: 30
Value : 01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78 > T WrapNonEx<
: 63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01 >ceptionThrows <
ctor args: ()
AssemblyRef #1 (23000001)
-------------------------------------------------------
Token: 0x23000001
Public Key or Token: b7 7a 5c 56 19 34 e0 89
Name: mscorlib
Version: 4.0.0.0
Major Version: 0x00000004
Minor Version: 0x00000000
Build Number: 0x00000000
Revision Number: 0x00000000
Locale: <null>
HashValue Blob:
Flags: [none] (00000000)
User Strings
-------------------------------------------------------
70000001 : ( 2) L"Hi"
Coff symbol name overhead: 0
===========================================================
===========================================================
===========================================================
```
我们还可以通过“视图”|“统计”看到程序集的统计信息。
File size : 3584
PE header size : 512 (496 used) (14.29%)
PE additional info : 1423 (39.70%)
Num.of PE sections : 3
CLR header size : 72 ( 2.01%)
CLR meta-data size : 608 (16.96%)
CLR additional info : 0 ( 0.00%)
CLR method headers : 2 ( 0.06%)
Managed code : 20 ( 0.56%)
Data : 2048 (57.14%)
Unaccounted : -1101 (-30.72%)
Num.of PE sections : 3
.text - 1024
.rsrc - 1536
.reloc - 512
CLR meta-data size : 608
Module - 1 (10 bytes)
TypeDef - 2 (28 bytes) 0 interfaces, 0 explicit layout
TypeRef - 4 (24 bytes)
MethodDef - 2 (28 bytes) 0 abstract, 0 native, 2 bodies
MemberRef - 4 (24 bytes)
CustomAttribute- 2 (12 bytes)
Assembly - 1 (22 bytes)
AssemblyRef - 1 (20 bytes)
Strings - 178 bytes
Blobs - 68 bytes
UserStrings - 8 bytes
Guids - 16 bytes
Uncategorized - 170 bytes
CLR method headers : 2
Num.of method bodies - 2
Num.of fat headers - 0
Num.of tiny headers - 2
Managed code : 20
Ave method size - 10
从上面可以看出文件大小以及文件各部分的大小比重。可以看到PE头和元数据占用了相当大的比重,而IL代码只占用了区区20字节。
将模块合并成程序集(重点)
程序集相关定义及解释
上一节的Progress.exe并非只是含由元数据的PE文件,它还是程序集(assembly)。程序集是一个或多个类型定义文件及资源的集合。在程序集的所有文件中,由一个文件容纳了清单。清单也是一个源数据表集合,表中主要包含程序集组成部分的文件名称。此外还描述了程序集的版本、语言文件、发布者、公开到处类型以及构成程序集的所有文件。
CLR操作的是程序集。换言之,CLR总是首先加载包含“清单”元数据表的文件,然后再根据“清单”来获取程序集中其他文件的名称。程序集的特点:
- 程序集定义了可重用的类型
- 程序集用一个版本号标记
- 程序集可以关联安全信息
类型为了顺利打包、版本控制、安全保护以及使用,必须放在程序集一部分的模块中。程序集可由多个文件构成:一些是含有元数据的PE文件、还有资源文件,为了便于理解,可将程序集是为一个逻辑EXE或DLL(作为库文件使用)。
为什么要引入“程序集”?可重用类型的逻辑表示于物理表示可以分来,例如程序集中可能包含多个类型,可以将常用类型放在一个文件,不常用的放在一个文件,对于不常用的文件,等到使用的是由再去下载部署它,不使用就永远不会下载该模块。
为什么要使用多程序集?
- 不同类型用不同文件
- 可在程序集中添加资源或数据文件(源代码资源分开)
- 不同程序集可用不同编程语言来实现
清单元数据表的应用
生成程序集要么选择现有的PE文件作为“清单”的宿主,要么创建单独的PE文件并只在其中包含清单。
使用/t[arget]:module开关,这个开发指示编译器生成一个不包含清单元数据的PE文件。这样生成的肯定是一个DLL PE文件。CLR要想访问其中的任何类型必须将其添加到一个程序集中。
选择现有的PE文件作为“清单”生成程序集
- 首先生成两个源代码文件PUT.cs(包含不常用类型),FUT.cs包含常用类型
- 编译PUT.cs到一个不含清单的模块,RUT.netmodule
csc /t:module RUT.cs
- 编译FUT.cs,将其作为程序集清单的宿主,并
将刚刚生成的模块添加到清单元数据表
csc /out:MultiFileLibrary.dll /t:library /addmodule:RUT.netmodule FUT.cs
image.png
上述命令执行完成后将生成MultiFileLibrary.dll库文件(客户端可用/r:MultiFileLibrary来引用程序集的类型),我们可以用ILDasm分别打开这两个文件并查看元数据,可以发现只有MultiFileLibrary.dll中含有元数据清单,并且包含了RUT.netmodule的所有公开导出类型。
(...省略定义和引用...)
Assembly
-------------------------------------------------------
Token: 0x20000001
Name : MultiFileLibrary
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.RuntimeCompatibilityAttribute :: instance void .ctor()
Length: 30
Value : 01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78 > T WrapNonEx<
: 63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01 >ceptionThrows <
ctor args: ()
CustomAttribute #2 (0c000002)
-------------------------------------------------------
CustomAttribute Type: 0a000002
CustomAttributeName: System.Runtime.CompilerServices.CompilationRelaxationsAttribute :: instance void .ctor(int32)
Length: 8
Value : 01 00 08 00 00 00 00 00 > <
ctor args: (8)
File #1 (26000001)
-------------------------------------------------------
Token: 0x26000001
Name : RUT.netmodule
HashValue Blob : 81 f1 75 f0 0b d6 29 b3 1f 30 52 a5 57 78 38 63 06 24 e4 02
Flags : [ContainsMetaData] (00000000)
ExportedType #1 (27000001)
-------------------------------------------------------
Token: 0x27000001
Name: RUT
Implementation token: 0x26000001
TypeDef token: 0x02000002
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] [BeforeFieldInit] (00100101)
客户端代码方法首次调用时:
- CLR检测作为参数、返回值或者局部变量而被方法引用的类型。
- CLR尝试加载所引用程序集中含有清单的文件。
- 如果要访问的类型恰好在这个文件中,CLR会执行内部登记允许使用该类型。
- 如果清单指出被引用的类型在不同文件中,CLR会尝试加载需要的文件,同样执行内部登记。
CLR并非一开始就加载所有可能用到的程序集。只有在调用的方法确实引用到了未加载程序集中类型时,才会加载程序集。
创建单独的PE文件只包含清单生成程序集
还是上一节用到的两个类型,RUT.cs、FUT.cs。并使用al命令。
csc /t:module RUT.cs
csc /t:module FUT.cs
al /out:MutilFileLibrary.dll /t:library FUT.netmodule RUT.netmodule
image.png
同时我们也可以使用ILDasm看到生成的out:MutilFileLibrary是只含有清单文件的。
image.png
我们也可以使用/t:exe生成一个可执行文件,但是需要指定程序入口/main
csc /t:module /r:MultiFileLibrary.dll Program.cs
al /out:Program.exe /t:exe /main:Program.Main Program.netmodule
image.png
可以看到生成的可执行文件里生成了一个全局函数__EntryPoint,并包含了以下代码(双击):
.method privatescope static void __EntryPoint$PST06000001() cil managed
{
.entrypoint
// 代码大小 8 (0x8)
.maxstack 8
IL_0000: tail.
IL_0002: call void [.module progress.dll]Program::Main()
IL_0007: ret
} // end of method 'Global Functions'::__EntryPoint
可以看到实际上是调用了Program的Main方法。(其他方法其实也是可以的,只是指定一个入口函数而已)。
本章的重点概念主要是程序集。如果生成程序集,为什么要使用多文件程序集,如果生成dll并引用等等。