概述
基于 IDA 对 .NET Framework 4.8 clr.dll(x64)的逆向分析,还原
RuntimeMethodHandle.GetFunctionPointer()背后的完整决策链——MethodDesc::TryGetMultiCallableAddrOfCode的 9 条分支,以及每类方法(FCall / 已 JIT / precode / 临时入口)分别如何拿到 entrypoint。关键字:#编程语言/DotNet/CLR 逆向/ida hook methoddesc precode
背景
在 .NET RASP / Hook 框架中,核心需求是拿到一个方法的真实 native code 入口地址。最常用的 RuntimeMethodHandle.GetFunctionPointer() 不保证返回真实 JIT 代码——很多情况下返回的是 precode(跳板 stub)。要理解它的返回语义,必须深入 clr.dll 的决策函数 MethodDesc::TryGetMultiCallableAddrOfCode。
分析对象
- clr.dll 4.8.4795.0(x64,
clr.pdb已入 IDA)- 配套复现工程:
FunctionPointerDemo(net48/net9.0 双框架)
GetFunctionPointer 调用链
托管层 mi.MethodHandle.GetFunctionPointer()
↓ QCall
CLR 原生 RuntimeMethodHandle::GetFunctionPointer (0x180066080)
↓
MethodDesc::GetMultiCallableAddrOfCode (0x18003ddd8)
↓ 失败则 FuncPtrStubs::GetFuncPtrStub 兜底
CLR 核心 MethodDesc::TryGetMultiCallableAddrOfCode (0x18003de10) ← 核心决策点
TryGetMultiCallableAddrOfCode 完整决策树
入参 accessFlags(GetFunctionPointer 传 CORINFO_ACCESS_UNMANAGED_CALLER_MAYBE|LDFTN=16)。决策顺序(每条分支 = 一种方法形态拿 entrypoint 的方式):
TryGetMultiCallableAddrOfCode(md, accessFlags)
│
├─① IsGenericMethodDefinition(md) → 抛 InvalidOperationException(未实例化泛型不可调)
│
├─② IsWrapperStub(md) || IsEnCAddedMethod(md)
│ → GetOrCreatePrecode() → GetMethodEntryPoint()
│ 【wrapper stub(unboxing/实例化 stub)与 EnC 新增方法:先确保有 precode,返回稳定入口】
│
├─③ accessFlags 无 UNMANAGED_CALLER 且 IsRemotingInterceptedViaVirtualDispatch(md)
│ → CRemotingServices::GetNonVirtualEntryPointForVirtualMethod(md)
│ 【Remoting 拦截:虚分发被拦截的虚方法,返回非虚入口点(remoting 桩)】
│
├─④ Module::IsEditAndContinueEnabled(md 的模块)
│ → GetOrCreatePrecode() → GetMethodEntryPoint()
│ 【EnC 启用的模块:必须用可回 patch 的 precode 入口,保证热更新时可改写】
│
├─⑤ (b6 & 7) == mcFCall(1) ←★ 读 MethodDesc+6 低 3 位分类位
│ 且 非remoting拦截 且 MethodTable flags 条件满足
│ → ECall::GetFCallImpl(md) ← 直接返回 FCall 的原生实现地址!
│ 【FCall 方法:绕过 precode 直达 ECall 表里的 C++ 实现】
│
├─⑥ 否则 IsPointingToNativeCode(md) ← 读 b[3]&1(HasStableEntryPoint) 与 b[3]&2(HasNativeCodeSlot)
│ → GetNativeCode(md) ← 直接返回已 JIT 的真实代码
│ 【★ 最理想分支:已 JIT 且无 precode 间接,entry == realCode】
│
├─⑦ b[3]&1 HasStableEntryPoint
│ → GetMethodEntryPoint(md) ← 返回方法当前入口(可能是 precode)
│ 【已 JIT 但仍带 precode 的方法】
│
├─⑧ MayHaveNativeCode(md)==0 或 IsRemotingInterceptedViaPrestub(md)
│ → GetOrCreatePrecode(md)
│ 【★ MayHaveNativeCode 读 MethodDesc+6 的 0x20(mdcStatic) 位。
│ 判定失败 = "不可能有 native code" → 只能给 precode(Hook 拿不到真实函数头的根因之一)】
│
└─⑨ md 所在 chunk 的 word >= 0
→ GetTemporaryEntryPoint(md) ← 临时入口点(PrecodeFixupThunk 场景)
否则 → return NULL
分支语义速记:
- ⑤ FCall → 直达 C++ 实现
- ⑥ 已 JIT 无 precode → 直达 native code(Hook 最理想)
- ⑦ 已 JIT 有 precode → 稳定入口(可能 E8/E9 跳板,需偏移跟随)
- ⑧⑨ 未 JIT / 异常场景 → precode / 临时入口(Hook 不能直接 patch 这里)
各被调函数语义(clr.dll 实测反编译)
| 函数 | 地址 | 功能 |
|---|---|---|
IsGenericMethodDefinition | 0x18000e19c | 是否泛型方法定义(未实例化)。是 → 直接抛 InvalidOperationException,Hook 不能挂未实例化泛型 |
IsWrapperStub | 0x180029228 | 是否 unboxing stub 或 instantiating stub(泛型包装桩)。这类方法的”入口”语义是桩本身,须先 GetOrCreatePrecode |
IsEnCAddedMethod | 0x18002ad0c | 是否 EnC(Edit and Continue)运行期新增方法 |
IsRemotingInterceptedViaVirtualDispatch | 0x18003dd64 | MayBeRemotingIntercepted && IsVtableMethod——remoting 透明代理场景的虚方法拦截 |
IsRemotingInterceptedViaPrestub | 0x18001123c | MayBeRemotingIntercepted && !IsVtableMethod && !RequiresInstArg——非虚方法的 remoting 拦截走 prestub |
MayBeRemotingIntercepted | 0x18003dd90 | 开头 test [rcx+6],0x20(mdcStatic)→ 静态方法直接 return 0;再排除 Object ctor/finalizer |
IsPointingToNativeCode | 0x18003df00 | 读 b[3]&1(HasStableEntryPoint)+ b[3]&2(HasNativeCodeSlot)+ Precode::IsPointingTo——判断 entry 是否已指向真实 JIT 代码 |
MayHaveNativeCode | 0x180010b70 | 综合分类位 + MethodTable flags(0xF0000/0x80000030)+ byte+6 的 0x20 位,判定”此方法可能存在 native code”。失败 → GetOrCreatePrecode |
GetNativeCode | 0x1800103dc | 读 b[3]&8(HasNativeCodeSlot)→ 按 s_ClassificationSizeTable[分类] 定位代码槽指针,& ~1 清标志位。无 slot 返回 0 |
GetMethodEntryPoint | 0x180006c78 | 读方法当前入口点槽:有 native code slot 走 ClassificationSizeTable 定位;否则从 MethodTable slot / chunk 间接解析 |
GetTemporaryEntryPoint | 0x18001df40 | 沿 MethodDescChunk 的 TempEntryPoints 表找/分配临时入口(PrecodeFixupThunk 形态的来源之一) |
GetOrCreatePrecode | 0x1801736dc | 有 TempEntry 就地升级为 precode(AllocMemTracker 分配),否则新分配 Precode 并回填 entrypoint |
ECall::GetFCallImpl | 0x180011578 | 查 ECall 表拿 FCall 的 C++ 实现地址(Console.WriteLine、String.Compare 这类内部方法不走 JIT) |
CRemotingServices::GetNonVirtualEntryPointForVirtualMethod | 0x18003e364 | remoting 场景:虚方法被透明代理拦截时返回非虚入口 |
MethodDesc+6 字节位语义(net48 x64)
位 含义 实测佐证 & 7分类(mcIL=0/mcFCall=1/mcPInvoke=2/mcEEImpl=3/mcArray=4/mcInstantiated=5/mcDynamic=7) String.ToUpper=0x080x08mdcHasNonVtableSlot 实例方法 0x08、静态方法 0x28 0x20mdcStatic(静态缓存位) MayBeRemotingIntercepted读此位0x80mdcRequiresInheritanceCheck 继承/override 链方法置位 ⚠️ 注意:lordmilko/DebugTools 的
mdcStatic=0x0080是 16 位 flags 新定义,对 net48 x64 的 byte+6 不适用。CoreCLR 现代源码布局已重构(mdfStatic = 0x0080在m_wFlags),不能拿新源码套旧字节偏移。
实测验证要点
- PrepareMethod 与 InvocationFlags 无关:
ReflectionInvocation::PrepareMethod(FCall 0x18065eca0)无条件走PrepareMethodDesc → MethodCallGraphPreparer::Run强制 JIT。m_invocationFlags是 mscorlib 托管侧的反射调用准入缓存,native 侧零消费。 - md+8(int32)= 真实代码相对 MethodDesc 的偏移:
realCode = md + ReadInt32(md+8) + 8——任何入口形态下都能跟随到真实代码。 - E8 跳板目标:未 JIT 方法的 entry
E8 rel32→ clr.dll 共享PrecodeFixupThunk;PrepareMethod后回填为E9 → 真实函数头。 - 扫描实验:21128 个 BCL 方法中 2171 个带 mdcRequiresInheritanceCheck(0x80),无手术 PrepareMethod 后仍 thunk 100 个(全为 async/Task/IL stub 类,by design)。
参考
- CoreCLR 源码对照:
src/coreclr/vm/method.cpp(TryGetMultiCallableAddrOfCode,main 分支行号 2245) - precode 结构:
src/coreclr/vm/precode.h(StubPrecodeData/FixupPrecodeData) - 本地复现工程:
Windows-API-Usage/.Net-Code/FunctionPointerDemo/(含 ReproMdcStatic / ReproMd8Probe / ReproSurgeryHunter 三个实验) - 关联工作文档:clr.dll逆向-PrepareMethod与mdcStatic位手术(Hook 框架视角的 mdcStatic 位手术分析)