.NET CLR 运行原理

原文: Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objectshtml

文章讨论了:程序员

    SystemDomain, SharedDomain和Default Domain算法

    对象布局和其余的内存细节chrome

    方法表布局编程

    方法分派bootstrap

 

文章使用的技术:数组

    .NET Framework网络

    C#数据结构

 

由于公共语言运行时(CLR)即将成为在Windows上建立应用程序的主角级基础架构, 多掌握点关于CLR的深度认识会帮助你构建高效的, 工业级健壮的应用程序. 在这篇文章中, 咱们会浏览,调查CLR的内在本质, 包括对象实例布局, 方法表的布局, 方法分派, 基于接口的分派, 和各类各样的数据结构.架构

 

咱们会使用由C#写成的很是简单的代码示例, 因此任何对编程语言的隐式引用都是以C#语言为目标的. 讨论的一些数据结构和算法会在Microsoft? .NET Framework 2.0中改变, 可是绝大多数的概念是不会变的. 咱们会使用Visual Studio? .NET 2003 Debugger和debugger extension Son of Strike (SOS)来窥视一些数据结构. SOS可以理解CLR内部的数据结构, 可以dump出有用的信息. 通篇, 咱们会讨论在Shared Source CLI(SSCLI)中拥有相关实现的类, 你能够从msdn.microsoft.com/net/sscli下载到它们. 图表1 会帮助你在搜索一些结构的时候到SSCLI中的信息.

图表1 SSCLI索引

Item SSCLI Path
AppDomain \sscli\clr\src\vm\appdomain.hpp
AppDomainStringLiteralMap \sscli\clr\src\vm\stringliteralmap.h
BaseDomain \sscli\clr\src\vm\appdomain.hpp
ClassLoader \sscli\clr\src\vm\clsload.hpp
EEClass \sscli\clr\src\vm\class.h
FieldDescs \sscli\clr\src\vm\field.h
GCHeap \sscli\clr\src\vm\gc.h
GlobalStringLiteralMap \sscli\clr\src\vm\stringliteralmap.h
HandleTable \sscli\clr\src\vm\handletable.h
InterfaceVTableMapMgr \sscli\clr\src\vm\appdomain.hpp
Large Object Heap \sscli\clr\src\vm\gc.h
LayoutKind \sscli\clr\src\bcl\system\runtime\interopservices\layoutkind.cs
LoaderHeaps \sscli\clr\src\inc\utilcode.h
MethodDescs \sscli\clr\src\vm\method.hpp
MethodTables \sscli\clr\src\vm\class.h
OBJECTREF \sscli\clr\src\vm\typehandle.h
SecurityContext \sscli\clr\src\vm\security.h
SecurityDescriptor \sscli\clr\src\vm\security.h
SharedDomain \sscli\clr\src\vm\appdomain.hpp
StructLayoutAttribute \sscli\clr\src\bcl\system\runtime\interopservices\attributes.cs
SyncTableEntry \sscli\clr\src\vm\syncblk.h
System namespace \sscli\clr\src\bcl\system
SystemDomain \sscli\clr\src\vm\appdomain.hpp
TypeHandle \sscli\clr\src\vm\typehandle.h

 

在咱们开始前请注意, 这篇文章提供的信息仅适用于在x86平台架构下的.NET Framework 1.1(有可能多数信息对于Shared Source CLI 1.0中, 一些互操做情形下的多数值得注意的异常来讲, 也仍是正确的). 对于.NET Framework 2.0来讲, 不少信息可能会改变, 因此不要建立依赖于这些内部结构不会改变的软件.

 

CLR辅助程序建立的域

=================

在CLR执行第一行托管代码以前, 它先建立三个应用程序域. 其中的两个是从托管代码中产生的, 是透明的, 甚至对于CLR宿主来讲都是不可见的. 这两个domain只能经过CLR bootstrap进程建立出来, 这个进程受助于两个垫板做用同样的dll文件, mscoree.dll和mscorwks.dll(当是多处理器系统的时候, 为mscorsvr.dll). 在图表2中, 你能够看到System Domain和Shared Domain, 这两个都是Singleton的(只用惟一一个实例). 第三个域是default app domain, 它是一个AppDomain类的实例, 也是惟一命名的domain. 对于简单的CLR宿主程序, 比方说控制台程序, default domain的名字是由可执行镜像的名字组成的. 其余的域能够经过在托管代码中使用AppDomain.CreateDomain方法, 或者在非托管宿主代码中经过调用ICORRuntimeHost接口, 来建立. 相似ASP.NET这样的复杂的宿主, 基于Web Site的数目来建立多个域.

图表2 CLR辅助程序建立的域

2009-11-12 8-45-03

 

系统域-System Domain

===================

系统域负责建立和初始化Shared Domain和default appdomain. 它加载系统库mscorlib.dll到Shared Domain中. 它还显式或隐式的保持着进程范围的字符串的字面值.

 

保存字符串的字面值(string interning)在.NET Framework 1.1中是一项有点点笨拙的优化特性, 由于CLR并不给assemblies机会来选择是否使用它. 不论如何, 它在全部的应用程序域范围内, 提供给定字符串值的惟一实例(相同值的字符串在内存中只有一份).

 

系统域还负责生成进程范围的接口ID, 这些接口ID被用来在每个AppDomain中建立InterfaceVtableMaps. 系统域记录并监控着进程中的全部域, 并实现了加载和卸载AppDomain的功能.

 

共享域-Shared Domain

===================

全部的域-中立的代码都被加载到shared domain中.

  • Mscorlib, 这个系统库, 是被全部的appdomain中的用户代码使用和须要的, 它会被自动的加载到SharedDomain中. 像Object, ValueType, Array, Enum, String, 还有Delegate之类的System命名空间中的基础类型, 都会在CLR辅助程序进程(CLR bootstrapping process)中, 被预先加载到SharedDomain里. 
  • 用户代码(user code)也能够被加载到该域中, 方法是经过在调用CorBindToRuntimeEx方法时, 指定LoaderOptimization属性. LoaderOptimization属性是由CLR宿主应用程序指定的.
  • 控制台程序能够给应用程序的main方法编写属性来加载代码到SharedDomain中, 这个属性是System.LoaderOptimizationAttribute.

共享域还管理由基地址(the base address)索引的assembly map, assembly map的功能相似于一种查找表, 这个查找表用于明确被加载到Default Domain中的assembly和在其它应用程序域的托管代码中建立的assembly的共享依赖关系.

默认域(Default Domain)是非共享的用户代码加载的地方.

 

默认域-DefaultDomain

===================

默认域是一个AppDomain的实例, 典型地, 应用程序代码在这个域中执行.

 

当一些应用程序须要在运行时建立额外的appdomain的时候(好比拥有插件式架构的应用程序, 或者是正在生成至关大量的运行时代码的应用程序), 多数的应用程序会在他们的生命期中建立一个这样的一个域: 全部执行在这个域中的代码都是在域层次上进行了上下文绑定的. 

若是一个应用程序有多个appdomain, 那么任何跨domain的访问都要经过.NET Remoting proxies(.net远程代理).

额外的domain内的上下文边界能够经过继承自System.ContextBoundObject的类型来建立.

每个AppDomain都有本身的SecurityDescriptor, SecurityContext和DefaultContext, 一样的, 还有本身的加载者堆(高频堆, 低频堆, 和Stub堆), 句柄表(句柄表, 大对象堆句柄表), 接口虚表映射管理器(Interface Vtable Map Manager), 和Assembly Cache.

 

加载者堆-LoaderHeaps

====================

加载者堆是用来加载各类各样的CLR runtime artifacts【译注:artifact这里能够理解为一种structure】和优化artifacts的, 这些artifacts在域的生命期中都存在.

这些堆按照能够预见的大小的块来增加, 从而最小化内存碎片.

加载者堆与GC堆(在对称的多处理器状况下, 是多重堆-multiple heap) 不一样, 不一样之处在于GC堆保存对象实例,而加载者堆保存的是整个类型系统.

常常访问到的artifact好比MethodTables, MeghodDescs, FieldDescs和InterfaceMaps, 都在高频堆上分配, 而不那么常常访问的数据结构好比说EEClass和ClassLoader还有ClassLoader的查找表, 在低频堆(LowFrequencyHeap)上分配.

StubHeap保存着不少stub,  stub能够帮助代码访问security (CAS), COM wrapper calls和P/Invoke.

 

简单在高层次上过了一遍各类域和加载者堆以后, 咱们如今来看一下以一个简单应用程序为上下文背景的, 这些结构的物理细节. 见图表3. 咱们将程序的执行中断在了"mc.Method1();", 而且使用SOS debugger extension的DumpDomain命令dump出了域的信息,  这里是编辑过的输出结果:

---------------------------------------------------

!DumpDomain
System Domain: 793e9d58, LowFrequencyHeap: 793e9dbc, 
HighFrequencyHeap: 793e9e14, StubHeap: 793e9e6c,
Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40

Shared Domain: 793eb278, LowFrequencyHeap: 793eb2dc,
HighFrequencyHeap: 793eb334, StubHeap: 793eb38c,
Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40

Domain 1: 149100, LowFrequencyHeap: 00149164,
HighFrequencyHeap: 001491bc, StubHeap: 00149214,
Name: Sample1.exe, Assembly: 00164938 [Sample1],
ClassLoader: 00164a78

图表3 Sample1.exe

using System;

public interface MyInterface1
{
    void Method1();
    void Method2();
}
public interface MyInterface2
{
    void Method2();
    void Method3();
}

class MyClass : MyInterface1, MyInterface2
{
    public static string str = "MyString";
    public static uint ui = 0xAAAAAAAA;
    public void Method1() { Console.WriteLine("Method1"); }
    public void Method2() { Console.WriteLine("Method2"); }
    public virtual void Method3() { Console.WriteLine("Method3"); }
}

class Program
{
    static void Main()
    {
        MyClass mc = new MyClass();
        MyInterface1 mi1 = mc;
        MyInterface2 mi2 = mc;

        int i = MyClass.str.Length;
        uint j = MyClass.ui;

        mc.Method1();
        mi1.Method1();
        mi1.Method2();
        mi2.Method2();
        mi2.Method3();
        mc.Method3();
    }
}

 

咱们的控制台程序, Sample1.exe, 被加载到名为"Sample1.exe"的AppDomain中.

Mscorlib.dll被加载到SharedDomain中, 可是它仍是被列在SystemDomain中, 由于他是核心的系统库.

每一个域中都分配了本身的高频堆,低频堆,和stub堆. 系统域和共享域使用一样的ClassLoader, 而Default AppDomain使用的是它本身的ClassLoader.

 

输出结果中并无显示出加载者堆保存的尺寸和已经committed的尺寸. 高频堆初始保留尺寸是32KB, committed的尺寸是4KB. 低频堆和Stub堆初始保留尺寸是8KB, committed的尺寸是4KB.

在SOS输出中一样没有显示出来的是InterfaceVtableMap堆. 每一个域都有一个InterfaceVtableMap堆(后面再用的时候就简写为IVMap), 在域初始化阶段它,被建立在本身的加载者堆上. IVMap堆初始保留大小为4KB, 初始committed的大小是4KB. 咱们将在接下来的部分中,探索类型布局的时候,讨论IVMap的重要性.

 

图表2 展现了默认的进程堆, JIT代码堆, GC堆(针对小对象的), 和大对象堆(针对大于等于85000字节的对象的), 经过他们来讲明了:这些堆和加载者堆在语义上的不一样.

just-in-time(JIT)编译器生成x86指令, 而且把它们存储在JIT代码堆上.

GC堆和大对象堆都是垃圾收集堆, 托管对象是在这些堆上实例化出来的.

 

 

类型基础- Type Fundamentals

========================

类型是在.NET编程中的基础单位. 在C#中, 一个类型使用关键字class, struct,和interface来声明. 多数的类型是显示的由程序员来建立的, 然而, 在特殊的互操做情形下, 或者在远程对象激活(.NET remoting)场景中, .NET CLR隐式的生成一些类型. 这些生成的类型包括COM和运行时可调用的包装器, 还有透明的代理.(COM and Runtime Callable Wrappers and Transparent Proxies).

 

咱们接下来探索一下类型基础, 从包含一个对象引用的栈开始.(典型地, 栈是一个对象实例开始他的生命期的位置.) 代码在图表4中, 其中包括一个简单的程序, 有调用静态方法的控制台入口点. Method1建立了一个类型为SmallClass的的实例, SmallClass中包括一个字节数组, 咱们经过这个数组来demo在大对象堆上的对象实例的建立. 代码的实用价值不高, 可是足够为咱们的讨论服务了.

图表 4 大对象和小对象-Large Objects and Small Objects

using System;

class SmallClass
{
    private byte[] _largeObj;
    public SmallClass(int size)
    {
        _largeObj = new byte[size];
        _largeObj[0] = 0xAA;
        _largeObj[1] = 0xBB;
        _largeObj[2] = 0xCC;
    }

    public byte[] LargeObj
    {
        get { return this._largeObj; }
    }
}

class SimpleProgram
{
    static void Main(string[] args)
    {
        SmallClass smallObj = SimpleProgram.Create(84930, 10, 15, 20, 25);
        return;
    }

    static SmallClass Create(int size1, int size2, int size3,
        int size4, int size5)
    {
        int objSize = size1 + size2 + size3 + size4 + size5;
        SmallClass smallObj = new SmallClass(objSize);
        return smallObj;
    }
}

 

图表5 显示了断点在Create方法中的"return smallObj;"语句的栈的一个快照(snapshot), 这是一个典型的fastcall的栈框架. (Fastcall是.NET的一种调用约定, 在这种调用约定下, 传递给函数的参数在可能的状况下会经过寄存器来传递, 其余的参数从右至左的压入栈中供函数调用, 函数调用结束后, 由函数自身将栈中的参数清除.)

值类型变量objSize存储在栈框架以内.

相似smallObj的引用类型以一个固定的大小(4字节的双字), 存储在栈中, 双字的内容是在普通GC堆上的对象实例的地址.

在传统C++中, 这是一个对象指针; 在托管世界中, 这是一个对象引用. 不论如何, 它包含对象实例的地址. 咱们将会对存储在对象引用的地址中的数据结构使用术语ObjectInstance.

 

图表5 简单程序的栈框架和堆-SimpleProgram Stack Frame and Heaps

2009-11-12 12-52-09

 

smallObj对象的实例(object instance), 存储在普通GC堆上, 其中包含一个字节数组, 叫作_largeObj, 这个字节数组的大小是85000字节(注意, 图中显示的是85016字节, 这是真实存储的空间大小.)

CLR对待大小>=85000字节的对象, 跟对待比这小的对象的方式不一样. 大对象分配在Large Object Heap(LOH)中, 而小对象是建立在普通GC堆上的. 由于普通GC堆对于对象的分配和垃圾收集是有优化的(因此适合存储小对象的效率高). 大对象堆是没有压缩的(夯实的), 而GC堆在GC垃圾收集发生的时候是压缩的. 更重要的是, 大对象堆(LOH)仅在彻底垃圾回收的时候才被释放(LOH is only collected on full GC collections).

 

smallObj的ObjectInstance包含TypeHandle(类型句柄), TypeHandle指向相关连的类型的MethodTable.

任何一个声明了的类型都仅有一个MethodTable, 而且全部一样类型的对象的实例都指向同一份MethodTable.

MethodTable包含

        关于这种类型的信息(属于哪个? interface, abstract class, concrete class, COM Wrapper仍是Proxy).

        实现了的接口数量

        为了方法分配而设立的接口映射表(interface map for the method dispatch)

        方法表中的槽的数量(方法表中方法的数量)(number of slots in the method table)

        一张尽是指向方法的实现的槽的表格

 

一个由MethodTable指向的重要的数据结构, 是EEClass. 在MethodTable展开以前, CLR类加载器(class loader)从元数据(Metadata)中建立出EEClass. 在图表4中, SmallClass的MethodTable指向它的EEClass. 这些结构指向他们的模块和assembly.

MethodTable和EEClass典型地分配在具体域的加载者堆上. 字节数组(Byte[])是一个特例. 方法表MethodTable和EEClass分配在共享域中的加载者堆上.

加载者堆是appdomain-specific的, 任何这里提到的数据结构(MethodTable和EEClass)一旦加载起来就不会被移除, 除非它的AppDomain被卸载掉.

一样, 默认的appdomain也不能被卸载掉, 所以代码直到CLR关闭都还存在着.

 

 

对象实例-ObjectInstance

====================

正如咱们提到的, 全部的值类型要么以inline(内联)地存储在线程栈中, 要么内联地存储在GC堆当中. 全部的引用类型都是在GC堆上或者大对象堆建立的. 图表6 显示了一个典型的对象实例的布局.

一个对象能够被如下的结构引用:

1. 基于栈的局部变量;

2. interop或者P/Invoke情形下的句柄表;

3. 寄存器(寄存器中的内容是:执行方法时的this指针或方法参数)

4. 服务于拥有finalizer方法的对象的finalizer queue.

OBJECTREF并不指向Object Instance的首地址, 而是指向一个以DWORD(4个字节)为单位的一个偏移量.

这个DWORD的偏移量叫作Object Header, 而且拥有一个指向SyncTableEntry表的索引值(a 1-based syncblk number). 经过索引的链锁效应, CLR在须要增加内存尺寸的状况下, 能够在内存中自由的移动SyncTableEntry表.

SyncTableEntry中保存着一个指回对象的weak reference, 这样CLR就能够追踪到SyncBlock的全部权(属于哪一个对象). Weak Reference可让GC在没有其余强引用的状况下, 收集到这个对象.

SyncTableEntry中还存着一个指向SyncBlock的指针, SyncBlock中保存着有用的信息, 可是这些信息不多被全部的对象实例使用到. 这些信息包括对象锁(object's lock), 它的Hash Code, 一些转换数据(thunking data), 和它的AppDomain index.

对多数的对象实例来讲, 他们当中没有为SyncBlock分配的存储空间, syncblk number是0. 然而,当线程执行遇到例如lock(obj), 或者obj.GetHashCode的时候, 就不一样了. 就像下面的代码同样:

SmallClass obj = new SmallClass()
// Do some work here
lock(obj) { /* Do some synchronized work here */ }
obj.GetHashCode();

 

图表6 对象实例布局-Object Instance Layout

2009-11-12 13-46-57

 

在这段代码中, smallObj会使用0(没有syncblk)作为它起始时的syncblk number. 那句lock语句引起了CLR建立一个syncblk entry的动做, 并用相应的数值来更新对象的object header. 因为C#的lock关键字能够展开为一个try-finally块, 用来使用Monitor类, 因此Monitor对象在为同步化(synchronization)而准备的syncblk中建立出来. 对GetHashCode方法的调用把对象的hash code填入到syncblk中.

 

SyncBlock中还有些其余的数据域, 它们有的用在COM的interop上, 有的用在针对非托管代码的marshaling delegate上. 可是这些数据域跟典型的对象使用无关.

 

TypeHandle的位置是紧跟着ObjectInstance中的syncblk number的. 为了保持连续性, 我会在详细阐述变量实例以后, 讨论TypeHandle.

在TypeHandle以后紧跟着一个实例的变量列表域. 默认状况下, 这个实例域会按照能让内存高效使用的方式来压缩, 或者按照能让内存读取高效的对齐来作最小程度的填充. 图表7中的代码显示了一个SimpleClass, 该class拥有不少包含不一样大小的变量的实例.

 

图表7 拥有实例变量的SimpleClass- SimpleClass with Instance Variables

class SimpleClass
{
    private byte b1 = 1;                // 1 byte
    private byte b2 = 2;                // 1 byte
    private byte b3 = 3;                // 1 byte
    private byte b4 = 4;                // 1 byte
    private char c1 = 'A';              // 2 bytes
    private char c2 = 'B';              // 2 bytes
    private short s1 = 11;              // 2 bytes
    private short s2 = 12;              // 2 bytes
    private int i1 = 21;                // 4 bytes
    private long l1 = 31;               // 8 bytes
    private string str = "MyString"; // 4 bytes (only OBJECTREF)

    //Total instance variable size = 28 bytes 

    static void Main()
    {
        SimpleClass simpleObj = new SimpleClass();
        return;
    }
}

 

图表8 显示出了SimpleClas对象实例在Visual Studio Debugger内存窗口中的一个例子. 咱们在图表7的return语句上下断点, 而后用在寄存器ECX中存储的simpleObj的地址来在内存窗口中显示对象的实例. 头4个字节的块就是syncblk number. 由于咱们以前没有在任何synchronizing的代码中使用这个实例, 它被设置为0. 以变量形式存在栈中的的对象引用, 指向偏移量为4的四个字节. 字节变量b1, b2, b3和b4都被一个挨着一个的排放着. 两个short型的变量被放在一块儿. 字符串型的变量str是一个4字节的OBJECTREF, 指向GC堆中字符串实际存在的地址. 字符串是一种特殊的类型, 在assembly加载的进程中, 它们的全部包含着相同内容的实例, 都会被指向相同的在全局字符串表中的那一份惟一实例. 这个进程叫作string interning, 是用来优化内存的使用的.

如同咱们以前提到的, 在.NET Framework1.1中, 一个assbmbly不可能从这个interning process中退出(opt out of), 尽管将来的CLR版本可能会修改这种能力.

 

图表8 调试器内存窗口中的object instance-  Debugger Memory Window for Object Instance

2009-11-12 14-42-55

 

 

因此, 默认状况下, 在源代码中声明的成员变量的字面顺序, 在内存中并不会被保留下来. 在Interop的场景下, 变量的字面顺序必须被正向的依次放到内存中, StructLayoutAttribute属性能够用来完成这个设定, 该属性接受LayoutKind枚举类型的变量做为参数. LayoutKind.Sequential会为marshaled的数据设定字面的顺序, 尽管在.NET Framework 1.1中,这个设定还不会对托管布局生效.(.NET Framework 2.0就会了). 在interop场景下, 你实在须要额外的填充(padding)和显式的对于数据域顺序的控制时,  LayoutKind.Explicit能够和FieldOffset这个修饰符结合起来在field level帮助您达到目的.

 

看过了原始内存的内容, 让咱们用SOS来看一下对象实例吧. 一个有用的命令是DumpHeap, 它能够列出针对某一类型的全部堆中的内容, 还有这一类型的全部实例.  不依赖寄存器, DumpHeap命令能够show出咱们建立的惟一实例的地址.

---------------------------------------------------
!DumpHeap -type SimpleClass
Loaded Son of Strike data table version 5 from 
"C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\mscorwks.dll"
 Address       MT     Size
00a8197c 00955124       36
Last good object: 00a819a0
total 1 objects
Statistics:
      MT    Count TotalSize Class Name
  955124        1        36 SimpleClass

 

整个对象的大小是36字节. 不论字符串多大, SimpleClass的instance中只包含一个DWORD OBJECTREF. SimpleClass的实例变量只占28个字节. 剩下的八个字节是由TypeHandle(4字节), 和syncblk number(4字节)组成的.

找到了simpleObj实例的地址后, 让咱们用DumpObj命令来dump出这个实例吧, 以下:

---------------------------------------------------
!DumpObj 0x00a8197c
Name: SimpleClass
MethodTable 0x00955124
EEClass 0x02ca33b0
Size 36(0x24) bytes
FieldDesc*: 00955064
      MT    Field   Offset                 Type       Attr    Value Name
00955124  400000a        4         System.Int64   instance      31 l1
00955124  400000b        c                CLASS   instance 00a819a0 str
    << some fields omitted from the display for brevity >>
00955124  4000003       1e          System.Byte   instance        3 b3
00955124  4000004       1f          System.Byte   instance        4 b4

 

如上所述, 由C#编译器生成的的默认布局是LayoutType.Auto. 对于结构体来讲是LayoutType.Sequential. 因为class loader从新安排了实例的数据域, 因此填充(padding)的部分达到了最小化. 咱们能够用ObjSize命令来dump出实例占用空间的图示. 这里是输出结果:

---------------------------------------------------
!ObjSize 0x00a8197c
sizeof(00a8197c) =       72 (    0x48) bytes (SimpleClass)

 

 

******************************************************************************************************

Son of Strike

在本文中, SOS调试器扩展时用来展示CLR数据结构内容的.

它是.NET Framework安装程序的一部分, 位置在%windir%\Microsoft.NET\Framework\v1.1.4322.

在你加载SOS到你的进程以前, 在Visual Studio .NET的工程属性里选择容许托管代码调试.(enable managed debugging)

添加SOS.dll所在的文件夹到环境变量中. 要在断点时, 加载SOS.dll, 打开Debug | Windows | Immediate. 在Immediate窗口中,

执行.load sos.dll命令. 用!help命令来获得关于debugger 命令的帮助. 更多关于SOS的信息, 参见the June 2004Bugslayer column

******************************************************************************************************

 

 

若是你从object graph的大小(72字节)减去SimpleClass实例的大小(36字节), 你会获得变量str的长度(36字节). 让咱们经过dump出这个字符串实例来确认一下吧. 输出结果以下:

---------------------------------------------------
!DumpObj 0x00a819a0
Name: System.String
MethodTable 0x009742d8
EEClass 0x02c4c6c4
Size 36(0x24) bytes

 

若是你把字符串的长度(36字节)加上SimpleClass实例的大小(36字节), 你就获得了对象的整个大小(72字节), 正与前面ObjSize命令的结果相同.

 

注意, ObjSize方法并不包括由syncblk架构占用的内存. 在.NET Framework 1.1中, CLR并不了解被非托管资源占据的内存, 好比说GDI对象, COM对象, 文件句柄等等. 所以, 他们的大小是不会被这个命令的结果报告中反映出来的.

 

TypeHandle, 是一个指向MethodTable的指针, 它的位置紧跟在syncblk number以后. 在一个对象实例建立以前, CLR会查询加载了的类型,

若是这个类型没有找到就加载它, 得到类型的MethodTable的地址, 建立对象实例, 而后填充对象的TypeHandle值. JIT编译器产生的代码使用TypeHandle来寻找MethodTable, 用于实现method dispatching. CLR能够在任什么时候候经过TypeHandle指向的MethodTable来反向追溯已经加载了的类型.

 

方法表- MethodTable

=================

任何一个类或者接口, 当他们加载到AppDomain当中的时候, 都会由一个叫作MethodTable数据结构来表明. 在对象的第一个实例都还没被加载的状况下, 建立出一个MethodTable是类的加载动做的执行成果。

ObjectInstance表明的是对象的状态, MethodTable表明的是对象的行为.

MethodTable把object instance与language compiler-generated memory-mapped metadata structures, 经过EEClass联系起来. 在MethodTable中的信息和metadata structure能够在托管代码中经过Systen.Type来访问到.

在托管代码中, 一个指向MethodTable的指针能够经过Type.RuntimeTypeHandle属性来得到. TypeHandle, 存在于ObjectInstance中, 它指向一个偏移量, 这个偏移量是从MethodTable的首地址算起的. 这里的偏移量默认是12字节。这开头的12个字节包含GC的一些信息, 咱们并不打算在这里讨论这些信息.

 

图表9展示了一个典型的MethodTable的布局. 咱们会show一些重要的TypeHandle的数据域, 可是为了一个更完整的列表, 仍是看图表吧. 让咱们从Base Instance Size开始吧, 由于它与运行时的内存轮廓有直接关系.

 

图表9 方法表布局- MethodTable Layout

cc163791.fig09(en-us)

 

 

基本实例尺寸-Base Instance Size

===========================

基本实例尺寸是由class loader计算出来的对象的大小, 是基于代码中的数据域声明来计算的. 如同前面讨论的, 当前GC的实现须要一个对象的大小至少是12个字节. 若是一个类没有任何的实例数据域被定义, 它会白白的用前4个字节做为占位字节. 剩下的8字节会被Object Header(可能包括一个syncblk number), 和TypeHandle占据. 再次强调, 对象的大小是能够被StructLayoutAttribute属性影响的.

 

看看图表3(MyClass和两个接口)中MyClass的MethodTable的内存快照吧(Visual Studio .NET 2003 memory window). 请拿它和SOS生成的输出结果进行比较. 在图表9中, 对象大小是在4字节的偏移量的地方的, 其值为12 (0x0000000C)字节. 下面是 SOS中命令DumpHeap的输出结果

--------------------------------------
!DumpHeap -type MyClass
 Address       MT     Size
00a819ac 009552a0       12
total 1 objects
Statistics:
    MT  Count TotalSize Class Name
9552a0      1        12    MyClass

 

 

方法槽表-Method Slot Table

======================

在MethodTable中内嵌的是一张指向各自方法的方法描述器(MethodDesc)的指针组成的表格. 他们的存在容许了这个类型拥有一些行为. Method Slot表是根据按以下顺序实现了的方法的线性表来建立的: 继承的虚函数, 新虚函数, 实例方法, 和静态方法.(Inherited virtuals, Introduced virtuals, Instance Methods, and Static Methods).

 

ClassLoader遍历当前类的, 基类的, 和接口的metadata, 而后建立出method table. 在layout process, 任何重载了的虚函数都会被取代, 取代并隐藏父类的方法, 建立新的slot, 必要的状况下复制slot. 对slot的复制对于建立一个illusion是必不可少的, 所谓illusion是指每一个接口都有他本身的小虚函数表. 然而, 复制的slot指向相同的物理实现. MyClass有三个实例方法, 一个类构造函数(.cctor), 和一个对象构造函数(.ctor). 对象构造函数是由C#编译器为全部没有显式定义构造函数的对象,自动生成的. 类构造函数是由编译器生成的, 由于咱们定义并初始化了一个静态变量. 图表10 显示出了MyClass

的方法表的布局. 布局显示出了10个方法, 为了IVMap,Method2有重复的slot,这个重复将会在后面介绍. 图表11显示了MyClass的方法表在编辑事后的SOS dump.

 

图表10 MyClass的方法表布局

2009-11-12 22-40-49

 

图表11 SOS Dump of MyClass Method Table

-----------------------------------------------
!DumpMT -MD 0x9552a0
  Entry  MethodDesc  Return Type       Name
0097203b 00972040    String            System.Object.ToString()
009720fb 00972100    Boolean           System.Object.Equals(Object)
00972113 00972118    I4                System.Object.GetHashCode()
0097207b 00972080    Void              System.Object.Finalize()
00955253 00955258    Void              MyClass.Method1()
00955263 00955268    Void              MyClass.Method2()
00955263 00955268    Void              MyClass.Method2()
00955273 00955278    Void              MyClass.Method3()
00955283 00955288    Void              MyClass..cctor()
00955293 00955298    Void              MyClass..ctor()

任何类型的头四个方法永远都会是ToString, Equals, GetHashCode, 和Finalize.

他们是从System.Object继承来的虚方法. Method2的slot是duplicated的, 可是两者都指向相同的方法描述器(method descriptor). 显式编码的.cctor会和静态方法分为一组, .ctor会和实例方法分为一组. (The explicitly coded .cctor and .ctor will be grouped with static methods and instance methods, respectively.)

 

方法描述- MethodDesc

======================

方法描述(Method Descriptor)(MethodDesc)是方法实现的的一种封装, CLR是知道,了解这种封装的. 有好几种Method Descriptor, 他们的存在不只使得调用托管代码的实现更容易, 并且使得对interop的实现的调用也变得容易了一些. 在这篇文章中, 咱们只研究以图表3的代码为上下文的托管MethodDesc.

一个MethodDesc是做为类加载过程的一部分(class loading process)而产生出来的, MethodDesc初始状况下指向中间语言Intermediate Language(IL).

每个MethodDesc都被一个叫作PreJitStub的填充, PreJitStub负责触发JIT的编译过程.

图表12展现了一个典型的布局. 方法表的slot entry实际指向PreJitStub, 而不是指向实际的MethodDesc. 这是一个从MethodDesc算起, 负5个字节的偏移量, 而且是每一个方法继承的8个字节的填充的一部分.

那五个字节包含调用PreJitStub函数的指令. 这5字节的偏移量能够从SOS的DumpMT命令的结果输出中看到(图表11的MyClass), 由于MethodDesc老是在MethodSlot Table入口指向的位置日后数5个字节的位置上. 紧接着第一个调用, 一个对于JIT编译函数的调用被触发. 在编译结束以后, 这五个字节所包含的调用指令会被覆盖为一条无条件转移到JIT编译的x86的代码的jmp指令.

图表12 Method Descriptor

2009-11-12 23-12-01

图表12中的Method Table Slot入口指向的代码的反汇编结果中, 显示出了对于PreJitStub的调用. 这是一个删节了的Method2的在JIT以前的反汇编代码:

-------------------------------
!u 0x00955263
Unmanaged code
00955263 call        003C3538        ;call to the jitted Method2()
00955268 add         eax,68040000h   ;ignore this and the rest 
                                     ;as !u thinks it as code

 

如今让咱们执行这个方法而且反汇编一样的地址:

--------------------------------
!u 0x00955263
Unmanaged code
00955263 jmp     02C633E8        ;call to the jitted Method2()
00955268 add     eax,0E8040000h  ;ignore this and the rest 
                                 ;as !u thinks it as code

 

只有从给定地址开始的五个字节内容是代码, 后面包含的是Method2的MethodDesc的数据. 这里的"!u"命令对这一点是不知情的, 因此生成了一些胡言乱语, 故尔你能够忽略那五个字节以后的任何东西(它们不是指令).

 

CodeOrIL在JIT编译以前, 包含方法实现的IL码的Relative Virtual Address(RVA). 这个数据域被一个标志位标识: 其中存储的是IL. 一经要求, CLR完成了编译以后, CLR会使用JIT处理过的代码的地址来更新这个数据域.让咱们从列表中选择一个方法, 而后使用DumpMT命令dump出来JIT编译以前和以后的MethodDesc吧:

---------------------------------
!DumpMD 0x00955268
Method Name : [DEFAULT] [hasThis] Void MyClass.Method2()
MethodTable 9552a0
Module: 164008
mdToken: 06000006 
Flags : 400
IL RVA : 00002068

编译以后, MethodDesc看起来像这样:

---------------------------------
!DumpMD 0x00955268
Method Name : [DEFAULT] [hasThis] Void MyClass.Method2()
MethodTable 9552a0
Module: 164008
mdToken: 06000006
Flags : 400
Method VA : 02c633e8

 

在方法描述器(method descriptor)中的Flags数据域会根据方法的类型来编码, 所谓方法类型是指: 静态方法, 实例方法, 接口方法, 或者是COM实现方法.

 

让咱们看看MethodTable的复杂的另外一面吧: Interface implementation.

托管环境下, Interface implementation被实现的看起来简单一些, 达到这个效果的方式是把全部的复杂度都吸取到布局过程当中. 下一步, 咱们将要show给你接口是如何布局的, 还有基于接口的方法分派(method dispatching)是如何工做的.

 

接口虚表映射和接口映射- Interface Vtable Map and Interface Map

=====================================================

在MethodTable中, 偏移量为12的位置, 存储着一个重要的指针, IVMap. 如图表9, IVMap指向一个AppDomain等级的映射表, 该映射表以一个进程等级的接口ID为索引. 这个接口ID是在接口类型第一次加载的时候生成的. 任何一个接口的实现都会有一个IVMap的入口. 若是MyInterface1被两个类实现了, 那么在接口的IVMap中就会有两个入口. 入口会指回到MyClass的方法表内嵌的sub-table的开头, 见图表9. 这个是method dispatching发生时, 基于接口的索引. IVMap是根据内嵌在方法表中的Interface Map的信息而建立出来的. Interface Map是在布局方法表的过程当中, 根据类的metadata建立出来的. 一旦类型加载结束, 只有IVMap会在method dispatching中使用到.

 

Interface Map在偏移量28的位置, 它会指向内嵌在MethodTable中的InterfaceInfo的入口. 这样, 对于两个被MyClass实现了的接口中的任何一个接口, 都会有两个入口了.

第一个InterfaceInfo入口的头四个字节指向MyInterface1的TypeHandle(参考图表9图表10).

接下来的WORD(两字节)被Flags占据(其中0是指继承自父类, 1指的是被当前类实现).

从Flags再接下来的WORD是Start Slot, 经过它, class loader得以编排接口实现的sub-table. 对于MyInterface1, 这个值是4, 意味slot 5 和slot 6指向implementation. 对于MyInterface2, 这个值是6, 因此, slot 7 和slot 8 指向implementation. 若是必要的话, ClassLoader会复制这些slot来建立illusion. 这里的illusion指在物理映射到相同的method descriptor的同时, 任何一个接口都获得了本身的实现.  在MyClass中, MyInterface1.Method2 和MyInterface2.Method2会指向相同的实现.

 

基于接口的方法分派(method dispatching)是经过IVMap发生的, 而直接的方法分派是经过各自槽的MethodDesc的地址发生的. 如前所述, .NET Framework使用fastcall这种调用约定. 若是可能的话, 头两个参数典型地被传入ECX和EDX寄存器(译注:参见文章汇编语言基础之六- 调用栈和各类调用约定的总结对比)(最左边参数, 经过ECX传递, 第二个, 经过EDX传递).

对象实例的方法中, 第一个参数永远是this指针, 经过ECX来传递. 下面的语句中"mov ecx, esi"展现了这一点.

-------------------------------------
mi1.Method1();
mov    ecx,edi                 ;move "this" pointer into ecx        
mov    eax,dword ptr [ecx]     ;move "TypeHandle" into eax 
mov    eax,dword ptr [eax+0Ch] ;move IVMap address into eax at offset 12
mov    eax,dword ptr [eax+30h] ;move the ifc impl start slot into eax
call   dword ptr [eax]         ;call Method1

mc.Method1();
mov    ecx,esi                 ;move "this" pointer into ecx
cmp    dword ptr [ecx],ecx     ;compare and set flags
call   dword ptr ds:[009552D8h];directly call Method1

这里的反汇编代码代表了在对MyClass的实例方法的直接调用中, 并无使用偏移量. JIT编译器将MethodDesc的地址直接的用在了代码中. 基于接口的方法分派是经过IVMap来发生的, 而且比直接分派须要多一些指令. 一条用来拿到IVMap的地址, 另外一条拿到MethodTable中接口实现的start slot. 而且, 将一个对象实例转换为一个接口也仅仅是拷贝一下这个指针到目标变量当中就能够了. 在图表3 【译注:原文中这个地方时图表2,显然有错误,应该是图表3】中, 语句 "mi1 = mc;" 只用了一条指令就把mc中的OBJECTREF拷贝给了mi1.

 

虚拟分派- Virtual Dispatch

========================

让咱们来看一下虚拟分派吧, 恩, 再比较一下跟基于接口的直接分派有什么不一样. 下面是图表3中对于虚方法MyClass.Method3调用的反汇编代码:

-----------------------------
mc.Method3();
Mov    ecx,esi               ;move "this" pointer into ecx
Mov    eax,dword ptr [ecx]   ;acquire the MethodTable address
Call   dword ptr [eax+44h]   ;dispatch to the method at offset 0x44

虚拟分派老是经过一个定死了的slot number来发生, 与给定的类的实现层次的MethodTable指针无关. 在MethodTable布局的时候, ClassLoader替换掉父类的实现, 而使用子类的实现. 结果, 对于父类对象的方法调用被分派到子类对象的实现上. 反汇编代码展示了分派经过slot number 8来发生的, debugger memory window中的DumpMT命令的输出结果也是这样(见图表10).

 

静态变量- Static Variables

========================

静态变量是MethodTable数据结构的重要组成部分. 他们在method table slot数组以后被分配在MethodTable上. 全部原始的静态类型都是内联的, 而静态值类型(结构体), 引用类型是经过AppDomain的handle table(句柄表)上的OBJECTREF来引用的. MethodTable中的OBJECTREF指向AppDomain的句柄表中的OBJECTREF, 这个OBJECTREF会使得堆上建立出来的对象实例一直存活下去, 直到AppDomain被卸载掉. 在图表9中, 一个静态的字符串变量, str, 指向句柄表中的OBJECTREF, 而这个OBJECTREF指向GC堆上的MyString.

 

EEClass

========================

EEClass在MethodTable被建立以前就存在了, 在与MethodTable结合的时候, 是一个类型声明的CLR版. 实际上, EEClass和MethodTable在逻辑上是一个数据结构(他们共同表明一个单个类型), 他们中的内容是基于使用频率不一样而分开的. 很是常用的数据域存在MethodTable中, 而不太常用的数据域存在EEClass中. 因此, JIT编译函数须要的信息(好比说names, fields, 和offsets)就存在EEClass中, 然而, 运行时须要的信息(好比虚表slot和GC信息)就存在MethodTable中.

 

加载到AppDomain中的任何一个类型都有一个EEClass. 这里所说的类型包括: 接口, 类, 抽象类, 数组, 和结构体. 任何一个EEClass 都是被执行引擎跟踪的树的节点. 为了诸如:加载类, 布局MethodTable, 辨别类型, 类型转换,这样的目的, CLR使用这个网络来导航到须要的EEClass结构. EEClass之间的孩子到父亲的关系是基于继承关系来建立的, 然而, 从父亲到孩子的关系是创建在继承关系和类的加载顺序的联合的基础上的. 随着在托管代码的执行, 新的EEClass节点被一个个的添加, 旧的节点之间的关系被不断修补, 新的节点关系也被创建起来.  

网络中EEClass的兄弟之间还有水平的关系呢. EEClass有三个数据域被用来创建加载起来的类型之间的关系: ParentClass, SiblingChain, 和ChildrenChain. 参见图表13, 来看看以图表4为上下文的MyClass的EEClass的扼要图解.

 

图表13展示了一些与咱们的讨论相关联的数据域. 由于咱们忽略了布局中的一些数据域, 咱们并无在这张图表中展示出真是的偏移量. EEClass有一个针对MethodTable的环形引用. EEClass也指向在默认AppDomain的高频堆中分配的MethodDesc块. 进程堆上有一个对FieldDesc对象列表的引用, 它提供了在MethodTable建立时的field布局信息. EEClass在AppDomain的低频堆上, 这样,操做系统能够更好的进行内存的页面管理, 所以减少了working set.

 

图表13 EEClass布局- EEClass Layout

================================

2009-11-13 11-50-06

图表13中的其余数据域光看名字就能理解他们在MyClass(图表3)上下文中的做用了. 让咱们看一下使用SOS工具dump出的EEClass的真实物理内存吧. 在mc.Method1这一行设定断点后, 运行图表3中的代码.首先, 经过运行命令!Name2EE得到MyClass的EEClass的地址:

------------------------------------

!Name2EE C:\Working\test\ClrInternals\Sample1.exe MyClass

MethodTable: 009552a0
EEClass: 02ca3508
Name: MyClass

 

Name2EE命令的第一个参数是模块名称, 这个模块名称能够经过DumpDomain命令来得到. 如今咱们有了EEClass的地址了, 咱们来dump出EEClass的内容:

-----------------------------------------

!DumpClass 02ca3508
Class Name : MyClass, mdToken : 02000004, Parent Class : 02c4c3e4 
ClassLoader : 00163ad8, Method Table : 009552a0, Vtable Slots : 8
Total Method Slots : a, NumInstanceFields: 0,
NumStaticFields: 2,FieldDesc*: 00955224

      MT    Field   Offset  Type           Attr    Value    Name
009552a0  4000001   2c      CLASS          static 00a8198c  str
009552a0  4000002   30      System.UInt32  static aaaaaaaa  ui 

 

图表13和DumpClass的输出结果看起来本质上是相同的. Metadata token(mdToken)表明着模块PE文件映射在内存中的metadata表的index.

指向System.Object. Sibling 链(图表13)的Parent类, 说明了他的加载是加载Program类的结果形成的.

 

MyClass有8个vtable slot(能够被虚拟分派的方法). 尽管Method1和Method2并非虚拟方法, 它们在经过接口来分派的时候,仍是被认为是虚函数并添加到列表中. 算上.cctor和.ctor到列表中, 这样你就一共有10个方法了. 这个类有两个静态域, 都被列在后面了. MyClass没有实例域. 其他的域都挺自我说明问题的.

 

结论

=========

咱们一块儿游历了一下CLR中最重要的一些内部信息. 很显然, 还有不少方面须要被覆盖到, 而且还要更加深刻, 可是咱们但愿这篇文章能够给你一个CLR怎么工做的大体印象. 这里展示的不少的信息在将来版本的CLR和.NET Framework中可能会改变, 可是尽管这篇文章覆盖到的数据结构更改了, 概念是不会变的.

 

-----------------------------------------------------------

做者介绍

Hanu Kommalapati is an Architect at Microsoft Gulf Coast District (Houston). In his current role at Microsoft, he helps enterprise customers in building scalable component frameworks based on the .NET Framework. He can be reached at hanuk@microsoft.com.

Tom Christian is an Escalation Engineer with Developer Support at Microsoft, working with ASP.NET and the .NET debugger extension for WinDBG (sos/psscor). He is based in Charlotte, NC and can be contacted attomchris@microsoft.com.

 

 

后记: 不少学习.NET的资料都推荐这篇文章, <Windows用户态程序高效排错>中称这篇文章是字字珠玑, 因而学友舍得花两天的时间翻译,校对这篇文章. 主要目的是让本身能更深入的理解文章所述及的技术细节. 但愿你能和我同样, 看了这篇文章后能有所收获.

 

其实文章中涉及到的技术不只仅是标题部分列出来的.NET Framework和C#。 若是读者懂一些汇编的基础知识,将会有更好的理解。个人博文中有个汇编基础的系列, 我以为做为这篇文章的一点知识准备挺适合的.

 

看了这篇文章以后, 相信你也以为.net不像从前那么神秘了,对么? :)

 

好多长句, 理清楚从句之间的从属和修饰关系很累人. 若是忠于原文, 光看懂长句就要花上一两分钟, 因此仍是把长句子拆成了许多短句. 方便你们快速获取一些印象. 有的时候概念仍是英文的好懂些, 就没翻译的那么完全. 有些中英文概念都在紧随其后的括号中有另一个的注解. 第一次出现的名词, 通常用英文直接写出, 后面概念重复了的时候才适当翻译成中文. 总之, 以易读为目标组织语言.

 

不知道你们的习惯如何, 我的感受在银屏上读文章就但愿一目了然, 因此为了概念和结构的清晰, 原文的某些段落被用换行拆开了.

 

翻译水平有限, 技术水平也有限, 让读者见笑了. 原文的连接在页面的顶部. 若是有困惑能够对照着看看.

欢迎批评指正!

posted @ 2009-11-11 23:13 中道学友 阅读(1144) 评论(0) 编辑

CLR是如何工做的

MetData和引擎初始化

====================

托管Assembly自己只包含CLR可识别的MetaData(元资料), 不包含机器指令. 托管Assembly都与mscoree.dll绑定. mscoree.dll在system32目录下, 全称是Microsoft Core Execution Engine. 它的功能是选择合适的CLR Execution Engine来加载.

2009-11-11 18-18-06

多个版本的CLR能够共存. CLR的目录在C:\Windows\Microsoft.NET\Framework. 当前系统中最新版本的CLR对应的mscoree.dll文件被拷贝到system32目录下.

 

当mscoree.dll加载后, 它根据托管代码的metadata和app.config, 选择恰当版本的引擎加载. 同时mscoree还负责判断应该用何种GC Flavor. GC Flavor包括Workstation GC和Server GC. 在CLR1中, Workstation GC对应到mscorwks.dll, 而Server GC对应到mscorsvr.dll文件. 在CLR2中虽然保留了mscorsvr.dll文件, 可是mscorwks.dll已经包含了两种GC Flavor的实现, 只须要加载mscorwks就能够了.

 

CLR加载后, 先初始化CLR须要的各类功能, 好比必要的全局变量, 引擎须要的模块(ClassLoader, assembly Loader, JitEngine, Copntext等), 启动Finalizer thread和GC thread, 建立System AppDomain和Shared AppDomain, 建立RCDebugger Thread, 加载CLR基础类(好比mscorlib.dll, system.dll)

 

当CLR引擎初始化完成后, CLR会找到当前exe的元数据, 而后找到Main函数, 编译Main函数, 执行Main函数.

 

JIT动态编译

=================

1. 全托管代码

假设C#函数foo1要调用foo2. 当CLR编译foo1的时候, 不管foo2是否已经编译成机器代码, call指令都是吧执行指向到跟foo2相关的一个内存地址(stub). 当执行这个call指令的时候, 若是foo2没有被CLR编译, stub中的代码就会把执行定向到CLR JitEngine, 这样对foo2的调用便致使了CLR JitEngine的启动来编译foo2函数. Jit Engine编译完成以后, CLR把编译好的机器代码拷贝到进程中由cLR管理的某一块内存(loader heap)上, 而后Jit Engine把编译好的foo2函数入口地址填回到stub中.

经过这样的技术, 第二次对foo2调用的时候, foo2的stub指向的已是编译好的地址了, 因而不须要再次编译. 固然第一次编译完成以后, JitEngine同时须要负责执行刚刚编译好的函数.

 

2. 托管代码调用非托管代码

在CLR的执行过程当中, 若是使用到的都是托管代码, 编译和执行就按照上面的逻辑进行. 可是不可避免的, 托管代码须要调用非托管代码. 这里分两种状况.

第一种是调用系统API和DLLImport. 好比CLR中使用FileStream打开一个文件, 最终仍是要调用到CreateFileW. 经过DLLImport调用自定义的非托管函数, 以及COM Interop也属于这种状况.

第二种是调用CLR Runtime的功能, 好比内存分配, 异常派发.

 

两种状况都使用stub技术. 对于第一种状况, 不关事PInvoke仍是COM Interop发生的时候, 托管代码调用的都是由CLR建立的stub. 在这个stub中CLR会作一些必要的工做, 而后把控制权交给对应的非托管代码. 必要的工做包括把必要的函数参数拷贝到非托管的内存上, marshal必要的类型, 锁住须要跟非托管代码交互的托管内存区域, 防止GC移动这块内存. 若是是COM Interop, 还包括对非托管接口指针进行必要的QueryInterface等等. 当非托管调用结束后, 执行权返回stub, 再次进行必要的工做后, 回到托管代码.

 

第二种状况中, 对CLR功能的调用每每是隐式发生的.

一类是编译器直接生成对CLR stub的调用. 好比new/throw关键词. 动态编译引擎对这些关键词的处理是生成函数调用到特殊的stub上, stub再把执行定位到CLR引擎中的关键函数. 就分配内存来说, 好比new一个StringBuilder object, 动态编译生成的指令吧执行权定向到特殊的stub, 该stub包含了指令来调用CLRzhong的内存分配函数, 同时传入类型信息.

另外一类是经过吧托管代码标示为internal call来编译. Internal call表示该托管函数实际上是某些unmanaged函数的映像, 编译引擎在编译internal call的时候, 会直接把标记的internalcall属性的CLR方法, 直接跟unmanaged的函数实现对应起来. 该对应关系是在CLR的实现中经过C++的一张静态表定义的.

 

GC内存管理

=================

CLR引擎初始化的时候会向操做系统申请连续内存做为managed heap. 全部的managed object都分配在managed heap中. 对于任何一种托管类型, 因为类型信息保存在metadata中, 因此CLR清楚如何生成正确的内存格式.

当托管类型分配请求定向到CLR中后, CLR首先检查managed heap是否足够. 若是足够, CLR直接使用鲜有内存, 根据类型信息填入必要的格式资料, 而后把地址传递给托管代码使用. 若是托管堆不够, CLR执行GC试图请扫除一部份内存. 若是GC没法清扫出内存, CLR 向OS请求更多的内存做为managed heap.若是OS拒绝内存请求, OutOfMemory就发生了. CLR内存分配的特色是:

 

1. 大多数状况下比非托管代码内存分配速度快. CLR保持内部指针指向当前托管堆中的free point. 只要内存足够, CLR直接把当前指针所在地址做为内存分配出去, 而后用指针加/减分配出去的内存的长度. 对于非托管代码的内存分配,无论是Heap Manager, 仍是Virtual Memory allocation, 都须要作相应计算才能找到合适的内存进行分配.

 

2. 因为托管对象受到CLR的管理, GC发生的时候CLR能够对托管object 进行随意移动, 而后休整保存object的stub信息, 保证托管代码不会受此影响. 移动object 能够防止内存碎片的产生, 提升内存使用效率. 对于非托管代码来讲, 因为程序能够直接使用指针, 因此没法进行内存碎片整理.

 

3. GC能够在任什么时候候触发, 可是GC不能在任什么时候候发生. 好比某一个线程正在作regex的匹配, 访问到大量的托管object, 不少object的地址保存到CPU寄存器上进行优化. 若是GC发生,致使object地址变化, 恢复运行后CPU寄存器上的指针可能就会无效. 因此GC必须在全部线程的执行状态都不会受到GC 影响的时候发生. 当线程的执行状态不受影响时, 该线程的PreEmptive GC属性石1, 不然是0.  这个开关受到CLR的控制, 不少stub中的代码会操做这个开关. 好比托管代码调用了MessageBox.Show, 该方法最后会调用到MessageBox API. 在stub调用API从托管代码变化到非托管代码前, stub会经过CLR内部方法把PreEmptive设定为1, 表示GC能够发生了. 大体的状况是, 当线程idle的时候(线程idle的时候确定是在等某一个系统API, 好比sleep护着WaitForSingleObject), PreEmptive为1. 当线程在托管代码中干活的时候, PreEmptive为0. 当GC触发的时候, GC必须等到全部的线程都进入了PreEmptive模式后, 才能够发生.

 

Exception Handling 异常处理

==========================

异常处理在CLR中也很是有特点. 好比, NullReferenceException和Access Violation实际上是密切相关的. 当编译的托管代码执行的时候, 对于NULL object的访问, 首先出发的是Access Violation. 可是聪明的CLR已经设定好了对应的FS:[0]寄存器来截获可能的异常. CLR截获异常后, 首先检查异常的类型, 对于Access Violation, CLR先检查当前的代码是不是托管代码, 对应的类型信息是什么. 发现是NULLobject访问后, CLR再把这个Access Violaiton异常标记为已处理, 而后生成对应的NullReferenceException抛出来. 当NullReferenceException被CLR设定的FS:[0]截获后, CLR发现异常是CLR Exception, 因而找对应的catch语句执行.

 

CLR异常发生以后能够打印出callstack, 缘由在于CLR能够经过原数据采集全部的类型信息, 同时CLR在thread中经过多种机制记录运行状态. 保存在Stack中的Frame就是其中的一种重要的数据结构. Frame是CLR保存在stack中的小块数据结构. 当therad的执行状态发生改变的时候, 好比在托管代码和非托管代码中切换, 异常产生, remoting调用等等的时候, CLR会恰当的插入Frame来标示状态的改变. thread中全部的frame是经过指针连接在一块儿的, 因此CLR能够方便的获取一个thread的各类状态状况.

 

总结:

1. 运行托管assembly的时候, 先会加载mscoree.dll.

2. 系统中最新的mscoree.dll被加载, 而后mscoree.dll根据托管assembly的metadata决定该加载那个版本的CLR. 同时加载GC Flavor.

3. CLR执行初始化

4. CLR找到当前exe的metadata, 找到, 编译, 执行main函数.

5. 过程当中后可能遇到另外的函数, 第一次运行的时候都要先编译, 而后用stub技术让调用者拿到编译后的函数入口, 完成调用.

6. 运行过程当中, 若是请求内存会用到GC的一些特性.

7. 出了异常, 会用到CLR的一些特性.

 

资料来源《Windows用户态程序高效排错》

posted @ 2009-11-11 18:22 中道学友 阅读(240) 评论(0) 编辑

快速识别汇编中等价的C语言语句(if, while, for, switch)

可能常常看汇编的朋友会一眼就认出跟C语言中一些语句等价的汇编代码, 经验使然也. 而不常常接触汇编的同窗, 可能就对相对繁琐的寄存器操做指令有点云里雾里了.

汇编是编译器翻译中级语言(也能够把C语言称做高级语言, 呵呵)的结果, 只要是机器作的事儿,通常都有规律可循. 那咱们如今就来看看一下这些基本语句的汇编规律吧.

注意:本文使用的汇编格式为GAS(Gnu ASembler GNU汇编器). 它同Intel文档中的格式以及微软编译器使用的格式差别很大,

具体请看文章AT&T汇编格式与Intel汇编格式的比较.

 

条件转移语句- if

============================

C语言中的if-else语句的通用形式以下

  1. if(test-expr)
  2.     then-statement;
  3. else
  4.     else-statement;

对于这种通用形式, 汇编实现一般会使用下面这种形式

  1.     ttest-expr;
  2.     if (t)
  3.         goto true;
  4.     else-statement
  5.         goto done;
  6. true:
  7.     then-statement
  8. done:

也就是汇编器为then-statement 和else-statement各自产生代码块, 并插入条件和无条件分支, 以保证正确代码块的执行.

 

下面咱们来看C语言源代码, 汇编结构的C代码, 和汇编代码的比较.

 

Code Snippet
  1. //----------Classic C code------------
  2. int absdiff(int xint y)
  3. {
  4.     if (x < y)
  5.         return y - x;
  6.     else
  7.         return x - y;
  8. }
  9. //----------Classic C code------------

 

 

 

Code Snippet
  1. //----------Equivalent Goto Version------------
  2. int gotodiff(int xint y)
  3. {
  4.     int rval;
  5.  
  6.     if (x < y)
  7.         goto less;
  8.     rval = x - y;
  9.     goto done;
  10. less:
  11.     rval = y - x;
  12. done:
  13.     return rval;
  14. }
  15. //----------Equivalent Goto Version------------

 

 

Code Snippet
  1. //----------Equivalent assembly Version------------
  2.     movl 8(%ebp),%edx          ;Get x
  3.     movl 12(%ebp),%eax         ;Get y
  4.     cmpl %eax,%edx             ;Compare x:y
  5.     jl .L3                     ;If <, goto less:
  6.     subl %eax,%edx             ;Compute y-x
  7.     movl %edx,%eax             ;Set as return value
  8.     jmp .L5                    ;Goto done:
  9. .L3:                           ;less:
  10.     subl %edx,%eax             ;Compute x-y as return value
  11. .L5:                           ;done:Begin completion code
  12. //----------Equivalent assembly Version------------

 

do-while循环

========================

do-while循环的通用形式是这样的:

  1. do
  2. {body-statement}
  3. while (test-expr);

循环的效果就是重复执行body-statement, 对test-expr求值, 若是不是0, 就继续循环. 注意, 循环体至少执行一次.

一般, do-while 的实现有下面的通用形式:

  1. loop:
  2.     body-statement
  3.     ttest-expr;
  4.     if (t)
  5.         goto loop;

 

下面是一个例子, 找找感受吧.

Code Snippet
  1. //----------Original C Version------------
  2. do{
  3.     int t = val + nval;
  4.     val = nval;
  5.     nval = t;
  6.     i++;
  7. while (i < n);
  8. //----------Original C Version------------

 

Code Snippet
  1. //----------Corresponding assembly code------------
  2. .L6loop:
  3.     leal (%edx,%ebx),%eax ;Compute t = val + nval
  4.     movl %edx,%ebx        ;copy nval to val
  5.     movl %eax,%edx        ;Copy t to nval
  6.     incl %ecx             ;Increment i
  7.     cmpl %esi,%ecx        ;Compare i:n
  8.     jl .L6 If less,       ;goto loop
  9. //---------Corresponding assembly code------------

 

while循环

========================

while语句循环的通用形式是这样的

  1. while(test-expr)
  2.     body-statement

与do-while的不一样之处在于对test-expr求值, 在第一次执行body-statement以前, 循环就可能终止了. 翻译成goto语句的形式就是

  1. loop:
  2.     ttest-expr;
  3.     if (!t)
  4.         goto done;
  5.     body-statement
  6.         goto loop;
  7. done:

 

 

 

 

 

 

 

 

 

 

 

这种翻译须要在内循环(也就是执行次数最多的代码部分)中, 有两条goto语句. 大多数的编译器将这段代码转换成do-while循环, 把一个条件分支语句从循环体中拿到外面来.

  1.     if (!test-expr)
  2.         goto done;
  3.     do
  4.     body-statement
  5.         while (test-expr);
  6. done:

而后, 再把这段代码换成带goto的语句的代码, 以下

  1.     ttest-expr;
  2.     if (!t)
  3.         goto done;
  4. loop:
  5.     body-statement
  6.         ttest-expr;
  7.     if (t)
  8.         goto loop;
  9. done:

 

for循环

========================

for循环的通用形式是这样的:

  1. for (init-exprtest-exprupdate-expr)
  2.     body-statement

C语言的标准说明, 这样的一个循环的行为与下面这段使用while循环的代码的行为同样:

  1. init-expr;
  2. while (test-expr){
  3.     body-statement
  4.     update-expr;
  5. }

而后再用前面讲过的从while到do-while的转换. 首先给出do-while形式

  1.     init-expr;
  2.     if (!test-expr)
  3.         goto done;
  4.     do{
  5.         body-statement
  6.         update-expr;
  7.     }while (test-expr);
  8. done:

再转换成goto代码

  1.     init-expr;
  2.     ttest-expr;
  3.     if (!t)
  4.         goto done;
  5. loop:
  6.     body-statement
  7.         update-expr;
  8.     ttest-expr;
  9.     if (t)
  10.         goto loop;
  11. done:

 

相信如今, 你已经对汇编中的循环指令簇有点模式的感受了吧? 呵呵. 咱们再来看一个switch语句, 而后收工.

switch语句

======================

switch语句提供了一个整数索引值, 经过它来进行多重分支. 那么switch语句和一组很长的if-else语句相比, 有什么优点呢? 我先把答案说出来, 而后看看汇编, 就知道了.

优点就是: 执行开关语句的时间与开关状况的数量无关.

能作到这样的缘由是跳转表. 跳转表是一个数组, 表项i是一个代码段的地址, 这个代码段实现的就是开关索引值等于i的时候应该采起的动做.

 

让咱们来看一个例子, 这个例子包含一些颇有意思的特征, 状况标号(case label)不连续, 好比101, 105; 一个状况有多个标号, 好比104, 106; 有些状况会落入其余状况(102), 由于该状况没有用break结尾.

  1. //----------Original C code------------
  2. int switch_eg(int x)
  3. {
  4.     int result = x;
  5.  
  6.     switch (x) {
  7.  
  8.         case 100:
  9.             result *= 13;
  10.             break;
  11.  
  12.         case 102:
  13.             result += 10;
  14.             /* Fall through */
  15.  
  16.         case 103:
  17.             result += 11;
  18.             break;
  19.  
  20.         case 104:
  21.         case 106:
  22.             result *= result;
  23.             break;
  24.  
  25.         default:
  26.             result = 0;
  27.     }
  28.  
  29.     return result;
  30. }
  31. //----------Original C code------------

说明问题的C的伪代码

  1. /* Next line is not legal C */
  2. code *jt[7] = {
  3.     loc_Aloc_defloc_Bloc_C,
  4.     loc_Dloc_defloc_D
  5. };
  6. int switch_eg_impl(int x)
  7. {
  8.     unsigned xi = x - 100;
  9.     int result = x;
  10.     if (xi > 6)
  11.         goto loc_def;
  12.     /* Next goto is not legal C */
  13.     goto jt[xi];
  14. loc_A/* Case 100 */
  15.     result *= 13;
  16.     goto done;
  17. loc_B/* Case 102 */
  18.     result += 10;
  19.     /* Fall through */
  20. loc_C/* Case 103 */
  21.     result += 11;
  22.     goto done;
  23. loc_D/* Cases 104, 106 */
  24.     result *= result;
  25.     goto done;
  26. loc_def/* Default case*/
  27.     result = 0;
  28. done:
  29.     return result;
  30. }

 

  1. //----------Corresponding assembly code------------
  2. //***********
  3. // Code that Set up the jump table access
  4. //***********
  5.     leal -100(%edx),%eax         ;Compute xi = x-100
  6.     cmpl $6,%eax                 ;Compare xi:6
  7.     ja .L9                       ;if >, goto done
  8.     jmp *.L10(,%eax,4)           ;Goto jt[xi]
  9. //Case 100
  10. L4:                              ;loc A:
  11.     leal (%edx,%edx,2),%eax      ;Compute 3*x
  12.     leal (%edx,%eax,4),%edx      ;Compute x+4*3*x
  13.     jmp .L3                      ;Goto done
  14. //Case 102
  15. L5:                              ;loc B:
  16.     addl $10,%edx                ;result += 10, Fall through
  17. //Case 103
  18. L6:                              ;loc C:
  19.     addl $11,%edx                ;result += 11
  20.     jmp .L3                      ;Goto done
  21. //Cases 104, 106
  22. L8:                              ;loc D:
  23.     imull %edx,%edx              ;result *= result
  24.     jmp .L3                      ;Goto done
  25. //Default case
  26. L9:                              ;loc def:
  27.     xorl %edx,%edx               ;result = 0
  28. //Return result
  29. L3:                              ;done:
  30.     movl %edx,%eax               ;Set result as return value
  31. //----------Corresponding assembly code------------

 

参考资料<深刻理解计算机系统>

posted @ 2009-11-11 11:24 中道学友 阅读(1439) 评论(0) 编辑

AT&T汇编格式与Intel汇编格式的比较

GCC采用的是AT&T的汇编格式, 也叫GAS格式(Gnu ASembler GNU汇编器), 而微软采用Intel的汇编格式. 
一 基本语法 
语法上主要有如下几个不一样. 
一、寄存器命名原则

AT&T Intel 说明
%eax eax Intel的不带百分号


二、源/目的操做数顺序

AT&T Intel 说明
movl %eax, %ebx mov ebx, eax Intel的目的操做数在前,源操做数在后


三、常数/当即数的格式

AT&T Intel 说明
movl $_value,%ebx mov eax,_value Intel的当即数前面不带$符号
movl $0xd00d,%ebx mov ebx,0xd00d 规则一样适用于16进制的当即数


四、操做数长度标识

AT&T Intel 说明
movw %ax,%bx mov bx,ax Intel的汇编中, 操做数的长度并不经过指令符号来标识

在AT&T的格式中, 每一个操做都有一个字符后缀, 代表操做数的大小. 例如:mov指令有三种形式:

movb  传送字节

movw  传送字

movl   传送双字

由于在许多机器上, 32位数都称为长字(long word), 这是沿用以16位字为标准的时代的历史习惯形成的.

---------摘自《深刻理解计算机系统》


五、寻址方式

AT&T Intel
imm32(basepointer,indexpointer,indexscale) [basepointer + indexpointer*indexscale + imm32)

两种寻址的实际结果都应该是

imm32 + basepointer + indexpointer*indexscale

AT&T的汇编格式中, 跳转指令有点特殊.

直接跳转, 即跳转目标是做为指令的一部分编码的.

        例如: jmp Label_1

间接跳转, 即跳转目标是从寄存器或存储器位置中读出的. 写法是在" * "后面跟一个操做数指示符.

        例如: jmp *%eax 用寄存器%eax中的值做为跳转目标

                 jmp *(%eax) 以%eax中的值做为读入的地址, 从存储器中读出跳转目标

--------摘自《深刻理解计算机系统》

 

下面是一些寻址的例子:

 

AT&T: ` -4(%ebp)'         至关于 Intel: ` [ebp - 4]'

 

AT&T: ` foo(,%eax,4)' 至关于 Intel: ` [foo + eax*4]'AT&T: ` foo(,1)'           至关于 Intel ` [foo]'AT&T: ` %gs:foo'           至关于 Intel` gs:foo
例子摘自 http://sourceware.org/binutils/docs/as/i386_002dMemory.html#i386_002dMemory
相关文章
相关标签/搜索