91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

CLR是怎么創建運行時對象

發布時間:2020-07-21 11:45:33 來源:億速云 閱讀:200 作者:Leah 欄目:編程語言

本篇文章給大家分享的是有關CLR是怎么創建運行時對象,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。

原文地址:
原文發布日期: 9/19/2005
原文已經被 Microsoft 刪除了,收集過程中發現很多文章圖都不全,那是因為原文的圖都不全,所以特收集完整全文。

目錄

  • 前言

  • CLR啟動程序(Bootstrap)創建的域

  • 系統域(System Domain)

  • 共享域(Shared Domain)

  • 默認域(Default Domain)

  • 加載器堆(Loader Heaps)

  • 類型原理

  • 對象實例

  • 方法表

  • 基實例大小

  • 方法槽表(Method Slot Table)

  • 方法描述(MethodDesc)

  • 接口虛表圖和接口圖(Interface Vtable Map and Interface Map)

  • 虛分派(Virtual Dispatch)

  • 靜態變量(Static Variables)

  • EEClass

  • 結論

前言

  • SystemDomain, SharedDomain, and DefaultDomain。

  • 對象布局和內存細節。

  • 方法表布局。

  • 方法分派(Method dispatching)。

因為公共語言運行時(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)中擁有相關實現的類, 你可以從  下載到它們.

圖表1 會幫助你在搜索一些結構的時候到SSCLI中的信息.

ITEMSSCLI PATH
AppDomainsscliclrsrcvmappdomain.hpp
AppDomainStringLiteralMapsscliclrsrcvmstringliteralmap.h
BaseDomainsscliclrsrcvmappdomain.hpp
ClassLoadersscliclrsrcvmclsload.hpp
EEClasssscliclrsrcvmclass.h
FieldDescssscliclrsrcvmfield.h
GCHeapsscliclrsrcvmgc.h
GlobalStringLiteralMapsscliclrsrcvmstringliteralmap.h
HandleTablesscliclrsrcvmhandletable.h
InterfaceVTableMapMgrsscliclrsrcvmappdomain.hpp
Large Object Heapsscliclrsrcvmgc.h
LayoutKindsscliclrsrcbclsystemruntimeinteropserviceslayoutkind.cs
LoaderHeapssscliclrsrcincutilcode.h
MethodDescssscliclrsrcvmmethod.hpp
MethodTablessscliclrsrcvmclass.h
OBJECTREFsscliclrsrcvmtypehandle.h
SecurityContextsscliclrsrcvmsecurity.h
SecurityDescriptorsscliclrsrcvmsecurity.h
SharedDomainsscliclrsrcvmappdomain.hpp
StructLayoutAttributesscliclrsrcbclsystemruntimeinteropservicesattributes.cs
SyncTableEntrysscliclrsrcvmsyncblk.h
System namespacesscliclrsrcbclsystem
SystemDomainsscliclrsrcvmappdomain.hpp
TypeHandlesscliclrsrcvmtypehandle.h

在我們開始前,請注意:本文提供的信息只對在X86平臺上運行的.NET Framework 1.1有效(對于Shared Source CLI 1.0也大部分適用,只是在某些交互操作的情況下必須注意例外),對于.NET Framework 2.0會有改變,所以請不要在構建軟件時依賴于這些內部結構的不變性。

CLR啟動程序(Bootstrap)創建的域

在CLR執行托管代碼的第一行代碼前,會創建三個應用程序域。其中兩個對于托管代碼甚至CLR宿主程序(CLR hosts)都是不可見的。它們只能由CLR啟動進程創建,而提供CLR啟動進程的是shim——mscoree.dll和mscorwks.dll (在多處理器系統下是mscorsvr.dll)。正如 圖2 所示,這些域是系統域(System Domain)和共享域(Shared Domain),都是使用了單件(Singleton)模式。第三個域是缺省應用程序域(Default AppDomain),它是一個AppDomain的實例,也是唯一的有命名的域。對于簡單的CLR宿主程序,比如控制臺程序,默認的域名由可執行映象文件的名字組成。其它的域可以在托管代碼中使用AppDomain.CreateDomain方法創建,或者在非托管的代碼中使用ICORRuntimeHost接口創建。復雜的宿主程序,比如 ASP.NET,對于特定的網站會基于應用程序的數目創建多個域。

圖 2 由CLR啟動程序創建的域 ↓

CLR是怎么創建運行時對象

系統域(System Domain)

系統域負責創建和初始化共享域和默認應用程序域。它將系統庫mscorlib.dll載入共享域,并且維護進程范圍內部使用的隱含或者顯式字符串符號。

字符串駐留(string interning)是 .NET Framework 1.1中的一個優化特性,它的處理方法顯得有些笨拙,因為CLR沒有給程序集機會選擇此特性。盡管如此,由于在所有的應用程序域中對一個特定的符號只保存一個對應的字符串,此特性可以節省內存空間。

系統域還負責產生進程范圍的接口ID,并用來創建每個應用程序域的接口虛表映射圖(InterfaceVtableMaps)的接口。系統域在進程中保持跟蹤所有域,并實現加載和卸載應用程序域的功能。

共享域(Shared Domain)

所有不屬于任何特定域的代碼被加載到系統庫SharedDomain.Mscorlib,對于所有應用程序域的用戶代碼都是必需的。它會被自動加載到共享域中。系統命名空間的基本類型,如Object, ValueType, Array, Enum, String, and Delegate等等,在CLR啟動程序過程中被預先加載到本域中。用戶代碼也可以被加載到這個域中,方法是在調用CorBindToRuntimeEx時使用由CLR宿主程序指定的LoaderOptimization特性。控制臺程序也可以加載代碼到共享域中,方法是使用System.LoaderOptimizationAttribute特性聲明Main方法。共享域還管理一個使用基地址作為索引的程序集映射圖,此映射圖作為管理共享程序集依賴關系的查找表,這些程序集被加載到默認域(DefaultDomain)和其它在托管代碼中創建的應用程序域。非共享的用戶代碼被加載到默認域。

默認域(Default Domain)

默認域是應用程序域(AppDomain)的一個實例,一般的應用程序代碼在其中運行。盡管有些應用程序需要在運行時創建額外的應用程序域(比如有些使用插件,plug-in,架構或者進行重要的運行時代碼生成工作的應用程序),大部分的應用程序在運行期間只創建一個域。所有在此域運行的代碼都是在域層次上有上下文限制。如果一個應用程序有多個應用程序域,任何的域間訪問會通過.NET Remoting代理。額外的域內上下文限制信息可以使用System.ContextBoundObject派生的類型創建。每個應用程序域有自己的安全描述符(SecurityDescriptor),安全上下文(SecurityContext)和默認上下文(DefaultContext),還有自己的加載器堆(高頻堆,低頻堆和代理堆),句柄表,接口虛表管理器和程序集緩存。

加載器堆(Loader Heaps)

加載器堆的作用是加載不同的運行時CLR部件和優化在域的整個生命期內存在的部件。這些堆的增長基于可預測塊,這樣可以使碎片最小化。加載器堆不同于垃圾回收堆(或者對稱多處理器上的多個堆),垃圾回收堆保存對象實例,而加載器堆同時保存類型系統。經常訪問的部件如方法表,方法描述,域描述和接口圖,分配在高頻堆上,而較少訪問的數據結構如EEClass和類加載器及其查找表,分配在低頻堆。代理堆保存用于代碼訪問安全性(code access security, CAS)的代理部件,如COM封裝調用和平臺調用(P/Invoke)。

從高層次了解域后,我們準備看看它們在一個簡單的應用程序的上下文中的物理細節,見 圖3。我們在程序運行時停在mc.Method1(),然后使用SOS調試器擴展命令DumpDomain來輸出域的信息。(請查看 Son of Strike了解SOS的加載信息)。這里是編輯后的輸出:

圖3 Sample1.exe

!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
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"的應用程序域。Mscorlib.dll被加載到共享域,不過因為它是核心系統庫,所以也在系統域中列出。每個域會分配一個高頻堆,低頻堆和代理堆。系統域和共享域使用相同的類加載器,而默認應用程序使用自己的類加載器。

輸出沒有顯示加載器堆的保留尺寸和已提交尺寸。高頻堆的初始化大小是32KB,每次提交4KB。SOS的輸出也沒有顯示接口虛表堆(InterfaceVtableMap)。每個域有一個接口虛表堆(簡稱為IVMap),由自己的加載器堆在域初始化階段創建。IVMap保留大小是4KB,開始時提交4KB。我們將會在后續部分研究類型布局時討論IVMap的意義。

圖2 顯示默認的進程堆,JIT代碼堆,GC堆(用于小對象)和大對象堆(用于大小等于或者超過85000字節的對象),它說明了這些堆和加載器堆的語義區別。即時(just-in-time, JIT)編譯器產生x86指令并且保存到JIT代碼堆中。GC堆和大對象堆是用于托管對象實例化的垃圾回收堆。

類型原理

類型是.NET編程中的基本單元。在C#中,類型可以使用class,struct和interface關鍵字進行聲明。大多數類型由程序員顯式創建,但是,在特別的交互操作(interop)情形和遠程對象調用(.NET Remoting)場合中,.NET CLR會隱式的產生類型,這些產生的類型包含COM和運行時可調用封裝及傳輸代理(Runtime Callable Wrappers and Transparent Proxies)。

我們通過一個包含對象引用的棧開始研究.NET類型原理(典型地,棧是一個對象實例開始生命期的地方)。 圖4中顯示的代碼包含一個簡單的程序,它有一個控制臺的入口點,調用了一個靜態方法。Method1創建一個SmallClass的類型實例,該類型包含一個字節數組,用于演示如何在大對象堆創建對象。盡管這是一段無聊的代碼,但是可以幫助我們進行討論。

圖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;" 代碼行斷點時的fastcall棧結構(fastcall時.NET的調用規范,它說明在可能的情況下將函數參數通過寄存器傳遞,而其它參數按照從右到左的順序入棧,然后由被調用函數完成出棧操作)。本地值類型變量objSize內含在棧結構中。引用類型變量如smallObj以固定大小(4字節DWORD)保存在棧中,包含了在一般GC堆中分配的對象的地址。對于傳統C++,這是對象的指針;在托管世界中,它是對象的引用。不管怎樣,它包含了一個對象實例的地址,我們將使用術語對象實例(ObjectInstance)描述對象引用指向地址位置的數據結構。

圖5 SimpleProgram的棧結構和堆

CLR是怎么創建運行時對象

一般GC堆上的smallObj對象實例包含一個名為 _largeObj 的字節數組(注意,圖中顯示的大小為85016字節,是實際的存貯大小)。CLR對大于或等于85000字節的對象的處理和小對象不同。大對象在大對象堆(LOH)上分配,而小對象在一般GC堆上創建,這樣可以優化對象的分配和回收。LOH不會壓縮,而GC堆在GC回收時進行壓縮。還有,LOH只會在完全GC回收時被回收。

smallObj的對象實例包含類型句柄(TypeHandle),指向對應類型的方法表。每個聲明的類型有一個方法表,而同一類型的所有對象實例都指向同一個方法表。它包含了類型的特性信息(接口,抽象類,具體類,COM封裝和代理),實現的接口數目,用于接口分派的接口圖,方法表的槽(slot)數目,指向相應實現的槽表。

方法表指向一個名為EEClass的重要數據結構。在方法表創建前,CLR類加載器從元數據中創建EEClass。 圖4中,SmallClass的方法表指向它的EEClass。這些結構指向它們的模塊和程序集。方法表和EEClass一般分配在共享域的加載器堆。加載器堆和應用程序域關聯,這里提到的數據結構一旦被加載到其中,就直到應用程序域卸載時才會消失。而且,默認的應用程序域不會被卸載,所以這些代碼的生存期是直到CLR關閉為止。

對象實例

正如我們說過的,所有值類型的實例或者包含在線程棧上,或者包含在 GC 堆上。所有的引用類型在 GC 堆或者 LOH 上創建。圖 6 顯示了一個典型的對象布局。一個對象可以通過以下途徑被引用:基于棧的局部變量,在交互操作或者平臺調用情況下的句柄表,寄存器(執行方法時的 this 指針和方法參數),擁有終結器( finalizer )方法的對象的終結器隊列。 OBJECTREF 不是指向對象實例的開始位置,而是有一個 DWORD 的偏移量( 4 字節)。此 DWORD 稱為對象頭,保存一個指向 SyncTableEntry 表的索引(從 1 開始計數的 syncblk 編號。因為通過索引進行連接,所以在需要增加表的大小時, CLR 可以在內存中移動這個表。 SyncTableEntry 維護一個反向的弱引用,以便 CLR 可以跟蹤 SyncBlock 的所有權。弱引用讓 GC 可以在沒有其它強引用存在時回收對象。 SyncTableEntry 還保存了一個指向 SyncBlock 的指針,包含了很少需要被一個對象的所有實例使用的有用的信息。這些信息包括對象鎖,哈希編碼,任何轉換層 (thunking) 數據和應用程序域的索引。對于大多數的對象實例,不會為實際的 SyncBlock 分配內存,而且 syncblk 編號為 0 。這一點在執行線程遇到如 lock(obj) 或者 obj.GetHashCode 的語句時會發生變化,如下所示:

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

圖 6 對象實例布局
CLR是怎么創建運行時對象

在以上代碼中, smallObj 會使用 0 作為它的起始的 syncblk 編號。 lock 語句使得 CLR 創建一個 syncblk 入口并使用相應的數值更新對象頭。因為 C# 的 lock 關鍵字會擴展為 try-finally 語句并使用 Monitor 類,一個用作同步的 Monitor 對象在 syncblk 上創建。堆 GetHashCode 的調用會使用對象的哈希編碼增加 syncblk 。
在 SyncBlock 中有其它的域,它們在 COM 交互操作和封送委托( marshaling delegates )到非托管代碼時使用,不過這和典型的對象用處無關。
類型句柄緊跟在對象實例中的 syncblk 編號后。為了保持連續性,我會在說明實例變量后討論類型句柄。實例域( Instance field )的變量列表緊跟在類型句柄后。默認情況下,實例域會以內存最有效使用的方式排列,這樣只需要最少的用作對齊的填充字節。圖 7 的代碼顯示了 SimpleClass 包含有一些不同大小的實例變量。

圖 7 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 顯示了在 Visual Studio 調試器的內存窗口中的一個 SimpleClass 對象實例。我們在圖 7 的 return 語句處設置了斷點,然后使用 ECX 寄存器保存的 simpleObj 地址在內存窗口顯示對象實例。前 4 個字節是 syncblk 編號。因為我們沒有用任何同步代碼使用此實例(也沒有訪問它的哈希編碼), syncblk 編號為 0 。保存在棧變量的對象實例,指向起始位置的 4 個字節的偏移處。字節變量 b1,b2,b3 和 b4 被一個接一個的排列在一起。兩個 short 類型變量 s1 和 s2 也被排列在一起。字符串變量 str 是一個 4 字節的 OBJECTREF ,指向 GC 堆中分配的實際的字符串實例。字符串是一個特別的類型,因為所有包含同樣文字符號的字符串,會在程序集加載到進程時指向一個全局字符串表的同一實例。這個過程稱為字符串駐留( string interning ),設計目的是優化內存的使用。我們之前已經提過,在 NET Framework 1.1 中,程序集不能選擇是否使用這個過程,盡管未來版本的 CLR 可能會提供這樣的能力。

圖 8 Debugger Memory Window for Object Instance
CLR是怎么創建運行時對象

所以默認情況下,成員變量在源代碼中的詞典順序沒有在內存中保持。在交互操作的情況下,詞典順序必須被保存到內存中,這時可以使用 StructLayoutAttribute 特性,它有一個 LayoutKind 的枚舉類型作為參數。 LayoutKind.Sequential 可以為被封送( marshaled )數據保持詞典順序,盡管在 .NET Framework 1.1 中,它沒有影響托管的布局(但是 .NET Framework 2.0 可能會這么做)。在交互操作的情況下,如果你確實需要額外的填充字節和顯示的控制域的順序, LayoutKind.Explicit 可以和域層次的 FieldOffset 特性一起使用。

看完底層的內存內容后,我們使用 SOS 看看對象實例。一個有用的命令是 DumpHeap ,它可以列出所有的堆內容和一個特別類型的所有實例。無需依賴寄存器, DumpHeap 可以顯示我們創建的唯一一個實例的地址。

!DumpHeap -type SimpleClass
Loaded Son of Strike data table version 5 from
"C:WINDOWSMicrosoft.NETFrameworkv1.1.4322mscorwks.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 的實例只包含一個 DWORD 的對象引用。 SimpleClass 的實例變量只占用 28 字節,其它 8 個字節包括類型句柄( 4 字節)和 syncblk 編號( 4 字節)。找到 simpleObj 實例的地址后,我們可以使用 DumpObj 命令輸出它的內容,如下所示:

!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 );因此類加載器重新排列實例域以最小化填充字節。我們可以使用 ObjSize 來輸出包含被 str 實例占用的空間,如下所示:

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

如果你從對象圖的全局大小( 72 字節)減去 SimpleClass 的大小( 36 字節),就可以得到 str 的大小,即 36 字節。讓我們輸出 str 實例來驗證這個結果:

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

如果你將字符串實例的大小(36字節)加上SimpleClass實例的大小(36字節),就可以得到ObjSize命令報告的總大小72字節。

請注意ObjSize不包含syncblk結構占用的內存。而且,在.NET Framework 1.1中,CLR不知道非托管資源占用的內存,如GDI對象,COM對象,文件句柄等等;因此它們不會被這個命令報告。

指向方法表的類型句柄在syncblk編號后分配。在對象實例創建前,CLR查看加載類型,如果沒有找到,則進行加載,獲得方法表地址,創建對象實例,然后把類型句柄值追加到對象實例中。JIT編譯器產生的代碼在進行方法分派時使用類型句柄來定位方法表。CLR在需要史可以通過方法表反向訪問加載類型時使用類型句柄。

Son of Strike
SOS調試器擴展程序用于本文化的顯示CLR數據結構的內容,它是 .NET Framework 安裝程序的一部分,位于 %windir%\Microsoft.NET\Framework\v1.1.4322。SOS加載到進程之前,在 Visual Studio 中啟用托管代碼調試。 添加 SOS.dll 所在的文件夾到PATH環境變量中。 加載 SOS.dll, 然后設置一個斷點, 打開 Debug|Windows|Immediate。然后在 Immediate 窗口中執行 .load sos.dll。使用 !help 獲取調試相關的一些命令,關于SOS更多信息,參考這里。

方法表

每個類和實例在加載到應用程序域時,會在內存中通過方法表來表示。這是在對象的第一個實例創建前的類加載活動的結果。對象實例表示的是狀態,而方法表表示了行為。通過EEClass,方法表把對象實例綁定到被語言編譯器產生的映射到內存的元數據結構(metadata structures)。方法表包含的信息和外掛的信息可以通過System.Type訪問。指向方法表的指針在托管代碼中可以通過Type.RuntimeTypeHandle屬性獲得。對象實例包含的類型句柄指向方法表開始位置的偏移處,偏移量默認情況下是12字節,包含了GC信息。我們不打算在這里對其進行討論。

圖 9 顯示了方法表的典型布局。我們會說明類型句柄的一些重要的域,但是對于完全的列表,請參看此圖。讓我們從基實例大小(Base Instance Size)開始,因為它直接關系到運行時的內存狀態。

圖 9 方法表布局

CLR是怎么創建運行時對象

基實例大小

基實例大小是由類加載器計算的對象的大小,基于代碼中聲明的域。之前已經討論過,當前GC的實現需要一個最少12字節的對象實例。如果一個類沒有定義任何實例域,它至少包含額外的4個字節。其它的8個字節被對象頭(可能包含syncblk編號)和類型句柄占用。再說一次,對象的大小會受到StructLayoutAttribute的影響。

看看圖3中顯示的MyClass(有兩個接口)的方法表的內存快照(Visual Studio .NET 2003內存窗口),將它和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)

在方法表中包含了一個槽表,指向各個方法的描述(MethodDesc),提供了類型的行為能力。方法槽表是基于方法實現的線性鏈表,按照如下順序排列:繼承的虛方法,引入的虛方法,實例方法,靜態方法。

類加載器在當前類,父類和接口的元數據中遍歷,然后創建方法表。在排列過程中,它替換所有的被覆蓋的虛方法和被隱藏的父類方法,創建新的槽,在需要時復制槽。槽復制是必需的,它可以讓每個接口有自己的最小的vtable。但是被復制的槽指向相同的物理實現。MyClass包含接口方法,一個類構造函數(.cctor)和對象構造函數(.ctor)。對象構造函數由C#編譯器為所有沒有顯式定義構造函數的對象自動生成。因為我們定義并初始化了一個靜態變量,編譯器會生成一個類構造函數。圖10顯示了MyClass的方法表的布局。布局顯示了10個方法,因為Method2槽為接口IVMap進行了復制,下面我們會進行討論。圖11顯示了MyClass的方法表的SOS的輸出。

圖10 MyClass MethodTable Layout
CLR是怎么創建運行時對象

圖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()

任何類型的開始4個方法總是ToString, Equals, GetHashCode, and Finalize。這些是從System.Object繼承的虛方法。Method2槽被進行了復制,但是都指向相同的方法描述。代碼顯示定義的.cctor和.ctor會分別和靜態方法和實例方法分在一組。

方法描述(MethodDesc)

方法描述(MethodDesc)是CLR知道的方法實現的一個封裝。有幾種類型的方法描述,除了用于托管實現,分別用于不同的交互操作實現的調用。在本文中,我們只考察圖3代碼中的托管方法描述。方法描述在類加載過程中產生,初始化為指向IL。每個方法描述帶有一個預編譯代理(PreJitStub),負責觸發JIT編譯。圖12顯示了一個典型的布局,方法表的槽實際上指向代理,而不是實際的方法描述數據結構。對于實際的方法描述,這是-5字節的偏移,是每個方法的8個附加字節的一部分。這5個字節包含了調用預編譯代理程序的指令。5字節的偏移可以從SOS的DumpMT輸出從看到,因為方法描述總是方法槽表指向的位置后面的5個字節。在第一次調用時,會調用JIT編譯程序。在編譯完成后,包含調用指令的5個字節會被跳轉到JIT編譯后的x86代碼的無條件跳轉指令覆蓋。

圖 12方法描述

CLR是怎么創建運行時對象

圖12的方法表槽指向的代碼進行反匯編,顯示了對預編譯代理的調用。以下是在 Method2 被JIT編譯前的反匯編的簡化顯示。

Method2:

!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

在此地址,只有開始5個字節是代碼,剩余字節包含了Method2的方法描述的數據。“!u”命令不知道這一點,所以生成的是錯亂的代碼,你可以忽略5個字節后的所有東西。

CodeOrIL在JIT編譯前包含IL中方法實現的相對虛地址(Relative Virtual Address ,RVA)。此域用作標志,表示是否IL。在按要求編譯后,CLR使用編譯后的代碼地址更新此域。讓我們從列出的函數中選擇一個,然后用DumpMT命令分別輸出在JIT編譯前后的方法描述的內容:

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

編譯后,方法描述的內容如下:

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

方法的這個標志域的編碼包含了方法的類型,例如靜態,實例,接口方法或者COM實現。讓我們看方法表另外一個復雜的方面:接口實現。它封裝了布局過程所有的復雜性,讓托管環境覺得這一點看起來簡單。然后,我們將說明接口如何進行布局和基于接口的方法分派的確切工作方式。

接口虛表圖和接口圖(Interface Vtable Map and Interface Map)

在方法表的第12字節偏移處是一個重要的指針,接口虛表(IVMap)。如圖9所示,接口虛表指向一個應用程序域層次的映射表,該表以進程層次的接口ID作為索引。接口ID在接口類型第一次加載時創建。每個接口的實現都在接口虛表中有一個記錄。如果MyInterface1被兩個類實現,在接口虛表表中就有兩個記錄。該記錄會反向指向MyClass方法表內含的子表的開始位置,如圖9所示。這是接口方法分派發生時使用的引用。接口虛表是基于方法表內含的接口圖信息創建,接口圖在方法表布局過程中基于類的元數據創建。一旦類型加載完成,只有接口虛表用于方法分派。

第28字節位置的接口圖會指向內含在方法表中的接口信息記錄。在這種情況下,對MyClass實現的兩個接口中的每一個都有兩條記錄。第一條接口信息記錄的開始4個字節指向MyInterface1的類型句柄(見圖9圖10)。接著的WORD(2字節)被一個標志占用(0表示從父類派生,1表示由當前類實現)。在標志后的WORD是一個開始槽(Start Slot),被類加載器用來布局接口實現的子表。對于MyInterface2,開始槽的值為4(從0開始編號),所以槽5和6指向實現;對于MyInterface2,開始槽的值為6,所以槽7和8指向實現。類加載器會在需要時復制槽來產生這樣的效果:每個接口有自己的實現,然而物理映射到同樣的方法描述。在MyClass中,MyInterface1.Method2和MyInterface2.Method2會指向相同的實現。

基于接口的方法分派通過接口虛表進行,而直接的方法分派通過保存在各個槽的方法描述地址進行。如之前提及,.NET框架使用fastcall的調用約定,最先2個參數在可能的時候一般通過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編譯器把方法描述的地址直接寫到代碼中。基于接口的分派通過接口虛表發生,和直接分派相比需要一些額外的指令。一個指令用來獲得接口虛表的地址,另一個獲取方法槽表中的接口實現的開始槽。而且,把一個對象實例轉換為接口只需要拷貝this指針到目標的變量。在圖2中,語句“mi1=mc”使用一個指令把mc的對象引用拷貝到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

虛分派總是通過一個固定的槽編號發生,和方法表指針在特定的類(類型)實現層次無關。在方法表布局時,類加載器用覆蓋的子類的實現代替父類的實現。結果,對父對象的方法調用被分派到子對象的實現。反匯編顯示了分派通過8號槽發生,可以在調試器的內存窗口(如圖10所示)和DumpMT的輸出看到這一點。

靜態變量(Static Variables)

靜態變量是方法表數據結構的重要組成部分。作為方法表的一部分,它們分配在方法表的槽數組后。所有的原始靜態類型是內聯的,而對于結構和引用的類型的靜態值對象,通在句柄表中創建的對象引用來指向。方法表中的對象引用指向應用程序域的句柄表的對象引用,它引用了堆上創建的對象實例。一旦創建后,句柄表內的對象引用會使堆上的對象實例保持生存,直到應用程序域被卸載。在圖9 中,靜態字符串變量str指向句柄表的對象引用,后者指向GC堆上的MyString。

EEClass

EEClass在方法表創建前開始生存,它和方法表結合起來,是類型聲明的CLR版本。實際上,EEClass和方法表邏輯上是一個數據結構(它們一起表示一個類型),只不過因為使用頻度的不同而被分開。經常使用的域放在方法表,而不經常使用的域在EEClass中。這樣,需要被JIT編譯函數使用的信息(如名字,域和偏移)在EEClass中,但是運行時需要的信息(如虛表槽和GC信息)在方法表中。

對每一個類型會加載一個EEClass到應用程序域中,包括接口,類,抽象類,數組和結構。每個EEClass是一個被執行引擎跟蹤的樹的節點。CLR使用這個網絡在EEClass結構中瀏覽,其目的包括類加載,方法表布局,類型驗證和類型轉換。EEClass的子-父關系基于繼承層次建立,而父-子關系基于接口層次和類加載順序的結合。在執行托管代碼的過程中,新的EEClass節點被加入,節點的關系被補充,新的關系被建立。在網絡中,相鄰的EEClass還有一個水平的關系。EEClass有三個域用于管理被加載類型的節點關系:父類(Parent Class),相鄰鏈(sibling chain)和子鏈(children chain)。關于圖4中的MyClass上下文中的EEClass的語義,請參考圖13

圖13只顯示了和這個討論相關的一些域。因為我們忽略了布局中的一些域,我們沒有在圖中確切顯示偏移。EEClass有一個間接的對于方法表的引用。EEClass也指向在默認應用程序域的高頻堆分配的方法描述塊。在方法表創建時,對進程堆上分配的域描述列表的一個引用提供了域的布局信息。EEClass在應用程序域的低頻堆分配,這樣操作系統可以更好的進行內存分頁管理,因此減少了工作集。

圖13 EEClass 布局

CLR是怎么創建運行時對象

圖13中的其它域在MyClass(圖3)的上下文的意義不言自明。我們現在看看使用SOS輸出的EEClass的真正的物理內存。在mc.Method1代碼行設置斷點后,運行圖3的程序。首先使用命令Name2EE獲得MyClass的EEClass的地址。

!Name2EE C:WorkingtestClrInternalsSample1.exe MyClass

MethodTable: 009552a0
EEClass: 02ca3508
Name: MyClass

Name2EE的第一個參數時模塊名,可以從DumpDomain命令得到。現在我們得到了EEClass的地址,我們輸出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文件中映射到內存的元數據表的MyClass索引,父類指向System.Object。從相鄰鏈指向名為Program的EEClass,可以知道圖13顯示的是加載Program時的結果。

MyClass有8個虛表槽(可以被虛分派的方法)。即使Method1和Method2不是虛方法,它們可以在通過接口進行分派時被認為是虛函數并加入到列表中。把.cctor和.ctor加入到列表中,你會得到總共10個方法。最后列出的是類的兩個靜態域。MyClass沒有實例域。其它域不言自明。

結論

我們關于CLR一些最重要的內在的探索旅程終于結束了。顯然,還有許多問題需要涉及,而且需要在更深的層次上討論,但是我們希望這可以幫助你看到事物如何工作。這里提供的許多的信息可能會在.NET框架和CLR的后來版本中改變,不過盡管本文提到的CLR數據結構可能改變,概念應該保持不變。

以上就是CLR是怎么創建運行時對象,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

clr
AI

威信县| 宣恩县| 东乌珠穆沁旗| 永胜县| 南投县| 荔浦县| 攀枝花市| 大丰市| 田东县| 榆中县| 莲花县| 长沙县| 定远县| 金寨县| 青州市| 卫辉市| 长丰县| 密云县| 读书| 荣成市| 盖州市| 大化| 鹤壁市| 通道| 磐安县| 揭东县| 三门县| 镇巴县| 霞浦县| 许昌市| 长宁县| 阿拉善右旗| 美姑县| 特克斯县| 二连浩特市| 久治县| 潞西市| 石柱| 武威市| 盐边县| 英山县|