項目緊趕慢趕總算在最近有了一些成績,因此沉寂了幾周以後,我也終於有時間寫點東西了。之前我寫過一篇文章對CIL作了一個簡單地介紹,不過不知道各位看官看的是否過癮,至少我以爲很不過癮。因此決定寫幾篇關於CIL的文章,即和各位看官一塊兒進行個交流,同時也是我本身總結和鞏固一下這些知識點。俗話說的好,「萬事開頭,Hello World」,那麼做爲我總結CIL的第一篇文章,就從Hello World開始吧。固然,正式開始寫CIL代碼以前,咱們還有點閒話要說,那就是運行時的選擇爲什麼是它?爲什麼是CIL?而CIL爲什麼又是基於堆棧的?內存或者寄存器難道不是更理想的選擇嗎?html
開始正文內容以前,我帶領你們先回顧一下《Mono爲什麼能跨平臺?聊聊CIL(MSIL)》的簡要內容:首先,用C#寫的代碼被C#的編譯器編譯成CIL(固然除了C#還有不少其餘的語言,好比VB等等),以後再有JIT編譯器在程序運行時即時編譯或者AOT(或者NGEN)進行提早編譯將CIL代碼編譯成對應平臺的機器碼,最後運行在平臺上的即是機器碼。我在那篇文章中提過,首先將各類不一樣的語言都統一編譯成CIL,再由CIL編譯成各個平臺的機器碼是跨平臺的基礎。那麼仔細想一想,必定有人會提出這樣的疑問,直接從C#編譯到機器碼,省略掉「多餘」的中間語言,是否是也可行呢?這個問題的確值得討論,同時也爲了我接下來的文章師出有名,因此首先聊聊CIL的「合法性」(用必要性這個詞也許更好)問題就成了我寫這篇文章的頭等大事。linux
首先提出咱們的論據一,那就是使用CIL這套體系對實現跨平臺的開銷要小的多的多。android
引入一個「多餘的」中間語言和兩個編譯器(C#----->CIL------>機器碼)聽上去老是要比只使用一種編譯器(C#-------->機器碼)的實現代價高的多,由於咱們的目的是C#代碼能編譯成機器能運行的機器碼,顯然一步到位是最直接有效的方式。相反,引入中間語言以後,咱們就須要實現兩種語言的分析和編譯,看上起的確畫蛇添足。但若是咱們考慮到跨平臺這個前提,就會發現中間語言是多麼的重要。ios
假設你能夠選擇的語言有N種(好比C#, VB, F#, JScript .NET,Boo...),而咱們的目標平臺有M種(win,mac,linux,ios,android...)。那麼若是咱們採用最直接的編譯方式,即從源代碼直接編譯成機器碼,那麼到底須要多少個編譯器呢?函數
答案很直接咯:須要N*M種編譯器。由於你須要爲每一種語言針對每個平臺寫一個編譯器。工具
若是咱們採用了中間語言呢?spa
咱們只須要爲N種語言寫N種編譯器,將它編譯成CIL代碼。再爲M種平臺寫M種編譯器,將上一步生成的CIL代碼編譯成M種平臺的機器碼。那麼此次咱們到底須要多少編譯器呢?設計
答案也很明顯:須要M+N種編譯器。code
因此,採用中間語言要比直接編譯代碼的開銷小的多得多。htm
假設,我對硬件語言一竅不通(固然事實上是這樣的。。。),但卻具有一種分析源代碼語義的特殊天賦(瞎掰的)。那麼要實現從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的地址這兩個地址中存放的值,而後用具體的方式將它們求和,並將結果存放在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 World」。那麼咱們第一個CIL語言的程序,就從Hello World開始吧。由於我使用的是mac機器,因此編譯.il文件所使用的工具是mono的ilasm。
那麼我就先新建一個.il文件,起名就叫作chen.il好了。
與C#不一樣,CIL並不要求方法必需要屬於一個類。因此,咱們無需定義一個類,只須要聲明一個主函數(按照C#的說法main)便可。其實在CIL中咱們應該管這種函數叫作「entrypoint」,也就是入口函數。只要定義了「entrypoint」,函數叫不叫main都可有可無,爲了演示這一點,咱們的函數名就叫作Fanyou好了。
那麼我就這樣寫一下咯:
上面就是個人Fanyou方法的定義了。和通常的語言同樣,包括方法簽名和方法體。可是在CIL語言中,方法的定義有如下須要注意的地方:
方法的定義以.method做爲標識,能夠在類中聲明,也能夠在類外聲明。
和C#同樣,CIL程序的入口也必須是靜態的,也就是意味着調用這個入口函數並不須要某個類的實例。固然,使用static關鍵字來標識。
入口的標識.entrypoint,這個標誌代表了該方法是CIL程序的入口。因此咯,只有一個函數能擁有.entrypoint標識。
.maxstack這個標識代表了預計使用的堆棧槽,這裏是1,由於咱們只是把「Hello World」這個字符串壓棧。舉個例子,若是像上文那樣作2個數相加的加法,則須要2個堆棧槽,首先須要將2個數壓棧,以後add操做符將2個數彈出並求和,最後將結果壓棧。因此最多須要2個棧槽。
ldstr操做符將「Hello World」壓棧,供以後的WriteLine方法使用。
call調用了mscorlib程序集中System.Console類的WriteLine方法。這裏call指明瞭WriteLine完整的簽名(void [mscorlib]System.Console::WriteLine(string))因此運行時能夠選擇WriteLine的正確地重載。
ret操做符則將結果返回給調用者。在這裏,做爲入口函數的返回,也意味着應用運行的結束。
有一些同窗可能也看過不少CIL語言的代碼,是否是發現它們每一條語句以前每每有一個「IL_0000:」這樣的東東?可是我你寫的代碼裏沒有啊!是否是你寫錯了?NO,NO,那個IL_XXXX其實僅僅是行號,是不會影響程序的運行的。
好啦,一個簡單地Hello World的確能帶來一些最基本的知識點,可是這個.il文件編譯以後能運行嗎?答案是NO。由於咱們貌似沒有定義什麼程序集的信息啊?因此咱們還要加入一些程序集的信息才能夠哦。那麼完整的代碼以下了:
而後,讓咱們編譯而且運行一下,看看咱們寫的實現了Fanyou方法,輸出Hello World的CIL代碼究竟是否能夠運行吧!
運行結果:
首先
ilasm chen.il
對chen.il這個CIL文件進行編譯,生成的結果是chen.exe
以後再運行chen.exe
mono chen.exe
能夠看到屏幕上輸出了「Hello World」。
OK,大功告成!