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

  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 机制有一定的理解。

C#语法糖系列 —— 第一篇:聊聊 params 参数底层玩法的更多相关文章

  1. C#语法糖系列 —— 第三篇:聊聊闭包的底层玩法

    有朋友好奇为什么将 闭包 归于语法糖,这里简单声明下,C# 中的所有闭包最终都会归结于 类 和 方法,为什么这么说,因为 C# 的基因就已经决定了,如果大家了解 CLR 的话应该知道, C#中的类最终 ...

  2. C#语法糖系列 —— 第二篇:聊聊 ref,in 修饰符底层玩法

    自从 C# 7.3 放开 ref 之后,这玩法就太花哨了,也让 C# 这门语言变得越来越多范式,越来越重,这篇我们就来聊聊 ref,本质上来说 ref 的放开就是把 C/C++ 指针的那一套又拿回来了 ...

  3. C#语法糖之第一篇:自动属性&隐式类型

    今天给大家分享一下C#语法糖的简单的两个知识点吧. 自动属性:在 C# 4.0 和更高版本中,当属性的访问器中不需要其他逻辑时,自动实现的属性可使属性声明更加简洁. 客户端代码还可通过这些属性创建对象 ...

  4. 深入理解javascript函数系列第一篇——函数概述

    × 目录 [1]定义 [2]返回值 [3]调用 前面的话 函数对任何一门语言来说都是一个核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即 ...

  5. 深入理解javascript函数系列第一篇

    前面的话 函数对任何一门语言来说都是核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即对象,程序可以随意操控它们.函数可以嵌套在其他函数中 ...

  6. 深入学习jQuery选择器系列第一篇——基础选择器和层级选择器

    × 目录 [1]id选择器 [2]元素选择器 [3]类选择器[4]通配选择器[5]群组选择器[6]后代选择器[7]兄弟选择器 前面的话 选择器是jQuery的根基,在jQuery中,对事件处理.遍历D ...

  7. C#语法糖之第二篇: 参数默认值和命名参数 对象初始化器与集合初始化器

    今天继续写上一篇文章C#4.0语法糖之第二篇,在开始今天的文章之前感谢各位园友的支持,通过昨天写的文章,今天有很多园友们也提出了文章中的一些不足,再次感谢这些关心我的园友,在以后些文章的过程中不断的完 ...

  8. Entity Framework 6.0 入门系列 第一篇

    Entity Framework 6.0 入门系列 第一篇 好几年前接触过一些ef感觉不是很好用,废弃.但是 Entity Framework 6.0是经过几个版本优化过的产物,性能和功能不断完善,开 ...

  9. 深入理解javascript对象系列第一篇——初识对象

    × 目录 [1]定义 [2]创建 [3]组成[4]引用[5]方法 前面的话 javascript中的难点是函数.对象和继承,前面已经介绍过函数系列.从本系列开始介绍对象部分,本文是该系列的第一篇——初 ...

随机推荐

  1. sqlmap之waf绕过

    #一点补充 在老版本的安全狗中,可通过构造payload: http://xx.xx.xx.xx/sqli-labs/Less-2/index.php/x.txt?id=1 and 1=1 可通过in ...

  2. 记录Markdown的学习

    目录 1. 引言 2. 标题 这是一级标题 这是二级标题 这是三级标题 这是四级标题 3. 文字相关 3.1 粗体 3.2 斜体 3.3 粗体和斜体 3.4 删除线 3.5 混合使用 3.6 反引号引 ...

  3. 【仿真】Carla之收集数据快速教程 (附完整代码) [7]

    收集过程可视化展示,随后进入正文: 参考与前言 看到仿真群对这类任务下(用carla收集数据然后再做训练等) 需求量大,顺手马上写一个好了,首先收集数据需要考虑清楚: 收集什么数据,需要什么样的数据格 ...

  4. 迷宫问题,打印所有路径,深度搜索,dfs

    #include<iostream> using namespace std; int maze [5][5] = { 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0 ...

  5. python 元组tuple 增删改查操作

    初始化: data_tuple = () data_tuple1 = (1,) data_tuple2 = tuple() 新增: data_tuple+data_tuple1 (data_tuple ...

  6. 线程池提交任务的两种方式:execute与submit的区别

    Java中的线程池在进行任务提交时,有两种方式:execute和submit方法. 一.execute和submit的区别 execute只能提交Runnable类型的任务,无返回值.submit既可 ...

  7. JS的箭头函数this作用域

    name="小刚"; let student={ name:"小明", printLog:function(){ // 这里绑定了顶层作用域,可以使用变量与方法 ...

  8. HTML 5中的DataList是什么?

    HTML 5中的DataList控件元素有助于提供自动完成功能的文本框,如下图所示. 下面是DataList控件功能的HTML代码: <input list="Country" ...

  9. composer安装报错

    问题报错:Fatal error: Declaration of Fxp\Composer\AssetPlugin\Repository\AbstractAssetsRepository::searc ...

  10. 网络协议之:socket协议详解之Socket和Stream Socket

    目录 简介 Socket是什么 Stream Socket 使用socat创建一个TCP服务器 使用ss检查TCP连接 使用nc连接socket 总结 简介 不管是在普通的网络编程中还是在netty中 ...