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

[笔记] Unity mono代码结构分析及阅读(七)——封装、继承、重载及MetaData

[复制链接]
发表于 2020-11-29 10:53 | 显示全部楼层 |阅读模式
这是基于unity mono代码阅读的第七篇。
上文已经大致分析了mono runtime怎样解码CIL指令并详细的分析了一下mono将CIL翻译到对应平台机器码的过程。不过上文的分析只是局限在Add这样的操作符和简单的Call这样的简单跳转操作的逻辑。
在上文的测试环境中,我们只是用了一个简单的System.Console.WriteLine系统调用来分析了一下。如果真的想深究一下,那么面向对象的语言总归会有那么几个东西绕不过去。
比如说,封装、继承、重载,重写,多态,泛型。其实重载和泛型理论上都属于多态的一种,有兴趣可以站内查看
说实话,上面的一些听起来耳熟能详的名词,在CLR实现的时候,会有各种各样的技术细节需要深究。这技术细节在阅读代码的时候就像一坨一坨奇怪的东西挡在路上,他认得你,你不认得他,虽然跳过去不影响整体阅读,但是不看的话,又会觉得有点不对劲。
今天我们就尝试阅读一下这一部分。由于多态的特殊性,所以会将重载,重写和泛型分开来讲,今天先讲讲重载和重写。
先从一个函数开始。
在我们调试的时候,经常会遇到一些从托管代码调过来的情况,比如下面这样
这两个奇怪的堆栈就是mono JIT引擎动态生成的代码段。这个代码段我们上文也讲过,都是又一些小的手写的机器码拼接而成,在用到一些跳转的时候会加入一些Trampoline,来完成这些跳转指令操作。
那么我们如果想知道这两个托管的代码段到底是什么函数,就需要mono_pmip函数登场了
  1. /**
  2. * mono_pmip:
  3. * @ip: an instruction pointer address
  4. *
  5. * This method is used from a debugger to get the name of the
  6. * method at address @ip.   This routine is typically invoked from
  7. * a debugger like this:
  8. *
  9. * (gdb) print mono_pmip ($pc)
  10. *
  11. * Returns: the name of the method at address @ip.
  12. */
  13. G_GNUC_UNUSED char *
  14. mono_pmip (void *ip)
  15. {
  16.         return get_method_from_ip (ip);
  17. }
复制代码
mono_pmip是个很有趣的函数,你可以在调试的时候,通过调用这个函数来查看托管地址段跟托管函数的关系。
比如笔者在用windbg调试的时候就可以使用.call mono!mono_pmip(0x2e10044)这个命令来查看当前托管代码段到底是哪个函数。当然,有时候也不那么好用。。。不过这不重要。。
我们直接看函数get_method_from_ip的实现
  1. /* debug function */
  2. G_GNUC_UNUSED static char*
  3. get_method_from_ip (void *ip)
  4. {
  5.     MonoJitInfo *ji;
  6.     char *method;
  7.     char *res;
  8.     MonoDomain *domain = mono_domain_get();
  9.     MonoDebugSourceLocation *location;
  10.     FindTrampUserData user_data;
  11.     ji = mono_jit_info_table_find(domain, ip);
  12.     if (!ji)
  13.     {
  14.         user_data.ip = ip;
  15.         user_data.method = NULL;
  16.         mono_domain_lock(domain);
  17.         g_hash_table_foreach(domain_jit_info(domain)->jit_trampoline_hash, find_tramp, &user_data);
  18.         mono_domain_unlock(domain);
  19.         if (user_data.method)
  20.         {
  21.             char *mname = mono_method_full_name(user_data.method, TRUE);
  22.             res = g_strdup_printf("<%p - JIT trampoline for %s>", ip, mname);
  23.             g_free(mname);
  24.             return res;
  25.         }
  26.         else
  27.             return NULL;
  28.     }
  29.     method = mono_method_full_name(ji->method, TRUE);
  30.     /* FIXME: unused ? */
  31.     location = mono_debug_lookup_source_location(ji->method, (guint32)((guint8*)ip - (guint8*)ji->code_start), domain);
  32.     res = g_strdup_printf(" %s + 0x%x (%p %p) [%p - %s]", method, (int)((char*)ip - (char*)ji->code_start), ji->code_start, (char*)ji->code_start + ji->code_size, domain, domain->friendly_name);
  33.     mono_debug_free_source_location(location);
  34.     g_free(method);
  35.     return res;
  36. }
复制代码
get_method_from_ip这个函数比较简单,先从当前domain的JIT info table里面查找一下,看当前要查询的ip是不是在jit info里面。JIT info table比较有趣,每当一个CIL函数被jit后,为了方便下次继续使用,mono就会把jit后的机器码就会缓存在domain的jit info table里面提升运行速度。如果在jit info table里面找不到,那么会继续在domain的jit_trampoline_hash里面搜索,trampoline我们以前已经说过是一系列跳转,调用的中转区域。如果jit info table 和trampoline hash都没找到就返回 null了。
如果在jit info table 或者trampoline里面找到了,我们进入下面的几个函数mono_method_full_name和mono_debug_lookup_source_location后面函数不用多说,就是解析当前地址所在代码行的。mono_method_full_name我们进去看一下
  1. char *
  2. mono_method_full_name (MonoMethod *method, gboolean signature)
  3. {
  4.         char *res;
  5.         char wrapper [64];
  6.         char *klass_desc = mono_type_full_name (&method->klass->byval_arg);
  7.         char *inst_desc = NULL;
  8.         if (method->is_inflated && ((MonoMethodInflated*)method)->context.method_inst){...}
  9.         else if (method->is_generic){...
  10.         if (signature)
  11.         {
  12.                 char *tmpsig = mono_signature_get_desc (mono_method_signature (method), TRUE);
  13.                 if (method->wrapper_type != MONO_WRAPPER_NONE)
  14.                         sprintf (wrapper, "(wrapper %s) ", wrapper_type_to_str (method->wrapper_type));
  15.                 else
  16.                         strcpy (wrapper, "");
  17.                 res = g_strdup_printf ("%s%s:%s%s (%s)", wrapper, klass_desc,
  18.                                                            method->name, inst_desc ? inst_desc : "", tmpsig);
  19.                 g_free (tmpsig);
  20.         } else {
  21.                 res = g_strdup_printf ("%s%s:%s%s", wrapper, klass_desc,
  22.                                                            method->name, inst_desc ? inst_desc : "");
  23.         }
  24.         g_free (klass_desc);
  25.         g_free (inst_desc);
  26.         return res;
  27. }
复制代码
先略过关于泛型和特化的部分(你看,monoCLR对泛型的处理是无孔不入的。。。),直接看下面signature部分
我们可以从if(signature)的不同分支看的出来
  1. if (signature)
  2. {       
  3.      res = g_strdup_printf ("%s%s:%s%s (%s)", wrapper, klass_desc,
  4.                                                            method->name, inst_desc ? inst_desc : "", tmpsig);
  5. }
  6. else
  7. {
  8.      res = g_strdup_printf ("%s%s:%s%s", wrapper, klass_desc,
  9.                                                            method->name, inst_desc ? inst_desc : "");
  10. }
复制代码
signature的字面意思是签名,实际上signature是用来描述函数参数和返回值的信息的。
我们看一下最终printf的结果
  1. System.OutOfMemoryException:.ctor (string)
复制代码
可见最终的参数“(string)”就是signature (%s)的输出
我们详细进入mono_method_signature和mono_signature_get_desc看一下。
额,先挑软柿子mono_signature_get_desc看一下
  1. char*
  2. mono_signature_get_desc (MonoMethodSignature *sig, gboolean include_namespace)
  3. {
  4.         int i;
  5.         char *result;
  6.         GString *res;
  7.         if (!sig)
  8.                 return g_strdup ("<invalid signature>");
  9.         res = g_string_new ("");
  10.         for (i = 0; i < sig->param_count; ++i) {
  11.                 if (i > 0)
  12.                         g_string_append_c (res, ',');
  13.                 mono_type_get_desc (res, sig->params [i], include_namespace);
  14.         }
  15.         result = res->str;
  16.         g_string_free (res, FALSE);
  17.         return result;
  18. }
复制代码
这个函数很简单,就像我们以前刚学c语言的时候写的简单字符串拼接一样,把MonoMethodSignature 多个参数用分号整理出来。mono_type_get_desc这个函数主要就是把通用类型和引用类型翻译成对应的string。
软柿子捏完了,我们现在来捏硬的mono_method_signature,代码就不贴了,笔者简单描述一下。。
首先会有一个cache缓存,如果以前已经拿过当前方法的Signature就直接返回。如果没有拿过,就开始拿。。
这一坨代码看起来无比复杂,一堆你认识的不认识的函数,真的去看,你会很快迷失在各种弯弯绕绕里面(天赋异禀的当我没说)。。
笔者对于这样复杂的代码,给的建议是,如果深究细节会容易陷入局部而看不到全体。
细节的研究还是放在调试中,结合调试看细节比较好带入。只看代码的时候就需要抓住几个重要的函数。
我们收收神,先看看mono_method_signature里有哪些重要的函数。
仔细看的话,会发现里面调用的函数大部分是mono_metadata_开头。似乎是一些元数据的操作。作为经常看代码的人,看到
  1. &img->tables [MONO_TABLE_METHOD]
复制代码
这样的宏,肯定会忍不住点进去看看。
这些枚举是什么,看起来像是一个个数据表的类型。那么这些数据类型到底是干啥的呢?遇事不决就来google吧
如果你的姿势正确
Metadata is encoded in a number of tables included on every CIL image. These tables contain type definitions, member definitions and so on, these constants are defined in the ECMA 335 specification Partition II section 22. The following table shows the C constants defined in the Mono runtime and how they map to the equivalent ECMA CLI metadata table:
简单点说,关于函数类的信息这些实际上都会记录到metadata里面,这些细节也是ECMA标准的一部分。
至此,我们已经可以大致了解mono_method_signature的具体操作了,他就是去Metadata里面找对应的方法的信息而已。
那么我们有办法查看的metadata是怎样的吗?
还真有。
还是磨刀不误砍柴工,我先新建一个新的C#代码并编译
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. namespace TestCPlus
  5. {
  6.         class A
  7.         {
  8.                 public void Test(int i)
  9.                 {
  10.                         System.Console.WriteLine("Test i");
  11.                 }
  12.                
  13.                 public void Test(String str)
  14.                 {
  15.                         System.Console.WriteLine("Test str");
  16.                 }       
  17.         }
  18.        
  19.     class Program
  20.     {
  21.         static void Main()
  22.         {
  23.             A a= new A();
  24.             a.Test("str");
  25.         }
  26.     }
  27. }
复制代码
上面是一个重载的栗子。我们随便找个可以解析metadata的工具打开。笔者用的是CFF Explorer。
metadata里面,会有两个同名的Test,但是他们的Signature是不一样的。不同的Signature对应的参数信息会在param表里面进行表述。
也就是在Metadata里面已经将两个看似相同的重载函数text区分的明明白白。
接下来我们一个一个看上面提到的几个关键字
重载
重载的定义
函数重载 (C#) 同一个范围内相同的函数名可以有多个定义。函数参数列表中的参数类型和个数不能完全相同(具有唯一签名)。
我们再看一下具体的il代码,把CIL dll用ILDASM打开。
可以看到在il代码里面,已经确定了要调用的函数。所以所谓的重载,其实可以看做是一个语法糖,具体的调用在编译阶段都已经确定。
真的到CLR层面,只需要从metadata里面获取对应重载所在的函数即可。代码层面就是上面的一堆MetaData函数一通操作。。
现在看起来重载还真的挺弱鸡的。。。
封装、继承
封装和继承我们放一起看
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. namespace TestCPlus
  5. {
  6.         class A
  7.         {
  8.                 virtual public void  Say()
  9.                 {
  10.                         System.Console.WriteLine("I Am A.");
  11.                 }               
  12.         }
  13.        
  14.         class B:A
  15.         {
  16.                 override public void  Say()
  17.                 {
  18.                         System.Console.WriteLine("I Am B.");
  19.                 }               
  20.         }
  21.        
  22.     class Program
  23.     {
  24.         static void Main()
  25.         {
  26.             A a= new B();
  27.                         a.Say();
  28.         }
  29.     }
  30. }
复制代码
继续生成测试CIL,class B继承于 Class A,A有一个Public 的方法Say 。
封装比较简单,直接可以在metadata里面看到
在当前方法的Flags里面就有Public的标记位。并且也说明了这是一个Virtual函数。
继承,继承的关系实际上也是在metadata里面可以查到,不过会麻烦一些。
我们先看Class A的extends属性,他指向当前的TypeDef index1,也就是默认的Type。这是一个标准类的实现。
现在我们来看A的继承类B,B的Extends就是Index2也就是刚刚我们看的ClassA。ildasm也是一样的通过extends来关联继承关系
每个Type也有对应的flags,我们点进去看一下
B的type类型是NoPublic,AutoLayout。
可见,封装和继承也可以在metadata里面查到线索,然后只要一通操作就可以了。
重写
重写会复杂一点
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. namespace TestCPlus
  5. {
  6.         class A
  7.         {
  8.                 virtual public void  Say()
  9.                 {
  10.                         System.Console.WriteLine("I Am A.");
  11.                 }               
  12.         }
  13.        
  14.         class B:A
  15.         {
  16.                 override public void  Say()
  17.                 {
  18.                         System.Console.WriteLine("I Am B.");
  19.                 }               
  20.         }
  21.        
  22.     class Program
  23.     {
  24.         static void Main()
  25.         {
  26.             A a= new B();
  27.                         a.Say();
  28.         }
  29.     }
  30. }
复制代码
继续用上次的测试代码,用CFF Explorer查看metadata
可以看到,在Type的def中B的方法是从method tab的第三个index开始。有着不同的描述。然后再看一下
  1. IL_0007:  callvirt   instance void TestCPlus.A::Say()
复制代码
有人会说,看到没,看到没,这个的确是A::Say()。本来笔者对这个也有点困惑,不过google 是万能的,简单搜索一下,就可以搜到
陈嘉栋大佬的名作,Call VS CallVitr 看方法调用。
建议读者先拜读一下大佬名作,然后再继续看下面的内容。从上面文章我们可以得知。Callvirt实际的操作就是从ldloc.0也就是我们刚刚生成Class B类中获取对应的实现。
那么这只是il层面的事情。真正的mono CLR层面怎么实现的呢?这篇文章实在是水太多了,我们下一篇文章再见吧。

本帖子中包含更多资源

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

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

本版积分规则

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

GMT+8, 2024-11-23 00:10 , Processed in 0.100713 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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