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

      網友投稿 879 2022-05-28

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

      本文涉及一些編譯原理基礎,我擔心沒學過編譯原理的讀者會覺得吃力,因此順帶介紹了編譯原理的基礎知識。當然,不會編譯原理也無法阻止你成功寫出一門腳本語言。

      因為原理太抽象了,而且為了嚴謹,理論總是把簡單的描述成復雜的。在實踐中你會發現,編譯器的實現比理解編譯器原理容易,你會發現——原來晦澀難懂的概念其實就是這么簡單,以至于你是通過實踐才懂得了編譯原理。畢竟紙上得來終覺淺,絕知此事要躬行。今天我們來介紹一些自制編程語言可能令人迷惑的問題。

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

      兩者最明顯的區別就是看它們各是誰的“菜”。兩者的共性是最終生成的指令都包含操作碼和操作數兩部分。

      編譯型程序所生成的指令是二進制形式的機器碼和操作數,即二進制流。同樣是數據,和文本文件相比,這里的數據是二進制形式,并不是文本字符串(如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行之后的源碼,重復分析語法、生成代碼的過程。總之是以語法分析為主線,語法分析把源碼按照語法來拆分成多個小部分,每次生成這一小部分的目標代碼或中間代碼。

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

      什么是符號表

      把符號表列出來是因為這個詞聽上去“挺唬”人的,由于看不見摸不著,很多初學者都以為它是個非常神秘的東西。其實符號表就是存儲符號的表,就是這么簡單。

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

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

      《自制編程語言》

      鄭鋼??著

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

      《操作系統真象還原》

      鄭鋼??著

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

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

      本文轉載自異步社區

      https://www.epubit.com/articleDetails?id=N05b36f9b-5f27-4240-ae20-65c600400095

      高性能計算 Web應用防火墻 WAF

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:鯤鵬云服務器移植SpeedSeq
      下一篇:激活碼方式注冊的實現原理述
      相關文章
      亚洲一级特黄特黄的大片| 亚洲成人福利网站| 亚洲日本VA午夜在线电影| 激情内射亚洲一区二区三区| 国产亚洲婷婷香蕉久久精品| 亚洲色成人网站WWW永久| 亚洲最大av无码网址| 亚洲国产成人久久综合一区77| 精品国产_亚洲人成在线| 亚洲国产aⅴ成人精品无吗| 亚洲色大成网站www| 亚洲色中文字幕在线播放| 中文日韩亚洲欧美制服| 亚洲一区AV无码少妇电影| 亚洲国产成人超福利久久精品 | 亚洲免费电影网站| 亚洲av永久无码精品三区在线4| 亚洲av永久无码精品三区在线4| 91丁香亚洲综合社区| 亚洲综合激情五月色一区| 中文有码亚洲制服av片| 亚洲精品宾馆在线精品酒店| 亚洲AV无码专区国产乱码不卡| 亚洲AV无码资源在线观看| 激情小说亚洲图片| av无码东京热亚洲男人的天堂| 亚洲VA综合VA国产产VA中| 亚洲一级特黄大片在线观看| 国产亚洲成av片在线观看| 亚洲AV无码成人网站久久精品大| 亚洲日本一区二区| 亚洲无成人网77777| 亚洲免费福利在线视频| 亚洲国产成人久久精品大牛影视| 激情小说亚洲色图| 亚洲综合AV在线在线播放| 亚洲国产人成网站在线电影动漫| 亚洲精品动漫在线| 亚洲中文字幕AV每天更新| 国产精品亚洲一区二区三区| 久久乐国产精品亚洲综合|