From 6296dcf9f57976cf17b27468c68a71e3386f9a4f Mon Sep 17 00:00:00 2001 From: marzent Date: Sat, 17 Feb 2024 19:57:31 +0100 Subject: [PATCH 1/2] make IsResumeGameAfterPluginLoad work with DllInject --- Dalamud.Boot/dllmain.cpp | 14 ++++-- Dalamud.Boot/rewrite_entrypoint.cpp | 67 +++++++++++++++++------------ Dalamud.Injector/EntryPoint.cs | 27 +++++++++--- Dalamud.Injector/GameStart.cs | 10 +++-- Dalamud/Dalamud.cs | 31 ++++++++++--- Dalamud/EntryPoint.cs | 39 ++++++++++++++--- Dalamud/NativeFunctions.cs | 34 +++++++++++++++ 7 files changed, 169 insertions(+), 53 deletions(-) diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index e6aa9c4ac5..5de5f1c6a1 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -9,12 +9,12 @@ HMODULE g_hModule; HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr); -HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { +HRESULT WINAPI InitializeImpl(char* pLoadInfo, HANDLE hMainThread) { g_startInfo.from_envvars(); std::string jsonParseError; try { - from_json(nlohmann::json::parse(std::string_view(static_cast(lpParam))), g_startInfo); + from_json(nlohmann::json::parse(std::string_view(pLoadInfo)), g_startInfo); } catch (const std::exception& e) { jsonParseError = e.what(); } @@ -153,14 +153,20 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { // utils::wait_for_game_window(); logging::I("Initializing Dalamud..."); - entrypoint_fn(lpParam, hMainThreadContinue); + entrypoint_fn(pLoadInfo, hMainThread); logging::I("Done!"); return S_OK; } +struct InitializeParams { + char* pLoadInfo; + HANDLE hMainThread; +}; + extern "C" DWORD WINAPI Initialize(LPVOID lpParam) { - return InitializeImpl(lpParam, CreateEvent(nullptr, TRUE, FALSE, nullptr)); + InitializeParams* params = static_cast(lpParam); + return InitializeImpl(params->pLoadInfo, params->hMainThread); } BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpReserved) { diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index 3a1672af7f..ef1827a088 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -3,7 +3,7 @@ #include "logging.h" #include "utils.h" -HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue); +HRESULT WINAPI InitializeImpl(char* pLoadInfo, HANDLE hMainThread); struct RewrittenEntryPointParameters { char* pEntrypoint; @@ -226,21 +226,13 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path throw std::runtime_error("corresponding base address not found"); } -/// @brief Rewrite target process' entry point so that this DLL can be loaded and executed first. +/// @brief Get the target process' entry point. /// @param hProcess Process handle. /// @param pcwzPath Path to target process. -/// @param pcwzLoadInfo JSON string to be passed to Initialize. -/// @return null if successful; memory containing wide string allocated via GlobalAlloc if unsuccessful -/// -/// When the process has just been started up via CreateProcess (CREATE_SUSPENDED), GetModuleFileName and alikes result in an error. -/// Instead, we have to enumerate through all the files mapped into target process' virtual address space and find the base address -/// of memory region corresponding to the path given. +/// @return address to entry point; null if successful. /// -extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) { - std::wstring last_operation; - SetLastError(ERROR_SUCCESS); +extern "C" void* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath) { try { - last_operation = L"get_mapped_image_base_address"; const auto base_address = static_cast(get_mapped_image_base_address(hProcess, pcwzPath)); IMAGE_DOS_HEADER dos_header{}; @@ -249,14 +241,36 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ IMAGE_NT_HEADERS64 nt_header64{}; }; - last_operation = L"read_process_memory_or_throw(base_address)"; read_process_memory_or_throw(hProcess, base_address, dos_header); - - last_operation = L"read_process_memory_or_throw(base_address + dos_header.e_lfanew)"; read_process_memory_or_throw(hProcess, base_address + dos_header.e_lfanew, nt_header64); const auto entrypoint = base_address + (nt_header32.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC ? nt_header32.OptionalHeader.AddressOfEntryPoint : nt_header64.OptionalHeader.AddressOfEntryPoint); + return static_cast(entrypoint); + } + catch (const std::exception& e) { + logging::E("Failed to retrieve entry point for 0x{:X}: {}", hProcess, e.what()); + return nullptr; + } +} + +/// @brief Rewrite target process' entry point so that this DLL can be loaded and executed first. +/// @param hProcess Process handle. +/// @param pcwzPath Path to target process. +/// @param pcwzLoadInfo JSON string to be passed to Initialize. +/// @return null if successful; memory containing wide string allocated via GlobalAlloc if unsuccessful +/// +/// When the process has just been started up via CreateProcess (CREATE_SUSPENDED), GetModuleFileName and alikes result in an error. +/// Instead, we have to enumerate through all the files mapped into target process' virtual address space and find the base address +/// of memory region corresponding to the path given. +/// +extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) { + std::wstring last_operation; + SetLastError(ERROR_SUCCESS); + try { + const auto entrypoint = GetRemoteEntryPointW(hProcess, pcwzPath); + if (!entrypoint) + throw std::runtime_error("GetRemoteEntryPointW"); last_operation = L"get_path_from_local_module(g_hModule)"; auto local_module_path = get_path_from_local_module(g_hModule); @@ -281,7 +295,7 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ auto& params = *reinterpret_cast(buffer.data()); params.entrypointLength = entrypoint_replacement.size(); - params.pEntrypoint = entrypoint; + params.pEntrypoint = static_cast(entrypoint); // Backup original entry point. last_operation = std::format(L"read_process_memory_or_throw(entrypoint, {}b)", entrypoint_replacement.size()); @@ -336,7 +350,7 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ /// @brief Entry point function "called" instead of game's original main entry point. /// @param params Parameters set up from RewriteRemoteEntryPoint. extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) { - HANDLE hMainThreadContinue = nullptr; + HANDLE hMainThread = nullptr; auto hr = S_OK; std::wstring last_operation; std::wstring exc_msg; @@ -352,13 +366,13 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength); FlushInstructionCache(GetCurrentProcess(), params.pEntrypoint, params.entrypointLength); - hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); - last_operation = L"hMainThreadContinue = CreateEventW"; - if (!hMainThreadContinue) - throw std::runtime_error("CreateEventW"); + last_operation = L"duplicate main thread handle"; + DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &hMainThread, 0, FALSE, DUPLICATE_SAME_ACCESS); + if (!hMainThread) + throw std::runtime_error("DuplicateHandle"); last_operation = L"InitializeImpl"; - hr = InitializeImpl(pLoadInfo, hMainThreadContinue); + hr = InitializeImpl(pLoadInfo, hMainThread); } catch (const std::exception& e) { if (hr == S_OK) { const auto err = GetLastError(); @@ -385,14 +399,11 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara desc.GetBSTR()).c_str(), L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO) ExitProcess(-1); - if (hMainThreadContinue) { - CloseHandle(hMainThreadContinue); - hMainThreadContinue = nullptr; + if (hMainThread) { + CloseHandle(hMainThread); + hMainThread = nullptr; } } - if (hMainThreadContinue) - WaitForSingleObject(hMainThreadContinue, INFINITE); - VirtualFree(¶ms, 0, MEM_RELEASE); } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index c784ec1d1c..82bfe4fb5f 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -555,7 +555,7 @@ private static int ProcessInjectCommand(List args, DalamudStartInfo dala } foreach (var process in processes) - Inject(process, AdjustStartInfo(dalamudStartInfo, process.MainModule.FileName), tryFixAcl); + Inject(process, AdjustStartInfo(dalamudStartInfo, process.MainModule.FileName), nint.Zero, tryFixAcl); Log.CloseAndFlush(); return 0; @@ -804,7 +804,7 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x))); } - var process = GameStart.LaunchGame( + var (process, mainThreadHandle) = GameStart.LaunchGame( Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, @@ -828,7 +828,7 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); - Inject(process, startInfo, false); + Inject(process, startInfo, mainThreadHandle, false); } var processHandleForOwner = IntPtr.Zero; @@ -908,7 +908,14 @@ private static DalamudStartInfo AdjustStartInfo(DalamudStartInfo startInfo, stri }; } - private static void Inject(Process process, DalamudStartInfo startInfo, bool tryFixAcl = false) + [StructLayout(LayoutKind.Sequential)] + private struct InitializeParams + { + public nuint LoadInfo; + public nint MainThread; + } + + private static void Inject(Process process, DalamudStartInfo startInfo, nint mainThreadHandle, bool tryFixAcl = false) { if (tryFixAcl) { @@ -944,8 +951,18 @@ private static void Inject(Process process, DalamudStartInfo startInfo, bool try throw new Exception("Unable to allocate start info JSON"); } + using var initParamsBuffer = new MemoryBufferHelper(process).CreatePrivateMemoryBuffer(Marshal.SizeOf(typeof(InitializeParams)) + 0x8); + + var initParams = new InitializeParams + { + LoadInfo = startInfoAddress, + MainThread = mainThreadHandle, + }; + + var initParamsAddress = initParamsBuffer.Add(ref initParams); + injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress); - injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode); + injector.CallRemoteFunction(initAddress, initParamsAddress, out var exitCode); // ====================================================== diff --git a/Dalamud.Injector/GameStart.cs b/Dalamud.Injector/GameStart.cs index e340489786..d5c82b30a4 100644 --- a/Dalamud.Injector/GameStart.cs +++ b/Dalamud.Injector/GameStart.cs @@ -25,10 +25,10 @@ public static class GameStart /// Don't actually fix the ACL. /// Action to execute before the process is started. /// Wait for the game window to be ready before proceeding. - /// The started process. + /// The started process and handle to the started main thread. /// Thrown when a win32 error occurs. /// Thrown when the process did not start correctly. - public static Process LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action beforeResume, bool waitForGameWindow = true) + public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action beforeResume, bool waitForGameWindow = true) { Process process = null; @@ -172,10 +172,12 @@ public static Process LaunchGame(string workingDir, string exePath, string argum { if (psecDesc != IntPtr.Zero) Marshal.FreeHGlobal(psecDesc); - PInvoke.CloseHandle(lpProcessInformation.hThread); } - return process; + NativeFunctions.DuplicateHandle(PInvoke.GetCurrentProcess(), lpProcessInformation.hThread, lpProcessInformation.hProcess, out var mainThreadHandle, 0, false, NativeFunctions.DuplicateOptions.SameAccess); + PInvoke.CloseHandle(lpProcessInformation.hThread); + + return (process, mainThreadHandle); } /// diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 8c858ce7cb..c075ab1531 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -44,8 +44,8 @@ internal sealed class Dalamud : IServiceType /// DalamudStartInfo instance. /// ReliableFileStorage instance. /// The Dalamud configuration. - /// Event used to signal the main thread to continue. - public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent) + /// Handle to the suspended main thread. + public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, nint mainThreadHandle) { this.StartInfo = info; @@ -72,7 +72,7 @@ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfigurati if (!configuration.IsResumeGameAfterPluginLoad) { - NativeFunctions.SetEvent(mainThreadContinueEvent); + this.ResumeThread(mainThreadHandle); ServiceManager.InitializeEarlyLoadableServices() .ContinueWith(t => { @@ -102,7 +102,7 @@ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfigurati if (faultedTasks.Any()) throw new AggregateException(faultedTasks); - NativeFunctions.SetEvent(mainThreadContinueEvent); + this.ResumeThread(mainThreadHandle); await Task.WhenAll(tasks); } @@ -113,7 +113,7 @@ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfigurati } finally { - NativeFunctions.SetEvent(mainThreadContinueEvent); + this.ResumeThread(mainThreadHandle); } }); } @@ -243,4 +243,25 @@ private void SetupClientStructsResolver(DirectoryInfo cacheDir) FFXIVClientStructs.Interop.Resolver.GetInstance.Resolve(); } } + + /// + /// Resumes a thread by incrementing its suspend count to zero, and then closing its handle. + /// + /// Handle to the thread to be resumed. + private void ResumeThread(nint threadHandle) + { + int previousSuspendCount; + + while ((previousSuspendCount = (int)NativeFunctions.ResumeThread(threadHandle)) > 1) + { + if (previousSuspendCount == -1) + { + Log.Error("Failed to resume main thread"); + return; + } + } + + NativeFunctions.CloseHandle(threadHandle); + } + } diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index d0f9e8845d..5254e041c4 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -50,8 +50,8 @@ public sealed class EntryPoint /// Initialize Dalamud. /// /// Pointer to a serialized data. - /// Event used to signal the main thread to continue. - public static void Initialize(IntPtr infoPtr, IntPtr mainThreadContinueEvent) + /// Handle to the (suspended) main thread. + public static void Initialize(IntPtr infoPtr, IntPtr mainThreadHandle) { var infoStr = Marshal.PtrToStringUTF8(infoPtr)!; var info = JsonConvert.DeserializeObject(infoStr)!; @@ -59,7 +59,12 @@ public static void Initialize(IntPtr infoPtr, IntPtr mainThreadContinueEvent) if ((info.BootWaitMessageBox & 4) != 0) MessageBoxW(IntPtr.Zero, "Press OK to continue (BeforeDalamudConstruct)", "Dalamud Boot", MessageBoxType.Ok); - new Thread(() => RunThread(info, mainThreadContinueEvent)).Start(); + var suspendSignal = new ManualResetEvent(false); + suspendSignal.Reset(); + + new Thread(() => RunThread(info, mainThreadHandle, suspendSignal)).Start(); + + suspendSignal.WaitOne(); } /// @@ -130,8 +135,9 @@ internal static void InitLogging(string baseDirectory, bool logConsole, bool log /// Initialize all Dalamud subsystems and start running on the main thread. /// /// The containing information needed to initialize Dalamud. - /// Event used to signal the main thread to continue. - private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent) + /// Handle to the (suspended) main thread. + /// Signal to notifiy the initiliazing thread once the main thread has been suspended. + private static void RunThread(DalamudStartInfo info, IntPtr mainThreadHandle, ManualResetEvent suspendSignal) { // Setup logger InitLogging(info.LogPath!, info.BootShowConsole, true, info.LogName); @@ -161,6 +167,23 @@ private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEv Thread.Sleep(info.DelayInitializeMs); } + var currentSuspendCount = (int)NativeFunctions.SuspendThread(mainThreadHandle) + 1; + suspendSignal.Set(); + + switch (info.LoadMethod) + { + case LoadMethod.Entrypoint: + if (currentSuspendCount != 1) + Log.Warning("Unexpected suspend count {} for main thread with Entrypoint", currentSuspendCount); + break; + case LoadMethod.DllInject: + if (currentSuspendCount != 1) + Log.Warning("Unexpected suspend count {} for main thread with DllInject", currentSuspendCount); + break; + default: + break; + } + Log.Information(new string('-', 80)); Log.Information("Initializing a session.."); @@ -175,7 +198,7 @@ private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEv if (!Util.IsWine()) InitSymbolHandler(info); - var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); + var dalamud = new Dalamud(info, fs, configuration, mainThreadHandle); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", Util.GetGitHash(), Util.GetGitHashClientStructs(), FFXIVClientStructs.Interop.Resolver.Version); dalamud.WaitForUnload(); @@ -192,7 +215,9 @@ private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEv } catch (Exception ex) { - Log.Fatal(ex, "Unhandled exception on main thread."); + suspendSignal.Set(); + NativeFunctions.ResumeThread(mainThreadHandle); + Log.Fatal(ex, "Unhandled exception on Dalamuds initialization thread."); } finally { diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs index 92dfe5dd7a..0443d5576f 100644 --- a/Dalamud/NativeFunctions.cs +++ b/Dalamud/NativeFunctions.cs @@ -1917,6 +1917,40 @@ public static extern bool WriteProcessMemory( /// The thread ID. [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId(); + + /// + /// Suspends a thread. + /// + /// Handle to the thread to be suspended. + /// If the function succeeds, the return value is the thread's previous suspend count. + /// If the function fails, the return value is (DWORD) -1. + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint SuspendThread(IntPtr hThread); + + /// + /// Resumes a thread that was suspended. + /// + /// Handle to the thread to be resumed. + /// If the function succeeds, the return value is the thread's previous suspend count. + /// If the function fails, the return value is (DWORD) -1. + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint ResumeThread(IntPtr hThread); + + /// + /// Closes an open object handle. + /// + /// + /// A valid handle to an open object. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error + /// information, call GetLastError. If the application is running under a debugger, the function will throw an exception if it receives + /// either a handle value that is not valid or a pseudo-handle value. This can happen if you close a handle twice, or if you call + /// CloseHandle on a handle returned by the FindFirstFile function instead of calling the FindClose function. + /// + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr hObject); } /// From a4ca85f4320787915d92fc495d5e5f8c806e9fc3 Mon Sep 17 00:00:00 2001 From: marzent Date: Sun, 18 Feb 2024 09:30:20 +0100 Subject: [PATCH 2/2] add new hybrid game injection --- Dalamud.Boot/DalamudStartInfo.cpp | 8 +- Dalamud.Boot/DalamudStartInfo.h | 1 + Dalamud.Boot/module.def | 5 +- Dalamud.Boot/rewrite_entrypoint.cpp | 8 +- Dalamud.Boot/veh.cpp | 3 + Dalamud.Common/LoadMethod.cs | 5 + Dalamud.Injector/EntryPoint.cs | 132 +++++++++++- Dalamud.Injector/GameStart.cs | 19 +- Dalamud.Injector/NativeFunctions.cs | 317 ++++++++++++++++++++++++++++ Dalamud/EntryPoint.cs | 10 +- 10 files changed, 492 insertions(+), 16 deletions(-) diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index d20265bf86..21ee5c2014 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -74,11 +74,13 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value) } else if (json.is_string()) { - const auto langstr = unicode::convert(json.get(), &unicode::lower); - if (langstr == "entrypoint") + const auto loadstr = unicode::convert(json.get(), &unicode::lower); + if (loadstr == "entrypoint") value = DalamudStartInfo::LoadMethod::Entrypoint; - else if (langstr == "inject") + else if (loadstr == "inject") value = DalamudStartInfo::LoadMethod::DllInject; + else if (loadstr == "hybrid") + value = DalamudStartInfo::LoadMethod::Hybrid; } } diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 5cee8f16b7..8ae0116bdf 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -29,6 +29,7 @@ struct DalamudStartInfo { enum class LoadMethod : int { Entrypoint, DllInject, + Hybrid, }; friend void from_json(const nlohmann::json&, LoadMethod&); diff --git a/Dalamud.Boot/module.def b/Dalamud.Boot/module.def index 047d825e54..3a12618ebc 100644 --- a/Dalamud.Boot/module.def +++ b/Dalamud.Boot/module.def @@ -1,5 +1,6 @@ LIBRARY Dalamud.Boot EXPORTS Initialize @1 - RewriteRemoteEntryPointW @2 - RewrittenEntryPoint @3 + GetRemoteEntryPointW @2 + RewriteRemoteEntryPointW @3 + RewrittenEntryPoint @4 diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index ef1827a088..7b53da1055 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -229,9 +229,9 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path /// @brief Get the target process' entry point. /// @param hProcess Process handle. /// @param pcwzPath Path to target process. -/// @return address to entry point; null if successful. +/// @return address to entry point; null if unsuccessful. /// -extern "C" void* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath) { +extern "C" char* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath) { try { const auto base_address = static_cast(get_mapped_image_base_address(hProcess, pcwzPath)); @@ -246,7 +246,7 @@ extern "C" void* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcw const auto entrypoint = base_address + (nt_header32.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC ? nt_header32.OptionalHeader.AddressOfEntryPoint : nt_header64.OptionalHeader.AddressOfEntryPoint); - return static_cast(entrypoint); + return entrypoint; } catch (const std::exception& e) { logging::E("Failed to retrieve entry point for 0x{:X}: {}", hProcess, e.what()); @@ -295,7 +295,7 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ auto& params = *reinterpret_cast(buffer.data()); params.entrypointLength = entrypoint_replacement.size(); - params.pEntrypoint = static_cast(entrypoint); + params.pEntrypoint = entrypoint; // Backup original entry point. last_operation = std::format(L"read_process_memory_or_throw(entrypoint, {}b)", entrypoint_replacement.size()); diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index ade295d02a..f889d68103 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -111,6 +111,9 @@ static void append_injector_launch_args(std::vector& args) break; case DalamudStartInfo::LoadMethod::DllInject: args.emplace_back(L"--mode=inject"); + break; + case DalamudStartInfo::LoadMethod::Hybrid: + args.emplace_back(L"--mode=hybrid"); } args.emplace_back(L"--logpath=\"" + unicode::convert(g_startInfo.BootLogPath) + L"\""); args.emplace_back(L"--dalamud-working-directory=\"" + unicode::convert(g_startInfo.WorkingDirectory) + L"\""); diff --git a/Dalamud.Common/LoadMethod.cs b/Dalamud.Common/LoadMethod.cs index ca50098e28..ad1ebe03b6 100644 --- a/Dalamud.Common/LoadMethod.cs +++ b/Dalamud.Common/LoadMethod.cs @@ -14,4 +14,9 @@ public enum LoadMethod /// Load Dalamud via DLL-injection. /// DllInject, + + /// + /// Load Dalamud via DLL-injection at the suspended entrypoint. + /// + Hybrid, } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 82bfe4fb5f..5d4f5f6c76 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -701,6 +701,10 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala { dalamudStartInfo.LoadMethod = LoadMethod.DllInject; } + else if (mode.Length > 0 && mode.Length <= 6 && "hybrid"[0..mode.Length] == mode) + { + dalamudStartInfo.LoadMethod = LoadMethod.Hybrid; + } else { throw new CommandLineException($"\"{mode}\" is not a valid Dalamud load mode."); @@ -804,6 +808,12 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x))); } + var entryPoint = nint.Zero; + var entryPointInstruction = new byte[1]; + + // waiting with a suspended main thread will always deadlock + waitForGameWindow = (dalamudStartInfo.LoadMethod != LoadMethod.Hybrid) && waitForGameWindow; + var (process, mainThreadHandle) = GameStart.LaunchGame( Path.GetDirectoryName(gamePath), gamePath, @@ -819,12 +829,54 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo))); Log.Verbose("RewriteRemoteEntryPointW called!"); } + + if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Hybrid) + { + DebugSetProcessKillOnExit(false); + entryPoint = GetRemoteEntryPointW(p.Handle, gamePath); + if (entryPoint == nint.Zero) + throw new EntryPointNotFoundException("Couldn't get game process entry point"); + Log.Verbose("Got remote entry point {0}", entryPoint); + var int3 = new byte[] { 0xCC }; + VirtualProtectEx(p.Handle, entryPoint, 1, MemoryProtection.ExecuteReadWrite, out var entryPointProtection); + Log.Verbose("Original entry point protection {0}", entryPointProtection); + ReadProcessMemory(p.Handle, entryPoint, entryPointInstruction, 1, out var numberOfBytesRead); + Log.Verbose("Original entry point instruction {0}", entryPointInstruction[0]); + WriteProcessMemory(p.Handle, entryPoint, int3, 1, out var numberOfBytesWritten); + if (numberOfBytesWritten != 1) + throw new Exception("Failed to set breakpoint at entry point"); + VirtualProtectEx(p.Handle, entryPoint, 1, entryPointProtection, out _); + FlushInstructionCache(p.Handle, entryPoint, 1); + } + }, + (p, mt) => + { + if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Hybrid) + { + var dwThreadId = WaitForBreakpointDebugEvent(entryPoint); + DecrementRip(mt); + VirtualProtectEx(p.Handle, entryPoint, 1, MemoryProtection.ExecuteReadWrite, out var entryPointProtection); + WriteProcessMemory(p.Handle, entryPoint, entryPointInstruction, 1, out var numberOfBytesWritten); + if (numberOfBytesWritten != 1) + throw new Exception("Failed to restore entry point instruction"); + VirtualProtectEx(p.Handle, entryPoint, 1, entryPointProtection, out _); + FlushInstructionCache(p.Handle, entryPoint, 1); + SuspendThread(mt); + if (!ContinueDebugEvent((uint)p.Id, dwThreadId, DBG_CONTINUE)) + throw new Exception("ContinueDebugEvent failed"); + if (DbgUiStopDebugging(p.Handle) != 0) + { + throw new Exception("Couldn't detach from target"); + } + } }, - waitForGameWindow); + waitForGameWindow, + !withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Hybrid); Log.Verbose("Game process started with PID {0}", process.Id); - if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.DllInject) + if (!withoutDalamud && (dalamudStartInfo.LoadMethod == LoadMethod.DllInject || + dalamudStartInfo.LoadMethod == LoadMethod.Hybrid)) { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); @@ -846,6 +898,79 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala return 0; } + private static uint WaitForBreakpointDebugEvent(nint exceptionAddress) + { + var dwThreadId = 0U; + var debugEventPtr = Marshal.AllocHGlobal(188); + + Log.Verbose("Waiting for debug breakpoint event..."); + + while (WaitForDebugEvent(debugEventPtr, uint.MaxValue)) + { + var debugEvent = Marshal.PtrToStructure(debugEventPtr); + + Log.Verbose("Got debug event code: {0}", debugEvent.dwDebugEventCode); + + if (debugEvent.dwDebugEventCode != EXCEPTION_DEBUG_EVENT) + { + ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); + continue; + } + + if (debugEvent.ExceptionDebugInfo.ExceptionRecord.ExceptionCode != EXCEPTION_BREAKPOINT) + { + ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); + continue; + } + + Log.Debug("Got debug breakpoint event with adress {0}", debugEvent.ExceptionDebugInfo.ExceptionRecord.ExceptionAddress); + + if (debugEvent.ExceptionDebugInfo.ExceptionRecord.ExceptionAddress != exceptionAddress) + { + ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); + continue; + } + + dwThreadId = debugEvent.dwThreadId; + Log.Information("Got entry point debug breakpoint event for TID {0}", dwThreadId); + break; + } + + Marshal.FreeHGlobal(debugEventPtr); + if (dwThreadId == 0) + { + Log.Error("Failed to wait for debug event"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + return dwThreadId; + } + + private static void DecrementRip(nint threadHandle) + { + // Similar to the debug event struct above, instead of fully defining every union and all + // structs, just allocate the size of the struct and then marshall into our partial one. + var context64Ptr = Marshal.AllocHGlobal(1232); + + try + { + var emptyContext64 = Marshal.PtrToStructure(context64Ptr); + emptyContext64.ContextFlags = ContextFlags.CONTEXT_CONTROL; + Marshal.StructureToPtr(emptyContext64, context64Ptr, false); + if (!GetThreadContext(threadHandle, context64Ptr)) + throw new Win32Exception(Marshal.GetLastWin32Error()); + var context64 = Marshal.PtrToStructure(context64Ptr); + context64.Rip--; + Marshal.StructureToPtr(context64, context64Ptr, false); + if (!SetThreadContext(threadHandle, context64Ptr)) + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + finally + { + Marshal.FreeHGlobal(context64Ptr); + } + } + private static Process GetInheritableCurrentProcessHandle() { if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess)) @@ -975,6 +1100,9 @@ private static void Inject(Process process, DalamudStartInfo startInfo, nint mai Log.Information("Done"); } + [DllImport("Dalamud.Boot.dll")] + private static extern nint GetRemoteEntryPointW(IntPtr hProcess, [MarshalAs(UnmanagedType.LPWStr)] string gamePath); + [DllImport("Dalamud.Boot.dll")] private static extern int RewriteRemoteEntryPointW(IntPtr hProcess, [MarshalAs(UnmanagedType.LPWStr)] string gamePath, [MarshalAs(UnmanagedType.LPWStr)] string loadInfoJson); diff --git a/Dalamud.Injector/GameStart.cs b/Dalamud.Injector/GameStart.cs index d5c82b30a4..6f82f6f5f0 100644 --- a/Dalamud.Injector/GameStart.cs +++ b/Dalamud.Injector/GameStart.cs @@ -24,11 +24,13 @@ public static class GameStart /// Arguments to pass to the executable file. /// Don't actually fix the ACL. /// Action to execute before the process is started. + /// Action to execute after the process is started. /// Wait for the game window to be ready before proceeding. + /// Attach to the game process as debugger. /// The started process and handle to the started main thread. /// Thrown when a win32 error occurs. /// Thrown when the process did not start correctly. - public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action beforeResume, bool waitForGameWindow = true) + public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action beforeResume, Action afterResume, bool waitForGameWindow = true, bool attach = false) { Process process = null; @@ -98,7 +100,7 @@ public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string wor ref lpProcessAttributes, IntPtr.Zero, false, - PInvoke.CREATE_SUSPENDED, + attach ? PInvoke.DEBUG_ONLY_THIS_PROCESS : PInvoke.CREATE_SUSPENDED, IntPtr.Zero, workingDir, ref lpStartupInfo, @@ -121,6 +123,8 @@ public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string wor PInvoke.ResumeThread(lpProcessInformation.hThread); + afterResume?.Invoke(process, lpProcessInformation.hThread); + // Ensure that the game main window is prepared if (waitForGameWindow) { @@ -174,7 +178,15 @@ public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string wor Marshal.FreeHGlobal(psecDesc); } - NativeFunctions.DuplicateHandle(PInvoke.GetCurrentProcess(), lpProcessInformation.hThread, lpProcessInformation.hProcess, out var mainThreadHandle, 0, false, NativeFunctions.DuplicateOptions.SameAccess); + NativeFunctions.DuplicateHandle( + PInvoke.GetCurrentProcess(), + lpProcessInformation.hThread, + lpProcessInformation.hProcess, + out var mainThreadHandle, + 0, + false, + NativeFunctions.DuplicateOptions.SameAccess); + PInvoke.CloseHandle(lpProcessInformation.hThread); return (process, mainThreadHandle); @@ -360,6 +372,7 @@ private static class PInvoke public const UInt32 SECURITY_DESCRIPTOR_REVISION = 1; + public const UInt32 DEBUG_ONLY_THIS_PROCESS = 0x00000002; public const UInt32 CREATE_SUSPENDED = 0x00000004; public const UInt32 TOKEN_QUERY = 0x0008; diff --git a/Dalamud.Injector/NativeFunctions.cs b/Dalamud.Injector/NativeFunctions.cs index 2a4654aafe..923dc7a4e1 100644 --- a/Dalamud.Injector/NativeFunctions.cs +++ b/Dalamud.Injector/NativeFunctions.cs @@ -758,6 +758,47 @@ public static extern IntPtr VirtualAllocEx( AllocationType flAllocationType, MemoryProtection flProtect); + /// + /// Changes the protection on a region of committed pages in the virtual address space of a specified process. + /// For more information, see https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotectex. + /// + /// + /// The handle to the process whose memory protection is to be changed. The handle must have the PROCESS_VM_OPERATION access + /// right. + /// + /// + /// A pointer to the base address of the region of pages whose access protection attributes are to be changed. All pages in + /// the specified region must be within the same reserved region allocated when calling the VirtualAlloc or VirtualAllocEx + /// function using MEM_RESERVE. The pages cannot span reserved regions that were allocated by separate calls to VirtualAlloc + /// or VirtualAllocEx using MEM_RESERVE. + /// + /// + /// The size of the region whose access protection attributes are changed, in bytes. The region of affected pages includes + /// all pages containing one or more bytes in the range from the lpAddress parameter to (lpAddress+dwSize). This means that + /// a 2-byte range straddling a page boundary causes the protection attributes of both pages to be changed. + /// + /// + /// The memory protection option. This parameter can be one of the memory protection constants. For the most common use, to + /// execute code in the region, the memory must be marked as PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE or + /// PAGE_EXECUTE_WRITECOPY. + /// + /// + /// A pointer to a variable that receives the previous access protection value of the first page in the specified region of + /// pages. If this parameter is NULL or does not point to a valid variable, the function fails. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended + /// error information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool VirtualProtectEx( + IntPtr hProcess, + IntPtr lpAddress, + UIntPtr dwSize, + MemoryProtection flNewProtect, + out MemoryProtection lpflOldProtect); + + /// /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualfreeex. /// Releases, decommits, or releases and decommits a region of memory within the virtual address space of a specified @@ -815,6 +856,255 @@ public static extern bool VirtualFreeEx( [DllImport("kernel32.dll", SetLastError = true)] public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + public const uint EXCEPTION_DEBUG_EVENT = 1; + public const uint EXCEPTION_BREAKPOINT = 0x80000003; + + [StructLayout(LayoutKind.Sequential)] + public struct ExceptionRecord + { + public uint ExceptionCode; + public uint ExceptionFlags; + public IntPtr NextExceptionRecord; + public IntPtr ExceptionAddress; + public uint NumberParameters; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 15, ArraySubType = UnmanagedType.U4)] + public uint[] ExceptionInformation; + } + + [StructLayout(LayoutKind.Sequential)] + public struct ExceptionDebugInfo + { + public ExceptionRecord ExceptionRecord; + public uint dwFirstChance; + } + + [StructLayout(LayoutKind.Sequential)] + public struct DebugEvent + { + public uint dwDebugEventCode; + public uint dwProcessId; + public uint dwThreadId; + public uint dw64PlatformPadding; + public ExceptionDebugInfo ExceptionDebugInfo; + } + + /// + /// Waits for a debugging event to occur in a debugged process. + /// + /// + /// Out parameter that receives information about the debugging event. + /// + /// + /// The time-out interval, in milliseconds. If this parameter is zero, the function tests for a debugging event and returns + /// immediately. If the parameter is INFINITE, the function does not return until a debugging event + /// has occurred. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended + /// error information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool WaitForDebugEvent(nint lpDebugEvent, uint dwMilliseconds); + + /// + /// If the thread specified by the dwThreadId parameter previously reported an EXCEPTION_DEBUG_EVENT debugging event, the function + /// stops all exception processing and continues the thread and the exception is marked as handled. For any other debugging event, + /// this flag simply continues the thread. + /// + public const uint DBG_CONTINUE = 0x00010002; + + /// + /// Enables a debugger to continue a thread that previously reported a debugging event. + /// + /// + /// The identifier of the process in which the debug event occurred. + /// + /// + /// The identifier of the thread that generated the debug event. + /// + /// + /// The flags that control how the debugging continues. Use DBG_CONTINUE for most debug events, including EXCEPTION_DEBUG_EVENT. + /// For exceptions, you can also use DBG_EXCEPTION_NOT_HANDLED to indicate the debugger did not handle the exception. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error + /// information, call GetLastError + /// . + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool ContinueDebugEvent( + uint dwProcessId, + uint dwThreadId, + uint dwContinueStatus); + + /// + /// Sets a flag indicating whether a debugged process is to be terminated when the debugger exits. + /// + /// + /// If this parameter is TRUE, the debugged process is terminated when the debugger exits. If it is FALSE, the debugged process + /// continues to run after the debugger exits. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error + /// information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool DebugSetProcessKillOnExit(bool killOnExit); + + /// + /// Stops the debugger from debugging the specified process. + /// + /// + /// The handle of the process from which the debugger will detach. + /// + /// + /// The function returns an NTSTATUS. + /// + [DllImport("ntdll.dll")] + public static extern uint DbgUiStopDebugging(IntPtr hProcess); + + /// + /// Suspends a thread. + /// + /// Handle to the thread to be suspended. + /// If the function succeeds, the return value is the thread's previous suspend count. + /// If the function fails, the return value is (DWORD) -1. + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint SuspendThread(IntPtr hThread); + + public enum ContextFlags : uint + { + CONTEXT_i386 = 0x10000, + + /// + /// SS:SP, CS:IP, FLAGS, BP. + /// + CONTEXT_CONTROL = CONTEXT_i386 | 0x01, + } + + /// + /// Beginning of the CONTEXT64 struct. + /// Only useable on handles for x64 threads. + /// + [StructLayout(LayoutKind.Sequential, Pack = 16)] + public struct Context64 + { + public ulong P1Home; + public ulong P2Home; + public ulong P3Home; + public ulong P4Home; + public ulong P5Home; + public ulong P6Home; + + public ContextFlags ContextFlags; + public uint MxCsr; + + public ushort SegCs; + public ushort SegDs; + public ushort SegEs; + public ushort SegFs; + public ushort SegGs; + public ushort SegSs; + public uint EFlags; + + public ulong Dr0; + public ulong Dr1; + public ulong Dr2; + public ulong Dr3; + public ulong Dr6; + public ulong Dr7; + + public ulong Rax; + public ulong Rcx; + public ulong Rdx; + public ulong Rbx; + public ulong Rsp; + public ulong Rbp; + public ulong Rsi; + public ulong Rdi; + public ulong R8; + public ulong R9; + public ulong R10; + public ulong R11; + public ulong R12; + public ulong R13; + public ulong R14; + public ulong R15; + public ulong Rip; + } + + /// + /// Retrieves the context of the specified thread. + /// + /// A + /// handle to the thread whose context is to be retrieved. The handle must have THREAD_GET_CONTEXT access to the thread. + /// + /// + /// A pointer to a CONTEXT structure that receives the context of the thread. The caller must initialize the ContextFlags member + /// of this structure to indicate which portions of a thread's context are requested. + /// + /// + /// If the function succeeds, the return value is true. If the function fails, the return value is false. To get extended error + /// information, call GetLastError. + /// + /// The GetThreadContext function is used to retrieve the context of the specified thread. A 64-bit application can retrieve the + /// context of a WOW64 thread using the Wow64GetThreadContext function. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetThreadContext(IntPtr hThread, IntPtr lpContext); + + /// + /// Sets the context for the specified thread. + /// + /// + /// A handle to the thread whose context is to be set. The handle must have THREAD_SET_CONTEXT access to the thread. + /// + /// + /// A pointer to a CONTEXT structure that contains the context to be set in the specified thread. The value of the ContextFlags + /// member of this structure specifies which portions of a thread's context to set. + /// + /// + /// If the function succeeds, the return value is true. If the function fails, the return value is false. To get extended error + /// information, call GetLastError. + /// + /// + /// The SetThreadContext function is used to set the context of the specified thread. A 64-bit application can set the context of + /// a WOW64 thread using the Wow64SetThreadContext function. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetThreadContext(IntPtr hThread, IntPtr lpContext); + + /// + /// Reads data from an area of memory in a specified process. The entire area to be read must be accessible, or the operation fails. + /// + /// + /// A handle to the process with memory that is being read. The handle must have PROCESS_VM_READ access to the process. + /// + /// + /// A pointer to the base address in the specified process from which to read. Before the data transfer occurs, the system verifies + /// that all data in the specified memory range is accessible for read access, and if it is not accessible, the function fails. + /// + /// + /// A pointer to a buffer that receives the data read from the address space of the specified process. + /// + /// + /// The number of bytes to be read from the specified process. + /// + /// + /// A pointer to a variable that receives the number of bytes transferred into the specified buffer. This parameter is optional. + /// If lpNumberOfBytesRead is NULL, the parameter is ignored. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get extended error + /// information, call GetLastError. The function fails if the requested read operation crosses into an area of the process that is inaccessible. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool ReadProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + [Out] byte[] lpBuffer, + int dwSize, + out IntPtr lpNumberOfBytesRead); + /// /// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or /// the operation fails. @@ -851,6 +1141,33 @@ public static extern bool WriteProcessMemory( int dwSize, out IntPtr lpNumberOfBytesWritten); + /// + /// Flushes the instruction cache for the specified process. This function is necessary if you are generating or + /// modifying code in memory for a process that is currently running. + /// For more information, see https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-flushinstructioncache. + /// + /// + /// A handle to a process whose instruction cache is to be flushed. This handle must have the PROCESS_VM_OPERATION + /// access right. For more information, see Process Security and Access Rights. + /// + /// + /// A pointer to the base of the region to be flushed. This parameter can be NULL. + /// + /// + /// The size of the region to be flushed if the lpBaseAddress parameter is not NULL, in bytes. If lpBaseAddress is NULL, + /// dwSize is ignored. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended + /// error information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool FlushInstructionCache( + IntPtr hProcess, + IntPtr lpBaseAddress, + int dwSize); + + /// /// Duplicates an object handle. /// diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 5254e041c4..c4e79148b0 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -168,19 +168,25 @@ private static void RunThread(DalamudStartInfo info, IntPtr mainThreadHandle, Ma } var currentSuspendCount = (int)NativeFunctions.SuspendThread(mainThreadHandle) + 1; + Log.Verbose("Current main thread suspend count {0}", currentSuspendCount); suspendSignal.Set(); switch (info.LoadMethod) { case LoadMethod.Entrypoint: if (currentSuspendCount != 1) - Log.Warning("Unexpected suspend count {} for main thread with Entrypoint", currentSuspendCount); + Log.Warning("Unexpected suspend count {0} for main thread with Entrypoint", currentSuspendCount); break; case LoadMethod.DllInject: if (currentSuspendCount != 1) - Log.Warning("Unexpected suspend count {} for main thread with DllInject", currentSuspendCount); + Log.Warning("Unexpected suspend count {0} for main thread with DllInject", currentSuspendCount); + break; + case LoadMethod.Hybrid: + if (currentSuspendCount != 2) + Log.Warning("Unexpected suspend count {0} for main thread with Hybrid injection", currentSuspendCount); break; default: + Log.Warning("Unknown LoadMethod {0}", info.LoadMethod); break; }