天天看點

用CIL寫程式:你好,沃爾德

為何是CIL?難道不能直接從C#,VB...編譯成機器碼碼?那為何又是棧式機?難道不能是記憶體,寄存器嗎?詳細分析一下這兩個問題之後,再讓我們從Hello World開始書寫CIL的旅程吧!

前言:

項目緊趕慢趕總算在年前有了一些成績,是以沉寂了幾周之後,小匹夫也終于有時間寫點東西了。以前匹夫寫過一篇文章,對CIL做了一個簡單地介紹,不過不知道各位看官看的是否過瘾,至少小匹夫覺得很不過瘾。是以決定寫幾篇關于CIL的文章,即和各位看官一起進行個交流,同時也是匹夫自己總結和鞏固一下這些知識點。俗話說的好,“萬事開頭,Hello World”,那麼作為匹夫總結CIL的第一篇文章,就從Hello World開始吧。當然,正式開始寫CIL代碼之前,我們還有點閑話要說,那就是運作時的選擇為何是它?為何是CIL?而CIL為何又是基于堆棧的?記憶體或者寄存器難道不是更理想的選擇嗎?

為何是CIL?

開始正文内容之前,匹夫帶領大家先回顧一下《Mono為何能跨平台?聊聊CIL(MSIL)》的簡要内容:首先,用C#寫的代碼被C#的編譯器編譯成CIL(當然除了C#還有很多其他的語言,比如VB等等),之後再有JIT編譯器在程式運作時即時編譯或者AOT(或者NGEN)進行提前編譯将CIL代碼編譯成對應平台的機器碼,最後運作在平台上的便是機器碼。小匹夫在那篇文章中提過,首先将各種不同的語言都統一編譯成CIL,再由CIL編譯成各個平台的機器碼是跨平台的基礎。那麼仔細想想,一定有人會提出這樣的疑問,直接從C#編譯到機器碼,省略掉“多餘”的中間語言,是不是也可行呢?這個問題的确值得讨論,同時也為了小匹夫接下來的文章師出有名,是以首先聊聊CIL的“合法性”(用必要性這個詞也許更好)問題就成了匹夫寫這篇文章的頭等大事。

論據一:考慮下“成本效益”

首先提出我們的論據一,那就是使用CIL這套體系對實作跨平台的開銷要小的多的多。

引入一個“多餘的”中間語言和兩個編譯器(C#----->CIL------>機器碼)聽上去總是要比隻使用一種編譯器(C#-------->機器碼)的實作代價高的多,因為我們的目的是C#代碼能編譯成機器能運作的機器碼,顯然一步到位是最直接有效的方式。相反,引入中間語言之後,我們就需要實作兩種語言的分析和編譯,看上起的确多此一舉。但如果我們考慮到跨平台這個前提,就會發現中間語言是多麼的重要。

假設你可以選擇的語言有N種(比如C#, VB, F#, JScript .NET,Boo...),而我們的目标平台有M種(win,mac,linux,ios,android...)。那麼如果我們采用最直接的編譯方式,即從源代碼直接編譯成機器碼,那麼到底需要多少個編譯器呢?

答案很直接咯:需要N*M種編譯器。因為你需要為每一種語言針對每一個平台寫一個編譯器。

如果我們采用了中間語言呢?

我們隻需要為N種語言寫N種編譯器,将它編譯成CIL代碼。再為M種平台寫M種編譯器,将上一步生成的CIL代碼編譯成M種平台的機器碼。那麼這次我們到底需要多少編譯器呢?

答案也很明顯:需要M+N種編譯器。

是以,采用中間語言要比直接編譯代碼的開銷小的多得多。

論據二:實作的難度

假設,匹夫對硬體語言一竅不通(當然事實上是這樣的。。。),但卻具備一種分析源代碼語義的特殊天賦(瞎掰的)。那麼要實作從C#到各個平台機器碼一步到位的編譯,匹夫就要去啃各種目标晶片的說明,将C#代碼轉化成對應晶片的機器碼。這聽上去就像是一條不歸路,因為你并不擅長這個領域而且工作量巨大,同時由于不擅長帶來的隐患難以估量。

換言之,這個難度太大了。

但是如果我們通過對C#進行語義分析,能十分容易的就生成一份和晶片無關的CIL代碼,那麼實作的難度相比直接從C#到機器碼那可是大大的降低了。因為CIL語言本身就十分簡單(至少匹夫這種粗人都能看懂),是以從源代碼到CIL的編譯器實作就十分容易。同時,也是因為CIL語言本身十分簡單,是以從CIL到機器碼的編譯器也十分簡單。

而且即便有新的平台出現,你也不需要為每種語言都寫一個針對新平台的編譯器,而隻需要實作一個從CIL到新平台機器碼的編譯器就可以了。

是以可以看到,CIL中間語言的出現,大大降低了跨平台的實作難度。

《Mono為何能跨平台?聊聊CIL(MSIL)》這篇文章中,小匹夫也給各位列舉了一些CIL的代碼,同時做了一些解釋,文中在介紹CIL不依托cpu的寄存器時寫了這樣一句話:

不錯,CIL是基于堆棧的,也就是說CIL的VM(mono運作時)是一個棧式機。

 那麼不知道各位看官是否也有這樣的疑問呢?那就是~~~~~~~

為什麼是棧式機?直接放在記憶體中不好嗎?

終于要聊聊小匹夫也覺得挺有趣的一個話題了。對啊,為什麼CIL基于堆棧呢?那麼我們首先就來聊聊什麼是“棧式機”。

假如讓你來...

假如讓你來設計一種機器語言,同時實作一個簡單地加法功能,簡單到什麼程度呢?比如a+b等于c這樣好了。那麼思路是什麼呢?

方案一:使用記憶體

add [a的位址], [b的位址], [結果的位址也就是c的位址]      

當機器遇到add操作符時,它就會去尋找a的位址和b的位址這兩個位址中存放的值,然後用balabala的方式将它們求和,并将結果存放在c的位址。

方案二:使用寄存器

當然匹夫也是一個學過彙編的漢子,也了解一點點單片機的知識,知道有一個叫做累加器的東西。累加器就屬于寄存器了,它主要用來儲存計算所産生的中間結果,最後将其轉存到其它寄存器或記憶體中。是以使用累加器的思路也很簡單,一開始将累加器設定為0,每個數字依序地被加到累加器中,當所有的數字都被加入後,結果才寫回到主記憶體中。

方案三:使用堆棧

等等,這個部分介紹的不是棧式機嗎?怎麼感覺有點跑題呢?好吧,拉回思緒,讓我們再來考慮下使用堆棧如何實作這個簡單地加法功能呢?

push a
push b
add
pop c      

add操作符首先将a,b彈出堆棧,然後将二者相加,再将結果壓棧。那麼,使用了這種方案的虛拟機,就被稱為“棧式機”。

是以如果要回答為何CIL的選擇是使用堆棧,那麼就繞不過堆棧和另外兩種方案的比較。

首先看一下我們做這種簡單加法時,硬體需要為我們提供一些什麼呢?對,就是存放這些值的臨時空間。所謂的臨時空間,就是說存儲這個值的空間隻有在需要這個值的時候才有用,其餘的時候你并不需要關心這個空間或者說它的位址到底是什麼。假設我們已經定義了一些操作符,比如Allocate用來配置設定記憶體,Call用來調用函數,Add用來求和,Store則是用來存儲資料。

首先我們直接使用記憶體來運作CIL,那麼遇到這樣的表達式:

x = A() + B() + C() + 100      

機器首先要為A()在記憶體上配置設定空間用來儲存它的傳回值,然後調用A()并将A()的傳回值儲存在之前配置設定給它的位址中,我們就管它叫做ret1好了。之後為B()在記憶體上配置設定空間來儲存B()的傳回值,接着調用B(),同樣将B()的傳回值儲存在剛才配置設定給它的記憶體中,我們暫時稱呼它ret2。這時,我們遇到了第一個“+”号,是以此時會為ret1和ret2相加的結果在記憶體上配置設定一個空間,并且将ret1和ret2相加,并将結果儲存在剛剛配置設定的記憶體中(我們稱為sum1),之後的過程以此類推。

Allocate ret1         //為A()的傳回值配置設定臨時空間ret1
Call A(),ret1         //調用A()并将結果儲存在ret1
Allocate ret2        //為B()的傳回值配置設定臨時空間ret2
Call B(),ret2         //調用B()并将結果儲存在ret2
Allocate sum1        //為第一次相加的結果配置設定臨時空間sum1
Add ret1,ret2,sum1      //使用Add操作符将ret1和ret2中的内容相加,并将結果儲存在sum1中。
...      

可以看到這樣的CIL代碼在每一步真正的邏輯執行之前,都會先在記憶體上配置設定一塊臨時空間,用來存儲我們此時需要的資料。如果使用堆棧,這個步驟是不需要,因為你将你需要的資料存儲在了堆棧之中,而非在記憶體上臨時去配置設定空間。是以,使用堆棧時,CIL代碼看上去也許像是這樣的:

push x的位址 // 将x的位址壓棧
call A()              // 現在堆棧中包含x的位址和A()的傳回值ret1
call B()              // 現在堆棧中包換x的位址,ret1,B()的傳回值ret2
add                   // 現在堆棧中包含x的位址,ret1 + ret2的結果sum1
call C()              // 現在堆棧中包含x的位址,sum1和C()的傳回值ret3
add                   // 現在堆棧中包含x的位址, ret1+ret2+ret3的傳回值sum2
push 100              // 現在堆棧中包含x的位址,sum2,以及100
add                   // 現在堆棧中包含x的位址, ret1+ret2+ret3+100的和sum3
store                 //将sum3存在x的位址中。      

同時,我們還可以看到如果CIL直接使用記憶體的話,由于在記憶體上的空間是臨時配置設定的,是以CIL代碼在運作時需要帶上它的操作數位址以及傳回位址,比如上例中的Add ret1,ret2,sum1,因為如果不告訴它這些位址,它就不知道該從何處得到資料,并将傳回的資料放在何處。

是以直接使用記憶體來運作CIL代碼,會使得CIL代碼變得十分的臃腫不堪,而且要做很多多餘的工作。是以不直接使用記憶體,而是使用堆棧的原因就是因為:如果我們僅僅隻是為了臨時存儲一些值,而在使用完這些值之後我們就不再關心這塊空間如何如何,顯然使用堆棧要比直接使用記憶體友善的多,簡潔的多。

至于為何不使用寄存器,小匹夫在上文提到的文章中已經解釋過了。簡單的講就是因為簡單。

好啦,到此為CIL正名的過程就結束啦。那麼下面就開始首尾呼應,結尾點題,從Hello World開始踏上我們的CIL語言的征程吧~~

Hello Wolrd 你好,沃爾德

本文開篇就提到了那句名言:“萬事開頭,Hello World”。那麼我們第一個CIL語言的程式,就從Hello World開始吧。因為匹夫使用的是mac機器,是以編譯.il檔案所使用的工具是mono的ilasm。

那麼匹夫就先建立一個.il檔案,起名就叫做chen.il好了。

用CIL寫程式:你好,沃爾德

與C#不同,CIL并不要求方法必須要屬于一個類。是以,我們無需定義一個類,隻需要聲明一個主函數(按照C#的說法main)即可。其實在CIL中我們應該管這種函數叫做“entrypoint”,也就是入口函數。隻要定義了“entrypoint”,函數叫不叫main都無關緊要,為了示範這一點,我們的函數名就叫做Fanyou好了。

那麼小匹夫就這樣寫一下咯:

用CIL寫程式:你好,沃爾德

上面就是小匹夫的Fanyou方法的定義了。和一般的語言一樣,包括方法簽名和方法體。但是在CIL語言中,方法的定義有以下需要注意的地方:

  1. 方法的定義以.method作為辨別,可以在類中聲明,也可以在類外聲明。
  2. 和C#一樣,CIL程式的入口也必須是靜态的,也就是意味着調用這個入口函數并不需要某個類的執行個體。當然,使用static關鍵字來辨別。
  3. 入口的辨別.entrypoint,這個标志表明了該方法是CIL程式的入口。是以咯,隻有一個函數能擁有.entrypoint辨別。
  4. .maxstack這個辨別表明了預計使用的堆棧槽,這裡是1,因為我們隻是把“Hello World”這個字元串壓棧。舉個例子,如果像上文那樣做2個數相加的加法,則需要2個堆棧槽,首先需要将2個數壓棧,之後add操作符将2個數彈出并求和,最後将結果壓棧。是以最多需要2個棧槽。
  5. ldstr操作符将“Hello World”壓棧,供之後的WriteLine方法使用。
  6. call調用了mscorlib程式集中System.Console類的WriteLine方法。這裡call指明了WriteLine完整的簽名(void [mscorlib]System.Console::WriteLine(string))是以運作時可以選擇WriteLine的正确地重載。
  7. ret操作符則将結果傳回給調用者。在這裡,作為入口函數的傳回,也意味着應用運作的結束。
  8. 有一些同學可能也看過很多CIL語言的代碼,是不是發現它們每一條語句之前往往有一個“IL_0000:”這樣的東東?但是匹夫你寫的代碼裡沒有啊!是不是你寫錯了?NO,NO,那個IL_XXXX其實僅僅是行号,是不會影響程式的運作的。

好啦,一個簡單地Hello World的确能帶來一些最基本的知識點,但是這個.il檔案編譯之後能運作嗎?答案是NO。因為上面的第6點也說了,調用了mscorlib程式集。但是我們貌似沒有引入什麼程式集啊?是以我們還要加入一些程式集的資訊才可以哦。那麼完整的代碼如下了:

用CIL寫程式:你好,沃爾德

然後,讓我們編譯并且運作一下,看看我們寫的實作了Fanyou方法,輸出Hello World的CIL代碼到底是否可以運作吧!

運作結果:

用CIL寫程式:你好,沃爾德

首先

ilasm chen.il      

對chen.il這個CIL檔案進行編譯,生成的結果是chen.exe

之後再運作chen.exe

mono chen.exe      

可以看到螢幕上輸出了“Hello World”。

OK,大功告成!

如果各位看官覺得文章寫得還好,那麼就容小匹夫跪求各位給點個“推薦”,謝啦~

裝模作樣的聲明一下:本博文章若非特殊注明皆為原創,若需轉載請保留原文連結(http://www.cnblogs.com/murongxiaopifu/p/4257264.html)及作者資訊慕容小匹夫

後記

CIL代碼雖然号稱不是很友好,但是作為C#程式員的确還是很有必要掌握一下。匹夫水準一般,能力有限,願抛磚引玉和大家共同探讨,共同進步。

用CIL寫程式:你好,沃爾德

本作品采用知識共享署名-非商業性使用-相同方式共享 2.5 中國大陸許可協定進行許可,我的部落格歡迎複制共享,但在同時,希望保留我的署名權陳嘉棟(慕容小匹夫),并且,不得用于商業用途。如您有任何疑問或者授權方面的協商,請給我留言。

知乎專欄:

Runtime

聯系方式:

Email:[email protected]