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}");}}

输出结果如下:

f1f0eb163546f1b202cce67aa320e077.png

可以看出如果给 方法形参 加上 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.entrypointIL_0000: nopIL_0001: ldc.i4.3IL_0002: newarr [mscorlib]System.Int32IL_0007: dupIL_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: nopIL_0018: ldc.i4.0IL_0019: newarr [mscorlib]System.Int32IL_001e: call void ConsoleApp2.Program::Test(int32[])IL_0023: nopIL_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 8IL_0000: nopIL_0001: ldstr "list.length={0}"IL_0006: ldarg.0IL_0007: ldlenIL_0008: conv.i4IL_0009: box [mscorlib]System.Int32IL_000e: call string [mscorlib]System.String::Format(string, object)IL_0013: call void [mscorlib]System.Console::WriteLine(string)IL_0018: nopIL_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.Int32IL_0007: dupIL_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 77D:\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              nopD:\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              nopD:\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              nopD:\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,调用完之后,初始化数组就出来了。

1778f42da390944f2c6e5af97ea12eab.png

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/288316.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

nginx 修改配置文件使之支持pathinfo,且隐藏index.php

声明环境&#xff1a; nginx centos6.8 使用lnmp一键包搭建环境&#xff08;2019年2月19日 &#xff09;以前使用过别的办法去修改配置文件&#xff0c;但是过于繁琐&#xff0c;最近发现新版本中&#xff0c;在nginx 的 conf目录下发现了文件“enable-php-pathinfo.conf”&am…

Android开发经典笔试面试题汇总(持续更新中)

1.我们都知道Handler是线程与Activity通信的桥梁&#xff0c;假设线程处理不当。你的机器就会变得非常慢&#xff0c;那么线程销毁的方法是&#xff1a;&#xff08;A&#xff09;A. onDestroy()B. onClear()C. onFinish()D. onStop()理解&#xff1a;正确销毁线程的方法应该是…

WCF服务自托管(Self-Hosting)

WCF寄宿方式是一种非常灵活的操作,可以寄宿在各种进程之中,常见的寄宿有:IIS服务、Windows服务、Winform程序、控制台程序中进行寄宿,从而实现WCF服务的运行,为调用者方便、高效提供服务调用。 当客户端和服务端之间的进程有明确的边界时,使用自托管,需要注意的…

学习进度04

第五周所花时间&#xff08;分钟&#xff09;100代码量&#xff08;行&#xff09;50博客量&#xff08;篇&#xff09;2了解到的知识点作为合作的团队要制定统一的标准转载于:https://www.cnblogs.com/liguoshuai/p/5352590.html

android 微信分享gif图,android后台动态创建图片并实现微信分享

今天就记录一下。先说明一下&#xff0c;之前没有做过类似的东西&#xff0c;百度了一两天才知道&#xff0c;说来很惭愧、有点笨&#xff0c;只能这样说。在我的脑里只明白&#xff0c;如果要动态创建图片&#xff1a;一、就是new 嘛二、就是LayoutInflater.from()这种。而微信…

python字符串前面去两位_在Python 3中删除字符串文字前面的'b'字符do

I am new in python programming and i am a bit confused. I try to get the bytes from a string to hash and encrypt but i gotb...b character in front of string just like the below example. Is any way avoid this?.Can anyone give a solution? Sorry for this si…

C语言之去掉https链接的默认443端口

1 问题 去掉https链接的默认443端口 2 代码实现 #include <stdio.h> #include <string.h> #include <stdlib.h>#define BOOL int #define TRUE 1 #define FALSE 0/**判断字符串str1是不是str2开头*/ int is_begin_with(const char *str1, char *str2) {if(…

仅需一个参数就可搞定OneProxy的VIP机制

文章转自&#xff1a;http://card.weibo.com/article/h5/s#cid1001603863326047255626&vid&extparam&from&wm0&ip182.50.119.226现在都讲求无单点的架构&#xff0c;OneProxy也不例外&#xff0c;虽然已经有进程级别的自动重起机制&#xff08;--keepalive…

关于在Windows下AndroidStudio.使用React-Native开发android报错红屏“run react-native start”解决

以下是报错&#xff0c;不过他已经给了解决办法&#xff0c;报错提示的大概中文译为“无法加载脚本&#xff0c;请确保你的Metro服务以及那个包正确”&#xff0c;由于我个人并不是专业安卓&#xff0c;公司项目没办法就上了&#xff0c;所以我就不关包了&#xff0c;包肯定是正…

AsyncLocalT在链路追踪中的应用

前言在项目生产中日志的记录是必不可少的&#xff0c;在.net项目中&#xff0c;要说日志组件&#xff0c;log4net绝对可有一席之地&#xff0c;随着公司业务的发展&#xff0c;微服务则必定无可避免。在跨服务中通过日志进行分析性能或者排查故障点&#xff0c;如何快速定位日志…

Windows运行命令大全

要打开Windows XP系统自带的程序,菜鸟一般是用鼠标点开始/程序(或桌面、快速启动)里的快捷方式,老鸟喜欢按下Win+R并输入运行命令。Windows XP的开始运行命令大全,其实都是C:\WINDOWS\system32下面的程序,只要在开始/运行/输入相应的程序名(命令)即可。对于常用运行命令…

【转】js老生常谈之this,constructor ,prototype

前言 javascript中的this,constructor ,prototype&#xff0c;都是老生常谈的问题&#xff0c;深入理解他们的含义至关重要。在这里&#xff0c;我们再来复习一下吧&#xff0c;温故而知新&#xff01; this this表示当前对象&#xff0c;如果在全局作用范围内使用this&#xf…

Linux+Oracle+12c+RAC+安装配置详细-GI安装

IP地址主机名用途实例名192.168.12.58oracle-rac03-db03 Public ip (节点1&#xff09;192.168.12.59oracle-rac04-db04Public ip(节点2&#xff09;192.168.12.73 rac03-db03-vipvip(节点1&#xff09;racdb3 asm1192.168.12.74rac04-db04-vipvip(节点2&#xff09;racdb4 …

android auto answer,Incoming call auto answer in android 4.0.3

问题I am working in Android technology last 1 years. Currently I want develop an application incoming call auto answer in Android 4.0.3 but in this version I am not getting any solution or cant find any api for this (ITelephony.aidl). Please suggest me.回答…

剑指offer之判断二叉树是不是平衡二叉树

1 问题 判断二叉树是不是平衡二叉树 平衡二叉搜索树&#xff08;Self-balancing binary search tree&#xff09;又被称为AVL树&#xff08;有别于AVL算法&#xff09;&#xff0c;且具有以下性质&#xff1a;它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1&#x…

主成分分析法_数学建模 || 葡萄酒的评价(1)主成分分析法

首先要说一下&#xff0c;这篇文章我在其他的平台发过&#xff0c;也是本人亲自写的&#xff0c;如果大家觉得眼熟的话放弃轻松&#xff0c;没有抄袭&#xff0c;主要是因为与我这 “葡萄酒的评价” 题目成系列了&#xff0c;因此在这里再把这个贴出来。2012 年 A 题葡萄酒的评…

微信jssdk开发,PHP,必要步骤

微信的文档几个重叠在一起&#xff0c;有点乱&#xff0c;自己用到就统计一下&#xff0c;减少字数直接上&#xff0c;毕竟懒。 一般说明步骤一&#xff1a;微信jssdk使用必须在微信公众平台进入其公众号设置&#xff0c;打开配置安全域名才可以。 安全域名则是请求调用微信接…

imagePreview接口调用微信自带图片播放器

2019独角兽企业重金招聘Python工程师标准>>> 在微信浏览器中&#xff0c;出现在网页上的图片通过点按一小段时间&#xff0c;可以调出微信隐藏的图片播放器&#xff0c;在播放器中看图可以随意放大缩小&#xff0c;体验更炫酷。不过这个功能默认只对通过微信后台编辑…

使用IdentityServer出现过SameSite Cookie这个问题吗?

原文作者&#xff1a;Sebastian Gingter原文链接&#xff1a;https://reurl.cc/Dygrgd译者&#xff1a;沙漠尽头的狼译文链接&#xff1a;https://reurl.cc/1ZYNoQ本文是作者2019年的一篇分享&#xff0c;里面的一些观点和使用的技术&#xff0c;对我们现在的开发依然有效&…

windows下sc create命令行添加/创建/修改服务

C:\Users\Administrator>HELP SC 描述: SC 是用于与服务控制管理器和服务进行通信的命令行程序。 用法: sc <server> [command] [service name] <option1> <option2>... 选项 <server> 的格式为 "\\ServerName"…