创建、检查和反编译世界上(几乎)最短的 C# 程序

创建、检查和反编译世界上(几乎)最短的 C# 程序

原文来自https://www.stevejgordon.co.uk/creating-inspecting-decompiling-the-worlds-smallest-csharp-program

253384af8854028f197780ffb7090c22.png

在这篇文章中,我认为创建世界上(几乎)最短的 C# 程序然后深入研究幕后发生的一些细节可能会很有趣。这篇文章不是为了解决现实世界的问题,但我希望你花时间阅读它是值得的。通过花时间深入研究我们日常认为理所当然的一些功能,我希望我们可以一起了解更多关于我们的代码如何转换为可以执行的东西的知识。

创建控制台应用程序

我们将通过从新项目对话框中选择“Console App”模板开始在 Visual Studio 中使用。

12808d21a7551367a6c74b0af618bd38.png
img

我们提供项目名称、位置和解决方案名称。这只是为了好玩,所以你可以看到我没有选择任何花哨的东西!不错的 ConsoleApp3,如果我不是在新安装的机器上写这个,我们可能至少在 ConsoleApp80 上!

40d6d281cc86b42e6c4d3ef5f05d0311.png
img

从 .NET 5 和 C# 9 开始,控制台应用程序模板默认使用Top-Level语句。我们将在此处使用Top-Level语句,但对于那些不是粉丝的人,在 Visual Studio 17.2 及更高版本中,您现在可以选中标记为“不使用Top-Level语句”的选项以更喜欢经典模板。

29dd7424fa97e5b2f99e9a8f29e5b899.png
img

片刻之后,相关文件被创建并且 Program.cs 文件被加载到编辑器中。

8d4cb06d6ad47543683943e3cb2c4c35.png
img

最初的应用程序已经很基础了,但我们可以进一步简化它。如果我们删除现有代码,我们可以用一条语句替换它。

return;
8cb88982fbf4a66d31a9d102bb4e97f0.png
img

这几乎是我们可以开发的最小、最短的 C# 程序,长度为 7 个字符。也许有人知道写更短的东西的技巧。

编辑:事实证明,有人这样做。正如nietras[1]在 Twitter 上[2]向我指出的那样,您可以使用空语句块 {} 减少到两个字符。好的!查看他们的博客文章[3]以获取更多详细信息。这就是现在最短的 C# 程序之一!

我们的单行代码是一个语句——它执行一个动作。C# 是一种编程语言,与所有人类语言一样,在结构、语法和语法方面必须遵循一些规则。该语言的语法由标记组成,这些标记可以一起解释形成更大的结构来表示声明、语句、表达式等。在我们的代码行中,我们有一个返回关键字标记,后跟一个分号标记。这一起表示将执行的单个语句。

return 语句属于一组称为跳转语句的语句。跳转语句将控制权转移到程序的另一部分。当在方法中到达 return 语句时,程序返回到调用它的代码,即调用者。为了理解这个特定的跳转语句,我们需要在几分钟内深入挖掘。

在我们运行应用程序之前,我将进行进一步的更改,以帮助我们在后面的帖子中区分事物。我要将 Program.cs 文件重命名为 TopLevel.cs 并保存应用程序。

5024f501624d93c1c4b37f213b08159c.png
img

执行应用程序

我们可以构建和运行这个应用程序,正如我们所料,它做的很少。Visual Studio 开发者控制台的输出如下:

C:\Users\SteveGordon\Code\Temp\ConsoleApp3\ConsoleApp3\bin\Release\net6.0\ConsoleApp3.exe (process 34876) exited with code 0. ``Press any key to close this window . . .

如果我们使用 dotnet run 和终端的发布配置执行项目,我们根本看不到任何事情发生。

PS C:\Users\SteveGordon\Code\Temp\ConsoleApp3\ConsoleApp3> dotnet run -c release``PS C:\Users\SteveGordon\Code\Temp\ConsoleApp3\ConsoleApp3>

因此,我们的简单应用程序是有效的,并且可以毫无例外地执行。它返回一个零退出代码,这意味着它完成而没有错误。下一个问题是,怎么做?运行时是否已更新以支持此类程序?

答案是,不,这是一个编译器功能,它似乎可以神奇地处理此类代码,在编译期间生成有效的 C# 程序。让我们来看看实际发生了什么。

汇编“魔术”

我们在编辑器或 IDE 中编写的代码可以利用许多 C# 语言功能。当我们构建应用程序时,编译器获取我们的代码并生成 .NET IL(中间语言)字节码。IL(在某些文档中又称为 MSIL 和 CIL)包括一组通用指令,可以通过编译 .NET 语言生成。这种中间形式是最终机器代码指令的垫脚石。.NET 通过称为即时编译的过程来实现这一点。当第一次调用方法时,JIT (RyuJIT) 采用 IL 字节码并生成特定于机器架构的指令。我们现在不会深入研究更详细的细节,重要的一点是有两个阶段可以得到最终的机器码。第一阶段,编译为 IL 发生在我们构建应用程序时,然后再部署它。第二阶段,

一些新的语言特性可能需要运行时更改来支持它们,但通常会避免这种情况。大多数功能都是在编译时实现的。后面的这些功能使用称为降低的东西将某些高级语言结构转换为更简单的结构,然后可以更轻松、更优化地转换为 IL。降低经常发生,通常不是我们需要考虑得太深的事情。编译器知道如何最好地转换我们编写的代码,以便将其编译成最终的 IL。

Top-Level语句是编译器功能,当我们使用它们时会发生一些神奇的事情。好吧,好吧,这不是魔术,只是在我们的代码中满足各种条件时巧妙地使用编译器。我们可以通过反编译我们的代码来了解更多。

检查和反编译代码

为了理解使我们的简短语句成为有效 C# 程序的机制,我们将检查生成的 DLL 并反编译代码。

作为构建过程的输出生成的 DLL 文件包含 IL 指令,以及运行时用来执行托管代码的 .NET 元数据。我们可以用来检查该文件中数据的一种工具是 ILDASM,它与 Visual Studio 一起安装。在我的机器上,我可以打开 Visual Studio 开发人员命令提示符并导航到包含我的控制台应用程序的构建工件的目录,针对位于那里的 DLL 文件启动 ILDASM。

ConsoleApp3\ConsoleApp3\bin\Release\net6.0> ildasm consoleapp3.dll

ILDAM 加载,显示控制台应用程序的类型和元数据。

54e94c891a09515be693718e1e099799.png
img

最值得注意的观察是,我们似乎有一个名为 Program 的东西,它看起来非常像是一个类,它就是!它包括类元数据、构造函数方法和另一种方法。这个方法被命名为

$,看起来像一个 void 返回方法,接受一个字符串数组参数。这个签名是不是很熟悉?我们可以在 ILDASM 上多花一些时间,但让我切换到另一个反编译器工具。对于下一步,我们有几个选择,所有这些都是免费工具。

•iLSpy[4]•Jetbrains dotPeek[5]•Telerik JustCompile[6]

所有这些都是有效的选择,主要取决于偏好问题。它们在核心功能方面具有非常相似的特性。我将使用 dotPeek,这是我在这些情况下最常用的工具。使用 dotPeek 打开 DLL 后,我们会看到程序集的树视图,与我们在 ILDASM 中看到的并无太大区别。

c0f4e152c631d31d27029366bbc35bb2.png
img

在根命名空间下面,我们可以再次观察到具有$ 方法的 Program 类。这个是从哪里来的?我们很快就会回答这个问题。在我们开始之前,让我们探索一下 dotPeek 还能向我们展示什么。

通过右击Program类,我们可以选择查看反编译的源码。这将获取程序集的 IL 代码并反转编译过程以返回 C# 代码。反编译代码的确切性质可能因工具而异。有时,必须使用最佳猜测来确定原始代码的外观以及可能使用了哪些 C# 语言功能。

这是我从 dotPeek 得到的结果:

using System.Runtime.CompilerServices;[CompilerGenerated]
internal class Program
{private static void <Main>$(string[] args){}public Program(){base..ctor();}
}

关于这里发生的事情的第一个提示是 Program 类的 CompilerGenerated 属性。这个类在我们的代码中不存在,但是编译器已经为我们生成(发出)了一个。该类包含一个静态 void 方法,其名称稍有不同寻常:

$这是编译器代表我们生成的合成入口点。编译器生成的类型和成员的名称通常带有不寻常的符号。虽然这样的名称在我们自己的 C# 代码中是非法的,但就 IL 和运行时而言,它们实际上是合法的。编译器生成的代码使用这些名称来避免与我们自己代码中定义的类型和成员的潜在冲突。否则,这个 Main 方法看起来就像我们在不使用Top-Level语句时可能包含在传统应用程序中的任何其他方法。

该类型的另一个方法是空构造函数。我明确配置了 dotPeek 来显示这一点。通常可以在我们自己的代码中跳过一个空的默认构造函数,但是如果我们没有显式声明一个,编译器仍然会添加一个。这个空的构造函数只是调用基类型 Object 的构造函数。

在这一点上,我们开始看到Top-Level语句的“魔力”在起作用。编译器有几个规则来确定应用程序的入口点。编译器现在寻找的一件事是我们的应用程序包含一个包含Top-Level(全局)语句的编译单元的情况。当找到这样的编译单元时,编译器将尝试在编译时发出标准的 Program 类和 main 方法。您会注意到,即使我们将Top-Level语句文件命名为 TopLevel.cs,这对合成 Program 类的类型命名没有影响。按照惯例,模板中的新应用程序有一个名为 Program.cs 的文件,主要是为了与开发人员期望的历史命名保持一致。

不过等一下,我刚才扔了一个新词,我们应该稍微回滚一下。编译单元是什么意思?

在编译期间,编译器对我们的代码进行词法分析(读取标记)并解析,最终构建一个语法树,根据语言规范在树视图中表示源代码。有几种方法可以查看语法树,但一种非常简单的方法是访问SharpLab.io[7]。SharpLab 是另一个非常有用的工具,用于检查浏览器中的反编译代码和 IL 代码。另一个方便的功能是能够查看我们代码的语法树。

558b168b87eac6a827c7f93761af9308.png
img

我们从 TopLevel.cs 文件中的单个 return 语句被解析为上面的树结构,包含几个节点。树的根是 CompilationUnit,它代表我们的源文件。因为我们所有的代码(是的,所有的一行!)都属于这个文件。每个元素都是根下的一个节点。

由 return 关键字标记和分号标记组成的 return 语句是该编译单元所拥有的全部内容。return 语句位于 GlobalStatement 节点下,这是树中Top-Level语句的表示方式。

当编译器遇到包含全局语句的 CompilationUnit,并且没有其他具有全局语句的 CompilationUnit 时,编译器能够识别Top-Level语句功能的使用并在 Program 类中生成合成 main 方法。我们的反编译揭示了这个过程的结果。反编译源中的合成 main 方法为空。我们的顶级代码包含一个 return 语句。任何Top-Level语句都将成为综合 main 方法主体的一部分。在我们的例子中,因为我们有一个空返回,所以方法体中不需要显式声明。当到达方法体的末尾时,它将默认返回。当到达 Main 方法的末尾时,我们的应用程序已完成执行,退出代码为零。

虽然我们不会在这篇文章中对 IL 进行深入探讨,但值得通过探索实际 IL 的样子来总结一下。IL 是一种非常简洁的字节码格式。反编译工具都支持以某种人类可读的形式查看 IL 的方法。请记住,构成该方法的实际指令代码通常只有 DLL 文件中的一或两个字节。这是 dotPeek 的 IL 查看器输出。

.class public auto ansi beforefieldinit Program extends [System.Runtime]System.Object
{.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()= (01 00 00 00 ).method public hidebysig specialname rtspecialname instance void .ctor () cil managed {IL_0000: ldarg.0IL_0001: call instance void [System.Runtime]System.Object::.ctor()IL_0006: ret}.method private hidebysig static void '<Main>$' (string[] args) cil managed {.entrypointIL_0000: ret}
}

详细介绍这一点可能最好留给以后的帖子。我们将把注意力集中在最后一个块上,它包括

$ 方法的信息和说明。我们可以在这个方法中看到一条名为“ret”的 IL 指令。出现在 DLL 文件中的实际指令代码是 0x2A。此语句从方法返回,可能带有返回值。如果您对 IL 的细节和本说明感到好奇,您可以花几个小时阅读ECMA 335 规范[8]

这是一个与 ret 指令有关的例外:

从当前方法返回。当前方法的返回类型(如果有)确定要从堆栈顶部获取并复制到调用当前方法的方法的堆栈中的值的类型。当前方法的评估堆栈应为空,除了要返回的值。

生成的 IL 不包括为我们生成的 void 返回方法压入堆栈的任何内容。

在运行时,即时编译器将 IL 指令进一步编译为运行时机器架构的适当汇编代码。

另一个有趣的亮点是该块顶部的 .entrypoint。这只能包含在应用程序的单个方法中。CIL 标头是 DLL 文件的一部分,它包含一个 EntryPointToken,它将方法定义为入口点。

cc754693d54a75bfa5b8d2a61e7ad01e.png
img

作为有关应用程序的元数据的一部分,存在一个 MethodDef 表,其中包括程序集的方法签名。我们的程序集中有两个,编译器生成的

$ 方法和合成 Program 类的默认构造函数。您会注意到 EntryPointToken 值与 MethodDef 表中$ 方法的标识符相匹配。e68d292d4fc093735160fb2c339ab5b2.png
img

当执行引擎(运行时的一部分)加载我们的程序集时,它会在入口点定位并开始执行我们的托管代码。

我们的入口点所做的就是立即返回。return jump 语句将控制权返回给调用者,在本例中为执行引擎(运行时),应用程序以代码 0 退出。在功能方面不是很令人兴奋,但即便如此,它还是给了我很多可写的东西!

概括

我认为这可能是结束对这个小型 C# 程序的探索的好地方。即使在这个小应用程序中,我们也可以挖掘许多其他有趣的东西。也许,如果人们有兴趣关于内部运作的信息,我将继续将其作为一系列帖子,重点关注其中的一些内容。就个人而言,我发现挖掘一些内部作品非常有趣。

在这篇文章中,我们创建了几乎最短的 C# 程序,编译并执行了它。然后我们对 DLL 进行反编译,以了解我们的单个语句如何导致编译器为我们的应用程序生成一个带有合成入口点的 Program 类。我们了解到,没有“魔法”,只是一个编译功能,它可以检测我们在编译单元正下方对语句的使用。编译器采用这些语句并将它们作为合成 main 方法的主体。在此过程中,我们使用了一些方便的工具,这些工具可用于检查 .NET DLL 中包含的 IL 和元数据,以及将 IL 反编译回有效的 C# 代码。

References

[1] nietras: https://twitter.com/nietras1
[2] 在 Twitter 上: https://twitter.com/nietras1/status/1537026998719197185
[3] 博客文章: https://nietras.com/2021/10/09/worlds-smallest-csharp-program/
[4] iLSpy: https://apps.microsoft.com/store/detail/ilspy/9MXFBKFVSQ13
[5] Jetbrains dotPeek: https://www.jetbrains.com/decompiler/
[6] Telerik JustCompile: https://www.telerik.com/products/decompiler.aspx
[7] SharpLab.io: https://sharplab.io/
[8] ECMA 335 规范: https://www.ecma-international.org/publications-and-standards/standards/ecma-335/

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

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

相关文章

Linux下画原理图和PCB

Linux下画原理图和PCBWindows下大名鼎鼎的Allegro和经典的Protel 99SE都是不支持Linux操作系统的。做Linux驱动开发免不了要看一下原理图和PCB。一般的做法有三种&#xff1a; 1.主机使用Windows系统&#xff0c;将Linux装在VMWARE之类的虚拟机中这样能够使用Windows下的软件看…

配置中心 App Configuration (二):Feature Flag 功能开关特性

写在前面Web服务开发过程中我们经常有这样的需求&#xff1a;某些功能我必须我修改了配置才启用&#xff0c;比如新用户注册送券等&#xff1b;某个功能需到特定的时间才启用&#xff0c;过后就失效&#xff0c;比如春节活动等&#xff1b;某些功能&#xff0c;我想先对10%的用…

oracle临时表空间

--查看临时表空间SELECT * FROM v$tablespace;SELECT * FROM dba_tablespaces;--查看所有临时表空间文件SELECT * FROM dba_data_files;--查看临时临时表空间文件SELECT * FROM dba_temp_files;--查看临时表空间组SELECT * FROM dba_tablespace_groups; --查找默认临时表空间SE…

ES 2022 正式发布!有哪些新特性?

2022 年 6 月 22 日&#xff0c;第 123 届 Ecma 大会批准了 ECMAScript 2022 语言规范[1]&#xff0c;这意味着它现在正式成为标准。 1 ECMAScript 2022编辑 本次发布的编辑有&#xff1a; Shu-yu Guo[2] Michael Ficarra[3] Kevin Gibbons[4] 2 ECMAScript 2022有什么新内…

联想(Lenovo)小新310经典版进bios方法

1&#xff0c;找到novo按钮。 2&#xff0c;在关机的状态下桶一下小孔&#xff0c;不用任何操作&#xff0c;电脑进入bios选择界面。转载于:https://www.cnblogs.com/senior-engineer/p/6761457.html

C#中的匿名类型

这节来讲一下C#中的匿名类型。匿名类在C#中&#xff0c;我们可以不去显示的声明一个类&#xff0c;而是通过匿名类去临时声明一个类结构去帮助我们去完成一些功能。声明一个匿名类&#xff0c;我们可以像下面这样做&#xff1a;var Anonymousnew {name"charles",year…

MySQL之MHA高可用集群

目录 一、MHA概述 1.1.MHA 是什么 1.2.MHA 的组成 1.3.MHA 的特点 二、MHA搭建准备 2.1.实验思路 三、MHA搭建 3.1配置主从复制 3.2.安装 MHA 软件 3.3.故障模拟 3.4.故障修复 四、总结 一、MHA概述 1.1.MHA 是什么 1.MHA&#xff08;MasterHigh Availability&…

Tensorflow之安装

1.fellow the instruction of https://www.tensorflow.org/install/install_linux#installing_with_anaconda 2.anaconda安装&#xff0c;修改~/.bash_profile为 export PATH~/anaconda2/bin:/usr/local/cuda/bin:$PATHexport LD_LIBRARY_PATH/usr/local/cuda/lib64:$LD_LIBRAR…

2、Saltstack的数据系统

一、Grainsgrains是salt用来收集minion端底层系统信息的接口。比如&#xff0c;操作系统type、域名 、IP地址、内存及其他相关系统属性信息等。存储在minion端&#xff0c;用于保存minion端数据信息。minion启动时才加载grains信息&#xff0c;所以他时静态的&#xff0c;Grain…

配置中心 App Configuration (一):轻松集成到Asp.Net Core

写在前面在日常开发中&#xff0c;我这边比较熟悉的配置中心有&#xff0c;携程Apollo&#xff0c;阿里Nacos(配置中心&#xff0c;服务治理一体)之前文章&#xff1a;Asp.Net Core与携程阿波罗(Apollo)的第一次亲密接触总体来说&#xff0c;Apollo和Nacos社区都比较活跃&#…

stop-hbase.sh一直处于等待状态

今天关闭HBase时&#xff0c;输入stop-hbase.sh一直处于等待状态 解决方法&#xff1a; 先输入&#xff1a;hbase-daemon.sh stop master 再输入&#xff1a;stop-hbase.sh就可以关闭HBase集群了。 转载于:https://www.cnblogs.com/lijinze-tsinghua/p/8667761.html

shell编程100例

1、编写hello world脚本 #!/bin/bash# 编写hello world脚本echo "Hello World!"2、通过位置变量创建 Linux 系统账户及密码 #!/bin/bash# 通过位置变量创建 Linux 系统账户及密码#$1 是执行脚本的第一个参数,$2 是执行脚本的第二个参数 useradd "$1" …

sqlserver 分页

select top 10 numComImg.* from( select row_number() over(order by id asc) as rownumber,* from (select * FROM [TCCLine].[dbo].[CLine_CommonImage]) as comImg)as numComImg where rownumber>40select top 10 * --10 为页大小from [TCCLine].[dbo].[CLine_CommonIma…

详解SpringMVC中Controller的方法中参数的工作原理[附带源码分析] good

目录 前言现象源码分析 HandlerMethodArgumentResolver与HandlerMethodReturnValueHandler接口介绍HandlerMethodArgumentResolver与HandlerMethodReturnValueHandler接口的具体应用常用HandlerMethodArgumentResolver介绍常用HandlerMethodReturnValueHandler介绍本文开头现象…

instancing render

当要绘制同一个东西很多次的时候&#xff0c;最简单的想法可能是循环调用glDrawArrays()&#xff0c;但这样会造成性能的损失。因为当显卡在渲染一个物体的时候&#xff0c;可能并不需要太多时间&#xff0c;但系统会花大量的时间&#xff0c;频繁的调用draw命令&#xff0c;再…

对不起,我不是一个自律的人

大家好&#xff0c;我是 &#x1f41f;&#x1f4a8;。前天&#xff0c;星球 的一位大学生朋友问了我几个问题&#xff1a;你大学时如何安排每日的时间&#xff1f;为什么能学那么多技术&#xff1f;你会学习到很晚吗&#xff1f;你是如何保持自律的&#xff1f;我觉得这几个问…

保证接口数据安全的10种方案

前言 大家好&#xff0c;我是程序汪&#xff0c;互联网项目需要特别注意数据安全&#xff0c;如果你简历上是互联网类型项目&#xff0c;安全方面肯定要能说出个一二三&#xff0c;下面分享下这方面的干货&#xff0c;大家可以记住几条&#xff0c;面试时好说道说道 我们日常…

Html5本地存储LocalStorage

HTML5 提供了两种在客户端存储数据的新方法&#xff1a; localStorage - 没有时间限制的数据存储sessionStorage - 针对一个 session 的数据存储在浏览器中打开审查元素&#xff08;如谷歌F12&#xff09;&#xff0c;在Resources下面可以查看里面的数据。 localStorage提供了几…

python 中的os模块

python os模块 Python os 模块提供了一个统一的操作系统接口函数一、对于系统的操作1、os.name 当前使用平台其中 ‘nt’ 是 windows&#xff0c;’posix’ 是linux 或者 unix2、os.sep输出操作系统的特定的路径分隔符。Win下为“\”&#xff0c;Linux下为“/”3、os.pathsep 输…

java第一季2.2

2019独角兽企业重金招聘Python工程师标准>>> 标识符&#xff1a; 是给变量类方法命名的符号、标识符开头可以_、字母、$命名&#xff0c;不可以用数字命名。关键字不可命名&#xff0c;大小写区分。不可以用非法字符 变量&#xff1a;变量类型。变量名。变量值。如&…