文章目录
接着前一篇文章,已经追根溯源到了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, ¶m)
{
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内部日志缓冲区
}
这就是完整启动过程了。