.NET 探索委托调用函数的过程

    从汇编的角度上看委托的调用不得不说是一件稍稍有趣的事情,想想我们常常的利用委托调用一个函数,但是我们不能连委托是怎么调用的都弄不明白,这样说出去是很丢脸的不是儿,何况这也不是什么好复杂的事儿,当然不同的 .NET/CLR 之间委托的调用过程可能会有差异,但大体应该是差不多的。

    我们知道 .NET/CLR 好的实现有很多,例如:sscli2.0/4.0 (Win32k 平台主要的 .NET/CLR 虚拟机).net/core、mono/cli 这些都是,它们之间的实现都不同,sscli 是由 Microsoft 提供的开源代码,它只是正式版的简化版本而虚拟机与基础框架是一致的,另外人们非要嘴巴咬到叫 .NET 只是这两年才开源的话,那么我可以告诉你早在 06 年的时候 Micrososft 就已经把 .NET 开源,另外 mono/cli 虚拟机的实现也并不差。

    我们先在你的C#应用中编写类似的代码,那么我们在 “Console.WriteLine(add(1, 2));” 处下INT3的breakpoint,运行此程序到达中断的时候,我们打开 “反汇编窗口(Crtl + Alt + G)”,另外建议利用 sos 会比较好进行 native-.net 调试,procexp64 这种神器还是建议必备使用(观测 .NET 应用的一些运行信息非常不错)。

    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;

    public static class program
    {
        static int Add(int x, int y)
        {
            return x + y;
        }

        public unsafe static void Main(string[] args)
        {
            Func<int, int, int> add = Add;
            Console.WriteLine(add(1, 2));
        }
    }

   我们可以看到 “反汇编” 窗口,具有与以下类似的汇编代码,此处代码序列就是调用一个委托的过程,但是我们的目的是弄明白这些指令对应的 “C/CC lang” 伪代码究竟是怎样的?

   ebp-40h = add 局部变量

   ebp-48h = 一个临时变量(我们并没有在代码中定义这个变量,所以它是一个由编译器生成的辅助计算的临时变量)

   我们知道 add 变量是一个 “Func<int, int, int>” 的委托类型,静态委托似乎需要即时编译一个JIT/Stub,然后从调用这个JIT/Stub从它JMP到目标的静态函数,那么我们将逐步的从上述的汇编中进行推测与验证是不是真的这样的?

   从上图反调试的所示的情况来看,的确是的,add._methodPtr 并不等于委托绑定的 Add 函数的地址,而是另外一个地址,同时这个按照 __fastcall 调用协议 “ecx、edx” 作为参一、参二那么是不需要PUSH的,但是它PUSH了一个 “2” const,也就说调用了 “委托绑定的 Add 函数” 使用了三个参数,但显然 “Add” 函数并不接受三个参数,假设它真的这样传递,那么就会存在潜在的堆栈溢出的问题,但是最终的结果则是它成功调用了指向 “0x00fc0038” 地址的 “Add” 函数,显然这个 “add._methodPtr” 指向的是一个 “JMP” 函数。

   Visual studio 中不利用 sos 调试的话想要看到 “0x055305B4” 中的代码就比较麻烦了,不过这都是些小事情,按照类似情况把下面的代码抓出来,基本都到差不差的。

   51 8b ca 8b 54 24 08 8b 44 24 04 89 44 24 08 58 83 c4 04 83 c0 10 ff 20 00 80 04 80 04 00 00 80 00 00 04 00

   {81,139,202,139,84,36,8,139,68,36,4,137,68,36,8,88,131,196,4,131,192,16,255,32,0,128,4,128,4,0,0,128,0,0,4,0}

    把它转换成汇编的字符串形式方法比较多,例如:通过易语言的“置入代码”命令,或者利用易语言的汇编插件来转,再比如像本人这样用 “汇编代码转换器” 之类的工具把这段机器代码转换成“汇编”代码的文本形式。

    

   我们可以从上图中得到 “机器代码” 对应的汇编代码,那么我们来尝试把它反编译成 “C/CC lang” 的 pseudocode 形式,看看最终的情况它到底是个什么结构。

+ldarg.2 [ebp+8]
+:retaddr 【ebp+4]
-----------------
+ecx
-----------------
esp+8 = ldarg.2
esp+4 = :retaddr
-------------
_targetPtr = ecx;
{
   ecx = edx;
   edx = ldarg.2;
   eax = :retaddr;
   ldarg.2 = eax;
}
eax = _targetPtr; 
goto (eax + 0x10); // (_target + 16u) = _methodPtrAux

   你可能会感到好奇,我是怎么知道 eax + 0x10 = _methodPtrAux 的?这需要请出一些 .NET 反编译工具或者直接查看 “sscli” 开源代码中关于 “Delegate” 的实现。

   我们可以看到在 “Delegate”  这个类型中一共有四个字段,我们知道在调用委托的过程中有这么一句话  “mov  ecx,dword ptr [ecx+4]” 意说把 “ecx+4” 的地址拷贝到 “ecx” 中,我们知道 “类与结构体” 在内存中导出布局是不同的,类需要预留一个字段用于标识 __vfptr 用以指向它的 __vftbl ,而结构体却不需要如此。

    而 ecx+4 的地址,正好等于 “Delegate::_target” 的位置,那么在 “Delegate::_target” + 16 那么就等于 “_methodPtrAux” 的地址,有一个稍微特殊的地方就是说 .NET里面 “引用类型” 之间默认是 8 个需要字节,结构体的情况也差不了多少,当然指定了结构体的内存分布那另当别论。

  

    到了这里基本上,本文探讨的问题基本是差不多了,至于 “委托” 绑定实例函数的话,基本没有什么好讨论的,到差不差,但是就从 “委托” 应用到 “静态函数” 还是 “实例函数” 上,那个效率更高相信各位看官自己心中已然有了答案,本人基本就不再这里一一累赘了,下面则是一段C/CC lang的示意伪代码,用于表示.NET调用委托时的过程。

// push stack order:从高到低, 从右到左

push 2
mov ecx, dword ptr [ebp-40h]
mov edx,1
mov eax,dword ptr [ecx+0Ch]
mov ecx,dword ptr [ecx+4]
call eax

mov dword ptr [ebp-48h],eax
mov ecx,dword ptr [ebp-48h]
call 7120646C

// __fastcall 调用协议(ECX,EDX寄存器作参一参二)
-------------------------
Pseudocode(C/CC lang)

_DWORD ebp_40h;  // add 函数地址
_DWORD ebp_48h;  // 一个寄存变量

ecx = ebp_40h;
edx = 1
{
  eax = ecx._methodPtr;
  ecx = ecx._target; 
}
eax = eax([void*] ecx, [int] edx, [int] 2);

ebp_48h = eax;
ecx = ebp_48h;
System.Console.WriteLine([int] ecx);

猜你喜欢

转载自blog.csdn.net/liulilittle/article/details/83056365