dotnet core 应用是如何跑起来的 通过AppHost理解运行过程
在 dotnet 的輸出路徑里面,可以看到有一個有趣的可執行文件,這個可執行文件是如何在框架發布和獨立發布的時候,找到 dotnet 程序的運行時的,這個可執行文件里面包含了哪些內容
在回答上面的問題之前,請大家嘗試打開?C:\Program Files\dotnet\sdk\5.0.100\AppHostTemplate\?這個文件夾。當然了,請將 dotnet 版本號修改為你本機的版本號。在這個文件夾里面,可以看到有一個文件叫 apphost.exe 的可執行文件。有趣的是在咱的 dotnet 項目的 obj 文件夾下也能找到叫這個名字的這個文件
更有趣的是在咱的 dotnet 項目的 obj 文件夾下的 apphost.exe 可執行文件和最終輸出的可執行文件是相同的一個文件
這有什么聯系呢?回答這個問題需要從 dotnet 的代碼開始。在 GitHub 完全開源的 dotnet 源代碼倉庫 https://github.com/dotnet/runtime 里面,將代碼拉到本地,可以在?dotnet runtime\src\installer\corehost\?文件里面看到很多有趣的邏輯
沒錯,其實 apphost.exe 的核心邏輯就放在?dotnet runtime\src\installer\corehost\?文件里面
打開?dotnet runtime\src\installer\corehost\corehost.cpp?文件,可以看到一段有趣的注釋
/*** Detect if the apphost executable is allowed to load and execute a managed assembly.** - The exe is built with a known hash string at some offset in the image* - The exe is useless as is with the built-in hash value, and will fail with an error message* - The hash value should be replaced with the managed DLL filename with optional relative path* - The optional path is relative to the location of the apphost executable* - The relative path plus filename are verified to reference a valid file* - The filename should be "NUL terminated UTF-8" by "dotnet build"* - The managed DLL filename does not have to be the same name as the apphost executable name* - The exe may be signed at this point by the app publisher* - Note: the maximum size of the filename and relative path is 1024 bytes in UTF-8 (not including NUL)* o https://en.wikipedia.org/wiki/Comparison_of_file_systems* has more details on maximum file name sizes.*/在?dotnet runtime\src\installer\corehost\corehost.cpp?文件的?exe_start?大概就是整個可執行文件的入口方法了,在這里實現的功能將包含使用 hostfxr 和 hostpolicy 來托管執行整個 dotnet 進程,以及主函數的調起。而在使用托管之前,需要先尋找 dotnet_root 也就是 dotnet 框架用來承載整個 dotnet 進程
上面的邏輯的核心代碼如下
const pal::char_t* dotnet_root_cstr = fxr.dotnet_root().empty() ? nullptr : fxr.dotnet_root().c_str();rc = hostfxr_main_bundle_startupinfo(argc, argv, host_path_cstr, dotnet_root_cstr, app_path_cstr, bundle_header_offset);而在進行獨立發布的時候,其實會在創建 fxr 對象的時候傳入 app_root 路徑,如下面代碼
hostfxr_resolver_t fxr{app_root};在 dotnet core 里面,和 dotnet framework 不同的是,在 dotnet core 的可執行程序沒有使用到系統給的黑科技,是一個完全的 Win32 應用程序,在雙擊 exe 的時候,將會執行一段非托管的代碼,在進入到 corehost.cpp 的?exe_start?函數之后。將會開始尋找 dotnet 托管入口,以及 dotnet 運行時,通過 hostfxr 的方式加載運行時組件,然后跑起來托管應用
那么在 dotnet 構建輸出的可執行文件又是什么?其實就是包含了 corehost.cpp 邏輯的 AppHost.exe 文件的魔改。在 corehost.cpp 構建出來的 AppHost.exe 文件,是不知道開發者的最終輸出包含入口的 dll 是哪個的,需要在構建過程中傳入給 AppHost.exe 文件。而 AppHost.exe 文件是固定的二進制文件,不接受配置等方式,因此傳入的方法就是通過修改二進制的內容了
這也就是為什么 AppHost.exe 放在 AppHostTemplate 文件夾的命名原因,因為這個?C:\Program Files\dotnet\sdk\5.0.100\AppHostTemplate\?文件夾的 AppHost.exe 是一個 Template 模版而已,在 corehost.cpp 文件里面,預定了一段大概是 1025 長度的空間用來存放 dotnet 入口 dll 路徑名。這個代碼就是本文上面給的很長的注釋下面的代碼
#define EMBED_HASH_HI_PART_UTF8 "c3ab8ff13720e8ad9047dd39466b3c89" // SHA-256 of "foobar" in UTF-8 #define EMBED_HASH_LO_PART_UTF8 "74e592c2fa383d4a3960714caef0c4f2" // 這兩句代碼就是 foobar 的 UTF-8 二進制的 SHA-256 字符串 #define EMBED_HASH_FULL_UTF8 (EMBED_HASH_HI_PART_UTF8 EMBED_HASH_LO_PART_UTF8) // NUL terminatedbool is_exe_enabled_for_execution(pal::string_t* app_dll) {constexpr int EMBED_SZ = sizeof(EMBED_HASH_FULL_UTF8) / sizeof(EMBED_HASH_FULL_UTF8[0]);// 這里給的是就是最長 1024 個 byte 的 dll 名,加上一個 \0 一共是 1025 個字符constexpr int EMBED_MAX = (EMBED_SZ > 1025 ? EMBED_SZ : 1025); // 1024 DLL name length, 1 NUL// 這就是定義在 AppHost.exe 二進制文件里面的一段空間了,長度就是 EMBED_MAX 長度,內容就是 c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2 這段字符串static char embed[EMBED_MAX] = EMBED_HASH_FULL_UTF8; // series of NULs followed by embed hash stringstatic const char hi_part[] = EMBED_HASH_HI_PART_UTF8;static const char lo_part[] = EMBED_HASH_LO_PART_UTF8;// 將 embed 的內容復制到 app_dll 變量里面pal::clr_palstring(embed, app_dll); }int exe_start(const int argc, const pal::char_t* argv[]) {// 讀取嵌入到二進制文件的 App 名,也就是 dotnet 的入口 dll 路徑,可以是相對也可以是絕對路徑pal::string_t embedded_app_name;if (!is_exe_enabled_for_execution(&embedded_app_name)){trace::error(_X("A fatal error was encountered. This executable was not bound to load a managed DLL."));return StatusCode::AppHostExeNotBoundFailure;}// 將 embedded_app_name 的內容賦值給 app_path 變量,這個變量的定義代碼我沒有寫append_path(&app_path, embedded_app_name.c_str());const pal::char_t* app_path_cstr = app_path.empty() ? nullptr : app_path.c_str();// 跑起來 dotnet 應用rc = hostfxr_main_bundle_startupinfo(argc, argv, host_path_cstr, dotnet_root_cstr, app_path_cstr, bundle_header_offset); }上面代碼不是實際的 corehost.cpp 的代碼,只是為了方便本文描述而修改的代碼
在實際輸出的 dotnet 可執行文件里面的邏輯是先從?C:\Program Files\dotnet\sdk\5.0.100\AppHostTemplate\?文件夾復制 AppHost.exe 出來,接著依靠上面代碼的?static char embed[EMBED_MAX] = EMBED_HASH_FULL_UTF8;?的邏輯,替換二進制文件的 embed 值的內容
在?dotnet runtime\src\installer\managed\Microsoft.NET.HostModel\AppHost\HostWriter.cs?文件中,將包含實際的替換邏輯,代碼如下
/// <summary>/// Embeds the App Name into the AppHost.exe/// If an apphost is a single-file bundle, updates the location of the bundle headers./// </summary>public static class HostWriter{/// <summary>/// hash value embedded in default apphost executable in a place where the path to the app binary should be stored./// </summary>private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);/// <summary>/// Create an AppHost with embedded configuration of app binary location/// </summary>/// <param name="appHostSourceFilePath">The path of Apphost template, which has the place holder</param>/// <param name="appHostDestinationFilePath">The destination path for desired location to place, including the file name</param>/// <param name="appBinaryFilePath">Full path to app binary or relative path to the result apphost file</param>/// <param name="windowsGraphicalUserInterface">Specify whether to set the subsystem to GUI. Only valid for PE apphosts.</param>/// <param name="assemblyToCopyResorcesFrom">Path to the intermediate assembly, used for copying resources to PE apphosts.</param>public static void CreateAppHost(string appHostSourceFilePath,string appHostDestinationFilePath,string appBinaryFilePath,bool windowsGraphicalUserInterface = false,string assemblyToCopyResorcesFrom = null){var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath);if (bytesToWrite.Length > 1024){throw new AppNameTooLongException(appBinaryFilePath);}void RewriteAppHost(){// Re-write the destination apphost with the proper contents.using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostDestinationFilePath)){using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor()){BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);appHostIsPEImage = PEUtils.IsPEImage(accessor);if (windowsGraphicalUserInterface){if (!appHostIsPEImage){throw new AppHostNotPEFileException();}PEUtils.SetWindowsGraphicalUserInterfaceBit(accessor);}}}}// 忽略代碼}}可以看到在 HostWriter 的邏輯就是找到 AppHost.exe 里面的?private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";?二進制內容,替換為 appBinaryFilePath 的內容
而除了這個之外,還有其他的邏輯就是包含一些資源文件,如圖標和程序清單等,將這些內容放入到 AppHost.exe 里面,這就是實際的輸出文件了
利用這個機制,咱可以更改可執行程序的內容,讓可執行程序文件,尋找其他路徑下的 dll 文件作為 dotnet 程序的入口,大概就可以實現將 exe 放在文件夾外面,而將 dll 放在文件夾里面的效果。原先的輸出就是讓 exe 和 dll 都在相同的一個文件夾,這樣看起來整個文件夾都很亂。也不利于進行 OTA 靜默升級。而將入口 exe 文件放在 dll 所在文件夾的外面,可以讓整個應用文件夾看起來更加清真
想要達成這個效果很簡單,如上面描述的原理,可以通過修改 AppHost.exe 文件的二進制內容,設置入口 dll 的路徑來實現
更改方法就是抄 HostWriter 的做法,替換 exe 里面對應的二進制內容,我從 dnSpy 里面抄了一些代碼,魔改之后放在github?歡迎小伙伴訪問
在拉下來 AppHostPatcher 之后,進行構建,此時的 AppHostPatcher 是一個命令行工具應用,支持將最終輸出的 exe 文件進行魔改。傳入的命令行參數只有兩個,一個是可執行文件的路徑,另一個就是新的 dll 所在路徑。如下面代碼
AppHostPatcher.exe Foo.exe .\Application\Foo.dll此時原本的 Foo.exe 將會尋找相同文件夾下的 Foo.dll 文件作為 dotnet 的入口程序集,而在執行上面代碼之后,雙擊 Foo.exe 將會尋找?Application\Foo.dll?作為入口程序集,因此就能將整個文件夾的內容,除了 exe 之外的其他文件放在其他文件夾里面
更多細節請看?Write a custom .NET Core runtime host
本文以上使用的代碼是在?https://github.com/dotnet/runtime?的 v5.0.0-rtm.20519.4 版本的代碼
總結
以上是生活随笔為你收集整理的dotnet core 应用是如何跑起来的 通过AppHost理解运行过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Newbe.ObjectVisitor
- 下一篇: 【招聘(上海)】 坚果云 招聘Windo