【.Net runtime】coreclr(.Net应用启动过程)

文章目录

接着前一篇文章,已经追根溯源到了coreclr相关的ExecuteAssembly,上层对coreclr的调用是调用动态库的封装,我们继续从源码往下跟

coreclr

CorHost2::ExecuteAssembly

源码在runtime/src/coreclr/vm/corhost.cpp

传入指定的托管程序集路径、命令行参数,在指定的 AppDomain 中加载该程序集,并执行程序集的入口方法,最终返回程序的退出码。

cpp 复制代码
CONTRACTL
{THROWS;ENTRY_POINT;}
CONTRACTL_END;

这是 CLR 源码特有的代码契约(Contract),是 CLR 内部的静态分析 / 运行时检查机制,作用是声明函数的行为约束:
THROWS:声明该函数允许抛出异常,托管代码的未处理异常要穿透到宿主层,不能被 CLR 内部静默处理;
ENTRY_POINT:声明该函数是 CLR 的一个入口点,是宿主调用 CLR 的核心边界方法,CLR 会对这类方法做特殊的栈检查、异常处理初始化;

Contract

我比较好奇这个宏是怎么回事,如果不感兴趣可以跳过。

cpp 复制代码
#define CONTRACTL CUSTOM_CONTRACTL(Contract)
#define CUSTOM_CONTRACTL(_contracttype)                                 \
        CONTRACTL_SETUP(_contracttype)

CONTRACTL最终会展开CONTRACTL_SETUP(Contract),别问为什么会封装这么多层

然后他有一个开关宏强制关闭运行时契约__FORCE_NORUNTIME_CONTRACTS__,默认Debug是关闭的也就是开启运行时契约,如果开启运行时契约会比关闭多一些内容(契约对象),这里简单看看默认的。

cpp 复制代码
#define CONTRACTL_SETUP(_contracttype)                                  \
    _contracttype ___contract;                                          \
    BOOL ___contract_enabled = Contract::EnforceContract();             \
    enum {___disabled = 0};                                             \
    if (!___contract_enabled)                                           \
        ___contract.Disable();                                          \
    else                                                                \
    {                                                                   \
        typedef __YouCannotUseAPostConditionHere __PostConditionOK;     \
        enum { ___CheckMustBeInside_CONTRACT = 1 };                     \
        Contract::Operation ___op = Contract::Setup;                    \
        enum {___disabled = 0};                                         \
        if (0)                                                          \
        {                                                               \
          ___run_preconditions:                                         \
            ___op = Contract::Preconditions;                            \
        }                                                               \
        if (0)                                                          \
        {                                                               \
        /* define for CONTRACT_END even though we can't get here */     \
          ___run_return:                                                \
            UNREACHABLE();                                              \
        }                                                               \
        UINT ___testmask = 0; 											\

#define CONTRACTL_END                                                                       \
        if (___op & Contract::Setup)                                                        \
        {                                                                                   \
            if (___testmask & Contract::PRECONDITION_Used)                                  \
            {                                                                               \
                goto ___run_preconditions;                                                  \
            }                                                                               \
        }                                                                                   \
        else if (___op & Contract::Postconditions)                                          \
        {                                                                                   \
            goto ___run_return;                                                             \
        }                                                                                   \
        ___CheckMustBeInside_CONTRACT;                                                      \
   }

if(0) 里面的代码永远不执行,为什么还要写?为了声明标签,配合CONTRACTL_END 宏使用goto语句。
___CheckMustBeInside_CONTRACT,强制契约块必须写在函数体内,写外部直接编译报错。
UNREACHABLE()相当于触发断言 __assume(0),他的定义在windows sdk(winnt.h)里,可能为了兼容性有好多版本的实现。

cpp 复制代码
#define REQUEST_TEST(thetest, todisable)   \
(___testmask |= (___CheckMustBeInside_CONTRACT, (___disabled ? (todisable) : (thetest))))

#define THROWS \
do { STATIC_CONTRACT_THROWS; REQUEST_TEST(Contract::THROWS_Yes, Contract::THROWS_Disabled); } while(0)

#define ENTRY_POINT   STATIC_CONTRACT_ENTRY_POINT

STATIC_CONTRACT_ENTRY_POINT是一个静态契约,所有的静态契约都是空宏,在inc/staticcontract.h里定义,主要用于编译期语义标记、团队规范、静态分析工具。

THROWS 会根据Contract类的相关的静态属性去改变___testmask 进而影响触不触发断言。

好,我们继续看ExecuteAssembly

下面就是检查AppDomainId,IsRuntimeActive,pwzAssemblyPath,argc/argv。

然后是初始化线程,CLR 是托管线程模型,所有执行托管代码的线程,都必须被 CLR 包装为托管线程,不能用原生的操作系统线程直接执行托管代码。

cpp 复制代码
//尝试从 CLR 的线程缓存中,获取当前操作系统线程对应的CLR 托管线程对象
Thread *pThread = GetThreadNULLOk();
if (pThread == NULL)
{
	//将当前原生线程初始化并包装为 CLR 托管线程,完成线程的托管环境配置(栈、异常处理器、GC 关联等)
    pThread = SetupThreadNoThrow(&hr);
    if (pThread == NULL)
    {
        goto ErrExit;
    }
}

然后是异常处理器 和 GC 安全检查,将被执行的程序集路径存入全局变量g_EntryAssemblyPath

然后加载托管程序集

cpp 复制代码
Assembly *pAssembly = AssemblySpec::LoadAssembly(pwzAssemblyPath);
#if defined(FEATURE_MULTICOREJIT)
	//多核 JIT 的预热入口,基于当前 AppDomain 的程序集,生成编译配置文件,启动后台线程进行预热编译
    pCurDomain->GetMulticoreJitManager().AutoStartProfile(pCurDomain);
#endif

多核 JIT 的作用是,在程序启动时,利用多核 CPU 提前编译程序的核心方法,避免运行时的 JIT 编译卡顿,提升程序启动速度和运行流畅度。

cpp 复制代码
{
    GCX_COOP();//声明当前代码块运行在协作式 GC 模式下
    PTRARRAYREF arguments = NULL; //PTRARRAYREF 是托管数组的内部指针类型
    GCPROTECT_BEGIN(arguments);
    arguments = SetCommandLineArgs(pwzAssemblyPath, argc, argv);//将原生命令行参数封装为 CLR 的托管数组对象
    //是否让宿主吞掉未处理的托管异常,默认关闭
    if(CLRConfig::GetConfigValue(CLRConfig::INTERNAL_Corhost_Swallow_Uncaught_Exceptions)){
        EX_TRY
            DWORD retval = pAssembly->ExecuteMainMethod(&arguments, TRUE /* waitForOtherThreads */);
            if (pReturnValue)
            {
                *pReturnValue = retval;
            }
        EX_CATCH_HRESULT (hr)
    }
    else{
    	//传入封装好的托管命令行参数数组,对应 Main(string[] args)
    	//第二个参数为真,表示 等待所有后台线程执行完成后再返回,保证程序的所有逻辑都执行完毕
        DWORD retval = pAssembly->ExecuteMainMethod(&arguments, TRUE /* waitForOtherThreads */);
        if (pReturnValue){
            *pReturnValue = retval;
        }
    }
    GCPROTECT_END();
}

GCPROTECT_BEGIN/END(arguments):GC 内存保护宏。

核心作用:

该托管数组对象 arguments 是在栈上声明的指针,GC 扫描堆内存时不会扫描栈;

如果不保护,GC 会认为该对象无引用,在执行过程中被误回收,导致内存访问错误;

这对宏会将指针加入GC 根集,GC 会识别为有效引用,执行完成后再移除,是 CLR 中非托管代码操作托管对象的必备安全机制。
EX_TRY/EX_CATCH_HRESULT:CLR 源码的异常捕获宏,替代原生 C++ 的 try/catch,专门捕获托管异常并转换为 HRESULT 错误码;

后面是资源释放(卸载异常处理器)和收尾逻辑。

Assembly::ExecuteMainMethod

cpp 复制代码
INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThreads)
{

👉声明契约块

cpp 复制代码
    CONTRACTL
    {
        INSTANCE_CHECK;//检查当前Assembly* this指针是有效实例,不能为空指针
        THROWS;
        GC_TRIGGERS;//执行过程中会触发 GC 垃圾回收
        MODE_ANY;//兼容 协作式 GC (GCX_COOP) + 抢占式 GC (GCX_PREEMP) 两种模式
        ENTRY_POINT;
        //用于测试阶段模拟内存不足 / 对象创建失败的场景,注入OutOfMemoryException异常,验证函数的容错能力
        INJECT_FAULT(COMPlusThrowOM());
    }

👉初始化基础变量,重置错误码

cpp 复制代码
    CONTRACTL_END;
    errno=0;
    HRESULT hr = S_OK;
    INT32   iRetVal = 0;
    //获取当前执行的原生线程对象(CLR的Thread类,非托管Thread)
    Thread *pThread = GetThread();
    //指向托管程序Main方法的元数据描述符
    MethodDesc *pMeth;

👉设置线程状态 + 切换 GC 模式 + 查找 Main 方法

cpp 复制代码
pThread->SetBackground(FALSE);//将当前线程设为【前台线程】,保证程序不会提前退出
GCX_COOP();
pMeth = GetEntryPoint();//根据.NET 规范查找 Main 方法
if (pMeth) {//如果找到Main方法
{

👉COM 互操作适配

cpp 复制代码
#ifdef FEATURE_COMINTEROP
   GCX_PREEMP();//抢占式
   Thread::ApartmentState state = Thread::AS_Unknown;
   //设置线程的 COM 单元状态(单线程 STA / 多线程 MTA),保证 Main 方法中调用 COM 组件时的兼容性
   state = SystemDomain::GetEntryPointThreadAptState(pMeth->GetMDImport(), pMeth->GetMemberDef());
   SystemDomain::SetThreadAptState(state);
#endif // FEATURE_COMINTEROP
 }

👉执行 Main 方法前的前置初始化

cpp 复制代码
 RunMainPre();//初始化托管上下文、加载依赖程序集、绑定配置文件
 Assembly* pRootAssembly = pMeth->GetAssembly();//获取Main方法所属的根程序集
 AppDomain::GetCurrentDomain()->SetRootAssembly(pRootAssembly);//标记当前AppDomain的根程序集
// ReadyToRun 预编译优化
#ifdef FEATURE_READYTORUN
 {
     if (pRootAssembly->GetModule()->IsReadyToRun())
     {
         pRootAssembly->GetModule()->GetReadyToRunInfo()->RegisterUnrelatedR2RModule();
     }
 }
#endif

预编译优化,将 IL 代码提前编译为机器码,启动速度提升 50%+。

👉初始化托管线程 + 执行 Main 方法

cpp 复制代码
 Thread::InitializationForManagedThreadInNative(pThread);
 RunManagedStartup();//初始化托管运行时:设置线程上下文、异常处理器、GC根
 hr = RunMain(pMeth, 1, &iRetVal, stringArgs);
 Thread::CleanUpForManagedThreadInNative(pThread);//清理托管线程的原生资源
}

👉后处理

cpp 复制代码
if (pMeth) {
#if !defined(TARGET_BROWSER)
	//执行Main后的清理:等待后台线程、释放资源、卸载程序集
  	if (waitForOtherThreads) RunMainPost();
#endif // !TARGET_BROWSER
} else {
    StackSString displayName;
   	GetDisplayName(displayName);//// 获取当前程序集的名称
   	COMPlusThrowHR(COR_E_MISSINGMETHOD, IDS_EE_FAILED_TO_FIND_MAIN, displayName);
}
IfFailThrow(hr);// 如果RunMain执行失败(hr≠S_OK),则抛出对应的托管异常
return iRetVal;
}

COMPlusThrowHR(...) : CLR 的原生抛托管异常的 API,这里抛出的就是 System.MissingMethodException,未找到适合的 Main 方法

RunMain

原生 C++ 层的封装中转函数,不直接执行托管 Main 方法,只做参数校验、数据封装、异常兜底,真正执行逻辑在内部调用的RunMainInternal
参数合法性校验

cpp 复制代码
HRESULT RunMain(MethodDesc *pFD ,
                short numSkipArgs,
                INT32 *piRetVal,
                PTRARRAYREF *stringArgs /*=NULL*/)
{
    STATIC_CONTRACT_THROWS;
    _ASSERTE(piRetVal);
    DWORD       cCommandArgs = 0;  // count of args on command line
    LPWSTR      *wzArgs = NULL; // command line args
    HRESULT     hr = S_OK;
    *piRetVal = -1;
    if (stringArgs == NULL)
        SetLatchedExitCode(0);
    if (!pFD) {
        _ASSERTE(!"Must have a function to call!");
        return E_FAIL;
    }

SetLatchedExitCode(0):CLR 内部函数,设置进程的最终退出码,0 代表正常退出,非 0 代表异常;这里是兜底,防止无参数时退出码残留脏值。

cpp 复制代码
    CorEntryPointType EntryType = EntryManagedMain;
    ValidateMainMethod(pFD, &EntryType);//验证 Main 方法的合法性
    if ((EntryType == EntryManagedMain) &&
        (stringArgs == NULL)) {
        return E_INVALIDARG;
    }
    ETWFireEvent(Main_V1);

CorEntryPointType枚举:CLR 定义的枚举,标记 Main 方法的合法类型,

EntryManagedMain:标准的托管 Main 方法(static void Main() / static int Main(string[]));

还有其他取值:比如EntryWinMain(WinForm 的 WinMain)、EntryConsoleMain(控制台 Main)等;
ValidateMainMethod(pFD, &EntryType) 做了什么?校验规则包括:

必须是static;方法名必须是Main(大小写敏感);返回值只能是void或int;参数只能是「无参」或「string []」;

不能是泛型方法、不能是虚方法、不能是抽象方法;

如果校验失败:这个函数会直接抛出托管异常(比如System.InvalidProgramException),函数终止执行;

如果校验成功:会把EntryType赋值为对应的合法类型,供后续逻辑使用。
ETW = Event Tracing for Windows,是 Windows 的原生性能监控机制,向系统写入开始执行 Main 方法的监控事件,开发人员可以用工具捕获这个事件,分析程序的启动耗时、性能瓶颈。
打包参数调用RunMainInternal

cpp 复制代码
    Param param;
    param.pFD = pFD;
    param.numSkipArgs = numSkipArgs;
    param.piRetVal = piRetVal;
    param.stringArgs = stringArgs;
    param.EntryType = EntryType;
    param.cCommandArgs = cCommandArgs;
    param.wzArgs = wzArgs;
    EX_TRY_NOCATCH(Param *, pParam, &param)
    {
        RunMainInternal(pParam);
    }
    EX_END_NOCATCH
//触发结束监控事件 + 返回结果
    ETWFireEvent(MainEnd_V1);
    return hr;
}

RunMainInternal

CLR 原生层的最后一个函数,直接执行托管 Main 方法的核心实现。

cpp 复制代码
static void RunMainInternal(Param* pParam)
{
    MethodDescCallSite  threadStart(pParam->pFD);

无返回值,因为所有异常已经在上游RunMain的EX_TRY_NOCATCH捕获,所有返回值通过指针回写,无需返回值。
MethodDescCallSite 是CLR 原生层的核心调用封装类,是 JIT 编译 + 托管方法调用 的工具类,专门用于原生代码调用托管方法,没有这个类,原生层无法直接执行托管方法。

传入Main方法的MethodDesc*,构造threadStart对象时,内部会做两件关键事:
JIT 编译 :把Main方法的IL 中间代码,编译为CPU 可执行的机器码(如果程序开启了ReadyToRun预编译,则跳过 JIT,直接绑定预编译的机器码);
绑定调用地址:把编译后的机器码地址,绑定到threadStart对象内部,为后续的Call()调用做好准备;

构建托管的 string[] args 命令行参数数组

cpp 复制代码
    PTRARRAYREF StrArgArray = NULL;//托管string[] args数组的原生引用
    GCPROTECT_BEGIN(StrArgArray);
    if (pParam->EntryType == EntryManagedMain
        || pParam->EntryType == EntryManagedMainAsync
        || pParam->EntryType == EntryManagedMainAsyncVoid)
    {
        if (pParam->stringArgs == NULL) {
        	//上游没传托管参数数组 → 原生层手动构建托管string[]
            StrArgArray = (PTRARRAYREF) AllocateObjectArray(
	            (pParam->cCommandArgs - pParam->numSkipArgs), 
	            g_pStringClass);
            for (DWORD arg = pParam->numSkipArgs; arg < pParam->cCommandArgs; arg++) {
                STRINGREF sref = StringObject::NewString(pParam->wzArgs[arg]);
                StrArgArray->SetAt(arg - pParam->numSkipArgs, (OBJECTREF) sref);
            }
        }
        else
            StrArgArray = *pParam->stringArgs;
    }
    ARG_SLOT stackVar = ObjToArgSlot(StrArgArray);

AllocateObjectArray(长度, 元素类型):CLR 原生层 API,在托管堆上创建一个指定长度、指定元素类型的托管数组,这里创建的是string[],因为第二个参数是g_pStringClass(CLR 全局的托管System.String类型元数据指针)。
ARG_SLOT:CLR 定义的通用参数槽类型,是「原生类型 → 托管调用栈兼容类型」的适配器。托管方法的所有参数,在被原生层调用时,都必须先转换为ARG_SLOT类型,才能被托管的调用栈识别。
ObjToArgSlot:转换宏,把托管对象的原生引用(如PTRARRAYREF)转换为ARG_SLOT类型,适配托管调用栈。

cpp 复制代码
    if (pParam->pFD->IsVoid())
    {	// 无返回值的Main,默认给操作系统返回0
        *pParam->piRetVal = 0;
        threadStart.Call(&stackVar);
    }
#if defined(TARGET_BROWSER)
//浏览器平台专属 → 异步 Main 方法
    else if (pParam->EntryType == EntryManagedMainAsync)
    {
        *pParam->piRetVal = 0;
        MethodDescCallSite mainWrapper(METHOD__ASYNC_HELPERS__HANDLE_ASYNC_ENTRYPOINT);
        OBJECTREF exitCodeTask = threadStart.Call_RetOBJECTREF(&stackVar);
        ARG_SLOT stackVarWrapper[] =
        {
            ObjToArgSlot(exitCodeTask)
        };
        mainWrapper.Call(stackVarWrapper);
    }
    else if (pParam->EntryType == EntryManagedMainAsyncVoid)
    {
        *pParam->piRetVal = 0;
        MethodDescCallSite mainWrapper(METHOD__ASYNC_HELPERS__HANDLE_ASYNC_ENTRYPOINT_VOID);
        OBJECTREF exitCodeTask = threadStart.Call_RetOBJECTREF(&stackVar);
        ARG_SLOT stackVarWrapper[] =
        {
            ObjToArgSlot(exitCodeTask)
        };
        mainWrapper.Call(stackVarWrapper);
    }
#endif // TARGET_BROWSER
    else
    {	//执行托管Main方法,接收int返回值
        *pParam->piRetVal = (INT32)threadStart.Call_RetArgSlot(&stackVar);
        SetLatchedExitCode(*pParam->piRetVal);
    }
    GCPROTECT_END();
    minipal_log_flush_all();//刷新所有CLR内部日志缓冲区
}

这就是完整启动过程了。

相关推荐
聪明努力的积极向上3 小时前
【设计】MySQL + C# 并发分批查询 DataTable Merge 偶发报错分析及解决方案
数据库·mysql·c#
我是唐青枫3 小时前
深入理解 C#.NET IEnumerable<T>:一切集合的起点
c#·.net
2501_930707784 小时前
使用C#代码重新排列 PDF 页面
开发语言·pdf·c#
zxy28472253014 小时前
利用C#的视觉库Halcon识别药盒多条形码,可用于追溯码识别(二)
c#·halcon·条码·追溯码·多条码
mudtools4 小时前
当传统工单遇见飞书:.NET系统的协作升级之旅
c#·自动化·.net·飞书
唐青枫4 小时前
深入理解 C#.NET Interlocked.Increment:原子操作的核心
c#·.net
钰fly12 小时前
C#异常处理 递归算法
c#
ejjdhdjdjdjdjjsl13 小时前
JSON序列化与反序列化实战指南
数据库·microsoft·c#