前七天,我一路追著自己的程式往底層走。
從最開始寫下的 C#
Console.WriteLine("Hello");
一路追到 Compiler、DLL、IL、JIT,最後才真正理解一段 C# 程式是怎麼變成 CPU 可以執行的 Machine Code。
但當我準備開始打造自己的 Mini Web Framework 時,我突然發現了一個以前從來沒有認真想過的問題。
我的 Program.cs 明明只有:
Console.WriteLine("第一行");
Console.WriteLine("第二行");
Console.WriteLine("第三行");
沒有 class Program。
也沒有:
static void Main(string[] args)
{
}
可是按下執行後,程式卻知道要從第一行開始執行。
那麼問題來了,如果我根本沒有寫 Main(),程式到底從哪裡開始?
以前學 C# 時,我對 Main() 的印象一直是:Main() 就是程式開始的地方。
傳統的 C# 程式通常長這樣:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello");
}
}
所以我一直很自然地認為:
啟動程式
↓
找到 Main()
↓
開始執行
但我的 Mini Web Framework 根本沒有這段程式。
它只有:
Console.WriteLine("第一行");
Console.WriteLine("第二行");
Console.WriteLine("第三行");
沒有 Main(),真的能執行嗎?
我先把 Program.cs 寫成:
Console.WriteLine("第一行");
Console.WriteLine("第二行");
Console.WriteLine("第三行");
仔細檢查後,可以確定:
整個檔案裡完全沒有:
Main()
但 Build 成功,而且程式依然可以正常執行。
也就是說,我沒有自己寫 Main()並不代表程式沒有辦法找到開始執行的位置。
那這個位置到底在哪裡?
Build 完成後,我打開:
MiniWebFramework.dll
接著找到 Program。
結果,我看到了一個很奇怪的東西:
點進去後,更重要的線索出現了:
.method private hidebysig static void '$'(string[] args) cil managed
而下面還有:
.entrypoint

這就奇怪了我的 C# 裡沒有 Main()
但是編譯完成的 DLL 裡卻出現了 $
而且它還被標示為:
.entrypoint
所以,我開始懷疑這個 Main 不是我寫的,而是 Compiler 幫我產生的。
查資料後,我才知道,原來我現在使用的是:
Top-level statements(頂層陳述式)
也就是我們可以直接寫:
Console.WriteLine("Hello");
而不必每次都寫:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello");
}
}
但這並不代表 Main() 的概念完全消失了。
更精確地說:
C# Compiler 會替 Top-level statements 產生程式的進入點。
所以原始碼看起來只有:
Console.WriteLine("Hello");
經過 Compiler 後,仍然會有一個讓程式開始執行的位置。
這也解釋了為什麼我會在 ILDasm 裡看到$
那如果我自己寫 Main() 呢?
既然 Compiler 可以替我處理,那我很好奇:
如果改回以前熟悉的寫法,結果會有什麼不同?
所以我把程式改成:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("第一行");
Console.WriteLine("第二行");
Console.WriteLine("第三行");
}
}
重新 Build再用 ILDasm 打開 DLL。

這一次,原本的$真的變成了Main
而且裡面同樣出現: .entrypoint
兩次實驗放在一起,差異就變得非常明顯。

我寫的 C# ILDasm:
Top-level statements
沒有自己寫 Main():
Compiler 產生 $
自己宣告 static void Main() 出現 Main
但它們有一個共同點:
.entrypoint
做到這裡,我才發現自己以前的理解其實少了一層。
以前我認為Main() = 程式的起點
但今天更精確的理解應該是可執行程式需要一個 Entry Point(進入點)。
Main() 是我們非常熟悉的一種呈現方式。
當我自己寫:
static void Main(string[] args)
它可以成為程式的 Entry Point。
今天最大的發現,不是 Top-level statements 可以少寫幾行程式碼。
而是我終於把:
Main()
和:
Entry Point
這兩個以前幾乎畫上等號的概念分開了,我以前看到 Main(),只知道,程式從這裡開始。
今天我第一次真正去追:
為什麼程式知道要從這裡開始?
甚至當我沒有自己寫 Main() 時,我還能透過 ILDasm 親眼看到 Compiler 替我產生的入口。
這讓 Program.cs 不再只是「每次建立專案都會自動出現的檔案」。我開始知道它在整個程式啟動流程裡扮演什麼角色。
這件事情對接下來的 Mini Web Framework 非常重要,因為未來我們會逐漸把:
Console.WriteLine("Hello");
變成真正的 Framework 啟動流程。
到時候,我們會需要:
Entry Point
↓
建立 Framework
↓
啟動 Server
↓
等待 Request
↓
處理 Request
↓
回傳 Response
也就是說,今天找到的 Entry Point,其實就是未來整個 Mini Web Framework 的第一站。
如果連程式從哪裡開始都不知道,就很難真正理解後面的 Web Server、Router、Middleware 到底是在什麼時候被建立、又是在什麼時候開始工作的。
現在,我已經知道程式從哪裡開始了。
但新的問題馬上出現。
我們目前寫的程式:
Console.WriteLine("第一行");
Console.WriteLine("第二行");
Console.WriteLine("第三行");
執行完第三行之後程式就結束了。
可是 Web Server 明明不一樣。
我打開一個網站時,Server 不可能:
啟動
↓
執行三行程式
↓
結束
它必須一直活著,一直等待新的 Request。
所以明天,我們要追下一個問題:
為什麼一般程式執行完就結束,但 Web Server 可以一直等 Request?
這會是我們第一次真正從「C# 程式怎麼執行」,走進:Web Framework 到底是怎麼運作的。