using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using MonoMod.Utils; namespace BepInEx.IL2CPP.Allocator { public class PageAllocatorException : Exception { public PageAllocatorException(string message) : base(message) { } } /// /// A general purpose page allocator for patching purposes. /// Allows to allocate pages (4k memory chunks) within the 1GB radius of a given address. /// /// Based on https://github.com/kubo/funchook internal abstract class PageAllocator { /// /// Common page size on Unix and Windows (4k). /// Call to will allocate a single page of this size. /// public const int PAGE_SIZE = 0x1000; /// /// Allocation granularity on Windows (but can be reused in other implementations). /// protected const int ALLOCATION_UNIT = 0x100000; protected const int PAGES_PER_UNIT = ALLOCATION_UNIT / PAGE_SIZE; private static PageAllocator instance; private readonly List allocatedChunks = new List(); /// /// Platform-specific instance of page allocator. /// public static PageAllocator Instance => instance ??= Init(); /// /// Allocates a single 64k chunk of memory near the given address /// /// Address near which to attempt allocate the chunk /// Allocated chunk /// Allocation failed protected abstract IntPtr AllocateChunk(IntPtr hint); /// /// Allocates a single page of size near the provided address. /// Attempts to allocate the page within the +-1GB region of the hinted address. /// /// Address near which to attempt to allocate the page. /// Address to the allocated page. public virtual IntPtr Allocate(IntPtr hint) { foreach (var allocatedChunk in allocatedChunks) { // Small shortcut to speed up page lookup if (allocatedChunk.UsedPages == PAGES_PER_UNIT) continue; for (var i = 0; i < allocatedChunk.Pages.Length; i++) { if (allocatedChunk.Pages[i]) continue; var pageAddr = allocatedChunk.GetPage(i); if (!IsInRelJmpRange(hint, pageAddr)) continue; allocatedChunk.Pages[i] = true; allocatedChunk.UsedPages++; return pageAddr; } } var chunk = new PageChunk { BaseAddress = AllocateChunk(hint) }; allocatedChunks.Add(chunk); chunk.Pages[0] = true; chunk.UsedPages++; return chunk.BaseAddress; } /// /// Frees the page allocated with /// /// public void Free(IntPtr page) { foreach (var allocatedChunk in allocatedChunks) { long index = (page.ToInt64() - allocatedChunk.BaseAddress.ToInt64()) / PAGE_SIZE; if (index < 0 || index > PAGES_PER_UNIT) continue; allocatedChunk.Pages[index] = false; return; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static long RoundUp(long num, long unit) { return (num + unit - 1) & ~ (unit - 1); } /// /// Checks if the given address is within the relative jump range. /// /// Source address to jump from. /// Destination address to jump to. /// True, if the distance between the addresses is within the relative jump range (usually 1GB), otherwise false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRelJmpRange(IntPtr src, IntPtr dst) { long diff = dst.ToInt64() - src.ToInt64(); return int.MinValue <= diff && diff <= int.MaxValue; } private static PageAllocator Init() { return PlatformHelper.Current switch { var v when v.Is(Platform.Windows) => new WindowsPageAllocator(), var v when v.Is(Platform.Linux) => new LinuxPageAllocator(), var v when v.Is(Platform.MacOS) => new MacOsPageAllocator(), _ => throw new NotImplementedException() }; } private class PageChunk { public readonly bool[] Pages = new bool[PAGES_PER_UNIT]; public IntPtr BaseAddress; public int UsedPages; public IntPtr GetPage(int index) { return BaseAddress + index * PAGE_SIZE; } } } internal static class PlatformExt { public static bool Is(this Platform pl, Platform val) { return (pl & val) == val; } } }