2010-11-27 68 views
5

我想了解CLR如何實現引用類型和多態性。我已經提到了Don Box的Essential .Net Vol 1,這對於驗證大部分內容非常有幫助。但是當我嘗試使用一些IL代碼來更好地理解時,我被以下問題卡住/困惑。callvirt如何在引擎蓋下工作?

我會盡我所能解釋問題。 考慮下面的代碼

class Base 
{ 
    public void m() 
    { 
     Console.WriteLine("Base.m"); 
    } 
} 
class Derived : Base 
{ 
    public void m() 
    { 
     Console.WriteLine("Derived.m"); 
    } 
} 

現在考慮如下所示的主要方法的IL一個簡單的控制檯應用程序。 我調整了IL由編譯器創建的手動理解和與ILAsm.exe

再次組裝
.class private auto ansi beforefieldinit Console1.Program 
     extends [mscorlib]System.Object 
{ 
    .method private hidebysig static void Main(string[] args) cil managed 
    { 
     .entrypoint 
     // Code size  44 (0x2c) 
     .maxstack 1 
     .locals init ([0] class Console1.Base d) 
     nop 
     newobj  instance void Console1.Base::.ctor() 
     stloc.0 
     ldloc.0 
     callvirt instance void Console1.Derived::m() 
     nop 
     call  string [mscorlib]System.Console::ReadLine() 
     pop 
     ret 
    } // end of method Program::Main 
} // end of class Console1.Program 

我期待這個代碼NOT運行作爲對象引用指向基礎的目的,並且沒有方法基礎對象的方法表將具有Derived類中定義的方法m()的條目。

但神奇的是,這段代碼執行Derived.m()!

因此,有兩個問題我不明白,在上面的代碼:

  1. 是什麼類型的意義在下面的IL代碼指定?我試圖通過將其更改爲不同的類型(例如System.Exception !!)來進行試驗,並且不報告錯誤。爲什麼??

    .locals的init([0]類Console1.Base d)

  2. 究竟如何callvirt的作品?呼叫如何被路由到Derived.m()?

在此先感謝!

問候, 阿賈伊

+0

@ulrichb:我不認爲他能做到這一點。它會像`Base b = new Base();((Derived)b).m`類似,除非他真的沒有使用cast(這會引發異常)。 – CodesInChaos 2010-11-27 16:19:51

+2

代碼是否可驗證? – CodesInChaos 2010-11-27 16:20:55

+0

@CodeInChaos:沒有代碼是不可驗證的! PEVerify給出「意外的堆棧類型」錯誤。 – ajay 2010-11-28 03:15:10

回答

5

我的猜測是抖動認識到Derived.m不是虛擬的,因此永遠不會指向其他任何地方。所以callvirt減少到空檢查和呼叫,而不是通過v表呼叫。

嘗試使Derived.m虛擬。我敢打賭它會拋出。

即使調用非虛方法,C#編譯器也會發出callvirt指令,如果它不能證明this!=null,所以它會得到空值檢查。在這種情況下,抖動足夠智能,可以用固定地址(甚至內聯)的普通呼叫代替虛擬呼叫。

而且你應該檢查你的代碼是否可驗證。我認爲這不是。

1

請注意,默認情況下,從本地機器上執行的代碼沒有被證實。這意味着可以編寫和執行無效的代碼。我懷疑你的主要功能不會按原樣傳遞。 PEVerify工具可以檢查程序集以確保代碼是類型安全的,或者可以通過Security Policy Administration從本地計算機或特定位置啓用這些代碼檢查。

locals語句中類型的用途是聲明局部變量的類型。這提供了類型驗證器所需的信息,以驗證成員對本地變量的訪問是否在正確類型的對象上進行操作。

Callvirt可以通過多種方式實現。最有可能的方式是以相同的方式實現C++ vtables:一個對象包含一個函數指針表。每個函數都位於表中預定義的偏移量處。要調用該函數,將加載並調用預定義偏移量處的地址。請注意,在某些情況下,如果對象的類型已知,則CLR可以執行其他優化。這是否完成,我不知道。

1

我認爲這是JIT編譯器優化的副作用。如果m()方法是虛擬的,那麼它必須生成機器代碼以將方法表指針從對象中挖掘出來,然後進行虛擬調用。但是這種方法不是虛擬的,JIT編譯器已經知道Derived類的方法表指針。所以它繞過指針檢索並直接提供。按照您的觀察進行通話。您可以通過檢查生成的機器碼來驗證我的猜測。

是的,IL驗證者在這裏沒有得分。通過讓Derived.m()方法修改只在Derived中聲明的字段,可以使其更有趣。我已經看到太多的Reflection.Emit代碼崩潰與AccessViolation,這是很大的驚喜。然而,它可能是故意的,不需要驗證無論如何都會崩潰的IL。不確定的是,利用這種驗證漏洞還沒有普遍。值得慶幸的。

2

您的代碼無法驗證(通過peverify運行)。我已經寫了一個關於callvirt如何工作的blog post,這可能會幫助您瞭解它的功能以及您的代碼執行方式。

請記住,CLR確實嘗試執行不可驗證的代碼,如果作爲正常程序運行;只有在實際上導致問題時纔會產生問題。

在您的示例中,在Base實例上調用Derived.m()可以工作,因爲對象實例的實際運行時二進制表示形式相同; this對象基本相同,並且不訪問對象的實例字段。

嘗試把一個實例字段訪問到這兩種方法,看看會發生什麼......