ValueTpye boxing及虛方法重寫及CallVirt指令實現解析
問題的提出,是源自Justin提出的一個case里面的一個問題,討論了n久沒得到一個答案,昨天justin周一早上一起來就又回憶起了這個問題,看來一直把這個問題放在腦子里面沒有放下,佩服啊佩服 ^_^ 遂決定深入研究一番,下面是問題的提出:
Boxed value type
In C#, the value type instance having pure user data is resided at stack without any type pointer. In some case, the value need be boxed, then a new boxed object is created from heap. My questions are:
l Assume the value type overrides an virtual function, such as ToString(). When call the function using value instance, the this points to the pure user data part of value instance in stack, however, if calling function using boxed value object, the this should point to the beginning of heap object (the type pointer, four bytes before pure user data). How does the ToString() implementation of value type distinguish between the two cases?
From debug of assembly instruction, I find the ToString() function always gets this pointing to the pure user data part even if it’s called from boxed object. But I have no idea which code move the this pointer forward four bytes?
首先,對于什么是值類型啥是引用類型,及其區別,以及派生結構層次,不屬于這里的話題,探討主要圍繞針對上面的這個問題的提出展開。
可以定義一個Struct的類型,來實現對ValueType的繼承。這個從Struct繼承出來的ValueType,還可以重寫從基類繼承的方法,一共只有三個方法可供重寫:Equals(),
GetHashCode(),ToString()。
好吧,看看上面的問題,首先解釋下上面的問題提出之前的一段文字:
Assume the value type overrides an virtual function, such as ToString(). When call the function using value instance, the this points to the pure user data part of value instance in stack, however, if calling function using boxed value object, the this should point to the beginning of heap object (the type pointer, four bytes before pure user data).
厄,首先明確一下,這段話是沒有任何問題的。我也曾懷疑過這句話里面的細微的地方的觀點,但是事實證明我是錯誤的…..
來構造一個所提出問題的案例先:
class Program
{
static void Main(string[] args)
{
TestValueType testValueType = new TestValueType(1214926);
Console.WriteLine(testValueType.ToString());
Object o = testValueType;
Console.WriteLine(o.ToString());
int i = 12345;
Console.WriteLine(i.ToString());
Console.ReadLine();
}
internal struct TestValueType
{
private int i;
public TestValueType(int initValue)
{
i = initValue;
}
//Can only override 3 Methods:Equals(),GetHashCode(),ToString()
public override string ToString()
{
return "Valuetype virtual method tostring() Override.";
}
}
}
對于提出的第一個問題:
How does the ToString() implementation of value type distinguish between the two cases?
我想,這個地方不僅僅是two case,而是有四種case:
1. 一個自定義的值類型沒有重寫tostring()方法的時候,對tostring的調用是如何實現的?tostring()方法的實現是存儲在什么地方的?
2. 一個自定義的值類型override了virtual的base class的tostring方法之后,對tostring的調用是如何進行的?這個tostring的實現是存放在什么地方的?也就是調用的哪個實現,如何調用的問題。
3. 一個boxed了的自定義的值類型的boxed狀態下,沒有重寫tostring方法的時候,這個tostring是如何調用的?
4. 上面的一個問題,一個重寫了tostring方法的自定義valuetype在boxed狀態下面,tostring的調用是如何實現的,存放在哪兒。
Ok,這個是回答他的問題的序言,當然,不是全部,還有Managed Pointer,Instance Pointer,this指針的關系callvirt的具體細節和幾個il指令背著我做的小動作問題..
好吧,解決這些問題,先從il語言入手,下面是Main方法的反編譯之后的il代碼:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 78 (0x4e)
.maxstack 2
.locals init ([0] valuetype ValutTypeTest.Program/TestValueType
testValueType,
[1] object o,
[2] int32 i)
IL_0000: nop
IL_0001: ldloca.s testValueType
IL_0003: ldc.i4 0x1289ce
IL_0008: call instance void
ValutTypeTest.Program/TestValueType::.ctor(int32)
IL_000d: nop
IL_000e: ldloca.s testValueType
IL_0010: constrained. ValutTypeTest.Program/TestValueType
IL_0016: callvirt instance string [mscorlib]System.Object::ToString()
IL_001b: call void [mscorlib]System.Console::WriteLine(string)
IL_0020: nop
IL_0021: ldloc.0
IL_0022: box ValutTypeTest.Program/TestValueType
IL_0027: stloc.1
IL_0028: ldloc.1
IL_0029: callvirt instance string [mscorlib]System.Object::ToString()
IL_002e: call void [mscorlib]System.Console::WriteLine(string)
IL_0033: nop
IL_0034: ldc.i4 0x3039
IL_0039: stloc.2
IL_003a: ldloca.s i
IL_003c: call instance string [mscorlib]System.Int32::ToString()
IL_0041: call void [mscorlib]System.Console::WriteLine(string)
IL_0046: nop
IL_0047: call string [mscorlib]System.Console::ReadLine()
IL_004c: pop
IL_004d: ret
} // end of method Program::Main
首先說說幾個pointer的關系。
對于一個放在計算堆棧里面的value type或者是自定義value type來說,如果想要使用這個類型里面的fields或者是members,需要提供Managed Pointer。也就是&valuetypename的值。在C++里面,沒有類似的Dotnet的值類型之內的概念,所以,這里的Managed Pointer就相當于一個object的instance Pointer,或者說是instance reference。對于Value Type的Managed Pointer,指向的是計算stack上面的data part。而對于一個對象來說,this pointer(instance Pointer)指向的是Obj ref,一個四個字節的地方。這個地方存放的數據,指向的是MethodTable。這四個字節的上面,是控制同步的一個塊和一個Object Header,這四個字節的下面,就是instance fields,包括一些fields和一些member方法的實現。
This指針在對value type的時候,指向的是stack上面的數據部分。
對于il的一個call方法指令,不管是value type,還是ref type,有的時候是需要指針的,有的時候不需要this指針。Metadata里面并不包含this指針,但是方法的簽名,使用一個叫做HASTHIS的bit位來標識是否需要this指針:“Signature: If the method is static, then the HASTHIS (0x20) bit in the calling convention shall be 0”。具體說來,如果一個方法的實現,是保持在type相關的內存中的,就不要這個標識位,如果每個type的instance保存一份實現,就用這個標識。區別這兩種情況,可以用C#里面的Stact關鍵字。
對于:
Object o = testValueType;
Console.WriteLine(o.ToString());
Il代碼是:
IL_000e: ldloca.s testValueType
IL_0010: constrained. ValutTypeTest.Program/TestValueType
IL_0016: callvirt instance string [mscorlib]System.Object::ToString()
IL_001b: call void [mscorlib]System.Console::WriteLine(string)
Constrained前綴修飾符,是一個非常有意思的修飾符,他必須和callvirt關鍵字一起使用,首先看起Stack轉換:
constrained. thisType(這里是ValutTypeTest.Program/TestValueType)
Stack Transition:
之前:…, ptr, arg1, … argN ? 之后:…, ptr, arg1, … argN
Ms沒有什么變換,但是Ptr(Managed Pointer)的內容,卻是有一個轉換邏輯:
l If thisType is a reference type (as opposed to a value type) then,ptr is dereferenced and passed as the ‘this’ pointer to the callvirt of method
l If thisType is a value type and thisType implements method then,ptr is passed unmodified as the ‘this’ pointer to a call of method implemented by thisType
l If thisType is a value type and thisType does not implement method then,ptr is dereferenced, boxed, and passed as the ‘this’ pointer to the callvirt of method
這樣,就得到了問題的回答:
對于一個value type(或者自定義的value type),仍然可以調用由類型繼承或者是從重寫的虛方法。比如Equals,GetHashCode,或者Tostring,因為CLR正好可以以非虛的方式調用這些方法。
如果重寫了一個自定義的value type類型的tostring方法,首先,this pointer(managed Pointer)指向的是stack上面的這個變量的data field,數據開始部分。然后在調用tostring方法的時候,首先讓constrained來檢驗下,接著發現了這個自定義的value type實現了tostring這個方法,好,這個時候不執行boxing的動作,直接采用il指令里面的call指令,然后直接調用stack上面的這個value type的data filed之后的tostring方法。
這種情況下,是沒有MethodTable,不和MethodTable進行交互。很重要的一點,值類型隱式為sealed類型,所以不可將一個值類型做為另外一個類型的基類使用。
如果自定義的value type類型,沒有實現了tostring方法,這個時候,constrained前綴的處理邏輯是,根據“ldloca.s testValueType”剛剛放到stack上面的value type的managed pointer,先把這個value type boxing,裝箱,然后把在heap上面新創建的object的ref,替換掉這個ptr(value type的managed pointer),(很重要的一點:這個時候,this指針指向的是obj ref)。這個時候,就換成了使用callvirt指令調用。這個時候,ptr上面的內容,就是object ref,heap上面的value type的instace fields的前四個字節的地方。這個obj ref里面的內容,指向的就是MethodTable。這個時候調用virtual方法,需要通過Vtable map,來具體定位到使用那個基類的方法。
尋找的流程是先看看當前的instance里面實現了相同簽名的方法沒,如果沒有,就找基類或者父類里面的相同簽名的。
如果,calling function using boxed value object,這個時候是如何實現的呢?還是上面的C#代碼,當如下面操作的時候:
Object o = testValueType;
Console.WriteLine(o.ToString());
不管這個時候testValueType實現沒實現tostring方法,這個時候,想當于直接調用一個Object Type的某個方法,是通過走MethodTable來尋找其實現的。
這種情況下,無論如何,在剛剛準備執行這個方法之前,this指針的內容,是一個Ref Type的instance fields的開始的部分。而具體的使用哪個方法,則是根據MethodTable來的。
另外一點需要了解,CLR的特性里面,提供可以以非虛的方式調用從類型或者基類繼承的方法。而且System.ValueType重寫了這些虛方法,由struct定義的自定義value type沒重寫這些方法。
了解了這點,如何還有疑惑的話,可以看下下面這個有助于理解的sample:
.class public value XXX
{
.method public void YYY( )
{
...
}
.method public virtual void ZZZ( )
{
...
}
}
.method public static void Exec( )
{
.entrypoint
.locals init(valuetype XXX xxx) // Variable xxx is an Instance of XXX
ldloca xxx // Load managed ptr to xxx
call instance void XXX::YYY( ) // Legal: access to value
// type member
// by managed ptr
ldloca xxx
callvirt instance void XXX::ZZZ( ) // Illegal: virtual call of
// methods possible only
// by object reference.
ldloca xxx
call instance void XXX::ZZZ( ) // Legal: nonvirtual call, access to value type member
// by managed ptr.
ldloc xxx // Load instance of XXX.
box valuetype XXX // Convert it to object reference.
callvirt instance void XXX::ZZZ( ) // Legal
...
}
這時,就涉及到文章最開始提出的第二個問題:
From debug of assembly instruction, I find the ToString() function always gets this pointing to the pure user data part even if it’s called from boxed object. But I have no idea which code move the this pointer forward four bytes?
這里,有一個比較重要的特性,也是callvirt指令來實現的,在具體調用每個方法開始的時候之前,callvirt實現了一個justin和問題的提出者,叫做“this指針偏移”的功能。當然,文章到此為止,還沒有證明這一點。
這個解釋邏輯,也是參考了大量的資料之后得到的一個假設吧。Callvirt,在執行的時候,有很多種不同的情況下都可以調用callvirt,譬如interface,虛方法,多態等等,callvirt會進行一個判斷,來判斷具體是哪種情況。如果是我們上面的對boxed value type的情況,就有一個this指針偏移的處理邏輯。
為了證明這點,咱可以參考Rotor是如何實現callvirt方法的:
首先查看Fjit.cpp的實現,這個頁面有萬把行,實現了大部分IL指令具體做了些啥。
switch (opcode)
{
case CEE_CALLVIRT:
JitResult = compileCEE_CALLVIRT();
break;
}
好吧,查看compileCEE_CALLVIRT的實現:
FJitResult FJit::compileHelperCEE_CALLVIRT(unsigned int token,
bool isReadOnly /* = false */)
{
jitInfo->getCallInfo(methodInfo->ftn,
tokenScope,
token,
0, // constraintToken -
tokenContext,
CORINFO_CALLINFO_CALLVIRT,
&virtCallInfo);
if (virtCallInfo.kind == CORINFO_VIRTUALCALL_LDVIRTFTN)
{
int this_ptr = findOffsetOfThisPtr(targetSigInfo);
emit_getSP((STACK_BUFFER + this_ptr - SIZE_STACK_SLOT));
emit_LDIND_I4(false);
emit_ldvirtftn_helper(token, jitInfo->getMemberParent(methodInfo->scope, token));
emit_save_TOS(); // squirel away the target ftn address
emit_POP_PTR(); // and remove from stack
}
argBytes = buildCall(&targetSigInfo, CALL_NONE, stackPadorRetBase, false);
sizeRetBuff = targetSigInfo.hasRetBuffArg() ? typeSizeInBytes(jitInfo, targetSigInfo.retTypeClass) : 0;
_ASSERTE (virtCallInfo.kind != CORINFO_CALL_CODE_POINTER);
if (virtCallInfo.kind == CORINFO_VIRTUALCALL_LDVIRTFTN)
{
emit_restore_TOS(); //push the saved target ftn address
// Now we can use the sequence for CALLI.
emit_calli(targetSigInfo.hasRetBuffArg() ? typeSizeInBytes(jitInfo,
targetSigInfo.retTypeClass) : 0);
}
else if (virtCallInfo.kind == CORINFO_VIRTUALCALL_STUB)
{
_ASSERTE (!virtCallInfo.stubLookup.lookupKind.needsRuntimeLookup);
_ASSERTE (virtCallInfo.stubLookup.constLookup.addr != NULL);
_ASSERTE(virtCallInfo.stubLookup.constLookup.accessType == IAT_PVALUE);
emit_call_stub((unsigned int) virtCallInfo.stubLookup.constLookup.addr);
}
else if (virtCallInfo.kind == CORINFO_CALL)
{
if (virtCallInfo.nullInstanceCheck)
{
emit_check_null_reference(false);
}
CORINFO_CONST_LOOKUP addrInfo;
jitInfo->getFunctionEntryPoint(targetMethod, IAT_VALUE, &addrInfo);
VALIDITY_CHECK(addrInfo.addr);
VALIDITY_CHECK(addrInfo.accessType == IAT_VALUE ||
addrInfo.accessType == IAT_PVALUE);
emit_callnonvirt((unsigned)addrInfo.addr, sizeRetBuff, addrInfo.accessType == IAT_PVALUE);
}
else if (virtCallInfo.kind == CORINFO_VIRTUALCALL_VTABLE)
{
if (jitInfo->getClassAttribs(targetClass,methodInfo->ftn) &
CORINFO_FLG_INTERFACE)
{
offset = jitInfo->getMethodVTableOffset(targetMethod);
_ASSERTE(!(methodAttribs & CORINFO_FLG_EnC));
unsigned InterfaceTableOffset;
InterfaceTableOffset = jitInfo->getInterfaceTableOffset(targetClass);
emit_callinterface_new(InterfaceTableOffset*4,
offset, sizeRetBuff );
}
else
{
offset = jitInfo->getMethodVTableOffset(targetMethod);
_ASSERTE(!(methodAttribs & CORINFO_FLG_DELEGATE_INVOKE));
emit_callvirt(offset, sizeRetBuff);
}
}
}
這里只截取了最后的一段,前面的完整性檢查之內的略掉。virtCallInfo,看到這樣的結構,字眼和判斷,和咱估計的情況差不多。然后來查看CORINFO_CALL_KIND這個結構體的定義:
enum CORINFO_CALL_KIND
{
CORINFO_CALL,
//下面的兩個CallVirt指令里面沒用
CORINFO_CALL_CODE_POINTER,
CORINFO_VIRTUALCALL_RESOLVED,
CORINFO_VIRTUALCALL_STUB,
CORINFO_VIRTUALCALL_LDVIRTFTN,
CORINFO_VIRTUALCALL_VTABLE
};
轉到corinfo.h文件里面,getCallInfo and CORINFO_CALL_INFO,這兩個東西是EE用來指示Fjit如何具體編譯不同情況下面的callvirt指令。
查看都是什么情況下使用不同的結構體,發現了
CORINFO_VIRTUALCALL_LDVIRTFTN這種情況下對this指針偏移的支持:
//找到this指針的地址
int this_ptr = findOffsetOfThisPtr(targetSigInfo);
//減去四個字節指向到instance fields
// #define SIZE_STACK_SLOT 4
emit_getSP((STACK_BUFFER + this_ptr - SIZE_STACK_SLOT));
emit_LDIND_I4(false);
emit_ldvirtftn_helper(token, jitInfo->getMemberParent(methodInfo->scope, token));
emit_save_TOS(); // squirel away the target ftn address
emit_POP_PTR(); // and remove from stack
emit_getSP方法最終指向了xp平臺下x86fjit.h的實現:
// push a pointer pointing 'n' bytes back in the stack
#define x86_getSP(n) deregisterTOS;
if (n == 0)
x86_mov_reg(x86DirTo, x86Big, x86_mod_reg(X86_EAX, X86_ESP));
else
x86_lea(x86_mod_base_scale_disp(X86_EAX, X86_ESP,
X86_NO_IDX_REG, n, 0));
inRegTOS = true;
最后,再說下,再看這些il指令的實現的時候,發現constrained的實現比較有意思,也就是基于上面給出的三種判斷邏輯情況下,使用了一個結構體:
enum CORINFO_THIS_TRANSFORM
{
CORINFO_NO_THIS_TRANSFORM,
CORINFO_BOX_THIS,
CORINFO_DEREF_THIS
};
來支持三種不同情況下面是this指針的轉換方式:
switch (callInfo.thisTransform)
{
case CORINFO_NO_THIS_TRANSFORM:
{
//不需要改變this指針(managed Pointer,ptr)的情況下直接調用call指令
return this->compileHelperCEE_CALL(funcToken,
callInfo.targetMethodHandle,false /*readonly*/);
}
//根據managed ptr的內容來裝箱(If thisType is a value type and thisType implements //method)
case CORINFO_BOX_THIS:
{
CORINFO_SIG_INFO targetSigInfo;
jitInfo->getMethodSig(callInfo.targetMethodHandle, &targetSigInfo);
// this is slightly ineffecient, especially when dealing with large
// valuetypes but effeciency is not paramount in fjit
// {... , objPtr, args} -> {..., objPtr, args, objPtr }
copyPtrUpAroundArgs(targetSigInfo);
// {..., objPtr, args, objPtr } -> {..., objPtr, args, *objPtr }
if( (retval = this->compileHelperCEE_LDOBJ(constraintToken)) != FJIT_OK)
return retval;
// {..., objPtr, args, *objPtr } -> {..., objPtr, args, boxedPtr }
if( (retval = this->compileHelperCEE_BOX(constraintToken)) != FJIT_OK)
return retval;
// {..., objPtr, args, boxedPtr } -> {... , boxedPtr, args}
copyPtrDownAroundArgs(targetSigInfo);
return this->compileHelperCEE_CALLVIRT(funcToken);
}
//最后一種情況(If thisType is a value type and thisType does not implement method),直接
//調用CALLVIRT
case CORINFO_DEREF_THIS:
{
CORINFO_SIG_INFO targetSigInfo;
jitInfo->getMethodSig(callInfo.targetMethodHandle, &targetSigInfo);
// {... , &this, args} -> {..., &this, args, &this }
copyPtrUpAroundArgs(targetSigInfo);
// it was a reference type
if( (retval = this->compileCEE_LDIND_REF()) != FJIT_OK)
return retval;
// {... , &this, args, this} -> {..., this, args}
copyPtrDownAroundArgs(targetSigInfo);
return this->compileHelperCEE_CALLVIRT(funcToken);
}
文章寫的匆忙,很多看資料的時候看到的細節可能忘記了沒寫上。歡迎大伙討論
有寫的不正確的地方,歡迎指正。^_^
6/24/2008 4:57:24 PM lbq1221119
posted on 2008-06-24 16:59 lbq1221119 閱讀(2389) 評論(15) 收藏 舉報
浙公網安備 33010602011771號