|
这是基于unity mono代码阅读的第八篇。
上面一系列文章分析了mono CLR如何翻译CL代码到机器码,并且通过一系列示例代码演示了一些面向对象的特性在il里是如何实现的。
本文会着重分析一下mono CLR内的另外一个重量级功能,关于object new的实现,并尝试在此基础上,做一个简单Memory Profiler。
本文不涉及底层的贝姆gc的实现,如果对这块有兴趣的可以查看一下西山居测试团队的文章
好,那我们就开始把。
还是按照惯例拿出我们前面一直用的测试工程。正好里面有Class A的new操作。直接开始调试
using System;
using System.Collections.Generic;
using System.Text;
namespace TestCPlus
{
class A
{
virtual public void Say()
{
System.Console.WriteLine("I Am A.");
}
}
class B:A
{
override public void Say()
{
System.Console.WriteLine("I Am B.");
}
}
class Program
{
static void Main()
{
A a= new B();
a.Say();
}
}
}
其实对于托管代码内的操作,可能有同学会存在无从下手的情况。的确,CLR的托管环境内的互相调用已经进入到了虚拟机的内部,实际操作可能与直观理解差异巨大。但是我们还是可以用我们前面常用的几个方法来解决。
第一个是google大法,在多种姿势搜索后,我们可以了解到如下的一个操作。
/* we usually get the class we need during initialization */
MonoImage *image = mono_assembly_get_image (assembly);
MonoClass *my_class = mono_class_from_name (image, "MyNamespace", "MyClass");
...
/* allocate memory for the object */
MonoObject *my_class_instance = mono_object_new (domain, my_class);
/* execute the default argument-less constructor */
mono_runtime_object_init (my_class_instance);
甚至mono都说了
For more complex constructors or if you want to have more control of the execution of the constructor, you can usemono_runtime_invoke()as explained in the previous section, after getting the MonoMethod* representing the constructor:
也就是说mono_runtime_object_init只是负责找到并初始化构造函数的操作。如果想不走正常的构造操作,可以直接调用mono_runtime_invoke手工初始化。也就是mono_runtime_object_init是对mono_runtime_invoke的一个封装。我们看代码也的确如此
void
mono_runtime_object_init (MonoObject *this)
{
MonoMethod *method = NULL;
MonoClass *klass = this->vtable->klass;
method = mono_class_get_method_from_name (klass, ".ctor", 0);
g_assert (method);
if (method->klass->valuetype)
this = mono_object_unbox (this);
mono_runtime_invoke (method, this, NULL, NULL);
}
接下来我们看最重要的
/**
* mono_object_new:
* @klass: the class of the object that we want to create
*
* Returns: a newly created object whose definition is
* looked up using @klass. This will not invoke any constructors,
* so the consumer of this routine has to invoke any constructors on
* its own to initialize the object.
*
* It returns NULL on failure.
*/
MonoObject *
mono_object_new (MonoDomain *domain, MonoClass *klass)
{
MonoVTable *vtable;
MONO_ARCH_SAVE_REGS;
vtable = mono_class_vtable (domain, klass);
if (!vtable)
return NULL;
return mono_object_new_specific (vtable);
}
mono_object_new是官方文档推荐的mono api,我们可以调用这个函数来生成一个新的MonoObject。其实看代码比较简单。一路跟下去大概是这样子的
mono!mono_object_new
->mono!mono_object_new_specific
-->mono!mono_object_new_alloc_specific
--->贝姆GC提供的malloc里面比较有意思的是mono_object_new_alloc_specific这个函数。
MonoObject *
mono_object_new_alloc_specific (MonoVTable *vtable)
{
MonoObject *o;
if (!vtable->klass->has_references)
{
o = mono_object_new_ptrfree (vtable);
}
else if (vtable->gc_descr != GC_NO_DESCRIPTOR)
{
o = mono_object_allocate_spec (vtable->klass->instance_size, vtable);
}
else
{
/* printf("OBJECT: %s.%s.\n", vtable->klass->name_space, vtable->klass->name); */
o = mono_object_allocate (vtable->klass->instance_size, vtable);
}
if (G_UNLIKELY (vtable->klass->has_finalize))
mono_object_register_finalizer (o);
if (G_UNLIKELY (profile_allocs))
mono_profiler_allocation (o, vtable->klass);
return o;
}
里面对是否还有引用(has_references),还有GC的描述符都进行了处理
if (G_UNLIKELY (profile_allocs))
mono_profiler_allocation (o, vtable->klass);
这一段也很有意思,mono会对所有的allocation做一个记录,记录下当前申请后返回的MonoObject o,和当前的申请的类所在的信息。如果有兴趣可以在这个mono_profiler_allocation里面做个回调,处理一下对应的信息,打日志或者做个数组存起来都是很不错的方法。然后mono的new的操作就这样结束了,是的你没看错,就这样结束了。。
如果你只是调用mono_object_new来申请mono对象的话,到此,你的对象已经申请好了。
可能有同学会问,但是上文中
static void Main()
{
A a= new B();
a.Say();
}
new B()这个操作是怎样在mono CLR里面解码并且触发mono_object_new的操作一点都没有讲来着。
不错,对于new B()这个C#代码编程CIL后怎样在CLR内执行的逻辑我们还没有涉及。对于IL的解码我们还是看mono_method_to_ir这个函数,我们在函数内搜搜是否有我们感兴趣的地方。比如有new 或者object的操作码。随便写个CEE_NewObject。一下就搜到了
case CEE_NEWOBJ: {
MonoInst *iargs [2];
MonoMethodSignature *fsig;
MonoInst this_ins;
MonoInst *alloc;
MonoInst *vtable_arg = NULL;
CHECK_OPSIZE (5);
token = read32 (ip + 1);
cmethod = mini_get_method (cfg, method, token, NULL, generic_context);
if (!cmethod)
goto load_error;
fsig = mono_method_get_signature (cmethod, image, token);
if (!fsig)
goto load_error;
mono_save_token_info (cfg, image, token, cmethod);
...
}
你看,这个代码一看就很靠谱,我们结合一下生产的CIL代码阅读更佳
.method /*06000006*/ private hidebysig static
void Main() cil managed
// SIG: 00 00 01
{
// Method begins at RVA 0x211c
// Code size 13 (0xd)
.maxstack 2
.locals /*11000001*/ init (class TestCPlus.A/*02000002*/ V_0)
IL_0000: /* 73 | (06)000003 */ newobj instance void TestCPlus.B/*02000003*/::.ctor() /* 06000003 */
IL_0005: /* 0A | */ stloc.0
IL_0006: /* 06 | */ ldloc.0
IL_0007: /* 6F | (06)000002 */ callvirt instance void TestCPlus.A/*02000002*/::Say() /* 06000002 */
IL_000c: /* 2A | */ ret
} // end of method Program::Main
我们在这个分支处打上断点,然后开始调试。
token = read32 (ip + 1);
这里里面最精髓的代码就是这一句。token = read32(ip +1)
取出来的Token 为0x6000003也就跟我们刚刚il代码里面的这句对应
IL_0000: /* 73 | (06)000003 */ newobj instance void TestCPlus.B/*02000003*/::.ctor()
/* 06000003 */那么(06) 000003到底是什么呢?
cmethod = mini_get_method (cfg, method, token, NULL, generic_context);
这句操作是从token里面取到了一个方法,那么怎样可以从token转换到方法呢?仔细跟下去会发现如下这个方法
MonoMethod *
mono_get_method_full (MonoImage *image, guint32 token, MonoClass *klass,
MonoGenericContext *context)
{
MonoMethod *result;
gboolean used_context = FALSE;
/* We do everything inside the lock to prevent creation races */
mono_image_lock (image);
if (mono_metadata_token_table (token) == MONO_TABLE_METHOD) {
if (!image->method_cache)
image->method_cache = g_hash_table_new (NULL, NULL);
result = g_hash_table_lookup (image->method_cache, GINT_TO_POINTER (mono_metadata_token_index (token)));
} else {
if (!image->methodref_cache)
image->methodref_cache = g_hash_table_new (NULL, NULL);
result = g_hash_table_lookup (image->methodref_cache, GINT_TO_POINTER (token));
}
mono_image_unlock (image);
if (result)
return result;
result = mono_get_method_from_token (image, token, klass, context, &used_context);
if (!result)
return NULL;
mono_image_lock (image);
if (!used_context && !result->is_inflated) {
MonoMethod *result2;
if (mono_metadata_token_table (token) == MONO_TABLE_METHOD)
result2 = g_hash_table_lookup (image->method_cache, GINT_TO_POINTER (mono_metadata_token_index (token)));
else
result2 = g_hash_table_lookup (image->methodref_cache, GINT_TO_POINTER (token));
if (result2) {
mono_image_unlock (image);
return result2;
}
if (mono_metadata_token_table (token) == MONO_TABLE_METHOD)
g_hash_table_insert (image->method_cache, GINT_TO_POINTER (mono_metadata_token_index (token)), result);
else
g_hash_table_insert (image->methodref_cache, GINT_TO_POINTER (token), result);
}
mono_image_unlock (image);
return result;
}
代码看起来超级长,但是我们简单看看,是不是跟我们第七篇文章里面分析的取meta的逻辑很相似?赶紧打开CFF_Exploer看看文件的MetaData
(06)000003实际上就是MetaData的Table 06 方法表里面的第000003个方法。也就是我们上文里面的Class B的构造函数。
IL_0000: /* 73 | (06)000003 */ newobj instance void TestCPlus.B至此,关于CLR里面如何解析New obj代码并取到要new的class的方法已经知道了。剩下的就是如何调用到mono_object_new,我们打一下断点,直接F5一下。
00 mono!mono_object_new_alloc_specific
01 mono!mono_object_new_specific+0x127
02 mono!mono_object_new+0x31
03 mono!mono_type_get_object+0x385
04 mono!mono_class_create_runtime_vtable+0xacb
05 mono!mono_class_vtable_full+0xaf
06 mono!mono_class_create_runtime_vtable+0xab5
07 mono!mono_class_vtable_full+0xaf
08 mono!mono_class_vtable+0x12
09 mono!mono_method_to_ir+0x245ce
最终在mono_method_to_ir会触发handle_alloc_from_inst
else if (context_used)
{
MonoInst *data;
int rgctx_info;
if (cfg->opt & MONO_OPT_SHARED)
rgctx_info = MONO_RGCTX_INFO_KLASS;
else
rgctx_info = MONO_RGCTX_INFO_VTABLE;
data = emit_get_rgctx_klass (cfg, context_used, cmethod->klass, rgctx_info);
alloc = handle_alloc_from_inst (cfg, cmethod->klass, data, FALSE);
*sp = alloc;
}
然后会触发mono_class_vtable的操作,最终会调用到mono_object_new。有兴趣的同学可以研究一下。
if (G_UNLIKELY (profile_allocs))
mono_profiler_allocation (o, vtable->klass);
有兴趣的同学可以关注一下上文提到的mono_profiler_allocation,并查找一下mono_profiler_allocation被引用的地方。看mono对于数组和string new操作的处理是怎样的。
下面我们来实现一个简单的Memory Profiler
MonoObject *
mono_object_new_alloc_specific (MonoVTable *vtable)
{
MonoObject *o;
if (!vtable->klass->has_references) {
o = mono_object_new_ptrfree (vtable);
} else if (vtable->gc_descr != GC_NO_DESCRIPTOR) {
o = mono_object_allocate_spec (vtable->klass->instance_size, vtable);
} else {
o = mono_object_allocate (vtable->klass->instance_size, vtable);
}
if (G_UNLIKELY (vtable->klass->has_finalize))
mono_object_register_finalizer (o);
printf("\n==================OBJECT: Stack Start======================");
mono_jit_walk_stack(print_stack_frame, TRUE, stdout);
printf("\n==================OBJECT: Stack End======================");
printf("\nOBJECT: %s.%s. Size %d \n", vtable->klass->name_space, vtable->klass->name, mono_object_get_size(o));
if (G_UNLIKELY (profile_allocs))
mono_profiler_allocation (o, vtable->klass);
return o;
}
一个朴实无华的printf就可以玩出花来,希望读者也可以动手实践一下。
至此我们关于CLR内new的操作就研究完啦,我们下次再见,拜拜。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|