Windbg之探索托管exe加载及函数JIT的过程
类似的文章网上已经汗牛充栋了,但作为个人的一个学习经历,我觉得还是有必要记录一下为好。
使用到的工具: Peview windbg
目标程序 ILText.exe
目标程序源码:
using System;
using System.Collections.Generic;
using System.Text;
namespace ILTest
{
public class HelloWorld
{
private static long n1;
private static long n2 = DateTime.Now.Ticks;
private static long n3 = DateTime.Now.Ticks;
private readonly static string ConstValue = "testStatic";
static HelloWorld()
{
n1 = DateTime.Now.Ticks;
}
private static void staticMethod(int n)
{
Console.WriteLine(n);
}
protected string Method1()
{
return null;
}
private string Method2()
{
int n;
string str;
str = DateTime.Now.ToString();
n = DateTime.Now.Year;
return null;
}
static void Main(string[] args)
{
Console.WriteLine("Hello, world.");
HelloWorld obj = new HelloWorld();
obj.Method2();
HelloWorld.staticMethod(10);
Console.ReadLine();
}
}
}
首先我们使用peview打开一个exe
我们可以看到该程序的入口点的偏移地址是299e.
然后我们使用windbg,选打开可执行文件,选上我们的目标exe文件。
敲入以下几个命令:
~0s
.load sos
ld iltest
ld mscoree
.load/ld mscorwks
ld kernel32
lm
可以看到该exe 的首地址是00400000,加上偏移地址等于是0040299e,ok,我们查看下该地址的汇编码:
u 0040299e
可以看到里边是一个简单的跳转指令,跳转到00402000, 用dds (Display Words and Symbols)查看一下该地址存了什么:
dds 00402000
至此,已经已经非常清楚了。任何托管的EXE程序中的入口点都是一条JMP指令直接跳转到MSCOREE.DLL的_CorExeMain函数执行。
接下来,我们继续探索JIT的过程。
Don Box 在《.NET本质论 第1卷:公共语言运行库》的第六章中提到方法表在JIT 之前,保存的都是 call mscorwks.dll!PreStubWorker 调用,直到第一次使用时,才会对目标 IL 代码进行 JIT 编译,并调用之。因此我们第一步可以在此函数上设置断点(bu mscorwks!PreStubWorker),看看系统是如何调用此函数的。
bu mscorwks!PreStubWorker
然后输入g命令,接着用!clrstack命令查看当前托管代码的堆栈,直到运行到Method2方法被调用。
或者我们可以采用另外一种方法,直接给托管方法Method2设断点。
然后我们可以查看HellowWorld的实例对应的methodtable,来查看method2在JIT之前存储的是什么。
!dso
!do 013a3834
!dumpmt -md 00a63098
可以看出,method2还没有被JIT过,我们看看此时它对应出的执行代码是什么。注意标记处的指令,第一条指令的地址来源于上图。
至此,可以看出method2还没有被JIT的情况下,会被引导至mscorwks,然后执行JIT.
我们继续输入g,让程序继续运行。
然后此时再查看methodtable
!dumpmt -md 00a63098
可以看出来method2已经被JIT了,另外跟上一次执行同样的命令比较,发现此次method2对应的entry已经变成了00da0180.
用dumpmd看看该方法
也一样可以看出方法已经被JIT,m_CodeOrIL此时存放的就是该方法的汇编码。我们看下对应的汇编代码是什么吧:
!u 00da0180
至此,我们应该已经看出来了,method2在JIT前后的入口点地址发生了变化,这就符合我们在很多文章所看到的,一个方法是以IL形式存放到托管模块中的,当第一次访问后,该方法会被JIT,生成相应的汇编码,之后的再次调用的话,就会直接方法JIT过的汇编地址。
那么,该地址既然有变化,是否意味着那么多调用 该方法的地方,相应的地址(method2的entry)也要变化呢?
我们可以看看本段代码中的Main方法对应的汇编码:
从上边的执行结果中我们可以查到main方法对应的入口地址是00da0100。
!u 00da0100
可以看出,method2对应的入口地址应该存放在0A630DCh中,我们看看该内存区域的东东:
咦,是00da0180,这个不正是我们上边已经查到的method2对应的entry么? 它是通过运行时计算得来的,因此,我们可以猜测,在JIT之前,0A630DCh存放的一定是00a6c01d。因此,也就不存在说调用方改变相应地址这回事了。