.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
相關文章
相關標籤/搜索