C#语法糖系列 —— 第一篇:聊聊 params 参数底层玩法
首先说说为什么要写这个系列,大概有两点原因。
- 这种文章阅读量确实高...
- 对 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
上面是 Main 和 Test 方法的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 参数底层玩法的更多相关文章
- C#语法糖系列 —— 第三篇:聊聊闭包的底层玩法
有朋友好奇为什么将 闭包 归于语法糖,这里简单声明下,C# 中的所有闭包最终都会归结于 类 和 方法,为什么这么说,因为 C# 的基因就已经决定了,如果大家了解 CLR 的话应该知道, C#中的类最终 ...
- C#语法糖系列 —— 第二篇:聊聊 ref,in 修饰符底层玩法
自从 C# 7.3 放开 ref 之后,这玩法就太花哨了,也让 C# 这门语言变得越来越多范式,越来越重,这篇我们就来聊聊 ref,本质上来说 ref 的放开就是把 C/C++ 指针的那一套又拿回来了 ...
- C#语法糖之第一篇:自动属性&隐式类型
今天给大家分享一下C#语法糖的简单的两个知识点吧. 自动属性:在 C# 4.0 和更高版本中,当属性的访问器中不需要其他逻辑时,自动实现的属性可使属性声明更加简洁. 客户端代码还可通过这些属性创建对象 ...
- 深入理解javascript函数系列第一篇——函数概述
× 目录 [1]定义 [2]返回值 [3]调用 前面的话 函数对任何一门语言来说都是一个核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即 ...
- 深入理解javascript函数系列第一篇
前面的话 函数对任何一门语言来说都是核心的概念.通过函数可以封装任意多条语句,而且可以在任何地方.任何时候调用执行.在javascript里,函数即对象,程序可以随意操控它们.函数可以嵌套在其他函数中 ...
- 深入学习jQuery选择器系列第一篇——基础选择器和层级选择器
× 目录 [1]id选择器 [2]元素选择器 [3]类选择器[4]通配选择器[5]群组选择器[6]后代选择器[7]兄弟选择器 前面的话 选择器是jQuery的根基,在jQuery中,对事件处理.遍历D ...
- C#语法糖之第二篇: 参数默认值和命名参数 对象初始化器与集合初始化器
今天继续写上一篇文章C#4.0语法糖之第二篇,在开始今天的文章之前感谢各位园友的支持,通过昨天写的文章,今天有很多园友们也提出了文章中的一些不足,再次感谢这些关心我的园友,在以后些文章的过程中不断的完 ...
- Entity Framework 6.0 入门系列 第一篇
Entity Framework 6.0 入门系列 第一篇 好几年前接触过一些ef感觉不是很好用,废弃.但是 Entity Framework 6.0是经过几个版本优化过的产物,性能和功能不断完善,开 ...
- 深入理解javascript对象系列第一篇——初识对象
× 目录 [1]定义 [2]创建 [3]组成[4]引用[5]方法 前面的话 javascript中的难点是函数.对象和继承,前面已经介绍过函数系列.从本系列开始介绍对象部分,本文是该系列的第一篇——初 ...
随机推荐
- 论php常见的问题[转载自原乌云]
论PHP常见的漏洞 ′ 雨. · 2015/01/14 10:08 0x00 前言 里面很多都是像laterain学习到的, 如果能考上cuit的话 自动献菊花了. 0x01 安装的问题 首先拿到一份 ...
- 进制转换 Java day6
今天周末学习的不多,只学习了一些二进制转十进制,八进制.十六进制,以及数据单位 二进制转十进制 我们都知道十进制转二进制就是除以2取余的方法.那二进制转到十进制又如何处理呢,今天我来学习以下 我们看看 ...
- 航模电池平衡头接线,1S-6S原理图
1-4S平衡头接线 S数代表几级锂电池串联,比如3S代表串联了3级,所以总电压为3*3.7V=11.4V.(每一级可能是多个电芯并联) 图源:百度贴吧 图源:5imx论坛 3S电池示例 B6充电器
- 教你识别一些sequence的相关问题
摘要:通过一些自定义的视图或者查询语句,批量识别集群的sequence相关问题 本文分享自华为云社区<GaussDB(DWS)运维 -- sequence常见运维操作>,作者: 譡里个檔. ...
- mysql索引失效原理(联合索引失效问题)
单值索引B+树图单值索引在B+树的结构里,一个节点只存一个键值对 联合索引开局一张图,由数据库的a字段和b字段组成一个联合索引. 从本质上来说,联合索引也是一个B+树,和单值索引不同的是,联合索引的键 ...
- 说说do...while和while的区别
一.do-while语句 do-while语句的语法: do{ statement }while(expression); 看下面示例: var i=10: do{ i+=2: }while(i< ...
- 学习Apache(四)
介绍 Apache HTTP 服务器被设计为一个功能强大,并且灵活的 web 服务器, 可以在很多平台与环境中工作.不同平台和不同的环境往往需要不同 的特性,或可能以不同的方式实现相同的特性最有效率. ...
- mysql常见命令参数(一)
1.mysql命令常用参数 1.--auto-rehash (tab键自动补全,表名及表字段) # mysql -u root --auto-rehash # vim my.cnf [mysql] a ...
- Lambda8 表达式
Lambda 表达式 Lambda 表达式是 JDK8 的一个新特性,可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构. JDK 也提 ...
- c++中的printf、和cout和cin后面跟指针的问题
printf里面打印指针的问题 而在c语言中,使用printf只需要使用不同的格式就可以区分打印出是字符串还是指针变量的值: cout和cin后面跟指针的问题 cout<<mm 和cin& ...