Some further cleanup.

This commit is contained in:
Ottermandias 2024-07-21 23:26:09 +02:00
parent ee5a21f7a2
commit ceaa9ca29a
3 changed files with 127 additions and 132 deletions

View file

@ -5,17 +5,26 @@ namespace Penumbra.Interop.Hooks.ResourceLoading;
public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable
{ {
private readonly PapRewriter _papRewriter = new(papResourceHandler); private readonly PapRewriter _papRewriter = new(papResourceHandler);
public void Enable() public void Enable()
{ {
_papRewriter.Rewrite(Sigs.LoadAlwaysResidentMotionPacks); ReadOnlySpan<string> signatures =
_papRewriter.Rewrite(Sigs.LoadWeaponDependentResidentMotionPacks); [
_papRewriter.Rewrite(Sigs.LoadInitialResidentMotionPacks); Sigs.LoadAlwaysResidentMotionPacks,
_papRewriter.Rewrite(Sigs.LoadMotionPacks); Sigs.LoadWeaponDependentResidentMotionPacks,
_papRewriter.Rewrite(Sigs.LoadMotionPacks2); Sigs.LoadInitialResidentMotionPacks,
_papRewriter.Rewrite(Sigs.LoadMigratoryMotionPack); Sigs.LoadMotionPacks,
Sigs.LoadMotionPacks2,
Sigs.LoadMigratoryMotionPack,
];
var stopwatch = Stopwatch.StartNew();
foreach (var sig in signatures)
_papRewriter.Rewrite(sig);
Penumbra.Log.Debug(
$"[PapHandler] Rewrote {signatures.Length} .pap functions for inlined GetResourceAsync in {stopwatch.ElapsedMilliseconds} ms.");
} }
public void Dispose() public void Dispose()
=> _papRewriter.Dispose(); => _papRewriter.Dispose();
} }

View file

@ -1,5 +1,6 @@
using Dalamud.Hooking; using Dalamud.Hooking;
using Iced.Intel; using Iced.Intel;
using OtterGui;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra.Interop.Hooks.ResourceLoading;
@ -7,175 +8,160 @@ namespace Penumbra.Interop.Hooks.ResourceLoading;
public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable
{ {
public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length);
private PeSigScanner Scanner { get; } = new(); private readonly PeSigScanner _scanner = new();
private Dictionary<IntPtr, AsmHook> Hooks { get; }= []; private readonly Dictionary<nint, AsmHook> _hooks = [];
private List<IntPtr> NativeAllocList { get; } = []; private readonly List<nint> _nativeAllocList = [];
private PapResourceHandlerPrototype PapResourceHandler { get; } = papResourceHandler;
public void Rewrite(string sig) public void Rewrite(string sig)
{ {
if (!Scanner.TryScanText(sig, out var addr)) if (!_scanner.TryScanText(sig, out var address))
{ throw new Exception($"Signature [{sig}] could not be found.");
throw new Exception($"Sig is fucked: {sig}");
}
var funcInstructions = Scanner.GetFunctionInstructions(addr).ToList(); var funcInstructions = _scanner.GetFunctionInstructions(address).ToArray();
var hookPoints = ScanPapHookPoints(funcInstructions).ToList();
var hookPoints = ScanPapHookPoints(funcInstructions).ToList();
foreach (var hookPoint in hookPoints) foreach (var hookPoint in hookPoints)
{ {
var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList();
var stringAllocation = NativeAlloc(Utf8GamePath.MaxGamePathLength);
var stringLoc = NativeAlloc(Utf8GamePath.MaxGamePathLength); // We'll need to grab our true hook point; the location where we can change the path at our leisure.
// This is going to be the first call instruction after our 'hookPoint', so, we'll find that.
// Pretty scuffed, this might need a refactoring at some point.
// We're doing it by skipping to our hookPoint's address in the list of instructions inside the function; then getting next CALL
var skipIndex = funcInstructions.IndexOf(instr => instr.IP == hookPoint.IP) + 1;
var detourPoint = funcInstructions.Skip(skipIndex)
.First(instr => instr.Mnemonic == Mnemonic.Call);
{ // We'll also remove all the 'hookPoints' from 'stackAccesses'.
// We'll need to grab our true hook point; the location where we can change the path at our leisure. // We're handling the char *path redirection here, so we don't want this to hit the later code
// This is going to be the first call instruction after our 'hookPoint', so, we'll find that. foreach (var hp in hookPoints)
// Pretty scuffed, this might need a refactoring at some point. stackAccesses.RemoveAll(instr => instr.IP == hp.IP);
// We're doing it by skipping to our hookPoint's address in the list of instructions inside the function; then getting next CALL
var detourPoint = funcInstructions.Skip(
funcInstructions.FindIndex(instr => instr.IP == hookPoint.IP) + 1
).First(instr => instr.Mnemonic == Mnemonic.Call);
// We'll also remove all the 'hookPoints' from 'stackAccesses'. var detourPointer = Marshal.GetFunctionPointerForDelegate(papResourceHandler);
// We're handling the char *path redirection here, so we don't want this to hit the later code var targetRegister = hookPoint.Op0Register.ToString().ToLower();
foreach (var hp in hookPoints) var hookAddress = new IntPtr((long)detourPoint.IP);
{
stackAccesses.RemoveAll(instr => instr.IP == hp.IP);
}
var pDetour = Marshal.GetFunctionPointerForDelegate(PapResourceHandler); var caveAllocation = NativeAlloc(16);
var targetRegister = hookPoint.Op0Register.ToString().ToLower(); var hook = new AsmHook(
var hookAddr = new IntPtr((long)detourPoint.IP); hookAddress,
[
"use64",
$"mov {targetRegister}, 0x{stringAllocation:x8}", // Move our char *path into the relevant register (rdx)
var caveLoc = NativeAlloc(16); // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves
var hook = new AsmHook( // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call
hookAddr, // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh
[ $"mov r9, 0x{caveAllocation:x8}",
"use64", "mov [r9], rcx",
$"mov {targetRegister}, 0x{stringLoc:x8}", // Move our char *path into the relevant register (rdx) "mov [r9+0x8], rdx",
// After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves
// We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call
// We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh
$"mov r9, 0x{caveLoc:x8}",
"mov [r9], rcx",
"mov [r9+0x8], rdx",
// We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway
$"mov rax, 0x{pDetour:x8}", // Get a pointer to our detour in place
"call rax", // Call detour
// Do the reverse process and retrieve the stored stuff
$"mov r9, 0x{caveLoc:x8}",
"mov rcx, [r9]",
"mov rdx, [r9+0x8]",
// Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call
"mov r8, rax",
], "Pap Redirection"
);
Hooks.Add(hookAddr, hook); // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway
hook.Enable(); $"mov rax, 0x{detourPointer:x8}", // Get a pointer to our detour in place
} "call rax", // Call detour
// Do the reverse process and retrieve the stored stuff
$"mov r9, 0x{caveAllocation:x8}",
"mov rcx, [r9]",
"mov rdx, [r9+0x8]",
// Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call
"mov r8, rax",
], "Pap Redirection"
);
_hooks.Add(hookAddress, hook);
hook.Enable();
// Now we're adjusting every single reference to the stack allocated 'path' to our substantially bigger 'stringLoc' // Now we're adjusting every single reference to the stack allocated 'path' to our substantially bigger 'stringLoc'
foreach (var stackAccess in stackAccesses) UpdatePathAddresses(stackAccesses, stringAllocation);
{ }
var hookAddr = new IntPtr((long)stackAccess.IP + stackAccess.Length); }
if (Hooks.ContainsKey(hookAddr)) private void UpdatePathAddresses(IEnumerable<Instruction> stackAccesses, nint stringAllocation)
{ {
// Hook already exists, means there's reuse of the same stack address across 2 GetResourceAsync; just skip foreach (var stackAccess in stackAccesses)
continue; {
} var hookAddress = new IntPtr((long)stackAccess.IP + stackAccess.Length);
var targetRegister = stackAccess.Op0Register.ToString().ToLower(); // Hook already exists, means there's reuse of the same stack address across 2 GetResourceAsync; just skip
var hook = new AsmHook( if (_hooks.ContainsKey(hookAddress))
hookAddr, continue;
[
"use64", var targetRegister = stackAccess.Op0Register.ToString().ToLower();
$"mov {targetRegister}, 0x{stringLoc:x8}", var hook = new AsmHook(
], "Pap Stack Accesses" hookAddress,
); [
"use64",
Hooks.Add(hookAddr, hook); $"mov {targetRegister}, 0x{stringAllocation:x8}",
hook.Enable(); ], "Pap Stack Accesses"
} );
_hooks.Add(hookAddress, hook);
hook.Enable();
} }
} }
private static IEnumerable<Instruction> ScanStackAccesses(IEnumerable<Instruction> instructions, Instruction hookPoint) private static IEnumerable<Instruction> ScanStackAccesses(IEnumerable<Instruction> instructions, Instruction hookPoint)
{ {
return instructions.Where(instr => return instructions.Where(instr =>
instr.Code == hookPoint.Code instr.Code == hookPoint.Code
&& instr.Op0Kind == hookPoint.Op0Kind && instr.Op0Kind == hookPoint.Op0Kind
&& instr.Op1Kind == hookPoint.Op1Kind && instr.Op1Kind == hookPoint.Op1Kind
&& instr.MemoryBase == hookPoint.MemoryBase && instr.MemoryBase == hookPoint.MemoryBase
&& instr.MemoryDisplacement64 == hookPoint.MemoryDisplacement64) && instr.MemoryDisplacement64 == hookPoint.MemoryDisplacement64)
.GroupBy(instr => instr.IP) .GroupBy(instr => instr.IP)
.Select(grp => grp.First()); .Select(grp => grp.First());
} }
// This is utterly fucked and hardcoded, but, again, it works // This is utterly fucked and hardcoded, but, again, it works
// Might be a neat idea for a more versatile kind of signature though // Might be a neat idea for a more versatile kind of signature though
private static IEnumerable<Instruction> ScanPapHookPoints(List<Instruction> funcInstructions) private static IEnumerable<Instruction> ScanPapHookPoints(Instruction[] funcInstructions)
{ {
for (var i = 0; i < funcInstructions.Count - 8; i++) for (var i = 0; i < funcInstructions.Length - 8; i++)
{ {
if (funcInstructions[i .. (i + 8)] is if (funcInstructions.AsSpan(i, 8) is
[ [
{Code : Code.Lea_r64_m}, { Code : Code.Lea_r64_m },
{Code : Code.Lea_r64_m}, { Code : Code.Lea_r64_m },
{Mnemonic: Mnemonic.Call}, { Mnemonic: Mnemonic.Call },
{Code : Code.Lea_r64_m}, { Code : Code.Lea_r64_m },
{Mnemonic: Mnemonic.Call}, { Mnemonic: Mnemonic.Call },
{Code : Code.Lea_r64_m}, { Code : Code.Lea_r64_m },
.., ..,
] ]
) )
{
yield return funcInstructions[i]; yield return funcInstructions[i];
}
} }
} }
private unsafe IntPtr NativeAlloc(nuint size) private unsafe nint NativeAlloc(nuint size)
{ {
var caveLoc = new IntPtr(NativeMemory.Alloc(size)); var caveLoc = (nint)NativeMemory.Alloc(size);
NativeAllocList.Add(caveLoc); _nativeAllocList.Add(caveLoc);
return caveLoc; return caveLoc;
} }
private static unsafe void NativeFree(IntPtr mem) private static unsafe void NativeFree(nint mem)
{ => NativeMemory.Free((void*)mem);
NativeMemory.Free(mem.ToPointer());
}
public void Dispose() public void Dispose()
{ {
Scanner.Dispose(); _scanner.Dispose();
foreach (var hook in Hooks.Values) foreach (var hook in _hooks.Values)
{ {
hook.Disable(); hook.Disable();
hook.Dispose(); hook.Dispose();
} }
Hooks.Clear();
foreach (var mem in NativeAllocList)
{
NativeFree(mem);
}
NativeAllocList.Clear(); _hooks.Clear();
foreach (var mem in _nativeAllocList)
NativeFree(mem);
_nativeAllocList.Clear();
} }
} }

View file

@ -51,13 +51,13 @@ public unsafe class ResourceLoader : IDisposable, IService
if (!resolvedPath.HasValue || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var utf8ResolvedPath)) if (!resolvedPath.HasValue || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var utf8ResolvedPath))
{ {
PapRequested?.Invoke(gamePath, gamePath, _resolvedData); PapRequested?.Invoke(gamePath);
return length; return length;
} }
NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length); NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length);
path[utf8ResolvedPath.Length] = 0; path[utf8ResolvedPath.Length] = 0;
PapRequested?.Invoke(gamePath, utf8ResolvedPath, _resolvedData); PapRequested?.Invoke(gamePath);
return utf8ResolvedPath.Length; return utf8ResolvedPath.Length;
} }