一句话结论

RuntimeHelpers.PrepareMethod 不读任何 m_invocationFlags(无条件强制 JIT);HookInfo 的「清 0x80 设 0x20(mdcStatic)」位手术影响的是 CLR 原生侧 TryGetMultiCallableAddrOfCode/MayHaveNativeCode/MayBeRemotingIntercepted 对 MethodDesc+6 字节的判定分支;取真实函数头靠 md + ReadInt32(md+8) + 8 偏移跟随,不靠手术。

2026-09-10 重大更新(x86 4.8.1 9181 实锤):手术的真正作用在 x86 上复现成功——Chanjet FastCGI 环境(System.dll IL 副本)中,MD+6 从 0x80 改为 0x20 后 PrepareMethod 会真 JIT(A/B 对比:+3 0x0B→0x39、md+8 回填 code-8、CodeAddr 真实函数头 vs 不手术仅登记不 JIT)。机制:MD+6 的 0x80 位(RequiresInheritanceCheck)使 PrepareMethodDesc 管线拦住 0x80 方法(只登记不 JIT),0x20(mdcStatic)方法则被 ExecuteGate 放行真 JIT。顺序约束:手术必须在首次 PrepareMethod 之前。 servicing 差异:9181 拦 0x80,9337+ 实测不拦(不手术也 JIT)——详见第六章。

关联文章:clr.dll逆向-GetFunctionPointer与Precode(GetFunctionPointer 决策链与 Precode 体系)

一、分析背景

某个 .NET Hook 框架的初始化组件(下文统称 HookInfo)在 x64 分支有一段位手术:PrepareOldMethodForHook:

C#csharp
byte bTargetMDC = bOriginMDC;
bTargetMDC &= 0x0F;        // 保留低 4 位
bTargetMDC |= mdcStatic;   // 0x20 —— 清掉 0x80(mdcRequiresInheritanceCheck),或上 0x20(mdcStatic)
SetInvocationFlags(OldMethod, bTargetMDC);
RuntimeHelpers.PrepareMethod(OldMethod.MethodHandle);
SetInvocationFlags(OldMethod, bOriginMDC);  // 还原

本次分析回答三个问题:

  1. PrepareMethod 是否通过 m_invocationFlags 判断是否执行?(答:否)
  2. GetInvocationFlags 能否改为属性访问?(答:读可以,写不行)
  3. 当年加 mdcStatic 到底为什么?(答:见第四节,复现后修正)

二、CLR 逆向结论(clr.dll 4.8.4795 x64,IDA 已加 22 条注释)

2.1 PrepareMethod(FCall,0x18065eca0)—— 与 InvocationFlags 无关

text
ReflectionInvocation::PrepareMethod(ReflectMethodObject*, inst, count)
  ├─ GetAttrs() & 0x400 (abstract) → 抛 Argument_CannotPrepareAbstract
  ├─ CheckRestore / 泛型 instantiation 校验 / FindOrCreateAssociatedMethodDesc
  └─ PrepareMethodDesc(md, ...)
        └─ MethodCallGraphPreparer::Run()   ← CER 准备 + IL 调用图遍历
  • 全程不读 mscorlib 侧 m_invocationFlags —— 那是托管对象(RuntimeMethodInfo)上的字段,native FCall 只持有 MethodDesc*,物理上无法读到
  • 无条件执行(内部 CER 检查去重),不存在”根据 InvocationFlags 跳过”的分支

2.2 m_invocationFlags 是纯托管概念

侧位置作用
mscorlib 托管RuntimeMethodInfo.m_invocationFlags 字段MethodInfo.Invoke 的准入校验缓存(NoInvoke/ContainsStackPointers → 抛异常)
clr.dll 原生不存在grep 全部 VM 代码零命中

托管 get_InvocationFlags IL 结构(net48 实测反汇编):读 m_invocationFlags 字段 → 测 INITIALIZED 位 → 未初始化则懒计算(GetSecurityFlags + 逐位 OR)→ 写回缓存。消费者:InvokeArgumentsCheck/UnsafeInvoke/RuntimeConstructorInfo。与 JIT 准备无关。

2.3 真正消费 MethodDesc+6 字节的函数(★ = 手术影响点)

函数地址读取点作用
TryGetMultiCallableAddrOfCode0x18003de10(b6 & 7) == 1 分类位;b[3]&1 HasStableEntryPointGetFunctionPointer 返回真代码 vs precode 的核心决策
MayHaveNativeCode0x180010b70(冷路径 0x18040408f)★test [rbx+6], 0x20mdcStatic 位参与”可直达 native code”判定
MayBeRemotingIntercepted0x18003dd90开头即 test [rcx+6], 0x20 → return 0mdcStatic 短路 remoting 拦截
IsVtableMethod0x1800070c0(冷路径 0x180108568)同读 0x20 与 0x80虚表判定

IDA 中已按 [HookInfo 分析] 前缀添加注释(11 处函数级 + 11 处函数内关键指令),Ctrl+F 可搜。

2.4 TryGetMultiCallableAddrOfCode 完整决策树(0x18003de10)

GetFunctionPointer() → QCall RuntimeMethodHandle::GetFunctionPointer(0x180066080)→ 本函数。入参 accessFlags(GetFunctionPointer 传 CORINFO_ACCESS_UNMANAGED_CALLER_MAYBE|LDFTN=16,v5 = a2 & 1 即 UNMANAGED_CALLER 位)。

决策顺序(每条分支 = 一种方法形态拿 entrypoint 的方式):

text
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)  ←★ byte+6 分类位(HookInfo WriteByte 影响的判定之一)
│      且 非remoting拦截 且 MethodTable 无 (0xF0000)==0xC0000(?)且 (MT & 0x20000000)==0
│      → ECall::GetFCallImpl(md)                ← 直接返回 FCall 的原生实现地址!
│        (GetFCallImpl 失败 → GetOrCreatePrecode)
│      【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 的方法(如需 backpatch 的场景)】
│
├─⑧ MayHaveNativeCode(md)==0 或 IsRemotingInterceptedViaPrestub(md)
│      → GetOrCreatePrecode(md)
│      【★ MayHaveNativeCode 读 byte+6 的 0x20(mdcStatic)——HookInfo 手术的机制命中点。
│         判定失败 = 该方法"不可能有 native code"→ 只能给 precode(Hook 拿不到真实函数头的根因之一)】
│
└─⑨ md 所在 chunk 的 word >= 0(a1[-8*chunkIndex-6])
       → GetTemporaryEntryPoint(md)            ← 临时入口点(PrecodeFixupThunk 场景)
   否则 → return NULL

2.5 各被调函数语义(clr.dll 实测反编译)

函数地址功能
IsGenericMethodDefinition0x18000e19c是否泛型方法定义(未实例化)。是 → 直接抛 InvalidOperationException, Hook 不能挂未实例化泛型
IsWrapperStub0x180029228是否 unboxing stub 或 instantiating stub(泛型包装桩)。这类方法的”入口”语义是桩本身,须先 GetOrCreatePrecode
IsEnCAddedMethod0x18002ad0c是否 EnC(Edit and Continue)运行期新增方法
IsRemotingInterceptedViaVirtualDispatch0x18003dd64MayBeRemotingIntercepted && IsVtableMethod——remoting 透明代理场景的虚方法拦截
IsRemotingInterceptedViaPrestub0x18001123cMayBeRemotingIntercepted && !IsVtableMethod && !RequiresInstArg —— 非虚方法的 remoting 拦截走 prestub
MayBeRemotingIntercepted0x18003dd90开头 test [rcx+6],0x20(mdcStatic)→ 静态方法直接 return 0;再排除 MT flags 0xE0000==0x20000(context-bound?)及 Object ctor/finalizer
IsPointingToNativeCode0x18003df00读 b[3]&1(HasStableEntryPoint)+ b[3]&2(HasNativeCodeSlot)+ Precode::IsPointingTo(entry, nativeCode)——判断 entry 是否已指向真实 JIT 代码
MayHaveNativeCode0x180010b70综合分类位 + MT flags(0xF0000==0xC0000 即 COM? / 0x80000030)+ byte+6 的 0x20 位,判定”此方法可能存在 native code”。失败 → GetOrCreatePrecode(Hook 拿不到函数头的根因场景)
IsRemotingInterceptedViaPrestub0x18001123c见上 MayBeRemotingIntercepted
GetNativeCode0x1800103dc读 b[3]&8(HasNativeCodeSlot)→ 按 s_ClassificationSizeTable[分类] 定位代码槽指针,& ~1(bit0 是标志位)。无 native code slot 返回 0
GetMethodEntryPoint0x180006c78读方法当前入口点槽:有 native code slot(b[3]&8)走 ClassificationSizeTable 定位;否则从 MethodTable slot / chunk 间接解析。返回的可能是 precode 或真实代码
GetTemporaryEntryPoint0x18001df40沿 MethodDescChunk 的 TempEntryPoints 表找/分配临时入口(PrecodeFixupThunk 形态的来源之一)。GetOrCreatePrecode 内部也依赖它
GetOrCreatePrecode0x1801736dc有 TempEntry 就地升级为 precode(AllocMemTracker 分配),否则新分配 Precode(StubPrecode/FixupPrecode)并回填 entrypoint
ECall::GetFCallImpl0x180011578查 ECall 表拿 FCall 的 C++ 实现地址(Console.WriteLine、String.Compare 这类内部方法不走 JIT)
CRemotingServices::GetNonVirtualEntryPointForVirtualMethod0x18003e364remoting 场景:虚方法被透明代理拦截时返回非虚入口
IBCLogger::LogMethodDescAccessStatic(多处)IBC(Incremental Build Compatibility)日志探针,retail 关闭,无功能影响

一句话总结分支语义:

  • ⑤ FCall → 直达 C++ 实现
  • ⑥ 已 JIT 无 precode → 直达 native code(Hook 最理想)
  • ⑦ 已 JIT 有 precode → 稳定入口(可能 E8/E9 跳板,需 HookInfo 的 md+8 跟随)
  • ⑧⑨ 未 JIT / 异常场景 → precode / 临时入口(Hook 不能直接 patch 这里)

2.6 byte+6 位语义(net48 x64 实测)

位含义实测佐证
& 7分类(mcIL=0/mcFCall=1/mcPInvoke=2/mcEEImpl=3/mcArray=4/mcInstantiated=5/mcDynamic=7)String.ToUpper=0x08(mcIL+HasNonVtableSlot)
0x08mdcHasNonVtableSlot实例方法 0x08、静态方法 0x28
0x20mdcStatic(静态缓存位)IsNullOrEmpty(静态)=0x28、ToUpper(实例)=0x08;MayBeRemotingIntercepted 读此位
0x80mdcRequiresInheritanceCheck继承/override 链方法置位;注意: lordmilko/DebugTools 的 mdcStatic=0x0080 是 16 位 flags 新定义,对 net48 x64 的 byte+6 不适用

CoreCLR 现代源码(method.hpp)中对应的是 mdfStatic = 0x0080(16 位 m_wFlags),布局已完全重构 —— 不能拿新源码套 net48 x64 的字节偏移。

三、md+8 语义与真实函数头获取(复现定案)

3.1 md+8(int32)= 真实 JIT 代码相对 MethodDesc 的偏移

text
realCode = md + ReadInt32(md+8) + 8

实测(ReproMd8Probe):StartWithShellExecuteEx md+8=0x82FEE0,md+0x82FEE0+8 = 0x7FF9A2F96A50 → 55 41 56 57(真实函数头)✓

HookInfo.Initialize() 的 E9/E8 跟随逻辑就是这个:Value = ReadInt32(md+8); Value = md + Value + 8,任何入口形态下都能拿到真实代码。

3.2 PrepareMethod 前后 entry 对比(无手术对照)

阶段entry形态
反射刚拿到(未 JIT)0x7FF94A550718E8 93 DE 7C 5F → clr.dll+0x18E5B0(PrecodeFixupThunk)
GetParameters 后同上无变化
PrepareMethod 后0x7FF94A44D080E9 63 C9 EF FF → 0x7FF94A44D080,bytes=48 83 EC 28 真实函数头

普通 PrepareMethod 就能让 entry 从 E8 回填成 E9 直达真实代码 —— 手术对这批样本非必需。

3.3 E8 跳板目标 = PrecodeFixupThunk

entry+5+rel32 = clr.dll+0x18E5B0(共享 PrecodeFixupThunk,所有未 JIT 方法指向同一地址)。GetJitFunPtrStatus 的 Injected 判定(b1 == 0xe9 || 0xe8)捕捉的就是这种形态。

四、复现实验(ReproMdcStatic / ReproMd8Probe / ReproSurgeryHunter)

配套复现工程 FunctionPointerDemo(net48,x64/x86 双构建)

4.1 广撒网扫描(ReproSurgeryHunter,21128 个 BCL 方法)

指标数值
扫描方法21128(mscorlib/System/System.Core/System.Data/System.Web)
带 0x802171
无手术 PrepareMethod 后仍 thunk100(51 去重后)
手术+PrepareMethod 后回填0(★★ 实锤 = 0)

手术在纯 BCL 环境未复现到”有效”场景。 仍 thunk 的方法全是 async/Task/IO 类(Stream.ReadAsync、TaskAwaiter.OnCompleted、CancellationToken.Equals…),by design(IL stub/delegate,md+8 编码 token 非偏移),手术天然无效。

4.2 md+8 两分类(仍 thunk 方法)

类别md+8 特征HookInfo 跟随落点占比
正值偏移~0xBC4E60(~12MB)0x7FF9A6BB****,bytes[0]=48/57 函数头形态 ✓28
负值 token0x9101B44D 等(高位 1)0x7FF9370D**** 垃圾23

→ HookInfo 的 md+md8+8 跟随对正值 md+8 有效,负值(token 类)落垃圾。

4.3 触发条件的真实语义

nOffsetPre8 < 0xFF(有符号 int32 比较):

  • Process.Start md+8=0xA2F97230 → 负数 → 恒 < 0xFF → 手术触发 ✓(此时方法已 JIT,entry=真实函数头,手术前后无变化)
  • StartWithShellExecuteEx md+8=0x82FEE0 → 正数且 ≥ 0xFF → 不触发
  • 未 JIT 方法 md+8 是小 offset(如 ReproDerived.Compute md+8=0)→ 触发检查

4.4 实验环境注意

  • WSL interop 会把 32 位 exe 拉成 64 位宿主 → x86 验证必须 Windows 侧 run-net48-x86.bat
  • x86 进程下手术分支不走(!Is64BitProcess 只裸 PrepareMethod);x86 的 md+8 是 m_codeOrIL 指针(大值)
  • MEMORY_BASIC_INFORMATION x64 布局:RegionSize 必须 UIntPtr 且 8 字节对齐(+24),否则 VirtualQuery 全误判

五、当年为什么加 mdcStatic —— 结论(2026-09-10 已实锤,见第六章)

  1. 不是”PrepareMethod 通过 m_invocationFlags 判断是否执行”——PrepareMethod 无条件执行
  2. 不是”让 GetFunctionPointer 返回真实函数头”——x64 普通 PrepareMethod 就能回填(E8→E9)
  3. 已实锤的真实作用(x86 4.8.1 9181,第六章):MD+6 的分类位决定 prepare 管线分流——0x80(继承检查)方法只登记不 JIT(等首次调用激活),0x20(mdcStatic)方法在 PrepareMethod 内部直接 JIT。手术 = 把方法从保守登记路径切到 JIT 路径。x64 的对应消费点在 GetFunctionPointer 决策链(MayHaveNativeCode),x86 的消费点在 PrepareMethodDesc 管线(ExecuteGate)
  4. 生产取头路径:Initialize() 的 md + ReadInt32(md+8) + 8 偏移跟随仍是主力;手术是让 PrepareMethod 真正生效的前置条件(x86 场景必需)

六、相关文件

文件说明
FunctionPointerDemo/ReproMdcStatic.csHookInfo 手术复现(8 候选对比)
ReproMd8Probe.csmd+8 语义探针(三阶段 PrepareMethod 前后)
ReproSurgeryHunter.csBCL 广撒网扫描(21128 方法)
ReproClassificationProbe.cs2026-09-10 新增:分类判定位指纹矩阵(12 值安全集,entry 前后字节对比 + B0 状态桩识别)+ SurgeryTest(手术有效性,带 fresh md8!=0 不可归因警告)+ DumpFollowup(双形态:带槽 md+md8+8 / 无槽 precode)+ StickyTest(登记粘性:首次 PM 后手术是否仍有效)。模式:CV=主域矩阵 / H/HS=子域对照 / D/E/F=Mine 维度拆解 / ST=粘性测试;COMPLUS_ZapDisable=1 可复现 IL-only dump 场景
IDA clr.dll 数据库22 条 [HookInfo 分析] 注释(x64)+ 9181 x86 版 30 函数命名/21 注释
Hook 框架初始化组件(HookInfo.cs)PrepareOldMethodForHook 位手术与 md+md8+8 跟随逻辑(L97-247)

七、改造建议(上轮可行性分析的落地)

环节属性方案可行性建议
读 INITIALIZED 位判断”反射缓存已暖”✅GetProperty("InvocationFlags") 优先,m_invocationFlags 字段回退,最后 +6 字节
写 MDC 分类位(0x20/0x80)❌保留 Marshal.WriteByte(+6) —— native 侧直接消费的内存位,托管层无等价入口
取真实函数头—已有 md+md8+8 跟随,无需改动

八、x86 4.8.1(9181)实锤:手术如何让 PrepareMethod 真 JIT

本章环境:Chanjet.TP.WebServer.FastCgi.exe(Nginx 反代 32 位 FastCGI 自宿主),CLR 4.8.9181.0 x86,System.dll 以 per-domain IL 副本加载(非 NGEN)。IDA 逆向对象:该环境 clr.dll(已标注 13 个函数)。

8.1 一个前置问题:为什么 v4.0.30319 的 clr.dll 下载不到 PDB

msdl(https://msdl.microsoft.com/download/symbols)只托管微软公开发布的二进制对应 PDB,索引以 PE 内嵌的 GUID+age 为准。下载不到只有三种可能:

  1. 非公开构建:内部重编/实验版/非 shipping 版本不在公开符号服务上(chk/retail 内部号);
  2. 公开但有条件:Win11 24H2/25H2 起 .NET Framework servicing 的符号改为随 Windows 更新分发,msdl 镜像滞后或只对部分 build 发布(实测本机 9337 GUID 直 404);
  3. GUID 输错:PDB 目录里取的 GUID 大小写/age 拼接错(正确格式 {32位大写HEX}{age十进制})。

排查口诀:!lmi clr(windbg,看 Symbol Type 与 PDB 路径)→ !sym noisy + .reload /f clr.dll(看 sym srv 实际请求 URL 与响应码)。本机 9337 即第 2 类(404);若 dump 里的 9181 能被 windbg 解析,说明 9181 在 msdl 有(或符号来自本地缓存/内部符号服务器)。

8.2 实测 A/B 对比(FastCGI 环境,System.dll IL 副本的 StartWithShellExecuteEx)

字段B:不改(原 0x80)A:改 0x20 后 PrepareMethod
MD+3(prepare 状态机)0x08 → 0x0B(登记完成)0x08 → 0x39(JIT 完成)
MD+70x00 → 0x20(已登记标志)0x00 → 0x20
md+8保持 0(无代码)0 → 0xfae3096c(相对偏移,指向 code_real_addr-8)
!name2ee IsJittedno(CodeAddr=ffffffff)yes
CodeAddr 内存—(无代码)55 8B EC 57 56 53 83 EC 30(真实函数头)

8.3 x86 9181 PrepareMethod 完整调用流

PlantUML diagram

分流本质:MD+6 的 classification 字节决定方法在 gate 链中的命运。0x80(mdcRequiresInheritanceCheck,方法在 override 链上)→ CLR 无法保证继承解析完成前 JIT 的代码不作废 → prepare 管线对它只挂账(+3 停在 0x0B、md+8 保持 0),等首次真实调用走常规 PreStubWorker 激活。0x20(mdcStatic,「无继承解析需求」)→ gate 全放行 → ExecuteGate 立即 JIT 并回填。

judge/gate 的消费位(指令级):judge1 = [MD+3]&4==0 + 协同谓词;judge2 与 ClsCmpGate = [MD+6]&7(低 3 位分类:0=IL)。0x20/0x80 高位不经这些点——分流发生在 gate 群内部的分散检查与 ExecuteGate 的 vtable 目标选择(详见 IDA 标注:PrepareMethodDesc 及其调用链 8 处注释)。

8.4 与 9337 的 servicing 差异(行为不同:9181 拦 / 9337 不拦)

本节为 2026-09-10 深夜修正版:早前发布的「9337 判定位漂移至 0x08」是错误归因——当时用「PM 后 md+8 是否变化(槽分配)」当 JIT 判据,实际上 md+8 变化只代表槽分配,JIT 与否要看 md+md8+8 是否出现真实函数头。

用 COMPLUS_ZapDisable=1(禁 NGEN,强制 IL-only,fresh 状态 md+8=0、+3=0x0B、b6=0x80 与 dump 完全同态)重跑后的修正结论:

场景9181(A/B dump 实测)9337(ZapDisable 实测)
0x80 不手术 + PrepareMethod不 JIT(+3=0x0B、md+8 恒 0、CodeAddr 不可用)JIT(+3=0x3B、md+8 回填、md+md8+8=55-8B-EC-57)
0x20 手术 + PrepareMethodJIT(+3=0x39、md+8 回填)JIT

结论修正为:9181 的 PrepareMethodDesc 管线(Finalize 执行循环 gate 链)确实拦截 0x80 方法;9337 的同类管线不拦(0x80 也直接 JIT)。servicing 差异是「拦不拦」的行为差异,不是「判定位漂移」。9337 版本的 JIT 触发点不在我逆向到的 PrepareMethodDesc 主体内(该函数只有登记二分 + SetFlag6),具体实现在入口函数的前后段,待后续逆向。

另:9-4 旧文的 x64 结论(PrepareMethod 无条件 ForceJIT)与 9337 x86 行为同构——9337 代两个架构行为一致,9181 x86 的拦截是旧 servicing 特有行为。这也解释了 9-4 时「x64 手术在 BCL 扫描 0 命中」——x64 上不手术也 JIT,手术自然测不出差异。

附带方法论教训(写给自己):跨进程验证 JIT 状态,唯一可信判据是 md + ReadInt32(md+8) + 8 处的字节内容;entry 指针是 PM 前缓存的 precode 地址,JIT 回填不反映在 precode 体,不能作为判据。

8.5 生产方案(x86 HookInfo 修复)

★ 顺序约束(关鍵):手术必须发生在首次 PrepareMethod 之前——首次 PM 会把方法登记进 module 注册表,之后再 PM 走「已登记」分支(SetFlag6 + Register 直接返回),不会 JIT。9-4 版 HookInfo 的「先 PM 再手术再 PM」结构在 9181 上无效就是这个原因。

C#csharp
byte b6 = Marshal.ReadByte(md + 6);
if ((b6 & 0x80) != 0)
{
    Marshal.WriteByte(md + 6, (byte)((b6 & 0x0F) | 0x20));   // 清 0x80,设 mdcStatic —— 与 x64 手术同公式
    try { RuntimeHelpers.PrepareMethod(handle); /* 然后安装 hook */ }
    finally { Marshal.WriteByte(md + 6, b6); }               // hook 装完立即还原(instance 伪装 static 窗口期最小化)
}
else
{
    RuntimeHelpers.PrepareMethod(handle);
}

配套校验(防 servicing 静默失效):PrepareMethod 后读 +3——0x0B→0x39(bit0 置位)且 md+md8+8 处出现 55 8B 函数头即成功;否则 fallback(precode patch / 激活时 hook)。注:9337+ 上不手术也 JIT(8.4),手术是无害冗余——两代 servicing 下这套代码都工作。

8.6 IDA 落库清单(9181 clr,13 函数 + 8 注释)

地址名称说明
0x1043D380ReflectionInvocation__PrepareMethod202 条,特征码命中
0x1007D996PrepareMethodDescecx=pMD, edx, 5栈参 retn 14h
0x100DC451PrepMD_IsRegistered_BinSearch二分 [modstruct+368] 表
0x1001EFE6PrepMD_GetModuleStruct
0x100937FC / 0x100DC6A9PrepMD_Unreg_InitRecord / FillAndEnqueuerecord 0x10 字节初始化 + 字段填充
0x1009387BPrepMD_Unreg_Finalize队列执行循环
0x10038AD7 / 0x100D6B06 / 0x101838ECClsCmpGate / ExecuteGate / StateMachinegate 链与执行动作
0x1015A768泛型哈希 gate29 桶,仅泛型路径(edx≠0)