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


    首先说说为什么要写这个系列,大概有两点原因。

    1. 这种文章阅读量确实高...
    2. 对 IL 和 汇编代码 的学习巩固

    所以就决定写一下这个系列,如果大家能从中有所收获,那就更好啦!

    一:params 应用层玩法

    首先上一段 测试代码

    
        class Program
        {
            static void Main(string[] args)
            {
                Test(100, 200, 300);
                Test();
            }
    
            static void Test(params int[] list)
            {
                Console.WriteLine($"list.length={list.Length}");
            }
        }
    
    

    输出结果如下:

    可以看出如果给 方法形参 加上 params 前缀,在传递 方法实参 上就特别灵活,点赞。

    接下来我们来看看,这么灵活的 实参传递 底层到底是怎么玩的?我们先从 IL 层面探究下。

    二:IL 层面解读

    ILSpy 打开我们的 exe ,看看 IL 代码

    
    .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 [mscorlib]System.Int32
    	IL_0007: dup
    	IL_0008: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>'::'9AC9CF706FBD14D039E0436219C5D852927E5F69295F2EF423AE897345197B2A'
    	IL_000d: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
    	IL_0012: call void ConsoleApp2.Program::Test(int32[])
    	IL_0017: nop
    	IL_0018: ldc.i4.0
    	IL_0019: newarr [mscorlib]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 [1]
    		.custom instance void [mscorlib]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 [mscorlib]System.Int32
    	IL_000e: call string [mscorlib]System.String::Format(string, object)
    	IL_0013: call void [mscorlib]System.Console::WriteLine(string)
    	IL_0018: nop
    	IL_0019: ret
    } // end of method Program::Test
    
    

    上面是 MainTest 方法的IL代码,我们逐一看一下。

    1. Test 方法

    int32[] list 参数看并没有所谓的 params,这也就说明它是 C#编译器 玩的一个手段而已,在方法调用前就已经构建好了。

    2. Main方法

    可以看到 IL 层面的 Test(100, 200, 300) 已经变成了下面五行代码。

    
    	IL_0002: newarr [mscorlib]System.Int32
    	IL_0007: dup
    	IL_0008: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>'::'9AC9CF706FBD14D039E0436219C5D852927E5F69295F2EF423AE897345197B2A'
    	IL_000d: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
    	IL_0012: call void ConsoleApp2.Program::Test(int32[])
    
    

    逻辑大概就是:

    • newarr 构建初始化 int[] 数组。
    • ldtoken 从程序元数据中提取 1,2,3
    • InitializeArray 来将 1,2,3 构建到数组中。

    有了这些指令,我相信 JIT 就知道怎么做了。

    再看 Test() 的 IL 指令只有一行 newarr [mscorlib]System.Int32 初始化。

    所以本质上来说,就是提前构建好 array,然后进行参数传递,仅此而已。。。

    三:汇编层面解读

    有了 newarr + ldtoken + call 三条指令,接下来我们读一下汇编层做了什么,使用 windbg 打开 exe,简化后的汇编代码如下:

    
    0:000> !U /d 009b0848
    Normal JIT generated code
    ConsoleApp2.Program.Main(System.String[])
    Begin 009b0848, size 77
    
    D:\net5\ConsoleApp4\ConsoleApp2\Program.cs @ 14:
    >>> 009b0848 55              push    ebp
    009b0849 8bec            mov     ebp,esp
    009b084b 83ec10          sub     esp,10h
    009b084e 33c0            xor     eax,eax
    009b0850 8945f4          mov     dword ptr [ebp-0Ch],eax
    009b0853 8945f8          mov     dword ptr [ebp-8],eax
    009b0856 8945f0          mov     dword ptr [ebp-10h],eax
    009b0859 894dfc          mov     dword ptr [ebp-4],ecx
    009b085c 833df042710000  cmp     dword ptr ds:[7142F0h],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 <PERF> (mscorlib_ni+0x185e) (6367185e)
    009b0870 ba03000000      mov     edx,3
    009b0875 e8b229d5ff      call    0070322c (JitHelp: CORINFO_HELP_NEWARR_1_VC)
    009b087a 8945f4          mov     dword ptr [ebp-0Ch],eax
    009b087d b9e04d7100      mov     ecx,714DE0h
    009b0882 e819c91f64      call    clr!JIT_GetRuntimeFieldStub (64bad1a0)
    009b0887 8945f8          mov     dword ptr [ebp-8],eax
    009b088a 8d45f8          lea     eax,[ebp-8]
    009b088d ff30            push    dword ptr [eax]
    009b088f 8b4df4          mov     ecx,dword ptr [ebp-0Ch]
    009b0892 e8f9c61f64      call    clr!ArrayNative::InitializeArray (64bacf90)
    009b0897 8b4df4          mov     ecx,dword ptr [ebp-0Ch]
    009b089a ff15904d7100    call    dword ptr ds:[714D90h] (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 <PERF> (mscorlib_ni+0x185e) (6367185e)
    009b08a6 33d2            xor     edx,edx
    009b08a8 e87f29d5ff      call    0070322c (JitHelp: CORINFO_HELP_NEWARR_1_VC)
    009b08ad 8945f0          mov     dword ptr [ebp-10h],eax
    009b08b0 8b4df0          mov     ecx,dword ptr [ebp-10h]
    009b08b3 ff15904d7100    call    dword ptr ds:[714D90h] (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
    
    

    1. newarr

    可以很清楚的看到,newarr 调用了 CLR 中的jithelper函数 CORINFO_HELP_NEWARR_1_VC 下的 JIT_NewArr1 方法,大家有兴趣可以看下 jitheapler.cpp,调用完之后,初始化数组就出来了。

    从dp看,数组只申明了 length=3,还并没有数组元素,也就说所谓的初始化。

    2. ldtoken & InitializeArray

    刚才也说到了, ldtoken 是在运行时提取元数据,那就必须要解析 PE 头,在 clr 层面有一个 PEDecoder::GetRvaData 方法就是用来解析运行时数据,它是发生在 ArrayNative::InitializeArray 方法中,所以我们下两个 bu 命令拦截。

    
    0:000> bu clr!ArrayNative::InitializeArray
    0:000> bu clr!PEDecoder::GetRvaData
    0:000> 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=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
    clr!ArrayNative::InitializeArray:
    64bacf90 6a78            push    78h
    0:000> 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=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
    clr!PEDecoder::GetRvaData:
    64befcfc 55              push    ebp
    0:000> 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> bp 64b63be5
    0:000> 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=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
    clr!Module::GetRvaField+0x40:
    64b63be5 5e              pop     esi
    
    

    从输出看,上面的 GetRvaField 方法的返回值会送到 eax 上,接下来我们验证下 eax 上的值是不是参数 100,200,300 。

    
    0:000> dp eax L3
    00402928  00000064 000000c8 0000012c
    
    

    上面三个就是 16进制的表示,接下来我们再验证下这三个值是怎么赋到初始化数组中的,可以用 ba 命令对 内存地址 进行拦截。

    
    0:000> ba r4 023e2338 + 0x8
    0:000> ba r4 023e2338 + 0x8 + 0x4
    0:000> ba r4 023e2338 + 0x8 + 0x4 + 0x4
    0:000> 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=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
    VCRUNTIME140_CLR0400!memcpy+0x50b:
    6a91d68b 83c704          add     edi,4
    0:000> 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=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
    VCRUNTIME140_CLR0400!memcpy+0x50b:
    6a91d68b 83c704          add     edi,4
    0:000> 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=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
    VCRUNTIME140_CLR0400!memcpy+0x50b:
    6a91d68b 83c704          add     edi,4
    
    0:000> dp 023e2338 L5
    023e2338  6368426c 00000003 00000064 000000c8
    023e2348  0000012c
    
    

    接下来稍微解释下 ba r4 023e2338 + 0x8 命令。

    • 023e2338 是初始化数组的首地址。

    • 023e2338+0x8 初始化数组第一个元素的地址。

    • 023e2338 + 0x8 + 0x4 初始化数组第二个元素的地址。

    • r4 按4byte对地址块读写进行监控。

    当三个断点命中后,可以看到初始化数组 023e2338 上的三个元素值都已经填上了,就说这么多吧,相信大家对 params 机制有一定的理解。

    图片名称
  • 相关阅读:
    Vue:object变化侦测
    Rust 围炉札记
    什么是单元测试(unit testing)
    bp神经网络预测模型优点,bp人工神经网络模型
    搭建智能桥梁,Amazon CodeWhisperer助您轻松编程
    Flutter 2.8 正式发布
    java+python离退休人员工资管理系统
    小程序自定义tabbar如何显示隐藏
    mysql5.7.35安装配置教程【超级详细安装教程】
    【OpenCV 例程200篇】208. Photoshop 对比度自动调整算法
  • 原文地址:https://www.cnblogs.com/huangxincheng/p/16164756.html