语义分析代码实现_Unity mono代码结构分析及阅读(八)——virtCall解析及函数重写功能的实现...

e998a664e7cee2ed0c739dcbcdd56edc.png

这是基于unity mono代码阅读的第八篇。

上文已经大致分析了封装、继承、重载的底层实现,根据上文我们可以知道,CIL的封装,继承,重载都是通过MetaData里面的各种table实现。简单点说可以看做一个一个的查表操作。

今天我们来看一下重写这个操作。

本文需要前置知识,如果没有看过本文前面第七篇的读者需要翻阅一下第七篇。

并且对于CIL Opcode里面的Call和VirtCall的区别,也需要读者提前阅读一下陈嘉栋大佬的大作

陈嘉栋:用MSIL写程序:从“call vs callvirt”看方法调用​zhuanlan.zhihu.com
1cf6c49a6efad681bcd7751dd70302f9.png

好了,那么我们现在开始。

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();
        }
    }
}

继续上次的测试代码,上面的Main函数内,类型Class A的实例a 是Class A的继承类 Class B new出来的。在调用a.Say()的时候就涉及到底层的重写操作。生成的il代码是这样

06ecc25dfecb2aee8cc1428c5a21d4b7.png

如果读者已经阅读过上文推荐的陈嘉栋大佬写的的“用MSIL写程序:从“call vs callvirt”看方法调用”。那么有一段黑体字一定再温习一遍。

因此,使用callvirt时,它关心的并不是变量定义时的类型是什么,而是变量最后是什么类的引用。也就是说callvirt关心的是变量的运行时类型,是变量真正指向的类型。

上文生成的il重写代码,恰恰是使用的CallVirt来调用的。那么这只是在il层面的调用,真正底层的实现是怎样的呢?今天我们就来一探究竟。

如果读者有看过笔者的前面几篇文章,可以知道CLR的Call和vircall调用都是懒调用,也就是生成代码的时候用一个mono_magic_trampoline来替换,真正被调用的时候会在mono_magic_trampoline生成需要跳转的函数,并进行跳转。

我们就在mono_magic_trampoline打个断点看一下。中间会被中断多次,我们只看方法为我们需要的方法的时候进行单步调试看调用逻辑。希望读者可以手动调试一下看看。

经过几次调试后,对于CallVirt这个操作,在mono_magic_trampoline是这样的处理的。

/**
 * mono_magic_trampoline:
 *
 *   This trampoline handles calls from JITted code.
 */
gpointer
mono_magic_trampoline (mgreg_t *regs, guint8 *code, gpointer arg, guint8* tramp)
{
	gpointer addr, compiled_method;
	gpointer *vtable_slot;
	gboolean generic_shared = FALSE;
	MonoMethod *m;
	MonoMethod *declaring = NULL;
	MonoMethod *generic_virtual = NULL;
	int context_used;
	gboolean proxy = FALSE;
	gboolean need_rgctx_tramp = FALSE;

	m = arg;

       if (m == MONO_FAKE_VTABLE_METHOD) 
       {
		int displacement;
		MonoVTable *vt = mono_arch_get_vcall_slot (code, regs, &displacement);
	        if (!vt)
                { 
		    ...	
		}
		if (displacement > 0) 
               {
			displacement -= G_STRUCT_OFFSET (MonoVTable, vtable);
			g_assert (displacement >= 0);
			displacement /= sizeof (gpointer);
			...
			m = mono_class_get_vtable_entry (vt->klass, displacement);
			if (mono_method_needs_static_rgctx_invoke (m, FALSE))
				need_rgctx_tramp = TRUE;
		} 
                else
               {
			/* We got here from an interface method: redirect to IMT handling */
			m = MONO_FAKE_IMT_METHOD;
			/*g_print ("vtable with disp %d at %pn", displacement, code);*/
		}
	}
        addr = compiled_method = mono_compile_method (m);
        return addr;
}

就是对于callVirt的调用,mono_magic_trampoline并不能直接拿到要编译的方法,而只能拿到一个MONO_FAKE_VTABLE_METHOD这个标记位。然后骚操作就来了。

mono通过函数mono_arch_get_vcall_slot拿到了一套MonoVTable。笔者在一开始调试的时候,始终没看懂mono_arch_get_vcall_slot在做什么。毕竟,这套代码是长的下面这样的

gpointer
mono_arch_get_vcall_slot (guint8 *code, mgreg_t *regs, int *displacement)
{
	guint8 buf [8];
	guint8 reg = 0;
	gint32 disp = 0;

	mono_breakpoint_clean_code (NULL, code, 8, buf, sizeof (buf));
	code = buf + 8;

	*displacement = 0;

	code -= 6;

	/* 
	 * A given byte sequence can match more than case here, so we have to be
	 * really careful about the ordering of the cases. Longer sequences
	 * come first.
	 * There are two types of calls:
	 * - direct calls: 0xff address_byte 8/32 bits displacement
	 * - indirect calls: nop nop nop <call>
	 * The nops make sure we don't confuse the instruction preceeding an indirect
	 * call with a direct call.
	 */
	if ((code [1] != 0xe8) && (code [3] == 0xff) && ((code [4] & 0x18) == 0x10) && ((code [4] >> 6) == 1)) {
		reg = code [4] & 0x07;
		disp = (signed char)code [5];
	}
    else if
        ((code [0] == 0xff) && ((code [1] & 0x18) == 0x10) && ((code [1] >> 6) == 2)) {
		reg = code [1] & 0x07;
		disp = *((gint32*)(code + 2));
	} else if ((code [1] == 0xe8)) {
			return NULL;
	} else if ((code [4] == 0xff) && (((code [5] >> 6) & 0x3) == 0) && (((code [5] >> 3) & 0x7) == 2)) {
		/*
		 * This is a interface call
		 * 8b 40 30   mov    0x30(%eax),%eax
		 * ff 10      call   *(%eax)
		 */
		disp = 0;
		reg = code [5] & 0x07;
	}
	else
		return NULL;

	*displacement = disp;
	return (gpointer)regs [reg];
}

说实话,这套代码,就算经常写硬编码的笔者也有点懵逼。而且这样的代码还会随着平台的变化而变化。。。x86和arm完全不一样。虽然有注释,但是注释对于一脸懵逼的我们实在是没啥卵用。

要是我是unity我也不想在其他新的平台再编写一套这样的东西是吧,这样看起来还是il2吹cpp是正道。。

这时候笔者的建议就是,不要着急不要着急,调调看,实在调不下去了,就掀桌子吧。多大的事。

笔者把这段代码调试了好多遍。。大致理清楚了前后思路,这里面涉及到一些x86指令集的相关知识,如果不想看啰嗦的部分可以直接跳过。。

如果从代码上看,这个函数的主要逻辑是从code 这个数组中进行一些校验后,去除对应的数值,用来填充displacement 和 regs。经过调试,我们可以大致确认,code里面存放的是jit后的调用当前callvirt代码,在经过mono_breakpoint_clean_code处理后的code从反汇编来看是这样的

4f1303bf2314758c7121b724588c38fa.png

前后被int 3调试中断包围,然后是二进制的 90 90 90 ff 50 38二进制机器码,对应到汇编就是一个简单地call eax+38h

mono_arch_get_vcall_slot所做的事情就是围绕上面的机器码进行的。回头再看看注释

* There are two types of calls:
* - direct calls: 0xff address_byte 8/32 bits displacement
* - indirect calls: nop nop nop <call>

看起来我们遇到了第二种nop nop nop <call>类型的call。当然代码里面这个顺序需要掉个个。实际上第一个if处理的是注释里面的第二种情况

if ((code [1] != 0xe8) && (code [3] == 0xff) && ((code [4] & 0x18) == 0x10) && ((code [4] >> 6) == 1)) {
		reg = code [4] & 0x07;
		disp = (signed char)code [5];
	}

然后 code[3] ff code [4] 50 code [5] 38,通过这一通操作mono_arch_get_vcall_slot获取到了reg和disp

9cb04f0ecbbcc6ccf9dbc8b290d8762b.png

disp = 0n56, reg = 0;有人说,这个disp是什么呢?disp =0n65 = 0x38

reg最后被regs 安排了。假设regs是寄存器的话,那么0号寄存器是哪个呢?在x86上我们可以认为0号寄存器为eax。

return (gpointer)regs [reg];

那么disp = 0x38 reg 0 = eax最后就跟汇编码里面的call eax+38h 对上了。

再回头看看,一下子就简单了。mono_arch_get_vcall_slot这个函数只是想从机器码里面取出当前MonoVTable *vt 和 displacement罢了。说实话,我是真的不喜欢这样的平台相关代码。。

回到mono_magic_trampoline后,我们继续往下看,对于重写的函数,我们获取到了当前的调用的 MonoVTable 和偏移后我们可以干什么

               if (displacement > 0) 
               {
			displacement -= G_STRUCT_OFFSET (MonoVTable, vtable);
			g_assert (displacement >= 0);
			displacement /= sizeof (gpointer);
			...
			m = mono_class_get_vtable_entry (vt->klass, displacement);
			if (mono_method_needs_static_rgctx_invoke (m, FALSE))
				need_rgctx_tramp = TRUE;
		} 

关键函数mono_class_get_vtable_entry,我们可以从displacement和vt带的klass里面获取到当前需要调用的多态方法。

这里要多插一句的是,MonoVTable 我们以为是类似class的虚函数表这样的东西。但是我们仔细查过代码的话,就发现,虚函数表其实是这样的。。

/* Generic vtable. Initialized by a call to mono_class_setup_vtable () */
MonoMethod **vtable;

class里面我们认为的虚表实际上真的是一个MonoMethod 数组。而且MonoVTable 这个东西实际上主要是用来描述一个类的相关信息的,跟虚函数表没啥关系。

/* This struct collects the info needed for the runtime use of a class,
 * like the vtables for a domain, the GC descriptor, etc.
 */
typedef struct {
	guint16 max_domain;
	/* domain_vtables is indexed by the domain id and the size is max_domain + 1 */
	MonoVTable *domain_vtables [MONO_ZERO_LEN_ARRAY];
} MonoClassRuntimeInfo;

/* the interface_offsets array is stored in memory before this struct */
struct MonoVTable {
	MonoClass  *klass;
	 /*
	 * According to comments in gc_gcj.h, this should be the second word in
	 * the vtable.
	 */
	void *gc_descr; 	
	MonoDomain *domain;  /* each object/vtable belongs to exactly one domain */
        gpointer    data; /* to store static class data */
        gpointer    type; /* System.Type type for klass */
	guint8     *interface_bitmap;
	guint16     max_interface_id;
	guint8      rank;
	USE_UINT8_BIT_FIELD(guint, remote      : 1); /* class is remotely activated */
	USE_UINT8_BIT_FIELD(guint, initialized : 1); /* cctor has been run */
	USE_UINT8_BIT_FIELD(guint, init_failed : 1); /* cctor execution failed */
	guint32     imt_collisions_bitmap;
	MonoRuntimeGenericContext *runtime_generic_context;
	/* do not add any fields after vtable, the structure is dynamically extended */
        gpointer    vtable [MONO_ZERO_LEN_ARRAY];	
};

扯远了,我们继续回来看mono_class_get_vtable_entry

/*
 * mono_class_get_vtable_entry:
 *
 *   Returns class->vtable [offset], computing it if neccesary.
 * LOCKING: Acquires the loader lock.
 */
MonoMethod*
mono_class_get_vtable_entry (MonoClass *class, int offset)
{
	MonoMethod *m;

 	if (class->rank == 1) 
        {
	 ....
	}

	if (class->generic_class)
        {
           ...
	} 
        else 
        {
		mono_class_setup_vtable (class);
		m = class->vtable [offset];
	}
	return m;
}

逻辑比较简单,初始化类的虚函数表,然后用我们上面得出的偏移来获取到对应的虚函数,进行调用。我们看一下class的信息。

0:000:x86> dt class
Local var @ 0xaff8b8 Type _MonoClass*
0x00e38eb8 
   +0x000 element_class    : 0x00e38eb8 _MonoClass
   +0x004 cast_class       : 0x00e38eb8 _MonoClass
   +0x008 supertypes       : 0x00e38f70  -> 0x00dc2000 _MonoClass
   ....
   +0x024 parent           : 0x00e38dc8 _MonoClass
   +0x028 nested_in        : (null) 
   +0x02c image            : 0x00e33e28 _MonoImage
   +0x030 name             : 0x00c50527  "B"
   +0x034 name_space       : 0x00c50504  "TestCPlus"
   +0x038 type_token       : 0x2000003
   +0x0ac vtable           : 0x00e39000  -> 0x00def530 _MonoMethod

可见,从上述一堆魔法操作后,我们从机器码里面拿到了正确的运行时类型class B,对应我们原来的代码

        static void Main()
        {
            A a= new B();
	    a.Say();
        }

然后就是class的虚函数表了。我们简单看一下

0:000:x86> dt _MonoMethod 00def530
mono!_MonoMethod
   +0x000 flags            : 0x1c6
   +0x002 iflags           : 0
   +0x004 token            : 0x6000008
   +0x008 klass            : 0x00dc2000 _MonoClass
   +0x00c signature        : 0x00dfc720 _MonoMethodSignature
   +0x010 name             : 0x02d1cb3e  "ToString"
...
   +0x014 slot             : 0y00000000000000000 (0)
0:000:x86> dt _MonoMethod 00def510
mono!_MonoMethod
   +0x000 flags            : 0x1c6
   +0x002 iflags           : 0
   +0x004 token            : 0x6000005
   +0x008 klass            : 0x00dc2000 _MonoClass
   +0x00c signature        : 0x00dfcd00 _MonoMethodSignature
   +0x010 name             : 0x02d14a13  "GetHashCode"
...
   +0x014 slot             : 0y00000000000000001 (0x1)
0:000:x86> dt _MonoMethod 00def4f0
mono!_MonoMethod
   +0x000 flags            : 0xc4
   +0x002 iflags           : 0
   +0x004 token            : 0x6000004
   +0x008 klass            : 0x00dc2000 _MonoClass
   +0x00c signature        : 0x00def560 _MonoMethodSignature
   +0x010 name             : 0x02d1cb1d  "Finalize"
...
   +0x014 slot             : 0y00000000000000010 (0x2)
0:000:x86> dt _MonoMethod 00def4d0
mono!_MonoMethod
   +0x000 flags            : 0x1c6
   +0x002 iflags           : 0
   +0x004 token            : 0x6000002
   +0x008 klass            : 0x00dc2000 _MonoClass
   +0x00c signature        : 0x00dfcce8 _MonoMethodSignature
   +0x010 name             : 0x02d14822  "Equals"
...
   +0x014 slot             : 0y00000000000000011 (0x3)
0:000:x86> dt _MonoMethod 00e38fe0
mono!_MonoMethod
   +0x000 flags            : 0xc6
   +0x002 iflags           : 0
   +0x004 token            : 0x6000004
   +0x008 klass            : 0x00e38eb8 _MonoClass
   +0x00c signature        : 0x00e38f88 _MonoMethodSignature
   +0x010 name             : 0x00c50531  "Say"
...
   +0x014 slot             : 0y00000000000000100 (0x4)

我们可以看到class B的续表有5个slot,索引分别为0-4。对应5个函数ToString,GetHashCode,Finalize,四个从system.object继承来的函数和一个自己的index 4 函数Say。

至此我们已经理清了大概的思路。

mono_magic_trampoline()
->MonoVTbale vt = mono_arch_get_vcall_slot(&displacement)
MonoMethod m = mono_class_get_vtable_entry (vt->klass, displacement);
addr = compiled_method = mono_compile_method (m);
return addr  

在重写的Say方法被调用的时候,在mono CLR内部会会先给这个重写的方法跳转到mono_magic_trampoline。

然后通过mono_arch_get_vcall_slot从调用过来的代码中取到当前运行时信息,比如说MonoVTbale 和对应的虚函数偏移。

在获取到MonoVTbale 和对应的虚函数偏移后CLR通过mono_class_get_vtable_entry获取到当前运行时的类 Class B的虚函数表,并且通过上文提取出来的slot也就是偏移,找到对应的调用函数B::Say()。然后通过mono_compile_method编译B::Say()并返回编译的地址完成调用。至此,我们对于重写这个逻辑基本上搞清楚了。

不过认真看到这里的同学可能会问,这里最重要的MonoVTbale vt 结构在mono_magic_trampoline被调用的时候已经出来了,并且slot也就是偏移也已经算好了,这个偏移和对应的MonoVTbale vt 是怎么出来的?

至于MonoVTbale vt ,是在一开始

  A a= new B();

的时候已经生成,然后被放在寄存器eax里面,变成了上面我们看到的汇编码里面的call eax+0x38里面的eax,而代码里面的0x38是mono根据MetaData里面的Class A的信息计算出来的。

6da923cb3de386dbd5933347aed4ef56.png

就是il代码里面的 void TestCPlus.A::Say()。A的Say函数所在的Slot在编译的时候便已经确定。而Class B继承于Class A,所以无论当前eax寄存器里面存放的是class A还是class B的实例,只要是调用A里面有的函数,对应的虚函数(vtable)的偏移也就是上面的0x38都是一样的,在偏移0x38处都是Say方法。

对这块实现有兴趣的同学可以看一下mono_class_setup_vtable代码,这边就不再赘述了。

/*
 * mono_class_setup_vtable:
 *
 *   Creates the generic vtable of CLASS.
 * Initializes the following fields in MonoClass:
 * - vtable
 * - vtable_size
 * Plus all the fields initialized by setup_interface_offsets ().
 * If there is an error during vtable construction, class->exception_type is set.
 *
 * LOCKING: Acquires the loader lock.
 */
void
mono_class_setup_vtable (MonoClass *class)

好吧,这块实在是有点绕。希望大家还是动手调试调试,这样对代码有更深入的了解。

我们下次再见。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值