我们用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下
然后采用填坑替换的方式把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的一些实现不得不说佩服。字符串二进制占坑替换还能这么玩。
下一步可编译dotnet.runtime\src\coreclr\hosts来生成corerun程序,让corerun指定clr路径和托管程序集运行程序(类似dotnet run命令)。会C/C++的可以具体看DotNetCore加载clr和托管程序集的具体过程。也可以自己定制DotNetCore的启动程序。