前言:
.Net7的CLR最具特色的一个地方,就是运行模型。因为它主宰了整个CLR的运行过程。
又因为其庞大的代码量,有的几十万行甚至百万行。所以理解起来非常不容易。本篇拆分来看下,里面一个细节Main函数(注意这里的Main指的是托管Main,因为还有非托管main,所以特别指出下以免混淆)是如何被CLR调用的。
概括
Main函数的调用,基本上是最后才会被CLR引擎所调用。为啥?因为,如果.Net标准类库,以及底层的非托管C++类库没有在Main函数被调用之前,加载完成,那么你强行调用Main函数就会出错。
我们可以在Windows/Linux平台通过Windbg或者是LLDB来看下托管的Main函数的堆栈
00007ff7f55703b0
coreclr.dll!CallDescrWorkerInternal
coreclr.dll!CallDescrWorkerWithHandler
coreclr.dll!MethodDescCallSite::CallTargetWorker
coreclr.dll!MethodDescCallSite::Call C++
coreclr.dll!RunMainInternal C++
coreclr.dll!``RunMain'::`30'
coreclr.dll!`RunMain'::`30'
coreclr.dll!RunMain C++
coreclr.dll!Assembly::ExecuteMainMethod
coreclr.dll!CorHost2::ExecuteAssembly
coreclr.dll!coreclr_execute_assembly C++
corerun.exe!run C++
corerun.exe!wmain C++
corerun.exe!invoke_main C++
corerun.exe!__scrt_common_main_seh C++
corerun.exe!__scrt_common_main C++
corerun.exe!wmainCRTStartup C++
kernel32.dll!BaseThreadInitThunk
ntdll.dll!RtlUserThreadStart
这个堆栈里面我们可以得出很多的信息:
比如:
1.最上面的地址:00007ff7f55703b0是.Net里面的托管Main函数的入口地址。
2.整个程序的运行最开始的代码是ntdll.dll模块里面的RtlUserThreadStart函数。
3.非托管的CLR的入口是wmain函数。
有了以上的信息,我们很容易分析到。托管的Main函数调用是在程序集的ExecuteMainMethod函数里面调用RunMain这个函数,整个函数的名字一看就知道是运行Main函数的。
此后通过CallDescrWorkerInternal来调用RyuJIT编译MSIL代码为机器码。返回到托管Main函数的开头地址。
也就是上面的这个地址:00007ff7f55703b0
分析:
这个过程看似分析非常简单,但是实际上会遇到很多问题。但是如果你只用单一的调试器,比如只用Windbg。你可能完全看不出来以下这些问题。
比如你在调试.Ctor,也就是.Net 7里面默认的构造函数的时候。如果在VS上Debug CLR上面,如果进入到托管Main的机器码当中,运行到.Ctor的机器码的地方。它会报一个不是错误的错误,而跟踪下来居然是MapViewOfFile这个微软的官方函数的错误,关于这一点可以看这里,之前写的一篇文章:点击这里查看之前文章
而另外如果你在Linux上面用LLDB调试的时候,会发现托管的Main入口断点可能会进不去的情况。
当然这些类似Bug但可能不是Bug东西,需要深厚的功力去发现和验证它到底是啥。
结尾:
作者:江湖评谈
欢迎关注我,带你了解.Net7的CLR的各种奇巧淫技。让.Net7,甚至后面的.Net8,9,10。在你面前毫无神秘可言。