CLR via C# - CLR基础

Posted by     "Jordon Li" on Friday, April 24, 2020

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

托管应用程序启动运行步骤:

  1. Windows 检查exe文件头
  2. 创建x86或x64进程
  3. 在进程地址空间加载对应版本的MSCorEE.dll
  4. 进程的主线程调用MSCorEE.dll中的_CorExeMain/_CorDllMain方法初始化CLR
  5. 加载exe程序集
  6. 调用Main方法
  7. 托管应用程序启动并运行

4. 执行程序集的代码

  • 第一次执行:

    • CLR为托管模块(元数据+IL)引用的所有类型,生成内部数据结构
      • 内部数据结构中包含当前引用类的每个方法的entry,entry的地址指向方法的实现
      • 对内部数据结构初始化,将entry指向包含在CLR中的未编档函数(JITCompiler
    • JITCompiler从元数据中查找被调用方法的IL
    • 验证IL,动态分配内存块,编译成本机cpu指令,保存到内存块中
    • 修改entry指向保存本机cpu指令的内存地址
    • 执行本机cpu指令,返回到Main方法
  • 再次执行:

  • 直接执行内存块中的本机cpu代码,返回到Main方法

  • IL和验证(verification)

    • IL是基于栈的:它的所有指令都要将操作数压入(push)执行栈,并从栈弹出(pop)结果。
    • IL指令:是无类型的(typeless)。
    • IL优势:
      • 对底层CPU的抽象
      • 应用程序的健壮性和安全性
    • 验证:将IL编译成本机CPU指令时,CLR会执行verification过程,确定IL代码所做的一切都是安全的。
    • AppDomain:CLR提供了在一个操作系统的进程中执行多个托管应用程序的能力。每个托管应用程序都在一个AppDomain中执行。
  • 不安全的代码

    C#编译器默认生成安全(safe)代码,但也允许开发人员写不安全(unsafe)代码。不安全代码的所有方法使用unsafe关键字标记,并使用/unsafe编译器开关来编译源码。

    • 不安全代码:允许直接操作内存地址和地址处的字节。
      • 用途:
        • 与非托管代码进行互操作
        • 提高对效率要求极高的算法的性能
      • 风险:
        • 可能破坏数据结构
        • 危害安全性
        • 造成新的安全漏洞
    • JIT编译unsafe方法:
      • 检查程序集是否被授予System.Security.Permissions.SecurityPermission权限
      • System.Security.Permissions.SecurityPermissionFlagSkipeVerification标志是否设置。

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操作程序集:

  1. 首先加载包含“清单”元数据表的文件
  2. 根据”清单“获取程序集中其他文件的名称

使用多文件程序集的优点:

  • 不同的类型用不同的文件,使文件能以”增量“模式下载,对应用程序进行部分或分批打包/部署。
  • 可以在程序集中添加资源或数据文件,使其成为程序集的一部分。
  • 程序集包含的各个类型可以用不同的编程语言来实现。

清单元数据表:

清单元数据表名称 说明
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查找程序集顺序:

  1. 工作目录
  2. CSC.exe所在目录,目录中包含CLR的各种DLL文件
  3. 使用/lib编译器开关指定的任何目录
  4. 使用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时才应创建发布者策略程序集。执行应用程序的全新安装不应安装发布者策略程序集。

「下次一定」

下次一定

使用微信扫描二维码完成支付