TOC
CLR via C# - CLR基础
CLR的执行模型
1. 将源代码编译成托管模块
托管代码(Managed Code
) 非托管代码(Native Code
)
-
CLR(Common Language Runtime)
:公共语言运行时,可由多种编程语言使用的运行时
。可用任何编程语言
开发代码,只要编译器
是面向CLR的。 -
编译器(
Code Compilers
)作用:检查语法 + 分析源代码 + 生成托管模块
。 -
托管模块(
Managed Module
):PE32(+)可移植执行体
(Protable Executable)。-
系统安全性:数据执行保护 + 地址空间布局随机化。
-
组成:PE32(+)头 + CLR头 + 元数据(Metadata) + IL(Intermediate Language)代码。
组成部分 说明 PE32(+)头 1. PE32格式:文件可以在win32/64上执行。PE32+格式:文件只能在win64上执行。2. 标识文件类型:GUI/CUI/Dll… 3. 时间标记:标记文件生成时间。4.*本机CPU代码有关信息 CLR头 1. 需要的CLR的major(主)和minor(次)版本。 2. 标志(flag)。 3. Mian方法的MethodDef token。 4. 强名称数字签名 5. 元数据表的大小和偏移量 元数据(Metadata) 元数据表:1. 定义表(definition table):定义的类型和成员表 2. 引用表(reference table):引用的类型和成员表 3. 清单表(manifest table): IL(Intermediate Language)代码/托管代码(Managed Code) 1. IL: 与CPU无关的、面向对象的机器语言。 2. 源码
–编译器–>(托管模块
>IL代码
)程序集 –CLR–>本机CPU指令
。
-
2. 将托管模块合并成程序集
- 程序集(Assembly):
- 一个或多个
模块/资源文件
的逻辑性分组
。 - 重用、安全性以及版本控制的
最小单元
。 - 含有
清单(manifest)
的托管模块。- 清单:元数据表的集合。构成程序的文件 + 文件中的public类 + 关联的资源或数据文件。
- 自描述(self-describing):包含与引用的程序集有关的信息。
- 一个或多个
3. 加载CLR
托管应用程序启动运行步骤:
- Windows 检查exe文件头
- 创建x86或x64进程
- 在进程地址空间加载对应版本的
MSCorEE.dll
- 进程的主线程调用
MSCorEE.dll
中的_CorExeMain/_CorDllMain
方法初始化CLR - 加载exe程序集
- 调用Main方法
- 托管应用程序启动并运行
4. 执行程序集的代码
-
第一次执行:
- CLR为托管模块(元数据+IL)引用的所有类型,生成内部数据结构
- 内部数据结构中包含当前引用类的每个方法的entry,entry的地址指向方法的实现
- 对内部数据结构初始化,将entry指向包含在CLR中的未编档函数(
JITCompiler
)
JITCompiler
从元数据中查找被调用方法的IL- 验证IL,动态分配内存块,编译成本机cpu指令,保存到内存块中
- 修改entry指向保存本机cpu指令的内存地址
- 执行本机cpu指令,返回到Main方法
- CLR为托管模块(元数据+IL)引用的所有类型,生成内部数据结构
-
再次执行:
-
直接执行内存块中的本机cpu代码,返回到Main方法
-
IL和验证(verification)
- IL是基于栈的:它的所有
指令
都要将操作数
压入(push)执行栈,并从栈弹出(pop)结果。 - IL指令:是无类型的(typeless)。
- IL优势:
- 对底层CPU的抽象
- 应用程序的健壮性和安全性
- 验证:将IL编译成本机CPU指令时,CLR会执行verification过程,确定IL代码所做的一切都是安全的。
AppDomain
:CLR提供了在一个操作系统的进程
中执行多个托管应用程序
的能力。每个托管应用程序都在一个AppDomain
中执行。
- IL是基于栈的:它的所有
-
不安全的代码
C#编译器默认生成
安全(safe)代码
,但也允许开发人员写不安全(unsafe)代码
。不安全代码的所有方法使用unsafe
关键字标记,并使用/unsafe
编译器开关来编译源码。- 不安全代码:允许直接操作内存地址和地址处的字节。
- 用途:
- 与非托管代码进行互操作
- 提高对效率要求极高的算法的性能
- 风险:
- 可能破坏数据结构
- 危害安全性
- 造成新的安全漏洞
- 用途:
- JIT编译unsafe方法:
- 检查程序集是否被授予
System.Security.Permissions.SecurityPermission
权限 System.Security.Permissions.SecurityPermissionFlag
的SkipeVerification
标志是否设置。
- 检查程序集是否被授予
- 不安全代码:允许直接操作内存地址和地址处的字节。
5. 本机代码生成器:NGen.exe
NGen.exe可以在应用程序安装到用户计算机时,将IL代码编译成本机代码,并保存到单独的文件中。
- 优点:
- 提高应用程序启动速度:本机代码不需要再编译。
- 减小应用程序的工作集(working set):一个应用程序被加载到多个进程中时,NGen.exe生成的文件可以通过
内存映射
的方式,同时映射到多个进程的地址空间中,实现代码共享,避免每个进程都需要一份单独的代码拷贝。
- 问题:
- 没有知识产权保护:
- 运行程序时,CLR需要访问程序集的元数据(用于反射和序列化等),发布的包中需要包含IL和元数据的程序集。
- NGen生成的文件不可用时,CLR会自动对程序集的IL代码进行JIT编译,IL代码必须处于可用状态。
- NGen生成的文件可能失去同步:CLR加载NGen生成的文件时,会将预编译代码的特征与当前执行环境进行比较,特征不匹配,NGen生成的五年间将无法使用。需要使用正常的JIT编译器进程。
- 较差的执行时性能:NGen无法像JIT编译器对执行环境进行很多假定,会生成较差的代码。
- 没有知识产权保护:
大型客户端应用程序分析工具:MPGO.exe:将与执行代码有关的信息写入profile并嵌入程序集文件中,NGen利用这些profile数据更好地优化本机映像。
6. Framework类库
Framework Class Library:一组DLL程序集地统称。
应用程序类型:
- Web service:
- ASP.NET XML Web Service
- Windows Communication Foundation(WCF)
- Web Forms / MVC application: 基于HTML
- Windows GUI application:可以直接与底层操作系统交换信息
- Windows Store
- Windows Presentation Foundation(WPF)
- Windows Forms
- Windows Console application
- Windows service
- Database StoreProcedure
- Class Library
7.通用类型系统
CLR一切都是围绕类型(Type)展开的,类型时CLR地根本。通用类型系统(Common Type System, CTS)
: 制定了一个正式的规范来描述类型的定义和行为。
CTS类型规则:
- 字段(Field):
- 作为对象状态一部分的数据变量
- 类型+名称
- 方法(Method):
- 针对对象执行操作的函数,通常会改变对象状态
- 名称+签名+修饰符
- 签名:参数数量(顺序)、参数类型、返回值、返回值类型
- 属性(Property):
- 允许再访问值之前,校验输入参数和对象状态
- 允许创建只读或只写的“字段”
- 事件(Event):
- 对象和其他相关对象的通知机制
CTS类型可见性/成员访问规则:
private
:成员只能由同一个类(class)类型(type)中的其他成员访问family
:成员可由派生类(子类)访问(C#protected
)family and assembly
: 成员可由同一个程序集中的派生类访问assembly
: 成员可由同一程序集中的任何类型访问 (C#internal
)family or assembly
:成员可由任何程序集中的派生类和同一程序集中的任何类型访问(C#protected intenal
)public
: 成员可由任何程序集中的任何类型访问
CTS规则:
- 所有类型最终必须从预定义的
System.Object
类型继承:保证了每个类型实例都有一组最基本的行为
IL:CLR的“语言”
代码的语言和代码的行为:使用不同编程语言写出行为完全一致的类型。
8. 公共语言规范
语言集成:CLR使用标准类型集、元数据(自描述的类型信息)、公共执行环境。
Common Language Specification(CLS)
:定义了所有语言都必须支持的最小功能集。任何编译器只有支持这个功能集,生成的类型才能兼容由其他符合CLS
、面向CLR
的语言生成组件。
CLR中类型的每个成员要么是字段(数据),要么是方法(行为)。
9. 与非托管代码的互操作
CLR允许在应用程序中同时包含托管和非托管代码。
CLR支持的互操作:
- 托管代码能调用DLL中的非托管代码:托管代码通过P/Invoke(Platform Invoke)机制调用DLL中的函数。
- 托管代码可以使用现有COM组件(服务器):使用非托管COM组件的类型库,可创建一个托管程序集来描述COM组件。
- 非托管代码可以使用托管类型(服务器)
Windows Runtime(WinRT)API:内部通过COM组件实现,但不是使用类型库文件,而是使用元数据标准描述API。
生成、打包、部署和管理应用程序及类型
1. .NET Framework 部署目标
Windows 不稳定和过于复杂的问题:
-
所有应用程序都使用来自Microsoft或其他厂商的
动态链接库(Dynamic-Link Library, DLL)
,DLL hell问题 -
安装的复杂性
-
安全性
2. 将类型生成到模块中
源代码文件 –csc.exe–> 可部署文件
/nostdlib
: 设置C#编译器不自动引用MSCorLib.dll
/t[arget]:exe
: 控制台用户界面(Console User Interface, CUI)/t[arget]:winexe
: 图形用户界面(Graphical User Interface, GUI)/t[arget]:appcontainerexe
: Windows Store应用
响应文件:包含一组编译器命令行开关的文本文件。不必每次编译项目时都手动指定命令行参数。
/noconfig
: 设置编译器忽略本地和全局CSC.rsp文件
3. 元数据概述
托管PE(Protable Executable)文件(托管模块):PE32(+)头
+ CLR头
+ 元数据
+ IL
。
元数据:由定义表(difinition table)
、引用表(reference table)
和清单表(manifest table)
组成的二进制数据块。
-
常用元数据定义表
元数据定义表名称 说明 ModuleDef 对模块进行标识的记录项:模块文件名+扩展名+版本ID TypeDef 模块定义的类型的记录项:类型名称+基类型+标志(public、private…)+索引(指向MethodDef中该类型的方法、FieldDef中该类型的字段、PropertyDef中该类型的属性、EventDef中该类型的事件) MethodDef 模块定义的方法的记录项:方法名称+标志(flag)+签名+方法的IL代码在模块中的偏移量。每个记录项引用ParamDef中的记录项:与方法参数有关的信息 FieldDef 模块定义的字段的记录项:标志(flag)+类型+名称 ParamDef 模块定义的参数的记录项:标志(flag)+类型+名称 PropertyDef 模块定义的属性的记录项:标志(flag)+类型+名称 EventDef 模块定义的事件的记录项:标志(flag)+名称 -
常用元数据引用表
元数据引用表名称 说明 AssemblyRef 模块引用的程序集的记录项:程序集名称(不含路径和扩展名)+版本号+语言文化(culture)+公钥token+标志(flag)+hash值 ModuleRef 引用类型的PE模块的记录项:模块文件名+扩展名(不含路径) TypeRef 模块引用类型的记录项:类型名称+引用(指向类型的位置) MemberRef 模块引用成员的记录项:成员(字段、方法、属性方法、事件方法)名称+签名
4. 将模块合并成程序集
程序集(Assembly):
- 一个或多个
类型定义文件
及资源文件
的集合。 - 一个或多个模块文件和资源文件组成的
逻辑单元
,其中有且只有一个后缀为.exe或.dll的主模块文件。.exe/.dll + .netmodule。 - 程序集是进行重用、版本控制和应用安全性设置的基本单元。它允许将类型和资源文件划分到单独的文件中。
程序集的文件:包含清单(manifest)元数据表
的文件+其他文件。清单元数据表使其成为了程序集。
- 清单:元数据表集合。清单使程序集具有了自描述性(self-describing)包含:
- 程序集所有文件
- 版本
- 语言文化
- 发布者
- public的类型
程序集(中包含清单元数据表的文件)特点:
- 程序集定义了可重用类型
- 程序集用一个版本号标记
- 程序集可以关联安全信息
程序集作用:将可重用类型的逻辑表示
和物理表示
分开。
CLR操作程序集:
- 首先加载包含“清单”元数据表的文件
- 根据”清单“获取程序集中其他文件的名称
使用多文件程序集的优点:
- 不同的类型用不同的文件,使文件能以”增量“模式下载,对应用程序进行部分或分批打包/部署。
- 可以在程序集中添加资源或数据文件,使其成为程序集的一部分。
- 程序集包含的各个类型可以用不同的编程语言来实现。
清单元数据表:
清单元数据表名称 | 说明 |
---|---|
AssemblyDef | 模块标识的程序集的单一记录项。程序集名称(不含路径和扩展名)+版本(major,minor,build,revision)+语言文化(culture)+标志(flag)+hash算法+发布者公钥(可为null)。 |
FileDef | 程序集PE文件和资源文件的记录项(清单本身所在文件除外,该文件记录在AssemblyDef的单一记录项中)。文件名+扩展名(不含路径)+hash值+标志(flags)。 |
ManifestResourceDef | 程序集资源记录项。资源名称+标志+FileDef的索引(指出资源/流包含在哪个文件中)。PE文件的流:不是独立的资源文件(.jpg/.gif…)。嵌入资源,额外包含偏移量:指出资源流在PE文件中的起始位置。 |
ExportedTypeDef | 程序集的PE模块中导出的public类型的记录项。类型名称+FileDef的索引(指出类型由那个文件实现)+TypeDef的引用 |
CLR并非一上来就加载所有可能用到的程序集,只有在调用的方法确实引用了未加载的程序集中的类型时,才会加载程序集。
附属程序集(satellite assembly):只包含资源的程序集,通常用于本地化。
程序集连接器:AL.exe
C#编译器:CSC.exe
5. 程序集版本资源信息
程序集中会嵌入标准的Win32版本资源。使用定制特性(Attribute)设置版本资源字段,特性应用于assembly级别(AssemblyInfo.cs)。
版本号格式:
- 主版本号(major)
- 次版本号(minor)
- 内部版本号(build)
- 修订号(revision)
程序集包含的三个版本号:
AssemblyFileVersion
:存储在Win32版本资源中。仅供参考,CLR既不检查,也不关心。AssemblyInformationalVersion
: 存储在Win32版本资源中。仅供参考,CLR既不检查,也不关心。AssemblyVersion
:存储在AssemblyDef清单元数据表中。CLR在绑定到强命名程序集时会用到。唯一的标识了程序集。
6. 语言文化
语言文化(culture):作为程序集身份标识的一部分,用包含主副标记(en-US…)的字符串进行标识。
语言文化中性(culture neutral):未指定具体语言文化的程序集。
附属程序集(satellite assembly):标记了语言文化的程序集。
7. 简单应用程序部署(私有部署的程序集)
私有部署的程序集(privately deployed assembly):在应用程序基目录或子目录部署的程序集。程序集文件不和其他任何应用程序共享,因为每个程序集用元数据注明了自己引用的程序集,不需要注册表设置。
8. 简单管理控制(配置)
为了实现对应用程序的管理控制,可在应用程序目录放入一个配置文件,CLR会解析文件内容来更改程序集文件的定位和加载策略。
XML配置文件名称:
- 可执行应用程序(EXE):配置文件必须在应用程序的基目录。EXE文件全名+.config。
- ASP.NET Web窗体应用程序:文件必须在Web应用程序的虚拟根目录中,Web.config。子目录可以有自己的Web.config,配置设置会得到继承。
Machine.config:机器上运行 的所有应用程序的默认设置。
共享程序集和强命名程序集
1. 两种程序集,两种部署
程序集种类 | 可以私有部署 | 可以全局部署 |
---|---|---|
弱命名程序集(weakly named assembly) | True | False |
强命名程序集(strongly named assembly) | True | True |
-
弱命名和强命名程序集结构完全相同,生成工具也相同。
-
强命名程序集使用发布者的公钥/私钥进行了签名。这一对密钥允许对程序集进行唯一性的标识、保护和版本控制。
-
私有部署的程序集:部署到应用程序基目录或者某个子目录的程序集。
-
全局部署的程序集:部署到一些公认位置的程序集。
2. 为程序集分配强名称
DLL hell
:两个相同文件名的程序集复制到相同的公认目录,造成正在使用就程序集的所有应用程序都无法正常工作(Windows共享DLL全部复制到System32目录)。
强命名程序集特性:
- 文件名(不计扩展名)
- 版本号(Version)
- 语言文化(Culture)
- 公钥(PublicKey)
- 公钥标记(PublicKeyToken):对公钥进行hash处理,获取hash值的最后8个字节。
加密技术:不仅能检查应用程序的二进制完整性,还允许每个发布者授予不同的权限。
- 标准的公钥/私钥加密
- GUID(Globally Unique Identifier,全局唯一标识符)
- URL(Uniform Resource Locator,统一资源定位符)
- URN(Uniform Resource Name,统一资源名称)
- …
只能对含清单的程序集文件进行签名,使用私钥对程序集进行签名,并将公钥嵌入清单。
对文件进行签名
:生成强命名程序集时,程序集的FileDef清单元数据表列出构成程序集的所有文件。每将一个文件添加到清单,都对文件内容进行hash处理。hash值和文件名一道存储到FileDef表中。
对程序集进行签名:
- 生成包含清单的PE文件后,会对PE文件完整内容进行hash处理
- hash值用发布者的私钥进行签名,得到RSA数字签名
- 将RSA存储到PE文件的一个保留区域
- PE文件的CLR头进行更新,反应数字签名在文件中的嵌入位置
- 发布者的公钥也嵌入PE文件的AssemblyDef清单元数据表中,AssemblyDef的记录项总是存储完整公钥,而不是公钥标记,以防文件被篡改
CLR在做出安全或信任决策时,永远不会使用公钥标记,因为几个公钥在hash处理后可能得到相同的公钥标记。
3. 全集程序集缓存
全局程序集缓存(Global Assembly Cache, GAC)
: 由多个应用程序访问的程序集必须放到公认的目录,而且CLR在检测到对该程序集的引用时,必须知道检查该目录,这个公认位置就是GAC。
GAC目录时结构化的:其中包含许多子目录,子目录的名称用算法生成。
不能将弱命名程序集放到GAC。
在GAC中进行全局部署是对程序集进行注册的一种形式。
4. 在生成的程序集中引用强命名程序集
只指定文件名(不含路径),CSC.exe查找程序集顺序:
- 工作目录
- CSC.exe所在目录,目录中包含CLR的各种DLL文件
- 使用/lib编译器开关指定的任何目录
- 使用LIB环境变量指定的任何目录
安装.NET Framework时,安装的Microsoft的程序集的两套拷贝:
- 一套安装在编译器/CLR目录(这些程序集只包含元数据,没IL代码,编译时不需要IL代码) - 为了方便在
编译时
生成程序集 - 一套安装在GAC的子目录(程序集同时包含元数据和IL代码) - 为了方便在
运行时
加载
5. 强命名程序集能防篡改
用私钥对程序集 进行签名,并将公钥和签名嵌入程序集,CLR就可验证程序集未被修改或破坏。
- 强命名程序集安装到GAC时:
- 系统对
包含清单的文件
的内容进行hash处理,将hash值与PE文件中嵌入的RSA数字签名(用公钥解除签名)进行比较。 - 系统还对
其他文件
的内容进行hash处理,并将hash值与清单文件的FileDef表中存储的hash值进行比较。 - 这个检查只在安装时执行一次。
- 系统对
- 从非GAC目录加载强命名程序集(应用程序基目录或配置文件codeBase元素指定的目录):
- CLR会在程序集加载后比较hash值。
- 每次应用程序执行并加载程序集时,都会对文件进行hash处理。
6. 延迟签名
延迟签名(delayed signing)/部分签名(partial signing)
:允许只使用公钥生成程序集,暂时不用私钥。
7. 私有部署强命名程序集
GAC中安装程序集的优势:
- GAC使多个应用程序共享,减少总体的物理内存消耗。
- 将新版本部署到GAC,让所有应用程序都通过发布者策略使用新版本。
- GAC实现对程序集多个版本的并行管理。
GAC中安装程序集的缺点:
- GAC通常受到严密保护,只有管理员才能在其中安装程序集。
- 违反“简单复制部署”的基本目标。
只有由多个应用程序共享的程序集才应该部署到GAC,不用共享的应私有部署。私有部署达成了“简单复制部署”目标,能更好的隔离应用程序和程序集。
8. CLR如何解析类型引用
解析引用类型时,CLR可能找到引用类型的地方:
- 相同文件中:
- 编译时就能发现对相同文件中的类型的访问,类型直接从文件加载
- 执行继续
- 早期绑定(early binding):编译时就能发现对相同文件中的类型的访问。
- 晚期绑定(late binding):在运行时通过反射机制绑定到类型并调用方法
- 不同文件,相同程序集:
- CLR确保被引用的文件在当前程序集元数据的FileDef表中
- 检查加载程序集清单的目录
- 加载被引用的文件
- 检查hash值以确保文件完整性
- 发现类型成员,执行继续
- 不同文件,不同程序集:
- CLR加载被引用程序集的清单文件
- 类型不再该青清单文件中,继续加载包含了类型的文件
- 发现类型成员,执行继续
CLR程序集标识:名称+版本+语言文化+公钥
GAC程序集标识:名称+版本+语言文化+公钥+CPU架构
9. 高级管理控制(配置)
系统允许使用和元数据所记录的不完全匹配的程序集版本。
<?xml version="1.0"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="" />
<dependentAssembly>
<assemblyIdentity name="" publicKeyToken="" culture="" />
<bindingRedirect oldVersion="oldVersion" newVersion="newVersion" />
<codeBase version="" href="" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="" publicKeyToken="" culture="" />
<bindingRedirect oldVersion="version1-version2" newVersion="newVersion" />
<publisherPolicy apply="no/yes" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
probing
:CLR定位弱命名程序集时,先从基目录下中查找,如果没有找到,从privatePath(为相对于应用程序的基目录的子目录)目录查找assemblyIdentity
:程序集公钥标记、名称、语言文化信息bindingRedirect
:定位到oldVersion版本的程序集时,改为定位同一程序集的newVersion版本codeBase
:查找程序集时,尝试从href的URL处发现它publisherPolicy
:应用或者忽略发布者策略文件- 发布者策略:发布者创建策略信息,新程序安装到用户机器上时,安装此策略信息。
发布者策略控制
- 发布者可以创建包含发布者策略配置文件的程序集,发布者策略程序集必须安装到GAC。
- 只有部署程序集更新或Service Pack时才应创建发布者策略程序集。执行应用程序的全新安装不应安装发布者策略程序集。
「下次一定」
下次一定
使用微信扫描二维码完成支付