找回密码
 立即注册
查看: 607|回复: 0

[笔记] Unity mono代码结构分析及阅读(九)——new操作符的实现

[复制链接]
发表于 2020-12-28 10:32 | 显示全部楼层 |阅读模式
这是基于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的操作就研究完啦,我们下次再见,拜拜。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-23 00:00 , Processed in 0.065767 second(s), 24 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表