-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathDetourRuntimeILPlatform.cs
More file actions
488 lines (408 loc) · 20.5 KB
/
DetourRuntimeILPlatform.cs
File metadata and controls
488 lines (408 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using MonoMod.Utils;
using System.Linq;
using Mono.Cecil.Cil;
using System.Threading;
#if !NET35
using System.Collections.Concurrent;
#endif
namespace MonoMod.RuntimeDetour.Platforms {
#if !MONOMOD_INTERNAL
public
#endif
abstract class DetourRuntimeILPlatform : IDetourRuntimePlatform {
protected abstract RuntimeMethodHandle GetMethodHandle(MethodBase method);
private readonly GlueThiscallStructRetPtrOrder GlueThiscallStructRetPtr;
// The following dicts are needed to prevent the GC from collecting DynamicMethods without any visible references.
// PinnedHandles is also used in certain situations as a fallback when getting a method from a handle may not work normally.
#if NET35
protected Dictionary<MethodBase, PrivateMethodPin> PinnedMethods = new Dictionary<MethodBase, PrivateMethodPin>();
protected Dictionary<RuntimeMethodHandle, PrivateMethodPin> PinnedHandles = new Dictionary<RuntimeMethodHandle, PrivateMethodPin>();
#else
protected ConcurrentDictionary<MethodBase, PrivateMethodPin> PinnedMethods = new ConcurrentDictionary<MethodBase, PrivateMethodPin>();
protected ConcurrentDictionary<RuntimeMethodHandle, PrivateMethodPin> PinnedHandles = new ConcurrentDictionary<RuntimeMethodHandle, PrivateMethodPin>();
#endif
public abstract bool OnMethodCompiledWillBeCalled { get; }
public abstract event OnMethodCompiledEvent OnMethodCompiled;
private IntPtr ReferenceNonDynamicPoolPtr;
private IntPtr ReferenceDynamicPoolPtr;
public DetourRuntimeILPlatform() {
// Perform a selftest if this runtime requires special handling for instance methods returning structs.
// This is documented behavior for coreclr, but affects other runtimes (i.e. mono) as well!
// Specifically, this should affect all __thiscalls
// Use reflection to make sure that the selftest isn't optimized away.
// Delegates are quite reliable for this job.
MethodInfo selftestGetRefPtr = typeof(DetourRuntimeILPlatform).GetMethod("_SelftestGetRefPtr", BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo selftestGetRefPtrHook = typeof(DetourRuntimeILPlatform).GetMethod("_SelftestGetRefPtrHook", BindingFlags.NonPublic | BindingFlags.Static);
_HookSelftest(selftestGetRefPtr, selftestGetRefPtrHook);
IntPtr selfPtr = ((Func<IntPtr>) Delegate.CreateDelegate(typeof(Func<IntPtr>), this, selftestGetRefPtr))();
MethodInfo selftestGetStruct = typeof(DetourRuntimeILPlatform).GetMethod("_SelftestGetStruct", BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo selftestGetStructHook = typeof(DetourRuntimeILPlatform).GetMethod("_SelftestGetStructHook", BindingFlags.NonPublic | BindingFlags.Static);
_HookSelftest(selftestGetStruct, selftestGetStructHook);
unsafe {
fixed (GlueThiscallStructRetPtrOrder* orderPtr = &GlueThiscallStructRetPtr) {
((Func<IntPtr, IntPtr, IntPtr, _SelftestStruct>) Delegate.CreateDelegate(typeof(Func<IntPtr, IntPtr, IntPtr, _SelftestStruct>), this, selftestGetStruct))((IntPtr) orderPtr, (IntPtr) orderPtr, selfPtr);
}
}
// Get some reference (not reference as in ref but reference as in "to compare against") dyn and non-dyn method pointers.
Pin(selftestGetRefPtr);
ReferenceNonDynamicPoolPtr = GetNativeStart(selftestGetRefPtr);
if (DynamicMethodDefinition.IsDynamicILAvailable) {
MethodBase scratch;
using (DynamicMethodDefinition copy = new DynamicMethodDefinition(_MemAllocScratchDummy)) {
copy.Name = $"MemAllocScratch<Reference>";
scratch = DMDEmitDynamicMethodGenerator.Generate(copy);
}
Pin(scratch);
ReferenceDynamicPoolPtr = GetNativeStart(scratch);
}
}
private void _HookSelftest(MethodInfo from, MethodInfo to) {
Pin(from);
Pin(to);
NativeDetourData detour = DetourHelper.Native.Create(
GetNativeStart(from),
GetNativeStart(to),
null
);
DetourHelper.Native.MakeWritable(detour);
DetourHelper.Native.Apply(detour);
DetourHelper.Native.MakeExecutable(detour);
DetourHelper.Native.FlushICache(detour);
DetourHelper.Native.Free(detour);
// No need to undo the detour.
}
#region Selftests
#region Selftest: Get reference ptr
[MethodImpl(MethodImplOptions.NoInlining)]
private IntPtr _SelftestGetRefPtr() {
Console.Error.WriteLine("If you're reading this, the MonoMod.RuntimeDetour selftest failed.");
throw new Exception("This method should've been detoured!");
}
private static unsafe IntPtr _SelftestGetRefPtrHook(IntPtr self) {
// This is only needed to obtain a raw IntPtr to a reference object.
return self;
}
#endregion
#region Selftest: Struct
// In 32-bit envs, struct must be 3 or 4+ bytes big.
// In 64-bit envs, struct must be 3, 5, 6, 7 or 9+ bytes big.
#pragma warning disable CS0169
private struct _SelftestStruct {
private readonly byte A, B, C;
}
#pragma warning restore CS0169
[MethodImpl(MethodImplOptions.NoInlining)]
private _SelftestStruct _SelftestGetStruct(IntPtr x, IntPtr y, IntPtr thisPtr) {
Console.Error.WriteLine("If you're reading this, the MonoMod.RuntimeDetour selftest failed.");
throw new Exception("This method should've been detoured!");
}
private static unsafe void _SelftestGetStructHook(IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e) {
// Normally, a = this, b = x, c = y, d = thisPtr, e = garbage
// For the general selftest, x must be equal to y.
// If b != c, b is probably pointing to the return buffer or this.
if (b == c) {
// Original order.
*((GlueThiscallStructRetPtrOrder*) b) = GlueThiscallStructRetPtrOrder.Original;
} else if (b == e) {
// For mono in Unity 5.6.X, a = __ret, b = this, c = x, d = y, e = thisPtr
*((GlueThiscallStructRetPtrOrder*) c) = GlueThiscallStructRetPtrOrder.RetThisArgs;
} else {
// For coreclr x64 __thiscall, a = this, b = __ret, c = x, d = y, e = thisPtr
*((GlueThiscallStructRetPtrOrder*) c) = GlueThiscallStructRetPtrOrder.ThisRetArgs;
}
}
#endregion
#endregion
#if UNITY_5_3_OR_NEWER //source https://github.com/Misaka-Mikoto-Tech/MonoHook
[StructLayout(LayoutKind.Sequential, Pack = 1)] // 好像在 IL2CPP 里无效
private struct __ForCopy
{
public long __dummy;
public MethodBase method;
}
protected virtual unsafe IntPtr GetFunctionPointer(MethodBase method,RuntimeMethodHandle handle)
{
// if (!LDasm.IsIL2CPP())
// return method.MethodHandle.GetFunctionPointer();
// else
__ForCopy __forCopy = new __ForCopy() {method = method};
long* ptr = &__forCopy.__dummy;
ptr++; // addr of _forCopy.method
IntPtr methodAddr = IntPtr.Zero;
if (sizeof(IntPtr) == 8)
{
long methodDataAddr = *(long*) ptr;
byte* ptrData = (byte*) methodDataAddr + sizeof(IntPtr) * 2; // offset of Il2CppReflectionMethod::const MethodInfo *method;
long methodPtr = 0;
methodPtr = *(long*) ptrData;
methodAddr = new IntPtr(*(long*) methodPtr); // MethodInfo::Il2CppMethodPointer methodPointer;
}
else
{
int methodDataAddr = *(int*) ptr;
byte* ptrData = (byte*) methodDataAddr + sizeof(IntPtr) * 2; // offset of Il2CppReflectionMethod::const MethodInfo *method;
int methodPtr = 0;
methodPtr = *(int*) ptrData;
methodAddr = new IntPtr(*(int*) methodPtr);
}
return methodAddr;
}
#else
protected virtual IntPtr GetFunctionPointer(MethodBase method, RuntimeMethodHandle handle)
=> handle.GetFunctionPointer();
#endif
protected virtual void PrepareMethod(MethodBase method, RuntimeMethodHandle handle)
=> RuntimeHelpers.PrepareMethod(handle);
protected virtual void PrepareMethod(MethodBase method, RuntimeMethodHandle handle, RuntimeTypeHandle[] instantiation)
=> RuntimeHelpers.PrepareMethod(handle, instantiation);
protected virtual void DisableInlining(MethodBase method, RuntimeMethodHandle handle) {
// no-op. Not supported on all platforms, but throwing an exception doesn't make sense.
}
public virtual MethodBase GetIdentifiable(MethodBase method) {
#if NET35
lock (PinnedMethods)
return PinnedHandles.TryGetValue(GetMethodHandle(method), out PrivateMethodPin pin) ? pin.Pin.Method : method;
#else
return PinnedHandles.TryGetValue(GetMethodHandle(method), out PrivateMethodPin pin) ? pin.Pin.Method : method;
#endif
}
public virtual MethodPinInfo GetPin(MethodBase method) {
#if NET35
lock (PinnedMethods)
return PinnedMethods.TryGetValue(method, out PrivateMethodPin pin) ? pin.Pin : default;
#else
return PinnedMethods.TryGetValue(method, out PrivateMethodPin pin) ? pin.Pin : default;
#endif
}
public virtual MethodPinInfo GetPin(RuntimeMethodHandle handle) {
#if NET35
lock (PinnedMethods)
return PinnedHandles.TryGetValue(handle, out PrivateMethodPin pin) ? pin.Pin : default;
#else
return PinnedHandles.TryGetValue(handle, out PrivateMethodPin pin) ? pin.Pin : default;
#endif
}
public virtual MethodPinInfo[] GetPins() {
#if NET35
lock (PinnedMethods)
return PinnedHandles.Values.Select(p => p.Pin).ToArray();
#else
return PinnedHandles.Values.ToArray().Select(p => p.Pin).ToArray();
#endif
}
public virtual IntPtr GetNativeStart(MethodBase method) {
method = GetIdentifiable(method);
bool pinGot;
PrivateMethodPin pin;
#if NET35
lock (PinnedMethods)
#endif
{
pinGot = PinnedMethods.TryGetValue(method, out pin);
}
if (pinGot)
return GetFunctionPointer(method, pin.Pin.Handle);
return GetFunctionPointer(method, GetMethodHandle(method));
}
public virtual void Pin(MethodBase method) {
method = GetIdentifiable(method);
#if NET35
lock (PinnedMethods) {
if (PinnedMethods.TryGetValue(method, out PrivateMethodPin pin)) {
pin.Pin.Count++;
return;
}
MethodBase m = method;
pin = new PrivateMethodPin();
pin.Pin.Count = 1;
#else
Interlocked.Increment(ref PinnedMethods.GetOrAdd(method, m => {
PrivateMethodPin pin = new PrivateMethodPin();
#endif
pin.Pin.Method = m;
RuntimeMethodHandle handle = pin.Pin.Handle = GetMethodHandle(m);
PinnedHandles[handle] = pin;
DisableInlining(method, handle);
if (method.DeclaringType?.IsGenericType ?? false) {
PrepareMethod(method, handle, method.DeclaringType.GetGenericArguments().Select(type => type.TypeHandle).ToArray());
} else {
PrepareMethod(method, handle);
}
#if !NET35
return pin;
#endif
}
#if !NET35
).Pin.Count);
#endif
}
public virtual void Unpin(MethodBase method) {
method = GetIdentifiable(method);
#if NET35
lock (PinnedMethods) {
if (!PinnedMethods.TryGetValue(method, out PrivateMethodPin pin))
return;
if (pin.Pin.Count <= 1) {
PinnedMethods.Remove(method);
PinnedHandles.Remove(pin.Pin.Handle);
return;
}
pin.Pin.Count--;
}
#else
if (!PinnedMethods.TryGetValue(method, out PrivateMethodPin pin))
return;
if (Interlocked.Decrement(ref pin.Pin.Count) <= 0) {
PinnedMethods.TryRemove(method, out _);
PinnedHandles.TryRemove(pin.Pin.Handle, out _);
}
#endif
}
public MethodInfo CreateCopy(MethodBase method) {
method = GetIdentifiable(method);
if (method == null || (method.GetMethodImplementationFlags() & (MethodImplAttributes.OPTIL | MethodImplAttributes.Native | MethodImplAttributes.Runtime)) != 0) {
throw new InvalidOperationException($"Uncopyable method: {method?.ToString() ?? "NULL"}");
}
using (DynamicMethodDefinition dmd = new DynamicMethodDefinition(method))
return dmd.Generate();
}
public bool TryCreateCopy(MethodBase method, out MethodInfo dm) {
method = GetIdentifiable(method);
if (method == null || (method.GetMethodImplementationFlags() & (MethodImplAttributes.OPTIL | MethodImplAttributes.Native | MethodImplAttributes.Runtime)) != 0) {
dm = null;
return false;
}
try {
dm = CreateCopy(method);
return true;
} catch {
dm = null;
return false;
}
}
public MethodBase GetDetourTarget(MethodBase from, MethodBase to) {
to = GetIdentifiable(to);
MethodInfo dm = null;
if (GlueThiscallStructRetPtr != GlueThiscallStructRetPtrOrder.Original &&
from is MethodInfo fromInfo && !from.IsStatic &&
to is MethodInfo toInfo && to.IsStatic &&
fromInfo.ReturnType == toInfo.ReturnType &&
fromInfo.ReturnType.IsValueType) {
int size = fromInfo.ReturnType.GetManagedSize();
// This assumes that 8 bytes long structs work fine in 64-bit envs but not 32-bit envs.
if (size == 3 || size == 5 || size == 6 || size == 7 || size > IntPtr.Size) {
Type thisType = from.GetThisParamType();
Type retType = fromInfo.ReturnType.MakeByRefType(); // Refs are shiny pointers.
int thisPos = 0;
int retPos = 1;
if (GlueThiscallStructRetPtr == GlueThiscallStructRetPtrOrder.RetThisArgs) {
thisPos = 1;
retPos = 0;
}
List<Type> argTypes = new List<Type> {
thisType
};
argTypes.Insert(retPos, retType);
argTypes.AddRange(from.GetParameters().Select(p => p.ParameterType));
using (DynamicMethodDefinition dmd = new DynamicMethodDefinition(
$"Glue:ThiscallStructRetPtr<{from.GetID(simple: true)},{to.GetID(simple: true)}>",
typeof(void), argTypes.ToArray()
)) {
ILProcessor il = dmd.GetILProcessor();
// Load the return buffer address.
il.Emit(OpCodes.Ldarg, retPos);
// Invoke the target method with all remaining arguments.
{
il.Emit(OpCodes.Ldarg, thisPos);
for (int i = 2; i < argTypes.Count; i++)
il.Emit(OpCodes.Ldarg, i);
il.Emit(OpCodes.Call, il.Body.Method.Module.ImportReference(to));
}
// Store the returned object to the return buffer.
il.Emit(OpCodes.Stobj, il.Body.Method.Module.ImportReference(fromInfo.ReturnType));
il.Emit(OpCodes.Ret);
dm = dmd.Generate();
}
}
}
return dm ?? to;
}
public uint TryMemAllocScratchCloseTo(IntPtr target, out IntPtr ptr, int size) {
/* We can create a new method that is of the same type (dynamic or non-dynamic) as the target method,
* assume that it will be closer to it than a new method of the opposite type, and use it as a pseudo-malloc.
*
* This is only (kinda?) documented on mono so far.
* See https://www.mono-project.com/docs/advanced/runtime/docs/memory-management/#memory-management-for-executable-code
* It seems to also be observed on .NET Framework to some extent, although no pattern is determined yet. Maybe x86 debug?
*
* In the future, this might end up requiring and calling new native platform methods.
* Ideally this should be moved into the native platform which then uses some form of VirtualAlloc / mmap hackery.
*
* This is quite ugly, especially because we have no direct control over the allocated memory location nor size.
* -ade
*/
if (size == 0 ||
size > _MemAllocScratchDummySafeSize) {
ptr = IntPtr.Zero;
return 0;
}
const long GB = 1024 * 1024 * 1024;
bool isNonDynamic = Math.Abs((long) target - (long) ReferenceNonDynamicPoolPtr) < GB;
bool isDynamic = Math.Abs((long) target - (long) ReferenceDynamicPoolPtr) < GB;
if (!isNonDynamic && !isDynamic) {
ptr = IntPtr.Zero;
return 0;
}
MethodBase scratch;
using (DynamicMethodDefinition copy = new DynamicMethodDefinition(_MemAllocScratchDummy)) {
copy.Name = $"MemAllocScratch<{(long) target:X16}>";
// On some versions of mono it is also possible to get dynamics close to non-dynamics by invoking before force-JITing.
if (isDynamic)
scratch = DMDEmitDynamicMethodGenerator.Generate(copy);
else
scratch = DMDCecilGenerator.Generate(copy);
}
Pin(scratch);
ptr = GetNativeStart(scratch);
DetourHelper.Native.MakeReadWriteExecutable(ptr, _MemAllocScratchDummySafeSize);
return _MemAllocScratchDummySafeSize;
}
/* Random garbage method that should JIT into enough memory for us to write arbitrary data to,
* but not too much for it to become wasteful once it's called often.
* Use https://sharplab.io/ to estimate the footprint of the dummy when modifying it.
* Make sure to measure both release and debug mode AND both x86 and x64 JIT results!
* Neither mono nor ARM are options on sharplab.io, but hopefully it'll be enough as well...
* Note that it MUST be public as there have been reported cases of visibility checks kicking in!
* -ade
*/
// Lowest measured so far: ret @ 0x19 on .NET Core x64 Release.
protected static readonly uint _MemAllocScratchDummySafeSize = 16;
protected static readonly MethodInfo _MemAllocScratchDummy =
typeof(DetourRuntimeILPlatform).GetMethod(nameof(MemAllocScratchDummy), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
public static int MemAllocScratchDummy(int a, int b) {
if (a >= 1024 && b >= 1024)
return a + b;
return MemAllocScratchDummy(a + b, b + 1);
}
protected class PrivateMethodPin {
public MethodPinInfo Pin = new MethodPinInfo();
}
public struct MethodPinInfo {
public int Count;
public MethodBase Method;
public RuntimeMethodHandle Handle;
public override string ToString() {
return $"(MethodPinInfo: {Count}, {Method}, 0x{(long) Handle.Value:X})";
}
}
private enum GlueThiscallStructRetPtrOrder {
Original,
ThisRetArgs,
RetThisArgs
}
}
}