给.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;
打完收功,下一次会介绍更复杂更有实战性的修改。附件里是测试文件和测试的动画,专为新手准备。