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 版本的代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306430.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法题目——整数划分(HRBUST-2004)

题目链接&#xff1a;HRBUST-2004 递归法: 1.当n1时&#xff0c;此时只有1种解{1}; 2.当m1时&#xff0c;此时也只有1种解{1,1,1, … 3.当n> m时&#xff0c;要分为最大数包含m和不包含m两种情况。 ●包含m:此时就是{mx1,2…}. 其中x1x23.… n-m, 就相当于求和为n-m,最…

dotnet core 应用是如何跑起来的 通过自己写一个 dotnet host 理解运行过程

在上一篇博客是使用官方提供的 AppHost 跑起来整个 dotnet 程序。本文告诉大家在 dotnet 程序运行到托管代码之前&#xff0c;所需要的 Native 部分的逻辑。包括如何寻找 dotnet 运行时&#xff0c;如何加载运行时和框架然后跑起来业务端的 dll 文件的逻辑在上一篇博客告诉大家…

算法题目——多米诺骨牌问题(POJ-2663)

题目链接&#xff1a;POJ-2663 设有形状一样的多米诺牌&#xff0c;每张牌恰好覆盖棋盘上相邻的两个方格&#xff0c;即一张多米诺牌是一张 1 行 2 列或者 2 行 1 列的牌。那么&#xff0c;是否能够把 32 张多米诺牌摆放到棋盘上&#xff0c;使得任何两张多米诺牌均不重叠&…

算法题目——求众数

在c中有两个关联容器&#xff0c; 第一种是map&#xff0c;内部是按照key排序的&#xff0c;从小到大 第二种是unordered_map&#xff0c;容器内部是无序的&#xff0c;使用hash组织内容的。 #include<iostream> #include<map> #include<algorithm> using n…

2020武汉dotNET俱乐部分享交流会圆满结束

经过长达2个多月的准备&#xff0c;终于在12月5日圆满的举行了武汉首次dotNET俱乐部线下分享交流活动。我们一共精心准备了3个目前比较热门的主题&#xff0c;分别如下&#xff1a;Jason分享的《ABP开发框架的扩展应用》Leo分享的《基于Kubernetes的DevOps平台应用》Eleven分享…

算法——常用的数据结构/模板/基础知识

常用的数据结构/模板/基础知识(一)c——优先队列(priority_queue)最大堆和最小堆的写法(二)c中的全排列函数next_permutation()(三)迭代器的使用(四)数学知识数列求和(五)求最大公约数和最小公倍数(六)将数值型数据转化为string 类型(一)c——优先队列(priority_queue) 最大堆…

对 精致码农大佬 说的 Task.Run 会存在 内存泄漏 的思考

一&#xff1a;背景 1. 讲故事这段时间项目延期&#xff0c;加班比较厉害&#xff0c;博客就稍微停了停&#xff0c;不过还是得持续的技术输出呀&#xff01;园子里最近挺热闹的&#xff0c;精致码农大佬分享了三篇文章&#xff1a;为什么要小心使用 Task.Run [https://www.c…

算法题目——爬楼梯(动态规划)

题目链接:70.爬楼梯 类似题目:1646.获取生成数组中最大值 本题大家如果没有接触过的话,会感觉比较难,多举几个例子,就可以发现其规律。 爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。 那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。 所以到…

Kubernetes 1.20 发布:妙啊

喜欢就关注我们吧&#xff01;Kubernetes 1.20 发布&#xff0c;这是 2020 年的第三版也是最终版。Kubernetes 1.20 还是最近这段时间以来&#xff0c;功能最密集的版本之一&#xff0c;此版本包含了 42 个增强功能&#xff0c;其中 11 个增强功能已逐步升级为稳定版&#xff0…

算法题目——生成括号匹配

题目链接:剑指offer.85 生成 n 对括号共需要 2n 步,每一步都面临两个选项,即生成左括号或者右括号,题目要求返回所有的情况,所以本问题很适合采用回溯法。 在生成左右括号时存在两个限制条件,一个是左括号或者右括号的数量都不能超过 n 个,第二个是已经生成的右括号数…

C# WPF开源控件库:MahApps.Metro

❝其实站长很久之前就知道这个开源WPF控件库了&#xff0c;只是一直欣赏不了这种风格&#xff0c;但也star了该项目。每次浏览该仓库时&#xff0c;发现star越来越多&#xff0c;也看到很多网友对它的褒奖&#xff0c;所以今天就向大家推荐这款WPF控件库。1. 具体有多优秀&…

golang 切片 接口_Golang语言常用关键字之 make 和 new

上一章中对于golang的语言基础说明如下&#xff1a;1 函数调用2 接口3 反射接下来我们来对golang的常用关键字进行说明&#xff0c;主要内容有&#xff1a;1. for 和 range2. select3. defer4. panic 和 recover5. make 和 new— — — — — — — — — — — — — — — —…

读书 | 数字化转型的道与术(上)

【数字化转型】| 作者 / Edison Zhou这是EdisonTalk的第312篇学习总结 最近在阅读钟华老师的新作《数字化转型的道与术》&#xff0c;记录和总结了一些学习笔记和感想&#xff0c;整理成文分享与你&#xff0c;本文为上半部分&#xff0c;希望能对也在参与数字化转型的各位童鞋…

算法题目——最长连续序列

题目链接:leetcode.128 思路: 1.现将数据存放到数组中去 2.再将数据存到一个集合set中去(方便去重,查找某一个元素是否存在于数组中) 3.循环该数组,若该元素的前一个元素不在数组中(通过集合来查找),说明它将会是一个连续序列的开始元素,在对该元素循环查看它的下一个元素…

如何在 ASP.NET Core 中实现全局异常拦截

异常是一种运行时错误&#xff0c;当异常没有得到适当的处理&#xff0c;很可能会导致你的程序意外终止&#xff0c;这篇就来讨论一下如何在 ASP.Net Core MVC 中实现全局异常处理&#xff0c;我会用一些 样例代码 和 截图 来说明这些概念。全局异常处理 其实在 ASP.Net Core M…

算法题目——被围绕的区域(dfs,bfs)

leetcode.130被围绕的区域 dfs解法: 深度优先遍历: 思路: 读取数据后 1.先将数据的四周进行bfs算法(因为只有与外围接触的点,才能不被包围) 算法执行中时,递归看看该点的上下左右有没有是O的,如果是O则标记为A 2.循环完四周之后,将数据中的O全部换成X,将全部的A换成O即得…

[Windows] 在 Microsoft Docs 网站中挖掘 MVVM 的各种学习资源

最近写了一些 MVVM 框架的文章&#xff0c;翻了一些 Microsoft Docs 的文档&#xff0c;顺便就对 MVVM 本身来了兴致&#xff0c;想看看更多当年相关的文档。在 MVVM 出现后十多年&#xff0c;我在不同的场合见到过多种 MVVM 的实现方式&#xff0c;也看到过各种 MVVM 框架的多…

算法题目——岛屿数量(bfs dfs)

题目链接:leetcode.200岛屿数量 dfs 重点:路过过的点做新标记 ,以防止重复路过 思路: 数据读取完成后 1.对所有数据进行一次循环 2.在循环内部,如果元素为‘1’则进入dfs算法 3.在深度优先遍历中,先将该元素标记为‘0’,在查看它的上下左右元素是否为‘1’,是‘1’则进…

注意| .NET开发者大会防疫须知 !

2020年12月19-20日中国.NET开发者大会将于苏州举办疫情常态化的情况下为确保大会顺利进行大会组委会从会议内容、会务筹备等方面均进行了全面的精细准备以下是组委会发布的参会防疫指南敬请所有现场参会的小伙伴认真阅读并严格按照防疫需求作相应准备▽为保证大会的顺利召开&am…

算法题目——省份数量(dfs,bfs)

题目链接:leetcode.547省份数量 dfs: 深度优先遍历:递归 思路:读入数据完成后 重点:建立一个数组记录该省份是否访问,新建一个元素记录省份圈的个数 1.对所有省份循环一次,如果该省份未被访问,则进入dfs 2.dfs中,对所有省份循环一遍,如果未被访问且省份index可以到达…