给.net程序打内存补丁(2)
tankaiha[NE365][FCG]
2006-8-25

    接上文。上次讲了个最简单的动态修改代码的方法,讲得比较简约。今天介绍个复杂点的代码修改,顺便多介绍一些基本概念。

一、修改目标
    先看今天修改的目标。这一次tmp.cs的代码如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace tmp
{
    //新增的类
    public class userClass1
    {
        public static void showMsg()
        {
            MessageBox.Show("You get it", "^_^");
        }
    };


    public partial class Form1 : Form
    {
        bool bRetVal;
        
        //定义一个变量,表示是否输入正确的字符串
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            string cmpText = textBox1.Text;
            if(cmpText=="tankaiha")
            {
                this.bRetVal = true;
            }
            else
            {
                this.bRetVal = false;
            }            
            
        }

        //永远不会执行这个
        private void neverUsed()
        {
            MessageBox.Show("You never get this");
        }
    }
}
    我们在代码新增了一个userClass1,里面有一个静态方法showMsg()。我们要在button1_Click返回前执行这个方法。先对比一下这次的修

改和上次的不同

                上次       本次
代码块大小      不变       改变
代码块类型    小型(tiny)   大型(fat)
    
其实这次难度也不大,更难的在以后介绍。来看一下反汇编代码(部分):

……..
    IL_0000:  /* 02   |                  */ ldarg.0
    IL_0001:  /* 7B   | (04)000003       */ ldfld      class [System.Windows.Forms/*23000001*/]

System.Windows.Forms.TextBox/*01000006*/ tmp.Form1/*02000002*/::textBox1 /* 04000003 */
    IL_0006:  /* 6F   | (0A)000028       */ callvirt   instance string [System.Windows.Forms/*23000001*/]System.Windows.Forms.Control/*0100001B*/::get_Text() /* 0A000028 */
    IL_000b:  /* 0A   |                  */ stloc.0
    IL_000c:  /* 06   |                  */ ldloc.0
    IL_000d:  /* 72   | (70)00003B       */ ldstr      "tankaiha" /* 7000003B */
    IL_0012:  /* 28   | (0A)000029       */ call       bool [mscorlib/*23000002*/]System.String/*01000024*/::op_Equality(string,string) /* 0A000029 */
    IL_0017:  /* 2C   | 08               */ brfalse.s  IL_0021

    IL_0019:  /* 02   |                  */ ldarg.0
    IL_001a:  /* 17   |                  */ ldc.i4.1
    IL_001b:  /* 7D   | (04)000004       */ stfld      bool tmp.Form1/*02000002*/::bRetVal /* 04000004 */
    IL_0020:  /* 2A   |                  */ ret

    IL_0021:  /* 02   |                  */ ldarg.0
    IL_0022:  /* 16   |                  */ ldc.i4.0
    IL_0023:  /* 7D   | (04)000004       */ stfld      bool tmp.Form1/*02000002*/::bRetVal /* 04000004 */
    IL_0028:  /* 2A   |                  */ ret
  } // end of method Form1::button1_Click

我们要在最后IL_0028 ret之前插入一个call。

二、相关基本概念
    先来看看.net里的call。如果你用ildasm比较多,会比较熟悉。MSIL里的call是0x28,后面接了个(XX)YYYYYY,这是它的操作数,合起来XXYYYYYY就是你要call的方法的token。Token就是一个代码该方法的唯一值。不只是方法,.net中的任何东东都有个token。反汇编后发现userClass1.showMsg()的token是0x06000006。XX是分类(06),YYYYYY(000006)是序号。用工具打开tmp.exe可以看得更清楚些。
 
 
    从上图中看到,00是module,01是TypeRef,02是TypeDef,当然,还有06代表Method。而Method中,000001是Form1::Dispose(),000002是Form1::InitializeComponent,还有我们要调用的userClass1::showMsg(),排在第6位。这些信息都是存储在#~流中,还有其它的流,如#Strings、#US和#Blog等。这些流分别存储不同的信息,具体见MSDN,与本文关系不大。
    .Net中的token还有个方法,就是在一个Module中他是不变的,不管是否在一个类里,都可以直接用token值调用。因此,我们只需要在ret前插入28 06 00 00 06。

    第二个要介绍的概念是tiny和fat的区别。下图是方法在内存中的布局。
  


说白了,方法体就是一块内存。很明显,tiny比fat少了SEH处理块。一般来说,有SEH肯定是fat的,二是代码超过64字节也是fat。我们这次处理的就是不含SEH的fat Method。(下次再说对SEH块的处理)
    基本概念先介绍这两个,下面看代码。

三、修改
    下面开始修改代码,仍然在JITCompilationStarted中,首先定义我们要插入的代码:
#pragma pack(1)
  struct 
  {
    BYTE insertcall; 
    DWORD method_token;
  } InsertCode;
#pragma pack()
  InsertCode.insertcall=0x28;
  InsertCode.method_token=0x06000006;
    
    这样,我们的代码就比原代码大了5字节,所以在分配空间时要加上:

  IMethodMalloc* pIMethodMalloc = NULL;
  IMAGE_COR_ILMETHOD* pNewMethod = NULL;
  hr = m_pICorProfilerInfo->GetILFunctionBodyAllocator(moduleId, &pIMethodMalloc);
  if (FAILED(hr)) 
  { goto exit; }


  pNewMethod = (IMAGE_COR_ILMETHOD*) pIMethodMalloc->Alloc(iMethodSize+sizeof(InsertCode)+1);//注意新空间

的size要改

  if (pNewMethod == NULL)
  { goto exit; }
  memcpy((void*)pNewMethod, (void*)pMethod, iMethodSize);
下面是对fat方法头的处理和修改
if(IsTinyHeader(pNewMethod)) 
  {
        ……
}
  else 
  {
    COR_ILMETHOD_FAT* newfatImage = (COR_ILMETHOD_FAT*)&pNewMethod->Fat;
    codeBytes = newfatImage->GetCode();
    ULONG codeSize = newfatImage->GetCodeSize()+sizeof(InsertCode);

    //这里更改,注意位置的选择
    memcpy(codeBytes+codeSize-sizeof(InsertCode)-1,&InsertCode,sizeof(InsertCode));
    codeBytes[codeSize-1]=0x2A;
    newfatImage->SetCodeSize(codeSize);
  }

    最后是将修改过的代码分配给新的方法,并释放空间。
   hr = m_pICorProfilerInfo->SetILFunctionBody(moduleId, tkMethod, (LPCBYTE) pNewMethod);
   if (FAILED(hr)) 
   { goto exit; }

   pIMethodMalloc->Release();

四、测试
    测试方法不变,不过这次给新手做了个动画。下面是前后结果对比,注意看红体字的codeSize前后对比和从第40个字节开始的更改。

1  17:59:04:078  516: tmp  funcitonId is a75930
2  17:59:04:078  516: tmp  JITCompilationStarted: ::tmp.Form1.button1_Click
3  17:59:04:078  516: tmp  target string is: tmp.Form1.button1_Click
4  17:59:04:078  516: tmp  enter fat code
5  17:59:04:078  516: tmp  Flags: 13
6  17:59:04:093  516: tmp  MaxStack: 2
7  17:59:04:093  516: tmp  CodeSize: 29
8  17:59:04:093  516: tmp  LocalVarSigTok: 11000001
……
44  19:44:59:062  3984: tmp  codeBytes[35] = 0x7D;
45  19:44:59:062  3984: tmp  codeBytes[36] = 0x04;
46  19:44:59:062  3984: tmp  codeBytes[37] = 0x00;
47  19:44:59:062  3984: tmp  codeBytes[38] = 0x00;
48  19:44:59:062  3984: tmp  codeBytes[39] = 0x04;
49  19:44:59:078  3984: tmp  codeBytes[40] = 0x2A;

50  17:59:04:250  516: tmp  enter fat code again
51  17:59:04:265  516: tmp  Flags: 13
52  17:59:04:265  516: tmp  MaxStack: 2
53  17:59:04:281  516: tmp  NewCodeSize: 2E
54  17:59:04:281  516: tmp  LocalVarSigTok: 11000001
……
90  19:44:59:343  3984: tmp  codeBytes[35] = 0x7D;
91  19:44:59:359  3984: tmp  codeBytes[36] = 0x04;
92  19:44:59:359  3984: tmp  codeBytes[37] = 0x00;
93  19:44:59:375  3984: tmp  codeBytes[38] = 0x00;
94  19:44:59:375  3984: tmp  codeBytes[39] = 0x04;
95  19:44:59:390  3984: tmp  codeBytes[40] = 0x28;
96  19:44:59:390  3984: tmp  codeBytes[41] = 0x06;
97  19:44:59:390  3984: tmp  codeBytes[42] = 0x00;
98  19:44:59:406  3984: tmp  codeBytes[43] = 0x00;
99  19:44:59:406  3984: tmp  codeBytes[44] = 0x06;
100  19:44:59:421  3984: tmp  codeBytes[45] = 0x2A;


    打完收功,下一次会介绍更复杂更有实战性的修改。附件里是测试文件和测试的动画,专为新手准备。

附件下载