留双龙 發表於 2022-4-19 13:10:00

C#语法糖系列 —— 第一篇:聊聊 params 参数底层玩法

<p>首先说说为什么要写这个系列,大概有两点原因。</p>
<ol>
<li>这种文章阅读量确实高...</li>
<li>对 IL 和 汇编代码 的学习巩固</li>
</ol>
<p>所以就决定写一下这个系列,如果大家能从中有所收获,那就更好啦!</p>
<h2 id="一params-应用层玩法">一:params 应用层玩法</h2>
<p>首先上一段 <code>测试代码</code>。</p>
<pre><code class="language-C#">
    class Program
    {
      static void Main(string[] args)
      {
            Test(100, 200, 300);
            Test();
      }

      static void Test(params int[] list)
      {
            Console.WriteLine($"list.length={list.Length}");
      }
    }

</code></pre>
<p>输出结果如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1cc6720a5d974d5a8efb341d47a40491~tplv-k3u1fbpfcp-zoom-1.image" alt="" loading="lazy"></p>
<p>可以看出如果给 <code>方法形参</code> 加上 <code>params</code> 前缀,在传递 <code>方法实参</code> 上就特别灵活,点赞。</p>
<p>接下来我们来看看,这么灵活的 <code>实参传递</code> 底层到底是怎么玩的?我们先从 IL 层面探究下。</p>
<h2 id="二il-层面解读">二:IL 层面解读</h2>
<p>用 <code>ILSpy</code> 打开我们的 exe ,看看 IL 代码</p>
<pre><code class="language-C#">
.method private hidebysig static
        void Main (
                string[] args
        ) cil managed
{
        // Method begins at RVA 0x2050
        // Code size 37 (0x25)
        .maxstack 8
        .entrypoint

        IL_0000: nop
        IL_0001: ldc.i4.3
        IL_0002: newarr System.Int32
        IL_0007: dup
        IL_0008: ldtoken field valuetype '&lt;PrivateImplementationDetails&gt;'/'__StaticArrayInitTypeSize=12' '&lt;PrivateImplementationDetails&gt;'::'9AC9CF706FBD14D039E0436219C5D852927E5F69295F2EF423AE897345197B2A'
        IL_000d: call void System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class System.Array, valuetype System.RuntimeFieldHandle)
        IL_0012: call void ConsoleApp2.Program::Test(int32[])
        IL_0017: nop
        IL_0018: ldc.i4.0
        IL_0019: newarr System.Int32
        IL_001e: call void ConsoleApp2.Program::Test(int32[])
        IL_0023: nop
        IL_0024: ret
} // end of method Program::Main

.method private hidebysig static
        void Test (
                int32[] list
        ) cil managed
{
        .param
                .custom instance void System.ParamArrayAttribute::.ctor() = (
                        01 00 00 00
                )
        // Method begins at RVA 0x2076
        // Code size 26 (0x1a)
        .maxstack 8

        IL_0000: nop
        IL_0001: ldstr "list.length={0}"
        IL_0006: ldarg.0
        IL_0007: ldlen
        IL_0008: conv.i4
        IL_0009: box System.Int32
        IL_000e: call string System.String::Format(string, object)
        IL_0013: call void System.Console::WriteLine(string)
        IL_0018: nop
        IL_0019: ret
} // end of method Program::Test

</code></pre>
<p>上面是 <code>Main</code> 和 <code>Test</code> 方法的IL代码,我们逐一看一下。</p>
<h3 id="1-test-方法">1. Test 方法</h3>
<p>从 <code>int32[] list</code> 参数看并没有所谓的 <code>params</code>,这也就说明它是 <code>C#编译器</code> 玩的一个手段而已,在方法调用前就已经构建好了。</p>
<h3 id="2-main方法">2. Main方法</h3>
<p>可以看到 IL 层面的 <code>Test(100, 200, 300)</code> 已经变成了下面五行代码。</p>
<pre><code class="language-C#">
        IL_0002: newarr System.Int32
        IL_0007: dup
        IL_0008: ldtoken field valuetype '&lt;PrivateImplementationDetails&gt;'/'__StaticArrayInitTypeSize=12' '&lt;PrivateImplementationDetails&gt;'::'9AC9CF706FBD14D039E0436219C5D852927E5F69295F2EF423AE897345197B2A'
        IL_000d: call void System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class System.Array, valuetype System.RuntimeFieldHandle)
        IL_0012: call void ConsoleApp2.Program::Test(int32[])

</code></pre>
<p>逻辑大概就是:</p>
<ul>
<li>用 <code>newarr</code> 构建初始化 <code>int[]</code> 数组。</li>
<li>用 <code>ldtoken</code> 从程序元数据中提取 <code>1,2,3</code>。</li>
<li>用 <code>InitializeArray</code> 来将 1,2,3 构建到数组中。</li>
</ul>
<p>有了这些指令,我相信 JIT 就知道怎么做了。</p>
<p>再看 <code>Test()</code> 的 IL 指令只有一行 <code>newarr System.Int32</code> 初始化。</p>
<p>所以本质上来说,就是提前构建好 array,然后进行参数传递,仅此而已。。。</p>
<h2 id="三汇编层面解读">三:汇编层面解读</h2>
<p>有了 <code>newarr + ldtoken + call</code> 三条指令,接下来我们读一下汇编层做了什么,使用 windbg 打开 exe,简化后的汇编代码如下:</p>
<pre><code class="language-C#">
0:000&gt; !U /d 009b0848
Normal JIT generated code
ConsoleApp2.Program.Main(System.String[])
Begin 009b0848, size 77

D:\net5\ConsoleApp4\ConsoleApp2\Program.cs @ 14:
&gt;&gt;&gt; 009b0848 55            push    ebp
009b0849 8bec            mov   ebp,esp
009b084b 83ec10          sub   esp,10h
009b084e 33c0            xor   eax,eax
009b0850 8945f4          mov   dword ptr ,eax
009b0853 8945f8          mov   dword ptr ,eax
009b0856 8945f0          mov   dword ptr ,eax
009b0859 894dfc          mov   dword ptr ,ecx
009b085c 833df042710000cmp   dword ptr ds:,0
009b0863 7405            je      009b086a
009b0865 e816f55264      call    clr!JIT_DbgIsJustMyCode (64edfd80)
009b086a 90            nop

D:\net5\ConsoleApp4\ConsoleApp2\Program.cs @ 15:
009b086b b95e186763      mov   ecx,offset mscorlib_ni!System.Text.Encoding.GetEncodingCodePage(Int32)$##6006719 &lt;PERF&gt; (mscorlib_ni+0x185e) (6367185e)
009b0870 ba03000000      mov   edx,3
009b0875 e8b229d5ff      call    0070322c (JitHelp: CORINFO_HELP_NEWARR_1_VC)
009b087a 8945f4          mov   dword ptr ,eax
009b087d b9e04d7100      mov   ecx,714DE0h
009b0882 e819c91f64      call    clr!JIT_GetRuntimeFieldStub (64bad1a0)
009b0887 8945f8          mov   dword ptr ,eax
009b088a 8d45f8          lea   eax,
009b088d ff30            push    dword ptr
009b088f 8b4df4          mov   ecx,dword ptr
009b0892 e8f9c61f64      call    clr!ArrayNative::InitializeArray (64bacf90)
009b0897 8b4df4          mov   ecx,dword ptr
009b089a ff15904d7100    call    dword ptr ds: (ConsoleApp2.Program.Test(Int32[]), mdToken: 06000002)
009b08a0 90            nop

D:\net5\ConsoleApp4\ConsoleApp2\Program.cs @ 16:
009b08a1 b95e186763      mov   ecx,offset mscorlib_ni!System.Text.Encoding.GetEncodingCodePage(Int32)$##6006719 &lt;PERF&gt; (mscorlib_ni+0x185e) (6367185e)
009b08a6 33d2            xor   edx,edx
009b08a8 e87f29d5ff      call    0070322c (JitHelp: CORINFO_HELP_NEWARR_1_VC)
009b08ad 8945f0          mov   dword ptr ,eax
009b08b0 8b4df0          mov   ecx,dword ptr
009b08b3 ff15904d7100    call    dword ptr ds: (ConsoleApp2.Program.Test(Int32[]), mdToken: 06000002)
009b08b9 90            nop

D:\net5\ConsoleApp4\ConsoleApp2\Program.cs @ 17:
009b08ba 90            nop
009b08bb 8be5            mov   esp,ebp
009b08bd 5d            pop   ebp
009b08be c3            ret

</code></pre>
<h3 id="1-newarr">1. newarr</h3>
<p>可以很清楚的看到,<code>newarr</code> 调用了 CLR 中的jithelper函数 <code>CORINFO_HELP_NEWARR_1_VC</code> 下的<code>JIT_NewArr1</code> 方法,大家有兴趣可以看下 <code>jitheapler.cpp</code>,调用完之后,初始化数组就出来了。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/56c404425c2f445ca99b59e0aed9ecfe~tplv-k3u1fbpfcp-zoom-1.image" alt="" loading="lazy"></p>
<p>从dp看,数组只申明了 <code>length=3</code>,还并没有数组元素,也就说所谓的初始化。</p>
<h3 id="2-ldtoken--initializearray">2. ldtoken &amp; InitializeArray</h3>
<p>刚才也说到了, ldtoken 是在运行时提取元数据,那就必须要解析 PE 头,在 clr 层面有一个 <code>PEDecoder::GetRvaData</code> 方法就是用来解析运行时数据,它是发生在 <code>ArrayNative::InitializeArray</code> 方法中,所以我们下两个 bu 命令拦截。</p>
<pre><code class="language-C#">
0:000&gt; bu clr!ArrayNative::InitializeArray
0:000&gt; bu clr!PEDecoder::GetRvaData
0:000&gt; g
Breakpoint 3 hit
eax=0019f500 ebx=0019f5ac ecx=024c2338 edx=006fb930 esi=00000000 edi=0019f520
eip=64bacf90 esp=0019f4f0 ebp=0019f508 iopl=0         nv up ei pl zr na pe nc
cs=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00000246
clr!ArrayNative::InitializeArray:
64bacf90 6a78            push    78h
0:000&gt; g
Breakpoint 0 hit
eax=00713448 ebx=00624044 ecx=0071344c edx=0071dc28 esi=00624044 edi=00624de0
eip=64befcfc esp=0019f400 ebp=0019f410 iopl=0         nv up ei pl zr na pe nc
cs=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00000246
clr!PEDecoder::GetRvaData:
64befcfc 55            push    ebp
0:000&gt; k
# ChildEBP RetAddr      
00 0019f410 64b63be5   clr!PEDecoder::GetRvaData
01 0019f410 64b63bb3   clr!Module::GetRvaField+0x40
02 0019f44c 64bad0a7   clr!FieldDesc::GetStaticAddressHandle+0xdd
03 0019f4ec 00680897   clr!ArrayNative::InitializeArray+0x11a
0:000&gt; bp 64b63be5
0:000&gt; g
eax=00402928 ebx=00624044 ecx=0071344c edx=0071dc28 esi=00624044 edi=00624de0
eip=64b63be5 esp=0019f40c ebp=0019f410 iopl=0         nv up ei pl nz na pe nc
cs=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00000206
clr!Module::GetRvaField+0x40:
64b63be5 5e            pop   esi

</code></pre>
<p>从输出看,上面的 <code>GetRvaField</code> 方法的返回值会送到 eax 上,接下来我们验证下 eax 上的值是不是参数 100,200,300 。</p>
<pre><code class="language-C#">
0:000&gt; dp eax L3
0040292800000064 000000c8 0000012c

</code></pre>
<p>上面三个就是 16进制的表示,接下来我们再验证下这三个值是怎么赋到初始化数组中的,可以用 <code>ba</code> 命令对 内存地址 进行拦截。</p>
<pre><code class="language-C#">
0:000&gt; ba r4 023e2338 + 0x8
0:000&gt; ba r4 023e2338 + 0x8 + 0x4
0:000&gt; ba r4 023e2338 + 0x8 + 0x4 + 0x4
0:000&gt; g
Breakpoint 3 hit
eax=0000000c ebx=00000000 ecx=00000003 edx=00000064 esi=00402928 edi=023e2340
eip=6a91d68b esp=0019f440 ebp=0019f4ec iopl=0         nv up ei pl nz na pe nc
cs=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00000206
VCRUNTIME140_CLR0400!memcpy+0x50b:
6a91d68b 83c704          add   edi,4
0:000&gt; g
Breakpoint 4 hit
eax=0000000c ebx=00000000 ecx=00000002 edx=000000c8 esi=0040292c edi=023e2344
eip=6a91d68b esp=0019f440 ebp=0019f4ec iopl=0         nv up ei pl nz na po nc
cs=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00000202
VCRUNTIME140_CLR0400!memcpy+0x50b:
6a91d68b 83c704          add   edi,4
0:000&gt; g
Breakpoint 5 hit
eax=0000000c ebx=00000000 ecx=00000001 edx=0000012c esi=00402930 edi=023e2348
eip=6a91d68b esp=0019f440 ebp=0019f4ec iopl=0         nv up ei pl nz na po nc
cs=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00000202
VCRUNTIME140_CLR0400!memcpy+0x50b:
6a91d68b 83c704          add   edi,4

0:000&gt; dp 023e2338 L5
023e23386368426c 00000003 00000064 000000c8
023e23480000012c

</code></pre>
<p>接下来稍微解释下 <code>ba r4 023e2338 + 0x8</code> 命令。</p>
<ul>
<li>
<p>023e2338 是初始化数组的首地址。</p>
</li>
<li>
<p>023e2338+0x8 初始化数组第一个元素的地址。</p>
</li>
<li>
<p>023e2338 + 0x8 + 0x4初始化数组第二个元素的地址。</p>
</li>
<li>
<p>r4按4byte对地址块读写进行监控。</p>
</li>
</ul>
<p>当三个断点命中后,可以看到初始化数组 <code>023e2338</code> 上的三个元素值都已经填上了,就说这么多吧,相信大家对 params 机制有一定的理解。</p>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/16164756.html
頁: [1]
查看完整版本: C#语法糖系列 —— 第一篇:聊聊 params 参数底层玩法