自制編程語言,六個令你迷惑的問題

自制編程語言和虛擬機,這是一個看似很深奧的課題,也涉及當今互聯網流行的主題,許多技術人員對其心馳神往,但要領悟其精髓寸步難行。php

《自制編程語言》按部就班、由淺到深地講解了豐富的基礎知識,覆蓋了常見的編譯原理入門知識,更難能難得的是,做者講解的知識具備其獨特的理解和視角,相信本書能讓讀者可以受益不淺。前端

                                                    

本文涉及一些編譯原理基礎,我擔憂沒學過編譯原理的讀者會以爲吃力,所以順帶介紹了編譯原理的基礎知識。固然,不會編譯原理也沒法阻止你成功寫出一門腳本語言。
python

由於原理太抽象了,並且爲了嚴謹,理論老是把簡單的描述成複雜的。在實踐中你會發現,編譯器的實現比理解編譯器原理容易,你會發現——原來晦澀難懂的概念其實就是這麼簡單,以致於你是經過實踐才懂得了編譯原理。畢竟紙上得來終覺淺,絕知此事要躬行。今天咱們來介紹一些自制編程語言可能使人迷惑的問題。web

編譯型程序和腳本程序的異同
shell

二者最明顯的區別就是看它們各是誰的「菜」。二者的共性是最終生成的指令都包含操做碼和操做數兩部分。編程

編譯型程序所生成的指令是二進制形式的機器碼和操做數,即二進制流。一樣是數據,和文本文件相比,這裏的數據是二進制形式,並非文本字符串(如ASCII碼或unicode等)形式。後端

若是二進制流按照有無格式來劃分,無格式的即是純粹的二進制流,程序的入口即是文件的開始。另一種是按照某種協議(即格式)組織的二進制流,好比Lnux下elf格式的可執行文件。它是硬件CPU的直接輸入,所以硬件CPU是「看獲得」編譯型程序所對應的指令的,CPU親自執行它,即機器碼是CPU的菜。數組

編譯型語言編譯出來的程序,運行時自己就是一個進程,它是由操做系統直接調用的,也就是由操做系統加載到內存後,操做系統將CS:IP寄存器(IA32體系架構的CPU)指向這個程序的入口,使它直接上CPU運行,這就是所說的CPU「看獲得」它。總之調度器在就緒隊列中能看到此進程。瀏覽器

腳本語言,也稱爲解釋型語言,如JavaScript、Python、Perl、Php、Shell腳本等。它們自己是文本文件,是做爲某個應用程序的輸入,這個應用程序是腳本解釋器。因爲只是文本,這些腳本中的代碼在腳本解釋器看來和字符串無異。緩存

也就是說,腳本中的代碼歷來沒真正上過CPU去執行,CPU的CS:IP寄存器歷來沒指向過它們,在CPU眼裏只看獲得腳本解釋器,而這些腳本中的代碼,CPU歷來就不知道有它們的存在,腳本程序卻因硬件CPU而間接「運行」着。

這就像家長給孩子生活費,孩子用生活費養了只狗狗,家長只關心孩子的成長,從不知道狗狗的存在,但狗狗卻間接地成長。這些腳本代碼看似在按照開發人員的邏輯在執行,本質上是腳本解釋器在時時分析這個腳本,動態根據關鍵字和語法來作出相應的行爲。

解釋器有兩大類,一類是邊解釋邊執行,另外一類是分析完整個文件後再執行。若是是第一類,那麼腳本中如有語法錯誤,先前正確的部分也會被正常執行,直到遇到錯誤才退出;若是是第二類,分析整個文件後才執行的目的是爲了建立抽象語法樹或者是用與之等價的遍歷去生成指令,有了指令以後再運行這些指令以表示程序的執行,這一點和編譯型程序是一致的。

腳本程序所生成的指令是文本形式的操做碼和操做數,即數據以文本字符串的形式存在。其中的操做碼稱爲opcode,一般opcode是自定義的,因此相應的操做數也要符合opcode的規則。爲了提升效率,一個opcode的功能每每至關於幾百上千條機器指令的組合。

若是虛擬機不是爲了效率,多半是用於跨平臺模擬程序運行。這種虛擬機所處理的opcode就是另外一體系架構的機器碼,好比在x86上模擬執行MIPS上的程序,運行在x86上的虛擬機所接收的opcode就是MIPS的機器碼。

除跨平臺模擬外,一般虛擬機的用途是提升執行效率,所以opcode不多按照實際機器碼來定義,不然還不如直接生成機器指令交給硬件CPU執行更快呢。故此種自定義的指令是虛擬機的輸入,即所謂虛擬機的菜。

虛擬機分爲兩大類,一類是模擬CPU,也就是用軟件來模擬硬件CPU的行爲,這種每每是給語言解釋器用的,好比Python虛擬機。另外一類是要虛擬一套完整的計算機硬件,好比用數組虛擬寄存器,用文件虛擬硬盤等,這種虛擬機每每是用來運行操做系統的,好比VMware,由於只有操做系統纔會操做硬件。

腳本程序是文本字符流(即字符串),其以文本文件的形式存儲在磁盤上。具體的文本格式由文本編譯器決定,執行時由解釋器將其讀到內存後,逐行語句地分析並執行。

執行過程多是先生成操做碼,而後交給虛擬機逐句執行,此時虛擬機起到的就是CPU的做用,操做碼即是虛擬機器的輸入。

固然也能夠不經過虛擬機而直接解析,由於解析源碼的順序就是按照程序的邏輯執行的順序,也就是生成語法樹的順序,所以在解析過程當中就能夠同時執行了,好比解析到 2+3 時就能夠直接輸出 5 了。

但方即是有限的,實現複雜的功能就不容易了,由於計算過程當中須要額外的數據結構,比較對於函數調用來講總該有個運行時棧來存儲參數和局部變量以及函數運行過程當中對棧的需求開銷。所以對於複雜功能,多數狀況下仍是專門寫個虛擬機來完成。

順便猜測一下解釋型語言是如何執行的。咱們在執行一個PHP腳本時,其實就是啓動一個C語言編寫出來的解釋器而已。這個解釋器就是一個進程,和通常的進程是沒有區別的,只是這個進程的輸入則是這個PHP腳本。在PHP解釋器中,這個腳本就是個長一些的字符串,根本不是什麼指令代碼之類。

只是這種解釋器瞭解這種語法,按照語法規則來輸出罷了。舉個例子,假設下面是文件名爲a.php的PHP代碼。

php解釋器分析文本文件a.php時,發現裏面的echo關鍵字,將其後面的參數獲取後就調用C語言中提供的輸出函數,好比printf((echo的參數))。PHP解釋器對於PHP腳本,就至關於瀏覽器對於JavaScript同樣。

不過這個徹底是我猜想的,我不知道PHP解釋器裏面的具體工做,以上只是爲了說清楚個人想法,請你們辯證地看。

說到最後,也許你有疑問,若是CPU的操做數是字符串的話,那CPU就能直接執行腳本語言了,爲何CPU不直接支持字符串做爲指令呢?後面會有分享。

腳本語言的分類

腳本語言大體可分爲如下4類。

(1)基於命令的語言系統

在這種語言系統中,每一行的代碼實際上就是命令和相應的參數,早期的彙編語言就是這種形式。此類語言系統編寫的程序就是解決某一問題的一系列步驟,程序的執行過程就是解決問題的過程,就像作菜同樣,步驟是提早寫好在腦子裏(或菜譜中)的。如如下炒菜腳本。

 

以上步驟中第1列都是命令,後面是命令的參數。其中把菜放進鍋後不斷地攪拌(示意而已,不用太嚴謹),因爲命令式語言系統中沒有循環語句,須要連續填入多個stir以實現連續多個相同的操做。會有一個解釋器逐行分析此文件,執行相應命令的處理函數。如下是一個解釋器示例。

   

(2)基於規則的語言系統

此類語言的執行是基於條件規則,當知足規則時便觸發相應的動做。其語言結構是謂詞邏輯→動做,如圖1-1所示。

                                             

                                                                  圖1-1

所以此類語言常稱爲邏輯語言,經常使用於天然語言處理及人工智能方面,典型的表明有Prolog。

(3)面向過程的語言系統

面向過程的語言系統咱們都比較熟悉,批處理腳本和shell腳本,perl、lua等屬於此類,和基於命令的語言系統相比,它能夠把一系列命令封裝成一個代碼塊供反覆調用。此代碼塊即是借用了數學中函數的概念,一個x對應一個y,即給一個輸入便有一個輸出,因而這個代碼塊便稱爲函數。

(4)面向對象的語言系統

現代腳本語言基本上都是面向對象,大夥兒用的都挺多的,好比python。不少讀者誤覺得只要語言中含有關鍵字class,那麼該語言就是面向對象的語言,這就不嚴謹了。由於在perl語言中也能夠經過關鍵字class定義一個類,但其內部實現上並非徹底面向對象,其本質是面向過程的語言。世界上第一款血統純正的面嚮對象語言是smalltalk,它在實現上就是一切皆對象,具備徹底面向對象的基因。

爲何CPU要用數字做爲指令

在以前小節「編譯型程序和腳本程序的異同」的結束處咱們討論過,爲何CPU不直接支持字符串做爲指令。我估計有的讀者會誤覺得CPU將直接執行彙編代碼,這是不對的,由於彙編代碼是機器碼的符號化表示,幾乎是與機器碼一一對應,但彙編代碼絕對不是機器語言。

你想,若是彙編代碼是機器指令的話,那麼CPU看到的輸入即是字符串,好比如下彙編代碼用於計算1+10-2。

彙編語言實際上是彙編器的輸入,對於彙編器來講,彙編代碼文件也是文本,所以其中mov指令也是字符串。若是讓CPU直接讀取彙編文件逐行分析各類字符串以判斷指令,這效率必然很是低下。

畢竟要比較的字符數太多,比較的次數多了效率固然就低了,所以把指令編號爲數字,這樣比較數字多省事。並且最主要的是,CPU更擅長處理數字,它自己的基因就是數字電路,數字計算是創建在數值處理的基礎上,這就是本質上二進制數據比文本ASCII碼更快更緊湊的緣由。

爲何腳本語言比編譯型語言慢

而腳本語言的編譯有兩類,一類是邊解釋邊執行,不產生指令,這個解釋過程最佔時間的部分就是字符串的比較過程,字符串比較的時間複雜度是O(n),也就是在比較n次以後解釋器才肯定了操做碼是什麼,而後再去獲取操做碼的操做數,你看能不慢嗎?而編譯型語言編譯後是機器碼,是二進制數字,所以可直接上CPU運行,而CPU擅長處理數字,比較一次數字即可肯定操做碼。

另外一類腳本語言是先編譯,再生成操做碼,最後交給虛擬機執行,這樣多了一個生成操做碼的過程,彷佛「顯得」更慢了。其實這都不是主要的。

你看,程序「執行」速度的快慢是比較出來的,編譯型語言在執行時已是二進制語言了,而大多數腳本語言在執行時仍是文本,必然要先有個編譯過程。

這裏面全是字符串處理,整個腳本的源碼對於編譯器來講就是一個長長的字符串,都要完整地進行各類比較,所以多了一個冗長的步驟,必然要慢。有些腳本系統爲減小編譯的過程,第一次編譯後將編譯結果緩存爲文件,如Python會將.py文件編譯後存儲爲.pyc文件,下次無須編譯直接運行即可。

可是,這樣無須二次編譯的腳本語言就能和編譯型程序媲美嗎?不見得磁盤IO是整個系統最慢的部分,解釋器讀取緩存文件難道不須要時間嗎?等等,有讀者說了,編譯型的程序被操做系統加載時也要從磁盤上讀取啊,這不同嗎?

固然不同,別忘了,腳本程序在執行時先要加載解釋器,解釋器也是位於硬盤上的文件,只是二進制可執行文件而已,依然須要讀取硬盤,而後解釋器再去從硬盤上讀取腳本語言文件並編譯腳本文件。

你看,編譯型程序在執行時只有1個IO,而腳本程序在執行時有兩個,比前者多了1個低速的IO操做,所以,腳本語言更慢一些是註定的。

既然腳本語言比較慢,爲何你們還要用

這裏的語言是指語言的編譯器或解釋器,如下簡稱爲語言。

語言慢並不影響整個系統,影響整個系統速度的短板並非語言自己,目前來講系統的瓶頸廣泛是在IO部分。語言再慢也比IO快一個數量級,並非語言執行速度快10倍後整個系統就快10倍,語言慢了,整個系統依然不受影響,這要看瓶頸是哪塊兒。

這就像動物園運送動物的船超載了,人們不會埋怨某些人太胖了,而是清楚地知道佔份量的主要是船上的大象,人的體重和大象根本就不是一個量級。

再說,即便是語言提速後,因爲IO這塊跟不上,依然會被阻塞(因爲是腳本語言,這裏阻塞的是腳本解釋器),並且因爲語言太慢而顯得阻塞時間更漫長。

爲何會阻塞呢?這種阻塞每每是因爲程序後續的指令須要從IO設備讀取到的數據,也就是說程序後面的步驟依賴這些數據,沒這些數據程序運行沒意義。好比說Web服務器先要讀取硬盤上的數據而後經過網卡發送給用戶,必須得到硬盤數據後,web服務器進程中那部分操做網卡發送數據的指令才能上CPU上執行。

因爲語言的解釋器是由CPU處理的,CPU速率確定比IO設備快太多,所以在等待IO設備響應的過程當中啥也幹不了。操做系統爲了讓寶貴的CPU資源獲得最大的利用,確定會把進程(二進制可執行程序或腳本語言的解釋器)加入阻塞隊列,讓其餘可直接運行的、不須要阻塞的進程使用CPU(阻塞指的是並不會上CPU運行,也就是將該進程從操做系統調度器的就緒隊列中去掉)。

而語言(腳本語言解釋器)再慢也比IO設備快,所以依然會由於更慢的IO而難逃阻塞的命運。也就是說,拖慢整個系統後腿的必定是系統中最慢的部分,而不管腳本語言多慢,IO設備老是會比語言更慢,所以「影響系統性能」這個黑鍋,腳本語言不能背。

另外一方面大夥兒喜歡用腳本語言的緣由是開發效率高,這也是腳本語言被髮明的初衷,不少在C中須要多個步驟才能實現的功能在腳本語言中一句話就搞定,固然更受開發人員歡迎了。

什麼是中間代碼

不少編譯器會將源語言先編譯爲中間代碼,最後再編譯爲目標代碼,但中間語言並非必需的。中間代碼簡稱IR,是介於源程序和機器語言之間的語言,有N元式(如三元式、四元式)、逆波蘭、樹等形式。

目標代碼是指運行在目標機器上的代碼,與目標機器的體系架構直接相關,編譯器幹嘛不直接生成目標代碼,多這一道程序有什麼好處呢?

(1)能夠跨平臺

因爲中間代碼並非目標代碼,所以能夠做爲全部平臺的公共語言,從而可經過中間代碼實現先後端分離。好比在多平臺、多語言的環境下開發可提升開發效率,只要在某一平臺上編譯出中間代碼後,中間代碼到目標代碼的剩餘工做能夠由目標平臺的編譯器繼續完成。

(2)便於優化

中間代碼更接近於源代碼,對於優化來講更直接有效。並且能夠在一種平臺上優化好中間代碼,再發送到其餘平臺編譯爲目標機器,提升優化效率。

什麼是編譯器的前端、後端

編譯器的先後端是由中間代碼來劃分的,如圖1-2所示。

                       

                                                                     圖1-2

前端主要負責讀取源碼,對源碼進行預處理,經過詞法分析把單詞變成Token流,而後進行語法分析,語義分析,將源碼轉換爲中間代碼。

後端負責把中間代碼優化後轉換爲目標代碼。

詞法分析、語法分析、語義分析和生成代碼並非串行執行

不少教材上會把編譯階段分爲幾個獨立的部分:

(1)詞法分析;

(2)語法分析;

(3)語義分析;

(4)生成中間代碼;

(5)優化中間代碼;

(6)生成目標代碼。

這容易給人形成「這幾個步驟是串行執行」的錯覺,即「從源碼到目標代碼必需要順序地執行這6個步驟」,其實不是這樣子的,至少一個高效的編譯器毫不會這樣作。

這只是在功能邏輯上的步驟,就拿前4步來講,它們是以語法分析爲主線,以並行的、穿插的方式在一塊兒執行的,即這4個步驟是隨語法分析同時開始,同時結束。

每一個步驟的功能實現由其實際的模塊完成,負責詞法分析的模塊稱爲詞法分析器,負責生成代碼的模塊稱爲代碼生成器,負責語法分析的模塊稱爲語法分析器。

咱們所說的編譯器就是由詞法分析器、語法分析器和代碼生成器組成的(若是有目標代碼優化的話還包括優化模塊)。

編譯工做的入口是語法分析,所以編譯是以調用語法分析器爲開始的,語法分析器會把詞法分析器和代碼生成器視爲兩個子例程去調用。換句話說,詞法分析器和代碼生成器只會被語法分析器調用,若是沒有語法分析器,它們就沒有「露臉兒」的機會。

所以說編譯是以語法分析器爲主線,由語法分析器穿插調用詞法分析器和代碼生成器並行完成的。

語法分析和語義分析儘管是兩個功能,但這其實能夠合併爲一個。由於在語法分析事後便知道了其語義。這個很好理解,畢竟語法就是語義的規則,規則是由編譯器(的設計者)制定的,那麼編譯器(的設計者)分析了本身設定的規則後固然就明白了語義(不可能不明白本身所制定規則的意義)。

好比讀英文句子,尤爲是複雜的長句,先找到句子謂語動詞,以謂語動詞爲分界線把句子拆分主謂兩大部分,在前一部分中找主語,後一部分中找賓語等,在分析完語法後句子的意思就搞清楚了。

也就是說,語法分析和語義分析是同時,又是先後腳的事兒,所以合併到一塊兒並不奇怪。你看,語法分析和語義分析確實是並行。

爲了語法分析的效率,詞法分析器每每是做爲一個子例程被語法分析器調用,即每次語法分析器須要一個單詞的token時就調用詞法分析器。你看,語法分析和詞法分析確實也是並行。

最後說生成代碼。目前生成代碼的方式叫語法制導,什麼是語法制導呢?就是在分析語法的「同時」生成目標代碼或中間代碼,實際上就是以語法分析爲導向,語法分析器在瞭解源碼語義後當即調用代碼生成器生成目標代碼或中間代碼,所以這也是和語法分析器並行。

提醒一下,並非在語法分析器分析完整個源碼後,再一次性地生成整個源碼對應的目標代碼或中間代碼,而是分析一部分源碼後就當即生成該部分源碼對應的目標代碼或中間代碼,這樣作比較高效且更容易實現。

舉個例子,好比源碼文件中有10行代碼,語法分析器不斷調用詞法分析器,每次得到一個單詞的token,把前3行源碼都讀完後肯定了源碼的語義,當即生成與這3行源碼同等意義的目標代碼或中間代碼。

而後語法分析器繼續調用詞法分析器讀取第4行以後的源碼,重複分析語法、生成代碼的過程。總之是以語法分析爲主線,語法分析把源碼按照語法來拆分紅多個小部分,每次生成這一小部分的目標代碼或中間代碼。

總結,爲了使編譯更加高效,詞法分析、語法分析、語義分析和生成代碼是以語法分析爲中心並行執行的,詞法分析和生成代碼都是被語法分析器調用的子例程。

什麼是符號表

把符號表列出來是由於這個詞聽上去「挺唬」人的,因爲看不見摸不着,不少初學者都覺得它是個很是神祕的東西。其實符號表就是存儲符號的表,就是這麼簡單。

你想,源碼中的那些符號總該存儲在某個地方,這樣在引用的時候才能找獲得,所以符號表的用途就是記錄文件中的符號。符號包括字符串、方法名、變量名、變量值等。符號放在表中的另外一個重要緣由是便於生成指令,使指令格式統一。

編譯器會把符號在符號表中的索引做爲指令的操做數,若是不用索引的話,指令就會很亂,好比若直接用函數名或字符串做爲操做數,指令就冗長了。「表」在計算機中並不專指「表格」,「表」是個籠統的概念,用以表示一切可供增、刪、改、查的數據結構,所以符號表能夠用任何結構來實現,好比鏈表、散列表、數組等。

                                                      

                                                              《自制編程語言》

                                                                    鄭鋼 著

本書全面從腳本語言和虛擬機介紹開始,講解了詞法分析的實現、一些底層數據結構的實現、符號表及類的結構符號表,常量存儲,局部變量,模塊變量,方法存儲、虛擬機原理、運行時棧實現、編譯的實現、語法分析和語法制導自頂向下算符優先構造規則、調試、查看指令流、查看運行時棧、給類添加更多的方法、垃圾回收實現、添加命令行支持命令行接口。

                                                     

                                                         《操做系統真象還原》

                                                                   鄭鋼 著

大學及研究生都有操做系統課程,這類人羣具備很高的學術能力,但書中講的過於抽象與晦澀,以致於不少學生對於此門課程恐懼到都提不出問題,只有會的人才能提出問題。操做系統理論書是沒法讓讀者理解什麼是操做系統的,學操做系統不能靠想像,他們須要看到具體的東西。

絕大多數技術人都對操做系統懷着好奇的心,他們渴望一本告訴操做系統究竟是什麼的書,裏面不要摻雜太多無關的管理性的東西,代碼量不大且是現代操做系統雛形,他們渴望很快看到本質而不花費大量的時間成本。

今日互動

你想爲本身的成長挑選哪本書?爲何?截止9月5日17時,留言+轉本活動到朋友圈,小編將抽獎選出2名讀者贈送紙書1本。(參與活動直達微信端自制編程語言,六個令你迷惑的問題

點擊閱讀原文,直接購買《自制編程語言》

閱讀原文

相關文章
相關標籤/搜索