为何DotNetCore的exe能双击运行

我们用DotNetCore开发的Winform和控制台程序按理编译的是中间代码。既然不是机器码,为何双击生成的.exe文件可以运行。DotNetCore和FrameWork不同,FrameWork有操作系统黑科技,来让双击的.Net程序运行。DotNetCore完全可以用独立部署,这种情况就是普通进程运行,必须得有可直接运行的机器码来加载运行时和托管程序集才能运行。

通过看DotNetCore源码发现dotnet.runtime\src\native\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.
 */

再看github的代码
https://github.com/dnSpy/dnSpy/blob/2fa5c978b1a9fb8d1979c8aa4cfa6d177bf5aa9c/Build/AppHostPatcher/Program.cs的实现代码

/*
    Copyright (C) 2014-2019 [email protected]
    This file is part of dnSpy
    dnSpy is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    dnSpy is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    You should have received a copy of the GNU General Public License
    along with dnSpy.  If not, see .
*/

using System;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace AppHostPatcher {
     
	class Program {
     
		static void Usage() {
     
			Console.WriteLine("apphostpatcher   ");
			Console.WriteLine("apphostpatcher  ");
			Console.WriteLine("apphostpatcher  -d ");
			Console.WriteLine("example: apphostpatcher my.exe -d bin");
		}

		const int maxPathBytes = 1024;

		static string ChangeExecutableExtension(string apphostExe) =>
			// Windows apphosts have an .exe extension. Don't call Path.ChangeExtension() unless it's guaranteed
			// to have an .exe extension, eg. 'some.file' => 'some.file.dll', not 'some.dll'
			apphostExe.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? Path.ChangeExtension(apphostExe, ".dll") : apphostExe + ".dll";

		static string GetPathSeparator(string apphostExe) =>
			apphostExe.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? @"\" : "/";

		static int Main(string[] args) {
     
			try {
     
				string apphostExe, origPath, newPath;
				if (args.Length == 3) {
     
					if (args[1] == "-d") {
     
						apphostExe = args[0];
						origPath = Path.GetFileName(ChangeExecutableExtension(apphostExe));
						newPath = args[2] + GetPathSeparator(apphostExe) + origPath;
					}
					else {
     
						apphostExe = args[0];
						origPath = args[1];
						newPath = args[2];
					}
				}
				else if (args.Length == 2) {
     
					apphostExe = args[0];
					origPath = Path.GetFileName(ChangeExecutableExtension(apphostExe));
					newPath = args[1];
				}
				else {
     
					Usage();
					return 1;
				}
				if (!File.Exists(apphostExe)) {
     
					Console.WriteLine($"Apphost '{
       apphostExe}' does not exist");
					return 1;
				}
				if (origPath == string.Empty) {
     
					Console.WriteLine("Original path is empty");
					return 1;
				}
				var origPathBytes = Encoding.UTF8.GetBytes(origPath + "\0");
				Debug.Assert(origPathBytes.Length > 0);
				var newPathBytes = Encoding.UTF8.GetBytes(newPath + "\0");
				if (origPathBytes.Length > maxPathBytes) {
     
					Console.WriteLine($"Original path is too long");
					return 1;
				}
				if (newPathBytes.Length > maxPathBytes) {
     
					Console.WriteLine($"New path is too long");
					return 1;
				}

				var apphostExeBytes = File.ReadAllBytes(apphostExe);
				int offset = GetOffset(apphostExeBytes, origPathBytes);
				if (offset < 0) {
     
					Console.WriteLine($"Could not find original path '{
       origPath}'");
					return 1;
				}
				if (offset + newPathBytes.Length > apphostExeBytes.Length) {
     
					Console.WriteLine($"New path is too long: {
       newPath}");
					return 1;
				}
				for (int i = 0; i < newPathBytes.Length; i++)
					apphostExeBytes[offset + i] = newPathBytes[i];
				File.WriteAllBytes(apphostExe, apphostExeBytes);
				return 0;
			}
			catch (Exception ex) {
     
				Console.WriteLine(ex.ToString());
				return 1;
			}
		}

		static int GetOffset(byte[] bytes, byte[] pattern) {
     
			int si = 0;
			var b = pattern[0];
			while (si < bytes.Length) {
     
				si = Array.IndexOf(bytes, b, si);
				if (si < 0)
					break;
				if (Match(bytes, si, pattern))
					return si;
				si++;
			}
			return -1;
		}

		static bool Match(byte[] bytes, int index, byte[] pattern) {
     
			if (index + pattern.Length > bytes.Length)
				return false;
			for (int i = 0; i < pattern.Length; i++) {
     
				if (bytes[index + i] != pattern[i])
					return false;
			}
			return true;
		}
	}
}

发现原来DotNetCore编译生成的.exe并不是中间代码,.dll参数托管语言的中间代码。该exe是由dotnet.runtime\src\native\corehost的C++代码生成的启动模板exe(直接是机器码),该exe负责寻找clr和托管程序集来加载运行。安装DotNetSDK后在C:\Program Files\dotnet\sdk\5.0.402\AppHostTemplate\apphost.exe。

在vs编译DotNetCore工程时候先把apphost.exe拷贝到项目的obj下
为何DotNetCore的exe能双击运行_第1张图片

然后采用填坑替换的方式把dll路径替换apphost.exe的占位部分就得的项目的运行exe。

那么自己也可以参照微软代码定制一个填坑程序,来改变exe和dll的相对路径,在有的时候让exe脱离出去,有更清晰的目录

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace NetCoreAppHostTemplateEdit
{
     
    /// 
	/// 在 dotnet.runtime\src\native\corehost\将会构建出 apphost.exe 文件
	/// 在安装dotnet sdk 的时候,将会输出到 C:\Program Files\dotnet\sdk\5.0.402\AppHostTemplate 
	/// apphost.exe将会在构建程序的时候,输出到项目obj文件夹里面
	/// 然后被替换执行的dll路径,根据dotnet.runtime\src\native\corehost\corehost.cpp 的注释
    /// 及https://github.com/dnSpy/dnSpy/blob/2fa5c978b1a9fb8d1979c8aa4cfa6d177bf5aa9c/Build/AppHostPatcher/Program.cs的代码,可以了解到,替换这个路径就可以自己定制执行的路径
	/// 
    public partial class FrmMian : Form
    {
     
        /**
        * 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.
        */

        /// 
        /// 最长路径长度,占位长度
        /// 
        private const int MaxPathBytes = 1024;

        public FrmMian()
        {
     
            InitializeComponent();
        }


        /// 
        /// 生成新程序
        /// 
        /// 
        /// 
        private void btnOk_Click(object sender, EventArgs e)
        {
     
            try
            {
     
                //老exe路径
                string oldExePath = txtOld.Text;
                FileInfo fiOld = new FileInfo(oldExePath);
                //老dll路径
                string oldDllPath = oldExePath.Replace(".exe", ".dll");
                //新exe路径
                string newExePath = Path.Combine(txtNew.Text, fiOld.Name);
                FileInfo fiNew = new FileInfo(oldExePath);
                //dll名字
                string oldDllName = fiOld.Name.Replace(".exe", ".dll");
                Uri url = new Uri(newExePath);
                //算新exe和dll的相对路径
                Uri relativeUrl = url.MakeRelativeUri(new Uri(oldDllPath));
                //得到路径的比特
                var origPathBytes = Encoding.UTF8.GetBytes(oldDllName + "\0");
                //得到新路径的比特
                var newPathBytes = Encoding.UTF8.GetBytes(relativeUrl + "\0");
                //不能超过最大限度
                if (newPathBytes.Length > MaxPathBytes)
                {
     
                    MessageBox.Show("新路径太长!", "提示");
                    return;
                }
                //拷贝exe到新目录
                File.Copy(oldExePath, newExePath);
                //读取exe到比特数组
                byte[] apphostExeBytes = File.ReadAllBytes(newExePath);
                //找到原路径偏移量
                int offset = GetOffset(apphostExeBytes, origPathBytes);
                if (offset < 0)
                {
     
                    MessageBox.Show("不能找到地址:" + oldDllName, "提示");
                    return;
                }
                if (offset + newPathBytes.Length > apphostExeBytes.Length)
                {
     
                    MessageBox.Show("新地址太长:" + relativeUrl, "提示");
                    return;
                }
                //替换比特
                for (int i = 0; i < newPathBytes.Length; i++)
                {
     
                    apphostExeBytes[offset + i] = newPathBytes[i];
                }
                //写回文件
                File.WriteAllBytes(newExePath, apphostExeBytes);
                MessageBox.Show("新exe和dll的相对路径:" + relativeUrl, "提示");
                MessageBox.Show(newExePath + "成功生成新文件!", "成功");
            }
            catch (Exception ex)
            {
     
                MessageBox.Show(ex.Message, "错误");
                return;
            }
        }

        /// 
        /// 选择老地址
        /// 
        /// 
        /// 
        private void btnOld_Click(object sender, EventArgs e)
        {
     
            OpenFileDialog of = new OpenFileDialog();
            of.Filter = "(exe文件)|*.exe";
            if (of.ShowDialog() == DialogResult.OK)
            {
     
                txtOld.Text = of.FileName;
            }
        }

        /// 
        /// 输出目录
        /// 
        /// 
        /// 
        private void btnNew_Click(object sender, EventArgs e)
        {
     
            FolderBrowserDialog fi = new FolderBrowserDialog();
            if (fi.ShowDialog() == DialogResult.OK)
            {
     
                txtNew.Text = fi.SelectedPath;
            }
        }

        /// 
        /// 得到偏移
        /// 
        /// 
        /// 
        /// 
        private int GetOffset(byte[] bytes, byte[] pattern)
        {
     
            int si = 0;
            var b = pattern[0];
            while (si < bytes.Length)
            {
     
                si = Array.IndexOf(bytes, b, si);
                if (si < 0)
                    break;
                if (Match(bytes, si, pattern))
                    return si;
                si++;
            }
            return -1;
        }

        /// 
        /// 匹配
        /// 
        /// 
        /// 
        /// 
        /// 
        private bool Match(byte[] bytes, int index, byte[] pattern)
        {
     
            if (index + pattern.Length > bytes.Length)
                return false;
            for (int i = 0; i < pattern.Length; i++)
            {
     
                if (bytes[index + i] != pattern[i])
                    return false;
            }
            return true;
        }

    }
}

为何DotNetCore的exe能双击运行_第2张图片

为何DotNetCore的exe能双击运行_第3张图片

看完DotNetCore的一些实现不得不说佩服。字符串二进制占坑替换还能这么玩。

下一步可编译dotnet.runtime\src\coreclr\hosts来生成corerun程序,让corerun指定clr路径和托管程序集运行程序(类似dotnet run命令)。会C/C++的可以具体看DotNetCore加载clr和托管程序集的具体过程。也可以自己定制DotNetCore的启动程序。

你可能感兴趣的:(DotNetCore,c#)