dotnet core 应用是如何跑起来的 通过AppHost理解运行过程
在 dotnet 的輸出路徑里面,可以看到有一個(gè)有趣的可執(zhí)行文件,這個(gè)可執(zhí)行文件是如何在框架發(fā)布和獨(dú)立發(fā)布的時(shí)候,找到 dotnet 程序的運(yùn)行時(shí)的,這個(gè)可執(zhí)行文件里面包含了哪些內(nèi)容
在回答上面的問(wèn)題之前,請(qǐng)大家嘗試打開(kāi)?C:\Program Files\dotnet\sdk\5.0.100\AppHostTemplate\?這個(gè)文件夾。當(dāng)然了,請(qǐng)將 dotnet 版本號(hào)修改為你本機(jī)的版本號(hào)。在這個(gè)文件夾里面,可以看到有一個(gè)文件叫 apphost.exe 的可執(zhí)行文件。有趣的是在咱的 dotnet 項(xiàng)目的 obj 文件夾下也能找到叫這個(gè)名字的這個(gè)文件
更有趣的是在咱的 dotnet 項(xiàng)目的 obj 文件夾下的 apphost.exe 可執(zhí)行文件和最終輸出的可執(zhí)行文件是相同的一個(gè)文件
這有什么聯(lián)系呢?回答這個(gè)問(wèn)題需要從 dotnet 的代碼開(kāi)始。在 GitHub 完全開(kāi)源的 dotnet 源代碼倉(cāng)庫(kù) https://github.com/dotnet/runtime 里面,將代碼拉到本地,可以在?dotnet runtime\src\installer\corehost\?文件里面看到很多有趣的邏輯
沒(méi)錯(cuò),其實(shí) apphost.exe 的核心邏輯就放在?dotnet runtime\src\installer\corehost\?文件里面
打開(kāi)?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?大概就是整個(gè)可執(zhí)行文件的入口方法了,在這里實(shí)現(xiàn)的功能將包含使用 hostfxr 和 hostpolicy 來(lái)托管執(zhí)行整個(gè) dotnet 進(jìn)程,以及主函數(shù)的調(diào)起。而在使用托管之前,需要先尋找 dotnet_root 也就是 dotnet 框架用來(lái)承載整個(gè) dotnet 進(jìn)程
上面的邏輯的核心代碼如下
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);而在進(jìn)行獨(dú)立發(fā)布的時(shí)候,其實(shí)會(huì)在創(chuàng)建 fxr 對(duì)象的時(shí)候傳入 app_root 路徑,如下面代碼
hostfxr_resolver_t fxr{app_root};在 dotnet core 里面,和 dotnet framework 不同的是,在 dotnet core 的可執(zhí)行程序沒(méi)有使用到系統(tǒng)給的黑科技,是一個(gè)完全的 Win32 應(yīng)用程序,在雙擊 exe 的時(shí)候,將會(huì)執(zhí)行一段非托管的代碼,在進(jìn)入到 corehost.cpp 的?exe_start?函數(shù)之后。將會(huì)開(kāi)始尋找 dotnet 托管入口,以及 dotnet 運(yùn)行時(shí),通過(guò) hostfxr 的方式加載運(yùn)行時(shí)組件,然后跑起來(lái)托管應(yīng)用
那么在 dotnet 構(gòu)建輸出的可執(zhí)行文件又是什么?其實(shí)就是包含了 corehost.cpp 邏輯的 AppHost.exe 文件的魔改。在 corehost.cpp 構(gòu)建出來(lái)的 AppHost.exe 文件,是不知道開(kāi)發(fā)者的最終輸出包含入口的 dll 是哪個(gè)的,需要在構(gòu)建過(guò)程中傳入給 AppHost.exe 文件。而 AppHost.exe 文件是固定的二進(jìn)制文件,不接受配置等方式,因此傳入的方法就是通過(guò)修改二進(jìn)制的內(nèi)容了
這也就是為什么 AppHost.exe 放在 AppHostTemplate 文件夾的命名原因,因?yàn)檫@個(gè)?C:\Program Files\dotnet\sdk\5.0.100\AppHostTemplate\?文件夾的 AppHost.exe 是一個(gè) Template 模版而已,在 corehost.cpp 文件里面,預(yù)定了一段大概是 1025 長(zhǎng)度的空間用來(lái)存放 dotnet 入口 dll 路徑名。這個(gè)代碼就是本文上面給的很長(zhǎng)的注釋下面的代碼
#define EMBED_HASH_HI_PART_UTF8 "c3ab8ff13720e8ad9047dd39466b3c89" // SHA-256 of "foobar" in UTF-8 #define EMBED_HASH_LO_PART_UTF8 "74e592c2fa383d4a3960714caef0c4f2" // 這兩句代碼就是 foobar 的 UTF-8 二進(jìn)制的 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]);// 這里給的是就是最長(zhǎng) 1024 個(gè) byte 的 dll 名,加上一個(gè) \0 一共是 1025 個(gè)字符constexpr int EMBED_MAX = (EMBED_SZ > 1025 ? EMBED_SZ : 1025); // 1024 DLL name length, 1 NUL// 這就是定義在 AppHost.exe 二進(jìn)制文件里面的一段空間了,長(zhǎng)度就是 EMBED_MAX 長(zhǎng)度,內(nèi)容就是 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 的內(nèi)容復(fù)制到 app_dll 變量里面pal::clr_palstring(embed, app_dll); }int exe_start(const int argc, const pal::char_t* argv[]) {// 讀取嵌入到二進(jìn)制文件的 App 名,也就是 dotnet 的入口 dll 路徑,可以是相對(duì)也可以是絕對(duì)路徑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 的內(nèi)容賦值給 app_path 變量,這個(gè)變量的定義代碼我沒(méi)有寫(xiě)append_path(&app_path, embedded_app_name.c_str());const pal::char_t* app_path_cstr = app_path.empty() ? nullptr : app_path.c_str();// 跑起來(lái) dotnet 應(yīng)用rc = hostfxr_main_bundle_startupinfo(argc, argv, host_path_cstr, dotnet_root_cstr, app_path_cstr, bundle_header_offset); }上面代碼不是實(shí)際的 corehost.cpp 的代碼,只是為了方便本文描述而修改的代碼
在實(shí)際輸出的 dotnet 可執(zhí)行文件里面的邏輯是先從?C:\Program Files\dotnet\sdk\5.0.100\AppHostTemplate\?文件夾復(fù)制 AppHost.exe 出來(lái),接著依靠上面代碼的?static char embed[EMBED_MAX] = EMBED_HASH_FULL_UTF8;?的邏輯,替換二進(jìn)制文件的 embed 值的內(nèi)容
在?dotnet runtime\src\installer\managed\Microsoft.NET.HostModel\AppHost\HostWriter.cs?文件中,將包含實(shí)際的替換邏輯,代碼如下
/// <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";?二進(jìn)制內(nèi)容,替換為 appBinaryFilePath 的內(nèi)容
而除了這個(gè)之外,還有其他的邏輯就是包含一些資源文件,如圖標(biāo)和程序清單等,將這些內(nèi)容放入到 AppHost.exe 里面,這就是實(shí)際的輸出文件了
利用這個(gè)機(jī)制,咱可以更改可執(zhí)行程序的內(nèi)容,讓可執(zhí)行程序文件,尋找其他路徑下的 dll 文件作為 dotnet 程序的入口,大概就可以實(shí)現(xiàn)將 exe 放在文件夾外面,而將 dll 放在文件夾里面的效果。原先的輸出就是讓 exe 和 dll 都在相同的一個(gè)文件夾,這樣看起來(lái)整個(gè)文件夾都很亂。也不利于進(jìn)行 OTA 靜默升級(jí)。而將入口 exe 文件放在 dll 所在文件夾的外面,可以讓整個(gè)應(yīng)用文件夾看起來(lái)更加清真
想要達(dá)成這個(gè)效果很簡(jiǎn)單,如上面描述的原理,可以通過(guò)修改 AppHost.exe 文件的二進(jìn)制內(nèi)容,設(shè)置入口 dll 的路徑來(lái)實(shí)現(xiàn)
更改方法就是抄 HostWriter 的做法,替換 exe 里面對(duì)應(yīng)的二進(jìn)制內(nèi)容,我從 dnSpy 里面抄了一些代碼,魔改之后放在github?歡迎小伙伴訪問(wèn)
在拉下來(lái) AppHostPatcher 之后,進(jìn)行構(gòu)建,此時(shí)的 AppHostPatcher 是一個(gè)命令行工具應(yīng)用,支持將最終輸出的 exe 文件進(jìn)行魔改。傳入的命令行參數(shù)只有兩個(gè),一個(gè)是可執(zhí)行文件的路徑,另一個(gè)就是新的 dll 所在路徑。如下面代碼
AppHostPatcher.exe Foo.exe .\Application\Foo.dll此時(shí)原本的 Foo.exe 將會(huì)尋找相同文件夾下的 Foo.dll 文件作為 dotnet 的入口程序集,而在執(zhí)行上面代碼之后,雙擊 Foo.exe 將會(huì)尋找?Application\Foo.dll?作為入口程序集,因此就能將整個(gè)文件夾的內(nèi)容,除了 exe 之外的其他文件放在其他文件夾里面
更多細(xì)節(jié)請(qǐng)看?Write a custom .NET Core runtime host
本文以上使用的代碼是在?https://github.com/dotnet/runtime?的 v5.0.0-rtm.20519.4 版本的代碼
總結(jié)
以上是生活随笔為你收集整理的dotnet core 应用是如何跑起来的 通过AppHost理解运行过程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Newbe.ObjectVisitor
- 下一篇: 2020武汉dotNET俱乐部分享交流会