創建、檢查和反編譯世界上(幾乎)最短的 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.pswp.cn/news/283756.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/283756.shtml
英文地址,請注明出處:http://en.pswp.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;變量類型。變量名。變量值。如&…