26
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

こちらのAdventCalendarの2日目の記事です。

はじめに

先日.NET6が正式リリースされました!

目につくものと言えばやはり、C#10や、SourceGeneratorなどがありますね!
「.NETのランタイムそのものがどのような仕組みで動いているか」なんてことは誰も気に留めません。
それはミドルウェアとしてとても理想的なことです。

しかし、エンジニアたるもの、やっぱり中身がどうなっているのか気になりませんか?
ということで、.NETアプリケーションを起動した瞬間から、エントリポイントが呼ばれるまでをトレースしてみましょう!

.NETCore時代の解説にはなりますが、.NETCoreの起動までを概念的に解説した良い記事があります。

本記事では、概念的な部分よりも、コード、実装にフォーカスていきます。
なので、その前に読んでおくと、イメージがしやすくなると思います。

相手は.NETのランタイム、そこそこ長くなってしまいましたが、安眠導入にお使いください。

対象・条件

  • .NETについてユーザーとしての知識はある程度ある方向けです(C#アプリが難なく書ける程度)
  • dotnet publish -r win-x64 --self-contained=falseによって出力されたExeを実行した場合とします
  • wmainからProgram.Mainを叩くまでのNativeコードを対象とします
  • 厳密には、.NETランタイムというより、.NETランタイムホストが大部分を占めます
  • JIT、GC、AppContextなどについては割愛します
  • バージョンは、dotnet/runtimev6.0.0を対象とします
  • 例外的処理や通過しない処理、重要度の低い処理など、本筋に関係ない部分は注釈なく省略・改変しています
  • コード中のコメントについて、英語は引用元、日本語は私の書いたもので区別しています

用語

  • Native/Unmanaged: C++で書かれていているコード、それをコンパイルしたモジュール(C++/CLIは今回考慮しません)
  • Managed: C#などの.NET言語で書かれているコード、それをコンパイルしたアセンブリ
  • モジュール: システム上で直接実行可能な、Exe、Dllの
  • アセンブリ: .NET上で実行可能なManagedなコードをコンパイルしたDll
  • システム: .NETランタイムより下で動いている系(基本的にはOSのこと)
  • Managedエントリ: C#で言うstatic void Program.Main(string[])のこと(エントリポイントの変更は考慮しない)
  • ランタイム(狭義): sharedディレクトリ下の種類ディレクトリ下のバージョンごとに切られたディレクトリ無いのファイル群をランタイムと呼ぶことにします(コード上ではFramework,FXとも表現されています)
  • ランタイム(広義): dotnet/runtime 全般
  • corehost: 公式のドキュメント上では、apphostcomhost,ijwhost, Entry-point Hosts)とも呼ばれています;コード上ではcorehostなので、corehostと呼ぶことにします

起動までの呼び出し履歴

まず全体像を掴むために、Program.Mainまでの呼出し履歴を見てみます。
「プロジェクトプロパティ>デバッグ>ネイティブコードをデバッグを有効にする」にチェックを入れることで、Nativeな呼出し履歴もすべて見ることができます。
これは、空のSolutionからC#のConsoleアプリプロジェクトを作っただけでできます。

.NETランタイムのモジュールも一緒にデバッグしたい場合、以下のようにすることでできます。

言語 モジュール 関数
C# ConsoleApp1.dll ConsoleApp1.Program.Main
C++ hostpolicy.dll coreclr_t::execute_assembly
C++ hostpolicy.dll run_app_for_context
C++ hostpolicy.dll run_app
C++ hostpolicy.dll corehost_main
C++ hostfxr.dll execute_app
C++ hostfxr.dll ::read_config_and_execute
C++ hostfxr.dll fx_muxer_t::handle_exec_host_command
C++ hostfxr.dll fx_muxer_t::execute
C++ hostfxr.dll hostfxr_main_startupinfo
C++ ConsoleApp1.exe exe_start
C++ ConsoleApp1.exe wmain
C++ ConsoleApp1.exe invoke_main()
C++ ConsoleApp1.exe __scrt_common_main_seh()
kernel32.dll @BaseThreadInitThunk@12()
ntdll.dll __RtlUserThreadStart()
ntdll.dll __RtlUserThreadStart@8()

まず、下から順番に見ていくと、謎の関数をいくつか経た後に、wmainが呼び出されています。この謎の関数については.NETというよりは、Windowsの仕組みの話になりますので割愛します。
VC++でWindowsアプリを作った事のある方ならおなじみかもしれませんが、wmainmainのWide文字バージョンです。
ここではWide文字とUnicode文字の差異は重要ではないので、mainだと思って大丈夫です。つまり、ここが.NETのランタイムにとってのエントリポイントとなります。

その後、Nativeであるhostfxr.dllhostopolicy.dll、そしてManagedであるConsoleApp1.dllと3つのDllが確認できます。
.NETのランタイムは、Managedな領域の管理やコールバック、GC、JITなどとの橋渡しをする本体部分(coreclr.dll)と、それを立ち上げるまでのホスト部分(ConsoleApp1.exehostfxr.dllhostopolicy.dll)に分かれています。
ここでこの4つのNativeモジュールについて、どんなものなのかを説明します。

各Nativeモジュール

以下に抽象的な説明があります。(特にこれに沿っているわけではないですが)

ConsoleApp1.execorehost

ConsoleApp1というのは、dotnet publishしたときのプロジェクト名によって変わりますが、デフォルト?というかこのモジュールコンパイル時の名前はcorehostです。
以降はcorehostと呼ぶことにします。「corehostがすべての始まりである」ということです。

話が.NETアプリのビルド時のことに逸れますが、--self-containedを有効にしても、このモジュール含めてNativeのモジュールはコンパイルされません。
hostfxr.dllhostopolicy.dllcoreclr.dllは、そもそもシステムにインストールされていることが前提なので出力しません。(--self-containedでは、SDKからコピーされるだけです)
corehostはSDKに含まれているものをコピーして、Managedプロジェクト名にリネームして出力します。(もう一工夫ありますが、後述)
更に話が逸れますが、普段使っているdotnetコマンドですが、このdotnet.exeもまたcorehostのリネームです。

corehostには色々なモードがありますが、基本的に

  • アプリケーションのエントリポイント(wmainをエクスポート)
  • hostfxr.dllを見つける
  • hostfxr.dllの関数を呼び出す
    といった役割を担います。

エントリポイントからコードを追っていきましょう!

以下のコードでは、host_path,app_path,app_rootを取得し、hostfxrを解決しています。
host_path,app_path,app_rootは、脈拍と受け継がれ深い部分でも使われるので、何を意味しているかを覚えておいてください。

runtime/src/native/corehost/corehost.cpp
int __cdecl wmain(const int argc, const pal::char_t* argv[])
{
    int exit_code = exe_start(argc, argv);
    return exit_code;
}

int exe_start(const int argc, const pal::char_t* argv[])
{
    pal::string_t host_path = /* corehostのパス; GetModuleFileNameW; (e.g. "./ConsoleApp1.exe") */ ;
    pal::string_t app_path; // Managedエントリのアセンブリのパス (e.g. "./ConsoleApp1.dll")
    pal::string_t app_root; // Managedエントリのアセンブリのパス (e.g. "./")
    pal::string_t embedded_app_name; // corehostに埋め込まれた、Managedエントリのアセンブリの相対パス(e.g. "./ConsoleApp.dll")

    // C#で言うTryGet操作、Exeに埋め込まれたManagedエントリの名前を抽出します(後述)
    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;
    }
    app_path.assign(get_directory(host_path));
    append_path(&app_path, embedded_app_name.c_str());
    app_root.assign(get_directory(app_path));

    hostfxr_resolver_t fxr{app_root}; // → hostfxr_resolver_t::hostfxr_resolver_t
    auto hostfxr_main_startupinfo = fxr.resolve_main_startupinfo(); // Dllエクスポート関数 hostfxr.dll!hostfxr_main_startupinfo を取得
    
    const pal::char_t* host_path_cstr = host_path.c_str();
    const pal::char_t* dotnet_root_cstr = fxr.dotnet_root().empty() ? nullptr : fxr.dotnet_root().c_str(); // .NETランタイムのインストールパス
    const pal::char_t* app_path_cstr = app_path.empty() ? nullptr : app_path.c_str();
    rc = hostfxr_main_startupinfo(argc, argv, host_path_cstr, dotnet_root_cstr, app_path_cstr);
    return rc;
}

is_exe_enabled_for_executionでManagedエントリを含むアセンブリを解決する

is_exe_enabled_for_executionは初見で何しているか解り辛いですが、以下のようなことをしています。

一見するとセキュリティチェックのようにも見えてしまうのですが、セキュリティは一切関係ありません。結果だけを見れば、単にManagedアセンブリの相対パスを取得しているにすぎません。
まずcorehostをコンパイルする際には、Managedアセンブリはまだありませんので以下のようなデータをPEのイメージの.data領域に埋め込んでおきます。

"c3ab8ff13720e8ad9047dd39466b3c89" // A
"74e592c2fa383d4a3960714caef0c4f2" // B
"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2" // AB

dotnet publishする際にcorehost<AppName>.exeにリネームして出力しますが、この時に、HostWriterでABの領域にManagedアセンブリの相対パスを上書きします。
ABの文字列を検索したとき、A, Bについては仮に連続配置されていてもA+BにはならないのでA, BはそのままABだけ上書きされます。(A, Bは実際には"c3a...c89\0", "74e...4f2\0"となっているため)
そして実行時には、まずABを取得し、A+Bと比較します。もしA+B == ABであればリネーム処理が正しくされていないということでエラーにします。

runtime/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
public static class HostWriter
{
    private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";
    private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);
    
    public static void CreateAppHost(
        string appHostSourceFilePath, string appHostDestinationFilePath, string appBinaryFilePath,
        bool windowsGraphicalUserInterface = false, string assemblyToCopyResorcesFrom = null, bool enableMacOSCodeSign = false)
    {
        using (FileStream appHostSourceStream = new FileStream(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
        using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true))
        using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite))
        {
            // c3ab8...0c4f2 を、appBinaryFilePath(e.g. "./ConsoleApp1.dll")でReplaceする
            var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath);
            BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);
            
            using FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create);
            BinaryUtils.WriteToStream(memoryMappedViewAccessor, fileStream, appHostSourceStream.Length);
        }
    }
}

#define EMBED_HASH_HI_PART_UTF8 "c3ab8ff13720e8ad9047dd39466b3c89" // SHA-256 of "foobar" in UTF-8
#define EMBED_HASH_LO_PART_UTF8 "74e592c2fa383d4a3960714caef0c4f2"
#define EMBED_HASH_FULL_UTF8    (EMBED_HASH_HI_PART_UTF8 EMBED_HASH_LO_PART_UTF8) // NUL terminated

bool is_exe_enabled_for_execution(/* OUT */ pal::string_t* app_dll)
{
    constexpr int EMBED_SZ = sizeof(EMBED_HASH_FULL_UTF8) / sizeof(EMBED_HASH_FULL_UTF8[0]);
    constexpr int EMBED_MAX = (EMBED_SZ > 1025 ? EMBED_SZ : 1025); // 1024 DLL name length, 1 NUL

    // "c3ab...3c89 74e59...0c4f2 \0" となっているので、HostWriterで引っかかる
    // つまり、コンパイル時は、embed:="c3ab8...0c4f2 \0" だけど、実行時には書き換えられているとが期待される(e.g. "./ConsoleApp1.dll")
    static char embed[EMBED_MAX] = EMBED_HASH_FULL_UTF8;
    
    // この二つが仮に、モジュール内のバイナリに連続して配置されたとしても、
    // "c3ab...3c89 \0 74e59...0c4f2 \0" となるので、間に挟まる "\0" によってHostWriterで引っかからない
    // つまり、コンパイル時も実行時も同じ hi_part:="c3ab...3c89 \0", lo_part:="74e59...0c4f2 \0" で変わらない
    static const char hi_part[] = EMBED_HASH_HI_PART_UTF8; 
    static const char lo_part[] = EMBED_HASH_LO_PART_UTF8; 

    // app_dll に embed をアサインして...
    // hi_len + lo_len == embed が真なら、HostWriterが書き換えていないということなのでエラーにするチェックをする
}

hostfxr_resolver_thostfxr.dllを解決する

hostfxr_resolverはコンパイルモードによってファイルが切り替わるので注意が必要です。
hostfxr_resolver_thostfxr.dllの場所を解決します。
ここで探しているのは、ランタイムの本体であるcoreclr.dllではなく、hostfxr.dllであり、「ランタイムのリゾルバのリゾルブ」という2段構えの1段目の処理です。

検索の優先順位は

  1. Exeのあるディレクトリ内
  2. 環境変数 "DOTNET_ROOT_x64"
  3. 環境変数 "DOTNET_ROOT"
  4. レジストリ"[HKEY_LOCAL_MACHINE\SOFTWARE\dotnet\Setup\InstalledVersions\x64]InstallLocation"
  5. ディレクトリ "C:\ProgramFiles\dotnet"

となっています。

runtime/src/native/corehost/apphost/standalone/hostfxr_resolver.cpp
hostfxr_resolver_t::hostfxr_resolver_t(const pal::string_t& app_root)
{
    if (!fxr_resolver::try_get_path(app_root, &m_dotnet_root, &m_fxr_path))
    {
        m_status_code = StatusCode::CoreHostLibMissingFailure;
        return;
    }
    if (!pal::load_library(&m_fxr_path, &m_hostfxr_dll)) // LoadLibraryExW で、hostfxr.dllをプロセスにロード
    {
        m_status_code = StatusCode::CoreHostLibLoadFailure;
        return;
    }
    m_status_code = StatusCode::Success;
}

bool fxr_resolver::try_get_path(const pal::string_t& root_path, pal::string_t* out_dotnet_root, pal::string_t* out_fxr_path)
{
    // "<root_path>/hostfxr.dll" が存在すれば、それを採用(root_pathは、Exeのあるディレクトリ)
    if (root_path.length() > 0 && library_exists_in_dir(root_path, LIBFXR_NAME, out_fxr_path))
    {
        out_dotnet_root->assign(root_path);
        return true;
    }

    pal::string_t default_install_location;
    pal::string_t dotnet_root_env_var_name;

    // .NETのランタイムがインストールされているディレクトリ(dotnet_root)をシステムに問い合わせる
    //
    // 1. 環境変数 "DOTNET_ROOT_x64" が設定されていれば、そこを起点とする
    // 2. 環境変数 "DOTNET_ROOT" が設定されていれば、そこを起点とする
    // 3. または、レジストリ"[HKEY_LOCAL_MACHINE\SOFTWARE\dotnet\Setup\InstalledVersions\x64]InstallLocation" が設定されていれば、そこを起点とする
    // 4. ディレクトリ "C:\ProgramFiles\dotnet" が存在すれば、そこを起点とする
    if (get_dotnet_root_from_env(&dotnet_root_env_var_name, out_dotnet_root)) {}
    else if(pal::get_dotnet_self_registered_dir(&default_install_location) out_dotnet_root->assign(default_install_location);
    else if(pal::get_default_installation_dir(&default_install_location)) out_dotnet_root->assign(default_install_location);

    pal::string_t fxr_dir = /* "<out_dotnet_root>/host/fxr" */ ;
    return get_latest_fxr(std::move(fxr_dir), out_fxr_path); // "<fxr_dir>/<一番大きいい数字(最新)>/hostfxr.dll" を探す
}

hostfxr_main_startupinfo_fn hostfxr_resolver_t::resolve_main_startupinfo()
{
    return reinterpret_cast<hostfxr_main_startupinfo_fn>(pal::get_symbol(m_hostfxr_dll, "hostfxr_main_startupinfo"));
}

hostfxr.dll

このモジュールは、.NETのランタイム本体を解決するためのモジュールです。

ここで、システムにインストールされた.NETランタイムの構成を確認しておきましょう。
以下は、.NET5と.NET6をインストールしている場合を想定しています。

C:\Program Files\dotnet
├ (省略)
├ host\fxr
| ├ 5.0.0\hostfxr.dll
| └ 6.0.0\hostfxr.dll
└ shared
  ├ (省略)
  ├ Microsoft.NETCore.App
  | ├ 5.0.0
  | | ├ (省略)
  | | ├ System.Core.dll
  | | ├ clrjit.dll
  | | ├ hostpolicy.dll
  | | └ coreclr.dll
  | └ 6.0.0
  | | ├ (省略)
  | | ├ System.Core.dll
  | | ├ clrjit.dll
  | | ├ hostpolicy.dll
  | | └ coreclr.dll
  └ Microsoft.WindowsDesktop.App
    ├ 5.0.0
    | ├ (省略)
    | └ System.Drawing.dll
    └ 6.0.0
      ├ (省略)
      └ System.Drawing.dll

このように、hostfxr.dllと、それ以外のランタイム本体は別でインストールされます。
hostfxr.dllは複数インストールされていても、最新のものしか使われません。
また、ランタイム本体の方は、ランタイムの種類ごとに、その中でバージョンごとに分けてインストールされています。

それでは、このhostfxrの実装を追っていきます。
まずは、corehostからhostfxr_main_startupinfo関数がGetProcAddressによって呼び出さされます

runtime/src/native/corehost/fxr/hostfxr.cpp
SHARED_API int HOSTFXR_CALLTYPE hostfxr_main_startupinfo(const int argc, const pal::char_t* argv[], const pal::char_t* host_path, const pal::char_t* dotnet_root, const pal::char_t* app_path)
{
    if (host_path == nullptr || dotnet_root == nullptr || app_path == nullptr) return StatusCode::InvalidArgFailure;

    host_startup_info_t startup_info(host_path, dotnet_root, app_path); // host_startup_info_t::host_startup_info_t はメンバへのコピーのみ
    return fx_muxer_t::execute(pal::string_t(), argc, argv, startup_info, nullptr, 0, nullptr);
}

途中の関数では、.NETランタイムのホストモード(host_mode_t)を決定しています。(後述)
関数呼出しをいくつか挟んで、read_config_and_executeまでたどり着くと、hostpolicy_dircorehost_init_tの取得、初期化をした後、それらを使ってランタイム本体を呼び出します。

runtime/src/native/corehost/fxr/fx_muxer.cpp
int fx_muxer_t::execute(
    const pal::string_t host_command, const int argc, const pal::char_t* argv[],
    const host_startup_info_t& host_info, pal::char_t result_buffer[], int32_t buffer_size, int32_t* required_buffer_size)
{
    // Detect invocation mode
    host_mode_t mode = detect_operating_mode(host_info); // host_mode_t::apphost; 後述

    int new_argoff;
    pal::string_t app_candidate;
    opt_map_t opts;
    int result = command_line::parse_args_for_mode(mode, host_info, argc, argv, &new_argoff, app_candidate, opts);

    // Transform dotnet [exec] [--additionalprobingpath path] [--depsfile file] [dll] [args] -> dotnet [dll] [args]
    result = handle_exec_host_command(
        host_command, host_info,
        app_candidate, opts, argc, argv, new_argoff,
        mode, false /*is_sdk_command*/, result_buffer, buffer_size, required_buffer_size);
    return result;
}

int fx_muxer_t::handle_exec_host_command(const pal::string_t& host_command, const host_startup_info_t& host_info,
    const pal::string_t& app_candidate, const opt_map_t& opts, int argc, const pal::char_t* argv[], int argoff,
    host_mode_t mode, const bool is_sdk_command, pal::char_t result_buffer[], int32_t buffer_size, int32_t* required_buffer_size)
{
    const pal::char_t** new_argv = argv;
    int new_argc = argc;
    // Transform dotnet [exec] [--additionalprobingpath path] [--depsfile file] [dll] [args] -> dotnet [dll] [args]
    return read_config_and_execute(
        host_command, host_info,
        app_candidate, opts, new_argc, new_argv,
        mode, is_sdk_command, result_buffer, buffer_size, required_buffer_size);
}

int read_config_and_execute(const pal::string_t& host_command, const host_startup_info_t& host_info,
    const pal::string_t& app_candidate, const opt_map_t& opts, int new_argc, const pal::char_t** new_argv,
    host_mode_t mode, const bool is_sdk_command, pal::char_t out_buffer[], int32_t buffer_size, int32_t* required_buffer_size)
{
    pal::string_t hostpolicy_dir;
    std::unique_ptr<corehost_init_t> init;
    int rc = get_init_info_for_app(
        host_command, host_info, app_candidate, opts,
        mode, is_sdk_command, hostpolicy_dir, init);
    // hostpolicy_dir には host_info.dotnet_root が入ってきています
    rc = execute_app(hostpolicy_dir, init.get(), new_argc, new_argv);
    return rc;
}

コマンドライン引数について

いろいろ捏ね繰り回しており、関数引数が山ほどあってウンザリします。
やっていること自体はオーソドックスで、引数を辞書構造にパースするだけのものです。

runtime/src/native/corehost/fxr/command_line
enum class known_options
{
    additional_probing_path,
    deps_file,
    runtime_config,
    fx_version,
    roll_forward,
    additional_deps,
    roll_forward_on_no_candidate_fx,
    __last // Sentinel value
};

const host_option KnownHostOptions[] =
{
    // { pal::char_t* option; pal::char_t* argument; pal::char_t* description; }
    { _X("--additionalprobingpath"), _X("<path>"), _X("Path containing probing policy and assemblies to probe for.") },
    { _X("--depsfile"), _X("<path>"), _X("Path to <application>.deps.json file.") },
    { _X("--runtimeconfig"), _X("<path>"), _X("Path to <application>.runtimeconfig.json file.") },
    { _X("--fx-version"), _X("<version>"), _X("Version of the installed Shared Framework to use to run the application.") },
    { _X("--roll-forward"), _X("<value>"), _X("Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable)") },
    { _X("--additional-deps"), _X("<path>"), _X("Path to additional deps.json file.") },
    { _X("--roll-forward-on-no-candidate-fx"), _X("<n>"), _X("<obsolete>") }
};

// Dictionary<known_options, List<string>> のような型
typedef std::unordered_map<known_options, std::vector<pal::string_t>, known_options_hash> opt_map_t;

int command_line::parse_args_for_mode(host_mode_t mode, const host_startup_info_t &host_info,
    const int argc, const pal::char_t *argv[], /*out*/ int *new_argoff,
    /*out*/ pal::string_t &app_candidate, /*out*/ opt_map_t &opts, bool args_include_running_executable)
{
    // だいたいこんな感じのことをする(LINQ風)
    for(int i = 0; i < argc; i += 2)
    {
        auto option = Enum.GetValues<known_options>()
            .First(opt => KnownHostOptions[(int)opt].option == to_lower(argv[i]));
        auto values = (*opts)[option];
        values.push_back(argv[i + 1]);
        (*opts)[option] = values;
    }
}

pal::string_t command_line::get_option_value(const opt_map_t &opts, known_options opt, const pal::string_t &default_value)
{
    // だいたいこんな感じのことをする(LINQ風)
    auto values = opts[opt];
    return values.Last();
}

ホストモード(host_mode_t

  • host_mode_t::muxer
    • "dotnet"コマンドによって起動された場合のモード
  • host_mode_t::apphost
    • corehostのリネームである、".exe" で起動された場合のモード
  • host_mode_t::split_fx
    • xunitや、1.x時代に使われていたモード(使うことはまずなさそうです)
  • host_mode_t::libhost
    • 例えば、COMActivationやセルフホストのネイティブアプリ(C++/CLIのことかな?)から呼び出されるなど、Exeではない何かから起動された場合のモード

今回の場合は、corehostをアプリケーション名にリネームするパターンなので、apphostモードになります。

runtime/src/native/corehost/fxr/fx_muxer.cpp
host_mode_t detect_operating_mode(const host_startup_info_t& host_info)
{
    if (coreclr_exists_in_dir(host_info.dotnet_root)) // "<dotnet_root>/coreclr.dll" の有無をチェック
    {
        // hostfxr_resolver によって解決された dotnet_root 直下に coreclr.dll は普通無い
        // .NETのランタイムインストーラを使った場合、"<dotnet_root>/shared/Microsoft.NETCore.App/6.0.0/coreclr.dll" となる

        // "<dotnet_root>/<app_name(e.g. ConsoleApp1)>.deps.json" または "./<app_name(e.g. ConsoleApp1)>.runtimeconfig.json"
        // が存在し、かつ、app_path が存在すれば apphost
        // そうでなければ、split_fx (1.x互換のレガシーモードっぽいので、もう気にしなくてOK?)
        return ... ? host_mode_t::apphost : host_mode_t::split_fx;
    }
    if (pal::file_exists(host_info.app_path))
    {
        return host_mode_t::apphost; // 今回はコレ!
    }
    return host_mode_t::muxer; // dotnetCLI起動の場合(e.g. "dotnet ConsoleApp1.dll") 
}

hostpolicy_dircorehost_init_tの取得、初期化

hostpolicy_dircorehost_init_tの取得、初期化を行うget_init_info_for_appは長いですが次のステップで処理を進めます。

hostpolicy.dllは、hostfxr.dllと違って、どのバージョン向けのアプリでも1つのもの(最新)が使われるものではなく、バージョンそれぞれに存在するものです。
なので、hostpolicy_dirを取得するためには、RuntimeConfigなどからターゲットのランタイムのバージョンを決定してあげる必要があります。

RollForwardは、実際に使う.NETランタイムのバージョンの決定ルールを表します。
システムにこのバージョンがインストールされていれば良いですが、.NETは少しだけ異なるバージョンが異なっていてもある程度、互換性があります。
hostfxrにはこの少しだけ異なるバージョンを柔軟に解決する仕組みがあり、それがRollForwardで、runtimeOptions/rollForwardでルールを指定できます。
「patch/feature/minor/major/latestPatch/latestFeature/latestMinor/latestMajor/disable」
の設定が存在し、"ver...." というバージョン表記に対し、どれぐらいの差異を許容するかというルールです。
このRollForwardの規定はマイナーバージョンです、つまり、メジャーバージョン(5.x/6.x)が一致していないと候補になりません。
RollForwardルールの決定は以下の優先度によって決定されます。(ルールの決定であり実際のバージョン解決はまた別です)

  1. コマンドライン引数 --roll-forward または --roll-forward-on-no-candidate-fx
  2. 環境変数 "DOTNET_ROLL_FORWARD"
  3. runtimeconfig.json の "framework/rollForward" または "framework/rollForwardOnNoCandidateFx"
  4. runtimeconfig.json の "runtimeOptions/rollForward" または "runtimeOptions/rollForwardOnNoCandidateFx"
  5. 環境変数 "DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"

RuntimeConfigはデフォルトで以下のような内容になります。

ConsoleApp1.runtimeconfig.json
{
  "runtimeOptions": {
    "tfm": "net6.0",
    // "rollForward": "Minor",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "6.0.0-rc.1.21430.1"
    }
  }
}

runtimeOptions/framework が基点となるランタイムバージョンです。
これを基点に、runtimeOptions/rollForward(未指定ならMinor)によって、バージョン差が許容内のランタイムを解決します。
RuntimeConfigのパースについての詳細は後述します。

runtime/src/native/corehost/fxr/fx_muxer.cpp
int get_init_info_for_app(const pal::string_t &host_command, const host_startup_info_t &host_info,
    const pal::string_t &app_candidate, const opt_map_t &opts, host_mode_t mode, const bool is_sdk_command,
    /*out*/ pal::string_t &hostpolicy_dir, /*out*/ std::unique_ptr<corehost_init_t> &init)
{
    // --runtimeconfig (runtimeconfig.jsonのパスを引数で指定「できる」(されない))
    // --depsfile (deps.jsonのパスを引数で指定「できる」(されない))
    // https://docs.microsoft.com/ja-jp/dotnet/core/tools/dotnet#runtime-options
    pal::string_t runtime_config = command_line::get_option_value(opts, known_options::runtime_config, _X(""));
    pal::string_t deps_file = command_line::get_option_value(opts, known_options::deps_file, _X(""));

    // override_settingは、RuntimeConfigの設定より強い優先度の設定を記録します
    // app.m_runtime_config.m_default_settings < app.m_runtime_config < app.m_runtime_config.m_override_settings
    runtime_config_t::settings_t override_settings;
    
    // コマンドライン引数 --roll-forward
    pal::string_t roll_forward = command_line::get_option_value(opts, known_options::roll_forward, _X(""));
    if (roll_forward.length() > 0)
    {
        override_settings.set_roll_forward(val);
    }
    // コマンドライン引数 --roll-forward-on-no-candidate-fx
    pal::string_t roll_fwd_on_no_candidate_fx = command_line::get_option_value(opts, known_options::roll_forward_on_no_candidate_fx, _X(""));
    if (roll_fwd_on_no_candidate_fx.length() > 0)
    {
        auto val = static_cast<roll_fwd_on_no_candidate_fx_option>(pal::xtoi(roll_fwd_on_no_candidate_fx.c_str()));
        override_settings.set_roll_forward(roll_fwd_on_no_candidate_fx_to_roll_forward(val));
    }
    // コマンドライン引数 --runtimeconfig または、
    // Managedエントリのアセンブリファイルの横においてある runtimeconfig.json を読み込みます
    // (e.g.  "./ConsoleApp1.runtimeconfig.json")
    // read_config関数によって、RuntimeConfigのパース結果が app.m_runtime_config に設定されます(後述)
    fx_definition_vector_t fx_definitions; // リストだけど、基本的には1つだけ(app)です
    auto app = new fx_definition_t(); // 候補となるランタイムに対応するオブジェクト?
    fx_definitions.push_back(std::unique_ptr<fx_definition_t>(app));
    int rc = read_config(*app, app_candidate, runtime_config, override_settings);

    // `Roll forward` 決定についてはここまで

    runtime_config_t app_config = app->get_runtime_config();
    bool is_framework_dependent = app_config.get_is_framework_dependent(); // runtimeconfig.json に framework または frameworks が含まれているか

    pal::string_t additional_deps_serialized;
    if (is_framework_dependent)
    {
        // コマンドライン引数 --fx-version があれば、必ずそれを使うようにする
        pal::string_t fx_version_specified = command_line::get_option_value(opts, known_options::fx_version, _X(""));
        if (fx_version_specified.length() > 0)
        {
            app_config.set_fx_version(fx_version_specified); // RollForwardをDisableにして、バージョンを指定し、固定する
        }
        // コマンドライン引数 --additional-deps または、環境変数 DOTNET_ADDITIONAL_DEPS
        pal::string_t additional_deps = command_line::get_option_value(opts, known_options::additional_deps, _X(""));
        additional_deps_serialized = additional_deps;
        if (additional_deps_serialized.empty())
        {
            pal::getenv(_X("DOTNET_ADDITIONAL_DEPS"), &additional_deps_serialized);
        }

        // If invoking using FX dotnet.exe, use own directory.
        if (mode == host_mode_t::split_fx)
        {
            auto fx = new fx_definition_t(app_config.get_frameworks()[0].get_fx_name(), host_info.dotnet_root, pal::string_t(), pal::string_t());
            fx_definitions.push_back(std::unique_ptr<fx_definition_t>(fx));
        }
        else
        {
            // ここまでで掻き集めたRuntimeConfigや引数による設定を加味して、実際に使うランタイムを解決する(後述)
            rc = fx_resolver_t::resolve_frameworks_for_app(host_info, override_settings, app_config, fx_definitions);
        }
    }

    std::vector<std::pair<pal::string_t, pal::string_t>> additional_properties;
    if (is_sdk_command) { ... } // 今回は通りません

    // アプリ側の runtimeconfig.json の runtimeOptions/additionalProbingPaths を解決します
    // (通常設定されていないので意味はなさそう?)
    const known_options opts_probe_path = known_options::additional_probing_path;
    std::vector<pal::string_t> spec_probe_paths = opts.count(opts_probe_path) ? opts.find(opts_probe_path)->second : std::vector<pal::string_t>();
    std::vector<pal::string_t> probe_realpaths = get_probe_realpaths(fx_definitions, spec_probe_paths);

    // 引数を沢山渡していますが、実際に中でされるのは、hostpolicy_dir に host_info.dotnet_root をコピーするだけです
    // hostpolicy_dir = host_info.dotnet_root
    if (!hostpolicy_resolver::try_get_dir(mode, host_info.dotnet_root, fx_definitions, app_candidate, deps_file, probe_realpaths, &hostpolicy_dir))
    {
        return StatusCode::CoreHostLibMissingFailure;
    }

    // 確定!
    init.reset(new corehost_init_t(host_command, host_info, deps_file, additional_deps_serialized, probe_realpaths, mode, fx_definitions, additional_properties));
    return StatusCode::Success;
}

int read_config(
    fx_definition_t& app, const pal::string_t& app_candidate,
    pal::string_t& runtime_config, const runtime_config_t::settings_t& override_settings)
{
    // runtime_configまたは、app_candidateの拡張子をjson,dev.jsonとして、config_file, dev_config_fileにセット
    pal::string_t config_file, dev_config_file;
    get_runtime_config_paths_from_app(runtime_config.empty() ? app_candidate : runtime_config, config_file, dev_config_file);

    app.parse_runtime_config(config_file, dev_config_file, override_settings);
    return StatusCode::Success;
}

RuntimeConfigのパース

runtime_config_t::parse_optsがパース処理の中心を担っています。
この関数では、以下のメンバをjsonから読み出し、runtime_config_tのメンバにマッピングします。

  • this.m_properties = runtimeOptions/configProperties
  • this.m_probe_paths = runtimeOptions/additionalProbingPaths
  • this.m_tfm = runtimeOptions/tfm
  • this.m_frameworks = runtimeOptions/framework, runtimeOptions/frameworks
  • this.m_included_frameworks = runtimeOptions/includedFrameworks
  • this.m_default_settings.has_roll_forward = runtimeOptions/rollForward, runtimeOptions/rollForwardOnNoCandidateFx
  • this.m_default_settings.roll_forward = runtimeOptions/rollForward, runtimeOptions/rollForwardOnNoCandidateFx
  • this.m_default_settings.has_apply_patches = runtimeOptions/applyPatches
  • this.m_default_settings.apply_patches = runtimeOptions/applyPatches

この一連のruntime_config_tのパース処理で注意すべきは、
内部で環境変数DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FXが存在すればそれでRollForwardを設定している点です。
ただし、m_default_settingsへの適用であり、優先度としては最低位です。
m_default_settings < this < m_override_settings

runtime/src/native/corehost/fx_definition.cpp
void fx_definition_t::parse_runtime_config(
    const pal::string_t& path, const pal::string_t& dev_path,
    const runtime_config_t::settings_t& override_settings)
{
    m_runtime_config.parse(path, dev_path, override_settings);
}

runtime/src/native/corehost/runtime_config.cpp
void runtime_config_t::parse(const pal::string_t& path, const pal::string_t& dev_path, const settings_t& override_settings)
{
    m_path = path;
    m_dev_path = dev_path;
    m_override_settings = override_settings;

    // Step #0: 初期値
    m_default_settings.set_apply_patches(true);
    roll_forward_option roll_forward = roll_forward_option::Minor;

    // Step #1: 環境変数"DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX" が設定されているなら、それでRollForwardを上書きします
    pal::string_t env_roll_forward_on_no_candidate_fx;
    if (pal::getenv(_X("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"), &env_roll_forward_on_no_candidate_fx))
    {
        auto val = static_cast<roll_fwd_on_no_candidate_fx_option>(pal::xtoi(env_roll_forward_on_no_candidate_fx.c_str()));
        roll_forward = roll_fwd_on_no_candidate_fx_to_roll_forward(val);
    }

    m_default_settings.set_roll_forward(roll_forward);

    // Parse the file
    m_valid = ensure_parsed();
}

bool runtime_config_t::ensure_parsed()
{
    json_parser_t json;
    json.parse_file(m_path);
    
    const auto& runtimeOpts = json.document().FindMember(_X("runtimeOptions"));
    return parse_opts(runtimeOpts->value);
}

ランタイムの解決の引数

ここまで、ランタイムの解決に必要な設定を掻き集めてきました。
ついにそれを使って、実際に使用するランタイムを決定します。

その前に、これらの引数について一旦整理をします。(これは、コード内のコメントの和訳±αです)

host_info

host_pathdotnet_rootapp_pathをセットにした構造体です。
それぞれ、Exeのパス、ランタイムの検索対象のディレクトリ、Managedエントリのアセンブリのパスを示しています。
dotnet_rootの下には、バージョンごとのディレクトリが切られていて、その中にそれぞれバージョンの異なるランタイムがインストールされていることが期待されます。

runtime/src/native/corehost/host_startup_info.h
struct host_startup_info_t
{
    pal::string_t host_path;    // The path to the current hosting binary.
    pal::string_t dotnet_root;  // The path to the framework.
    pal::string_t app_path;     // For apphost, the path to the app dll; for muxer, not applicable as this information is not yet parsed.
};

override_settings

最優先で考慮される、ランタイムの解決に用いる設定です。(コマンドライン引数による設定)
fx_reference_tで渡された構造体のバージョンを指定する部分は無視されます。

runtime/src/native/corehost/runtime_config.h
struct runtime_config_t::settings_t
{
    bool has_apply_patches;
    bool apply_patches;

    bool has_roll_forward;
    roll_forward_option roll_forward;
};

config

この中にもm_override_settingsが含まれていますが、生で渡されたoverride_settingsと同じインスタンスであり、直接使われるのは後者の方となります。
".runtimeconfig.json"による設定をパースした構造体です。

dotnet/runtime/src/native/corehost/runtime_config.h
class runtime_config_t
{
    std::unordered_map<pal::string_t, pal::string_t> m_properties;
    fx_reference_vector_t m_frameworks;
    fx_reference_vector_t m_included_frameworks;
    settings_t m_default_settings;   // the default settings (Steps #0 and #1)
    settings_t m_override_settings;  // the settings that can't be changed (Step #5)
    std::vector<std::string> m_prop_keys;
    std::vector<std::string> m_prop_values;
    std::list<pal::string_t> m_probe_paths;

    pal::string_t m_tfm;

    // This is used to detect cases where rollForward is used together with the obsoleted
    // rollForwardOnNoCandidateFx/applyPatches.
    // Flags
    enum specified_setting
    {
        none = 0x0,
        specified_roll_forward = 0x1,
        specified_roll_forward_on_no_candidate_fx_or_apply_patched = 0x2
    } m_specified_settings;

    pal::string_t m_dev_path;
    pal::string_t m_path;
    bool m_is_framework_dependent;
    bool m_valid;
}

dotnet/runtime/src/native/corehost/fx_reference.h
class fx_reference_t
{
    bool apply_patches;

    version_compatibility_range_t version_compatibility_range;
    bool roll_to_highest_version;

    // This indicates that when resolving the framework reference the search should prefer release version
    // and only resolve to pre-release if there's no matching release version available.
    bool prefer_release;

    pal::string_t fx_name;
    pal::string_t fx_version;
    fx_ver_t fx_version_number;
};

effective_parent_fx_ref

親ランタイムの情報です。
これからランタイムを解決するわけですが、その解決をしようとしているランタイムの情報を渡せるとのことらしいです。
今回はnullptrが渡されているので考慮する必要はありません。

fx_definitions

見つかったランタイムが格納されていく、呼出し側が結果を受け取るためのリストです。
順序は、アプリ(末端)→ ルートフレームワーク(Microsoft.NETCore.App)の並びになります。

1つ目の要素には、RuntimeConfig を読み込んだ appfx_definition_t)がセットされています。
ただし、RuntimeConfigとDeps以外のメンバはセットされていないので、無効扱いです。(しかもそのRuntimeConfigは別引数で渡されている)

class fx_definition_t
{
    pal::string_t m_name;
    pal::string_t m_dir;
    pal::string_t m_requested_version;
    pal::string_t m_found_version;
    runtime_config_t m_runtime_config;
    pal::string_t m_deps_file;
    deps_json_t m_deps;
};
typedef std::vector<std::unique_ptr<fx_definition_t>> fx_definition_vector_t;

ランタイムの解決処理

実際に使用するランタイムを決定します。
引数を捏ね繰り回して、最終的に、fx_definitionsに解決されたランタイムが追加されます

runtime/src/native/corehost/fxr/fx_resolver.cpp
StatusCode fx_resolver_t::resolve_frameworks_for_app(
    const host_startup_info_t & host_info, const runtime_config_t::settings_t& override_settings,
    const runtime_config_t & app_config, fx_definition_vector_t & fx_definitions)
{
    fx_resolver_t resolver;
    // Read the shared frameworks; retry is necessary when a framework is already resolved, but then a newer compatible version is processed.
    fx_definitions.resize(1); // Erase any existing frameworks for re-try
    rc = resolver.read_framework(host_info, override_settings, app_config, /*effective_parent_fx_ref*/ nullptr,  fx_definitions);

    // 本来は、resolver.read_framework が StatusCode::FrameworkCompatRetryを返してくることがあり、その際のリトライ処理があります
    return rc;
}

StatusCode fx_resolver_t::read_framework(const host_startup_info_t & host_info,
    const runtime_config_t::settings_t& override_settings, const runtime_config_t & config,
    const fx_reference_t * effective_parent_fx_ref, fx_definition_vector_t & fx_definitions)
{
    // 辞書構造の fx_resolver_t::m_effective_fx_references, fx_resolver_t::m_oldest_fx_references に追加します
    // This reconciles duplicate references to minimize the number of resolve retries.
    update_newest_references(config);

    StatusCode rc = StatusCode::Success;

    // Loop through each reference and resolve the framework
    for (const fx_reference_t& original_fx_ref : config.get_frameworks())
    {
        fx_reference_t fx_ref = original_fx_ref;

        if (effective_parent_fx_ref != nullptr && effective_parent_fx_ref->get_roll_to_highest_version())
        {
            // 親ランタイムの roll_to_highest_version で上書きします
            fx_ref.set_roll_to_highest_version(true);
        }

        const pal::string_t& fx_name = fx_ref.get_fx_name();
        const fx_reference_t& current_effective_fx_ref = m_effective_fx_references[fx_name];
        fx_reference_t new_effective_fx_ref;

        // fx_definitionsの1つ目はセットされていますが、無効なのでこの検索は見つかりません
        auto existing_framework = std::find_if(
            fx_definitions.begin(),
            fx_definitions.end(),
            [&](const std::unique_ptr<fx_definition_t> & fx) { return fx_name == fx->get_name(); });
        if (existing_framework == fx_definitions.end())
        {
            // 以下のようなことをしています
            // m_effective_fx_references[fx_name] = fx_ref.fx_version_number > current_effective_fx_ref.fx_version_number ? fx_ref : current_effective_fx_ref
            // 今回は current_effective_fx_ref は fx_ref と同じなので、fx_ref、つまり RuntimeConfig の runtimeOptions/framework になります
            
            // Reconcile the framework reference with the most up to date so far we have for the framework.
            // This does not read any physical framework folders yet.
            // Since we didn't find the framework in the resolved list yet, it's OK to update the effective reference
            // as we haven't processed it yet.
            rc = reconcile_fx_references(fx_ref, current_effective_fx_ref, new_effective_fx_ref);
            m_effective_fx_references[fx_name] = new_effective_fx_ref;

            // この関数が、実際にランタイムのインストールディレクトリを舐めて、
            // インストールされているバージョン一覧を取得し、RollForwardを考慮してバージョンとそのランタイムのパスを決定します
            // Resolve the effective framework reference against the the existing physical framework folders
            fx_definition_t* fx = resolve_framework_reference(new_effective_fx_ref, m_oldest_fx_references[fx_name].get_fx_version(), host_info.dotnet_root);

            // Do NOT update the effective reference to have the same version as the resolved framework.
            // This could prevent correct resolution in some cases.
            // For example if the resolution starts with reference "2.1.0 LatestMajor" the resolution could
            // return "3.0.0". If later on we find another reference "2.1.0 Minor", while the two references are compatible
            // we would not be able to resolve it, since we would compare "2.1.0 Minor" with "3.0.0 LatestMajor" which are
            // not compatible.
            // So instead leave the effective reference as is. If the above situation occurs, the reference reconciliation
            // will change the effective reference from "2.1.0 LatestMajor" to "2.1.0 Minor" and restart the framework resolution process.
            // So during the second run we will resolve for example "2.2.0" which will be compatible with both framework references.

            fx_definitions.push_back(std::unique_ptr<fx_definition_t>(fx));

            // 見つかったランタイムの runtimeconfig.json を読み込みます
            // (これが存在し、この中に runtimeOptions/framework が設定されていると、再帰的に検索されるってコト?)
            // Recursively process the base frameworks
            pal::string_t config_file;
            pal::string_t dev_config_file;
            get_runtime_config_paths(fx->get_dir(), fx_name, &config_file, &dev_config_file);
            fx->parse_runtime_config(config_file, dev_config_file, override_settings);

            // 再帰呼び出し; 今回は、new_config.frameworks が空なので何もせずに制御が帰ってきます
            runtime_config_t new_config = fx->get_runtime_config();
            rc = read_framework(host_info, override_settings, new_config, &new_effective_fx_ref, fx_definitions);
        }
        else
        {
            // fx_definitions に解決済みのランタイム情報が設定されている場合; 今回は通りません
        }
    }
    return rc;
}

fx_definition_t* resolve_framework_reference(const fx_reference_t & fx_ref,
    const pal::string_t & oldest_requested_version, const pal::string_t & dotnet_dir)
{
    // dotnet_dir(corehost で決定した dotnet_root)と、
    // システムにインストールされている.NETランタイムの場所を取得してリストにします
    std::vector<pal::string_t> hive_dir;
    get_framework_and_sdk_locations(dotnet_dir, &hive_dir);

    pal::string_t selected_fx_dir;
    pal::string_t selected_fx_version;
    fx_ver_t selected_ver;

    for (pal::string_t dir : hive_dir)
    {
        auto fx_dir = /* "<dir>/shared/Microsoft.NETCore.App" */;

        // Roll forward is disabled when:
        //   roll_forward is set to Disable
        //   roll_forward is set to LatestPatch AND
        //     apply_patches is false AND
        //     release framework reference (this is for backward compat with pre-release rolling over pre-release portion of version ignoring apply_patches)
        //   use exact version is set (this is when --fx-version was used on the command line)
        if ((fx_ref.get_version_compatibility_range() == version_compatibility_range_t::exact) ||
            ((fx_ref.get_version_compatibility_range() == version_compatibility_range_t::patch) && (!fx_ref.get_apply_patches() && !fx_ref.get_fx_version_number().is_prerelease())))
        {
            // RollForwardが無効の場合、"<dir>/shared/Microsoft.NETCore.App/<version>" ディレクトリをランタイムの場所として解決します
            // ディレクトリが存在しない場合は当然エラーになるけど
            append_path(&fx_dir, fx_ref.get_fx_version().c_str());
            if (pal::directory_exists(fx_dir))
            {
                selected_fx_dir = fx_dir;
                selected_fx_version = fx_ref.get_fx_version();
                break;
            }
        }
        else
        {
            // fx_dir ディレクトリ下のディレクトリを舐めて、インストールされているランタイムのバージョンのリストを構築します
            std::vector<fx_ver_t> version_list = ...;

            // version_list から RollForwardの条件を満たすバージョンを選択します
            fx_ver_t resolved_ver = resolve_framework_reference_from_version_list(version_list, fx_ref);

            pal::string_t resolved_ver_str = resolved_ver.as_str();
            append_path(&fx_dir, resolved_ver_str.c_str());
            selected_ver = resolved_ver;
            selected_fx_dir = fx_dir;
            selected_fx_version = resolved_ver_str;

            // 本来は hive_dir が複数あることを考慮して、ひとつ前のLoopの際の selected_ver との比較処理があります
        }
    }
    return new fx_definition_t(fx_ref.get_fx_name(), selected_fx_dir, oldest_requested_version, selected_fx_version);
}

.NETランタイム本体の起動をする hostpolicy.dll の起動

ここで、コンテキストは hostfxr.dllからhostpolicy.dllへ移る呼出しを行います。
呼出しは2段階に分かれていて、それぞれはグローバル変数で協調されるということです。
そのため、hostfxr.dll側でアトミックを保証する処理が含まれ煩雑になっています。

  1. corehost_init_t の転送(hostpolicy.dll!corehost_load
  2. 実行(hostpolicy.dll!corehost_main
runtime/src/native/corehost/fxr/fx_muxer.cpp
// Tracks the active host context. This is the context that was used to load and initialize hostpolicy and coreclr.
// It will only be set once both hostpolicy and coreclr are loaded and initialized. Once set, it should not be changed.
// This will remain set even if the context is closed through hostfxr_close. Since the context represents the active
// CoreCLR runtime and the active runtime cannot be unloaded, the active context is never unset.
std::unique_ptr<host_context_t> g_active_host_context;

// impl_dll_dir は dotnet_root が来ます(corehost が解決したランタイムのインストール場所; バージョンディレクトリの上、sharedの上)
static int execute_app(const pal::string_t& impl_dll_dir, corehost_init_t* init, const int argc, const pal::char_t* argv[])
{
    {
        std::unique_lock<std::mutex> lock{ g_context_lock };
        g_context_initializing_cv.wait(lock, [] { return !g_context_initializing.load(); });

        if (g_active_host_context != nullptr)
        {
            trace::error(_X("Hosting components are already initialized. Re-initialization to execute an app is not allowed."));
            return StatusCode::HostInvalidState;
        }

        g_context_initializing.store(true);
    }

    pal::dll_t hostpolicy_dll;
    hostpolicy_contract_t hostpolicy_contract{};
    corehost_main_fn host_main = nullptr;

    int code = load_hostpolicy(impl_dll_dir, &hostpolicy_dll, hostpolicy_contract);

    // Obtain entrypoint symbol
    host_main = hostpolicy_contract.corehost_main;

    // Leak hostpolicy - just as we do not unload coreclr, we do not unload hostpolicy

    {
        // Track an empty 'active' context so that host context-based APIs can work properly when
        // the runtime is loaded through non-host context-based APIs. Once set, the context is never
        // unset. This means that if any error occurs after this point (e.g. with loading the runtime),
        // the process will be in a corrupted state and loading the runtime again will not be allowed.
        std::lock_guard<std::mutex> lock{ g_context_lock };
        assert(g_active_host_context == nullptr);
        g_active_host_context.reset(new host_context_t(host_context_type::empty, hostpolicy_contract, {}));
        g_active_host_context->initialize_frameworks(*init); // init から g_active_host_context へのコピー
        g_context_initializing.store(false);
    }

    g_context_initializing_cv.notify_all();

    {
        // hostpolicy.dll!corehost_load で、ランタイムの起動用の情報群(corehost_init_t のインターフェイス)を転送
        // https://github.com/dotnet/runtime/blob/v6.0.0/src/native/corehost/hostpolicy/hostpolicy.cpp#L304
        const host_interface_t& intf = init->get_host_init_data();
        hostpolicy_contract.load(&intf);
        // hostpolicy.dll!corehost_main を実行!
        // https://github.com/dotnet/runtime/blob/v6.0.0/src/native/corehost/hostpolicy/hostpolicy.cpp#L414
        code = host_main(argc, argv);
        (void)hostpolicy_contract.unload();
    }

    return code;
}

int load_hostpolicy(const pal::string_t& lib_dir, pal::dll_t* h_host, hostpolicy_contract_t& hostpolicy_contract)
{
    int rc = hostpolicy_resolver::load(lib_dir, h_host, hostpolicy_contract); // 後述
    return StatusCode::Success;
}

hostpolicy.dll のロード

hostpolicy.dll のエクスポート関数を hostpolicy_contract_t にマッピングします

runtime/src/native/corehost/fxr/standalone/hostpolicy_resolver.cpp
// impl_dll_dir は dotnet_root が来ます(corehost が解決したランタイムのインストール場所; バージョンディレクトリの上、sharedの上)
int hostpolicy_resolver::load(const pal::string_t& lib_dir, pal::dll_t* dll, hostpolicy_contract_t &hostpolicy_contract)
{
    std::lock_guard<std::mutex> lock{ g_hostpolicy_lock };
    if (g_hostpolicy == nullptr)
    {
        pal::string_t host_path = /* "<lib_dir>"/hostpolicy.dll */ ;
        pal::load_library(&host_path, &g_hostpolicy);
        
        // Obtain entrypoint symbols
        g_hostpolicy_contract.corehost_main = reinterpret_cast<corehost_main_fn>(pal::get_symbol(g_hostpolicy, "corehost_main"));
        g_hostpolicy_contract.load = reinterpret_cast<corehost_load_fn>(pal::get_symbol(g_hostpolicy, "corehost_load"));
        g_hostpolicy_contract.unload = reinterpret_cast<corehost_unload_fn>(pal::get_symbol(g_hostpolicy, "corehost_unload"));
        g_hostpolicy_contract.corehost_main_with_output_buffer = reinterpret_cast<corehost_main_with_output_buffer_fn>(pal::get_symbol(g_hostpolicy, "corehost_main_with_output_buffer"));

        // It's possible to not have corehost_main_with_output_buffer.
        // This was introduced in 2.1, so 2.0 hostpolicy would not have the exports.
        // Callers are responsible for checking that the function pointer is not null before using it.
        g_hostpolicy_contract.set_error_writer = reinterpret_cast<corehost_set_error_writer_fn>(pal::get_symbol(g_hostpolicy, "corehost_set_error_writer"));
        g_hostpolicy_contract.initialize = reinterpret_cast<corehost_initialize_fn>(pal::get_symbol(g_hostpolicy, "corehost_initialize"));

        // It's possible to not have corehost_set_error_writer and corehost_initialize. These were
        // introduced in 3.0, so 2.0 hostpolicy would not have the exports. In this case, we will
        // not propagate the error writer and errors will still be reported to stderr. Callers are
        // responsible for checking that the function pointers are not null before using them.
        g_hostpolicy_dir = lib_dir;
    }
    else
    {
        // ロード済みの場合(今回はとおりません)
    }

    // Return global values
    *dll = g_hostpolicy;
    hostpolicy_contract = g_hostpolicy_contract;
    return StatusCode::Success;
}

hostpolicy.dll

hostfxr.dllから、2段階でパラメータの転送、(hostpolicy.dll!corehost_load)実行(hostpolicy.dll!corehost_main)が呼び出されるところから始まります。
hostpolicy.dllは、<DOTNET_ROOT>\shared\Microsoft.NETCore.App\6.0.0\下にあるモジュールです。つまり、hostfxrで解決されたランタイムバージョンごとにhostpolicy.dllが存在します。(逆にhostfxr.dllはランタイムのバージョンとは関係なく存在しています)
hostpolicyで行う処理は主に、hostfxrから受け取ったパラメータをcoreclrが求める形に成形し、coreclrを初期化(coreclr_initialize)してManagedエントリを実行する関数(execute_assembly)を実行させます。

パラメータ(host_interface_t)の受け取り

まずはパラメータについてですが、これはhostfxrで解釈した引数やRuntimeConfig、Deps、決定したランタイムのバージョン、パスなどに、バリデーションデータを追加した構造になっています。

runtime/src/native/corehost/host_interface.h
struct host_interface_t
{
    size_t version_lo;                // Just assign sizeof() to this field.
    size_t version_hi;                // Breaking changes to the layout -- increment HOST_INTERFACE_LAYOUT_VERSION
    strarr_t config_keys;
    strarr_t config_values;
    const pal::char_t* fx_dir;
    const pal::char_t* fx_name;
    const pal::char_t* deps_file;
    size_t is_framework_dependent;
    strarr_t probe_paths;
    size_t patch_roll_forward;
    size_t prerelease_roll_forward;
    size_t host_mode;
    const pal::char_t* tfm;
    const pal::char_t* additional_deps_serialized;
    const pal::char_t* fx_ver;
    strarr_t fx_names;
    strarr_t fx_dirs;
    strarr_t fx_requested_versions;
    strarr_t fx_found_versions;
    const pal::char_t* host_command;
    const pal::char_t* host_info_host_path;
    const pal::char_t* host_info_dotnet_root;
    const pal::char_t* host_info_app_path;
    size_t single_file_bundle_header_offset;
    // !! WARNING / WARNING / WARNING / WARNING / WARNING / WARNING / WARNING / WARNING / WARNING
    // !! 1. Only append to this structure to maintain compat.
    // !! 2. Any nested structs should not use compiler specific padding (pack with _HOST_INTERFACE_PACK)
    // !! 3. Do not take address of the fields of this struct or be prepared to deal with unaligned accesses.
    // !! 4. Must be POD types; only use non-const size_t and pointer types; no access modifiers.
    // !! 5. Do not reorder fields or change any existing field types.
    // !! 6. Add static asserts for fields you add.
};

実際に呼び出される関数はこのような定義担っています。host_interface_thostpolicy_init_tにコピー(+解釈)して、Global変数にセットします。
Global変数は、hostfxrからの2段目の呼出し(hostpolicy.dll!corehost_main)で使われます。
(何故わざわざGlobal変数を使ってまで、2段階に分けてるのかは謎)

runtime/src/native/corehost/hostpolicy/hostpolicy.cpp
SHARED_API int HOSTPOLICY_CALLTYPE corehost_load(host_interface_t* init)
{
    std::lock_guard<std::mutex> lock{ g_init_lock };

    g_init = hostpolicy_init_t{};
    hostpolicy_init_t::init(init, &g_init);
    g_init_done = true;
    return StatusCode::Success;
}

host_interface_tからhostpolicy_init_tへのコピー(+解釈)は比較的素直な処理担っています。
おおよそ、host_interface_t* inputのメンバをhostpolicy_init_t* initにディープコピーしている感じです。

make_palstr_arrは、Interface表現のリストをstd::vectorにデシリアライズ(?)しているだけなので、分かりやすいように代入式に書き換えています。
また、旧バージョンのhostfxrとの分岐処理も省いています。

runtime/src/native/corehost/hostpolicy/hostpolicy_init.cpp
bool hostpolicy_init_t::init(host_interface_t* input, hostpolicy_init_t* init)
{
    // host_interface_t::version_hi, host_interface_t::version_lo のチェックを省略

    init->cfg_keys =  input->config_keys;
    init->cfg_values =  input->config_values;
    init->deps_file = input->deps_file;
    init->is_framework_dependent = input->is_framework_dependent;
    init->probe_paths =  input->probe_paths;
    init->patch_roll_forward = input->patch_roll_forward;
    init->prerelease_roll_forward = input->prerelease_roll_forward;
    init->host_mode = (host_mode_t)input->host_mode;
    init->tfm = input->tfm;
    init->additional_deps_serialized = input->additional_deps_serialized;
    for (size_t i = 0; i < input->fx_names.len; ++i)
    {
        auto fx = new fx_definition_t(input->fx_names[i], input->fx_dirs[i], input->fx_requested_versions[i], input->fx_found_versions[i]);
        init->fx_definitions.push_back(std::unique_ptr<fx_definition_t>(fx));
    }
    init->host_command = input->host_command;
    init->host_info.host_path = input->host_info_host_path;
    init->host_info.dotnet_root = input->host_info_dotnet_root;
    init->host_info.app_path = input->host_info_app_path;
    return true;
}

coreclrの立ち上げの実行(hostpolicy.dll!corehost_main

runtime/src/native/corehost/hostpolicy/hostpolicy.cpp
SHARED_API int HOSTPOLICY_CALLTYPE corehost_main(const int argc, const pal::char_t* argv[])
{
    // これは、SingleFileMode用の処理やTracerの初期化など; 今回は重要な処理を含みません
    int rc = corehost_main_init(g_init, argc, argv, _X("corehost_main"));

    arguments_t args;
    rc = create_hostpolicy_context(g_init, argc, argv, true /* breadcrumbs_enabled */, &args);
    rc = create_coreclr();
    return run_app(args.app_argc, args.app_argv);
}

int create_hostpolicy_context(hostpolicy_init_t &hostpolicy_init, const int argc, const pal::char_t *argv[],
    bool breadcrumbs_enabled, /*out*/ arguments_t *out_args = nullptr)
{
    // g_context_initializingのMutexロック省略

    // hostpolicy_init, argc, argvを、argsの各メンバにコピー
    arguments_t args;
    parse_arguments(hostpolicy_init, argc, argv, args);
    if (out_args != nullptr) *out_args = args;

    std::unique_ptr<hostpolicy_context_t> context_local(new hostpolicy_context_t());
    int rc = context_local->initialize(hostpolicy_init, args, breadcrumbs_enabled);
    g_context.reset(context_local.release());

    // g_context_initializingのMutexリリース省略
    return StatusCode::Success;
}

struct arguments_t
{
    host_mode_t host_mode;
    pal::string_t host_path;
    pal::string_t app_root;
    pal::string_t deps_path;
    pal::string_t core_servicing;
    std::vector<pal::string_t> probe_paths;
    pal::string_t managed_application;
    std::vector<pal::string_t> global_shared_stores;
    pal::string_t dotnet_shared_store;
    std::vector<pal::string_t> env_shared_store;
    pal::string_t additional_deps_serialized;
}

hostpolicy_context_t初期化

hostpolicy_context_t::initializeは主に2つの処理を行います。
まずはdeps_resolver_t::resolve_probe_pathsprobe_paths_tを解決します。
その後、これをcoreclr_propertiesに積み替えを行います。
breadcrumbsというのがありますが、これはTracer用途っぽいので、深く考えなくてもよさそうです。

runtime/src/native/corehost/hostpolicy/hostpolicy_context.cpp
struct hostpolicy_context_t
{
    pal::string_t application;
    pal::string_t clr_dir;
    pal::string_t clr_path;
    host_mode_t host_mode;
    pal::string_t host_path;

    bool breadcrumbs_enabled;
    mutable std::unordered_set<pal::string_t> breadcrumbs;

    coreclr_property_bag_t coreclr_properties;
    std::unique_ptr<coreclr_t> coreclr;
};

int hostpolicy_context_t::initialize(hostpolicy_init_t &hostpolicy_init, const arguments_t &args, bool enable_breadcrumbs)
{
    application = args.managed_application; // Managedエントリのアセンブリ(e.g. L"C:\ConsoleApp1\ConsoleApp1.dll")
    host_mode = hostpolicy_init.host_mode;
    host_path = args.host_path; // 起動したExe(e.g. L"C:\ConsoleApp1\ConsoleApp1.exe")
    breadcrumbs_enabled = enable_breadcrumbs; // true

    deps_resolver_t resolver
    {
        args,
        hostpolicy_init.fx_definitions,
        /* root_framework_rid_fallback_graph */ nullptr, // This means that the fx_definitions contains the root framework
        hostpolicy_init.is_framework_dependent
    };
    probe_paths_t probe_paths;

    // Setup breadcrumbs.
    pal::string_t policy_name = _STRINGIFY(HOST_POLICY_PKG_NAME); // L"runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy"
    pal::string_t policy_version = _STRINGIFY(HOST_POLICY_PKG_VER); // L"6.0.0"
    // Always insert the hostpolicy that the code is running on.
    breadcrumbs.insert(policy_name);
    breadcrumbs.insert(policy_name + _X(",") + policy_version);
    resolver.resolve_probe_paths(&probe_paths, &breadcrumbs /* enable_breadcrumbs==falseの時はnullptr */);

    // self-contained の場合、CoreLib.dll はBundleに入っていることが期待されるので、この辺はされなくなります
    clr_path = probe_paths.coreclr;
    clr_dir = get_directory(clr_path); // Get path in which CoreCLR is present.
    probe_paths.tpa.append(clr_dir + "System.Private.CoreLib.dll");

    const fx_definition_vector_t &fx_definitions = resolver.get_fx_definitions();

    pal::string_t fx_deps_str = get_root_framework(fx_definitions).get_deps_file(); // Microsoft.NETCore.App.deps.json"

    // resolver.m_fx_definitions のそれぞれの deps.json を取得します
    // e.g. "C:\...\ConsoleApp1.deps.json;C:\...\Microsoft.NETCore.App.deps.json"
    pal::string_t app_context_deps_str = ...;

    // Build properties for CoreCLR instantiation
    pal::string_t app_base;
    resolver.get_app_dir(&app_base);

    coreclr_properties.add(common_property::TrustedPlatformAssemblies, probe_paths.tpa.c_str());
    coreclr_properties.add(common_property::NativeDllSearchDirectories, probe_paths.native.c_str());
    coreclr_properties.add(common_property::PlatformResourceRoots, probe_paths.resources.c_str());
    coreclr_properties.add(common_property::AppContextBaseDirectory, app_base.c_str());
    coreclr_properties.add(common_property::AppContextDepsFiles, app_context_deps_str.c_str());
    coreclr_properties.add(common_property::FxDepsFile, fx_deps_str.c_str());
    coreclr_properties.add(common_property::ProbingDirectories, resolver.get_lookup_probe_directories().c_str());
    coreclr_properties.add(common_property::RuntimeIdentifier, get_current_runtime_id(true /*use_fallback*/).c_str());

    // 今回は、以下のPropertyは処理されません
    // // hostpolicy_init.cfg_keys に Microsoft.NETCore.DotNetHostPolicy.SetAppPaths を反映します
    // if (...)
    // {
    //     coreclr_properties.add(common_property::AppPaths, app_base.c_str());
    //     coreclr_properties.add(common_property::AppNIPaths, app_base.c_str());
    // }
    // pal::string_t startup_hooks;
    // if (pal::getenv(_X("DOTNET_STARTUP_HOOKS"), &startup_hooks))
    // {
    //     coreclr_properties.add(common_property::StartUpHooks, startup_hooks.c_str());
    // }
    // if (bundle::info_t::is_single_file_bundle())
    // {
    //     // Single-File の場合、common_property::BundleProbeに&bundle_probe(ポインタアドレス)をセット
    //     coreclr_properties.add(common_property::BundleProbe, $"0x{&bundle_probe}");
    // }
    return StatusCode::Success;
}

deps_resolver_t::resolve_probe_paths

deps.jsonに記述されたManagedアセンブリのことを、TPA(Trusted Platform Assemblies)と呼ぶそうです。

runtime/src/native/corehost/hostpolicy/deps_resolver.cpp
bool deps_resolver_t::resolve_probe_paths(probe_paths_t* probe_paths, std::unordered_set<pal::string_t>* breadcrumb, bool ignore_missing_assemblies)
{
    // deps.json による依存しているManagedアセンブリのDllを解決します
    resolve_tpa_list(&probe_paths->tpa, breadcrumb, ignore_missing_assemblies);
    // deps.json による依存しているNativeモジュールのDllを解決します(nativeに入るのはそのディレクトリ)
    resolve_probe_dirs(deps_entry_t::asset_types::native, &probe_paths->native, breadcrumb);
    // deps.json による依存しているリソースを解決します(resourcesに入るのはそのディレクトリのディレクトリ)
    resolve_probe_dirs(deps_entry_t::asset_types::resources, &probe_paths->resources, breadcrumb);
    
    // If we found coreclr and the jit during native path probe, set the paths now.
    probe_paths->coreclr = m_coreclr_path;
    return true;
}

create_coreclr

coreclr.dll!coreclr_initializeを呼び出します。
引数にはありませんが、Global変数を経由して先ほど初期化したhostpolicy_context_tを利用します。

runtime/src/native/corehost/hostpolicy/hostpolicy.cpp
int HOSTPOLICY_CALLTYPE create_coreclr()
{
    coreclr_bind(libcoreclr_path);

    std::vector<char> host_path;
    pal::pal_clrstring(g_context->host_path, &host_path);
    const char *app_domain_friendly_name = g_context->host_mode == host_mode_t::libhost ? "clr_libhost" : "clrhost";

    // Create a CoreCLR instance
    auto hr = coreclr_t::create(g_context->clr_dir, host_path.data(), app_domain_friendly_name, 
        g_context->coreclr_properties, g_context->coreclr);
}

bool coreclr_bind(const pal::string_t& libcoreclr_path)
{
    coreclr_resolver_t::resolve_coreclr(libcoreclr_path, coreclr_contract);
    return true;
}

bool coreclr_resolver_t::resolve_coreclr(const pal::string_t& libcoreclr_path, coreclr_resolver_contract_t& coreclr_resolver_contract)
{
    coreclr_resolver_contract.coreclr = nullptr;
    coreclr_resolver_contract.coreclr_initialize = reinterpret_cast<coreclr_initialize_fn>(coreclr_initialize);
    coreclr_resolver_contract.coreclr_shutdown = reinterpret_cast<coreclr_shutdown_fn>(coreclr_shutdown_2);
    coreclr_resolver_contract.coreclr_execute_assembly = reinterpret_cast<coreclr_execute_assembly_fn>(coreclr_execute_assembly);
    coreclr_resolver_contract.coreclr_create_delegate = reinterpret_cast<coreclr_create_delegate_fn>(coreclr_create_delegate);
    return true;
}

pal::hresult_t coreclr_t::create(
    const pal::string_t& libcoreclr_path,
    const char* exe_path,
    const char* app_domain_friendly_name,
    const coreclr_property_bag_t &properties,
    std::unique_ptr<coreclr_t> &inst)
{
    host_handle_t host_handle;
    domain_id_t domain_id;

    int propertyCount = properties.count();
    std::vector<std::vector<char>> keys_strs(propertyCount);
    std::vector<const char*> keys(propertyCount);
    std::vector<std::vector<char>> values_strs(propertyCount);
    std::vector<const char*> values(propertyCount);
    int index = 0;
    std::function<void (const pal::string_t &,const pal::string_t &)> callback = [&] (const pal::string_t& key, const pal::string_t& value)
    {
        pal::pal_clrstring(key, &keys_strs[index]);
        keys[index] = keys_strs[index].data();
        pal::pal_clrstring(value, &values_strs[index]);
        values[index] = values_strs[index].data();
        ++index;
    };
    properties.enumerate(callback);

    pal::hresult_t hr;
    hr = coreclr_contract.coreclr_initialize(
        exe_path, app_domain_friendly_name, propertyCount,
        keys.data(), values.data(),  &host_handle, &domain_id);
    inst.reset(new coreclr_t(host_handle, domain_id));
    return StatusCode::Success;
}

run_app

coreclr.dll!execute_assemblyを呼び出します。
引数にはありませんが、Global変数を経由して先ほど初期化したhostpolicy_context_tを利用します。

runtime/src/native/corehost/hostpolicy/hostpolicy.cpp
int HOSTPOLICY_CALLTYPE run_app(const int argc, const pal::char_t *argv[])
{
    const std::shared_ptr<hostpolicy_context_t> context = g_context; // 本来はMutex操作を伴う
    return run_app_for_context(*context, argc, argv);
}

int run_app_for_context(const hostpolicy_context_t &context, int argc, const pal::char_t **argv)
{
    // Initialize clr strings for arguments
    std::vector<std::vector<char>> argv_strs = argv; // 本来は->vec変換
    std::vector<const char*> argv_local = argv; // 本来は->vec変換
    std::vector<char> managed_app = context.application; // 本来はstr->vec<char>変換

    // Execute the application
    unsigned int exit_code;
    auto hr = context.coreclr->execute_assembly(
        (int32_t)argv_local.size(), argv_local.data(),
        managed_app.data(), &exit_code);

    // Shut down the CoreCLR
    hr = context.coreclr->shutdown(reinterpret_cast<int*>(&exit_code));
    return exit_code;
}

pal::hresult_t coreclr_t::execute_assembly(int argc, const char** argv,
    const char* managed_assembly_path, unsigned int* exit_code)
{
    // Managedプログラムが稼働している間、execute_assembly がブロックし続けています
    // つまり、間接的にProgram.Main をよびだします
    return coreclr_contract.coreclr_execute_assembly(_host_handle, _domain_id, argc, argv, managed_assembly_path, exit_code);
}

coreclr.dll

スタックトレースにはみえていませんでしたが、execute_assemblyMainの間にcoreclrの関数が存在しています。
(VisualStudioがManagedの一部として省略しちゃうのかな?)

言語 モジュール 関数
C# ConsoleApp1.dll ConsoleApp1.Program.Main
C++ coreclr.dll この間にも更にCLIのVMの処理がある
C++ coreclr.dll RunMainInternal
C++ coreclr.dll RunMain
C++ coreclr.dll Assembly::ExecuteMainMethod
C++ coreclr.dll CorHost2::ExecuteAssembly
C++ coreclr.dll coreclr_execute_assembly
C++ hostpolicy.dll coreclr_t::execute_assembly

coreclrは、ランタイムの中核を成す、本体ともいうべきモジュールです。CLIの型メタデータを管理したり、JIT、VM、GCなどの管理も行います。

coreclr_initialize

IID_ICLRRuntimeHost4を取得し、各初期化メソッドを呼び出していきます。
既にかなり長くなってしまっているので、この中については詳細は割愛しますが、COM風になっているだけで別にCOMを介しているわけではありません。(名残と互換性?)
内部では、ppUnk = (ICLRRuntimeHost4)new CorHost2();としているだけです。

runtime/src/coreclr/dlls/mscoree/unixinterface.cpp
extern "C" DLLEXPORT int coreclr_initialize(
    const char* exePath, const char* appDomainFriendlyName, int propertyCount,
    const char** propertyKeys, const char** propertyValues, void** hostHandle, unsigned int* domainId)
{
    HRESULT hr;

    LPCWSTR* propertyKeysW;
    LPCWSTR* propertyValuesW;
    BundleProbeFn* bundleProbe = nullptr;
    bool hostPolicyEmbedded = false;
    PInvokeOverrideFn* pinvokeOverride = nullptr;

    ConvertConfigPropertiesToUnicode(propertyKeys, propertyValues,  propertyCount, &propertyKeysW, &propertyValuesW, &bundleProbe, &pinvokeOverride, &hostPolicyEmbedded);

    g_hostpolicy_embedded = hostPolicyEmbedded;

    if (pinvokeOverride != nullptr)
    {
        PInvokeOverride::SetPInvokeOverride(pinvokeOverride, PInvokeOverride::Source::RuntimeConfiguration);
    }

    ReleaseHolder<ICLRRuntimeHost4> host;

    hr = CorHost2::CreateObject(IID_ICLRRuntimeHost4, (void**)&host); // 中で、QueryInterface
    IfFailRet(hr);

    ConstWStringHolder appDomainFriendlyNameW = StringToUnicode(appDomainFriendlyName);

    if (bundleProbe != nullptr)
    {
        static Bundle bundle(exePath, bundleProbe);
        Bundle::AppBundle = &bundle;
    }

    // This will take ownership of propertyKeysWTemp and propertyValuesWTemp
    Configuration::InitializeConfigurationKnobs(propertyCount, propertyKeysW, propertyValuesW);

    STARTUP_FLAGS startupFlags;
    InitializeStartupFlags(&startupFlags);

    hr = host->SetStartupFlags(startupFlags);
    IfFailRet(hr);

    hr = host->Start();
    IfFailRet(hr);

    hr = host->CreateAppDomainWithManager(
        appDomainFriendlyNameW,
        // Flags:
        // APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS
        // - By default CoreCLR only allows platform neutral assembly to be run. To allow
        //   assemblies marked as platform specific, include this flag
        //
        // APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP
        // - Allows sandboxed applications to make P/Invoke calls and use COM interop
        //
        // APPDOMAIN_SECURITY_SANDBOXED
        // - Enables sandboxing. If not set, the app is considered full trust
        //
        // APPDOMAIN_IGNORE_UNHANDLED_EXCEPTION
        // - Prevents the application from being torn down if a managed exception is unhandled
        //
        APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS |
        APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP |
        APPDOMAIN_DISABLE_TRANSPARENCY_ENFORCEMENT,
        NULL,                    // Name of the assembly that contains the AppDomainManager implementation
        NULL,                    // The AppDomainManager implementation type name
        propertyCount,
        propertyKeysW,
        propertyValuesW,
        (DWORD *)domainId);

    if (SUCCEEDED(hr))
    {
        host.SuppressRelease();
        *hostHandle = host;
    }
    return hr;
}

execute_assembly

マクロによるお膳立てコードや、エラーハンドルコードが大量にありますが、バッサリ省略してあります。
重要な部分のみ見ていくと結構シンプルで、ManagedエントリのアセンブリからMainメソッド(CallSite)を探してきて、引数をStackに積んでCILのCall命令を発行します。

runtime/src/coreclr/dlls/mscoree/unixinterface.cpp
extern "C" DLLEXPORT int coreclr_execute_assembly(
    void* hostHandle, unsigned int domainId,
    int argc, const char** argv,
    const char* managedAssemblyPath, unsigned int* exitCode)
{
    *exitCode = -1;
    ICLRRuntimeHost4* host = reinterpret_cast<ICLRRuntimeHost4*>(hostHandle);

    ConstWStringArrayHolder argvW;
    argvW.Set(StringArrayToUnicode(argc, argv), argc);
    ConstWStringHolder managedAssemblyPathW = StringToUnicode(managedAssemblyPath);

    HRESULT hr = host->ExecuteAssembly(domainId, managedAssemblyPathW, argc, argvW, (DWORD *)exitCode);
    IfFailRet(hr);

    return hr;
}

runtime/src/coreclr/vm/corhost.cpp
HRESULT CorHost2::ExecuteAssembly(
    DWORD dwAppDomainId, LPCWSTR pwzAssemblyPath,
    int argc, LPCWSTR* argv, DWORD *pReturnValue)
{
    AppDomain *pCurDomain = SystemDomain::GetCurrentDomain();
    Thread *pThread = GetThreadNULLOk();

    Assembly *pAssembly = AssemblySpec::LoadAssembly(pwzAssemblyPath);
    pCurDomain->GetMulticoreJitManager().AutoStartProfile(pCurDomain);

    // Here we call the managed method that gets the cmdLineArgs array.
    SetCommandLineArgs(pwzAssemblyPath, argc, argv);

    PTRARRAYREF arguments = NULL;

    arguments = (PTRARRAYREF)AllocateObjectArray(argc, g_pStringClass);
    for (int i = 0; i < argc; ++i)
    {
        STRINGREF argument = StringObject::NewString(argv[i]);
        arguments->SetAt(i, argument);
    }
    
    *pReturnValue = pAssembly->ExecuteMainMethod(&arguments, TRUE /* waitForOtherThreads */);
}

runtime/src/coreclr/vm/assembly.cpp
INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThreads)
{
    MethodDesc *pMeth = GetEntryPoint();
    RunStartupHooks();
    hr = RunMain(pMeth, 1, &iRetVal, stringArgs);
}

HRESULT RunMain(MethodDesc *pFD, short numSkipArgs, INT32 *piRetVal, PTRARRAYREF *stringArgs /*=NULL*/)
{
    CorEntryPointType EntryType = EntryManagedMain;
    ValidateMainMethod(pFD, &EntryType);

    Param param;
    param.pFD = pFD;
    param.numSkipArgs = numSkipArgs;
    param.piRetVal = piRetVal;
    param.stringArgs = stringArgs;
    param.EntryType = EntryType;
    param.cCommandArgs = cCommandArgs;
    param.wzArgs = wzArgs;
    RunMainInternal(pParam);
}

static void RunMainInternal(Param* pParam)
{
    MethodDescCallSite  threadStart(pParam->pFD);

    // 引数をStackに積む
    // Build the parameter array and invoke the method.
    // Allocate a COM Array object with enough slots for cCommandArgs - 1
    StrArgArray = (PTRARRAYREF) AllocateObjectArray((pParam->cCommandArgs - pParam->numSkipArgs), g_pStringClass);
    // Create Stringrefs for each of the args
    for (DWORD arg = pParam->numSkipArgs; arg < pParam->cCommandArgs; arg++) {
        STRINGREF sref = StringObject::NewString(pParam->wzArgs[arg]);
        StrArgArray->SetAt(arg - pParam->numSkipArgs, (OBJECTREF) sref);
    }
    ARG_SLOT stackVar = ObjToArgSlot(StrArgArray);

    // Set the return value to 0 instead of returning random junk
    *pParam->piRetVal = 0;
    threadStart.Call(&stackVar); // ILのCall命令を発行(多分)
}

まとめ

今回は、ホスト部分(起動部分)を中心に見ていきました。
ランタイムの起動までに起こっている処理が何となく感じられたならと思います。

ランタイムの機能としては他にも、JITやGC、VM、アセンブリ内のTypeの管理などのパートがあり、今回見たのはほんの片鱗にすぎません。
それらも好奇心をそそられますので、そのうち読んでみたいところです。

dotnet/runtime The MIT License (MIT) Copyright (c) .NET Foundation and Contributors All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?