學習筆記之程式設計達到一個高的境界就是自制指令碼語言(圖)

minemi發表於2018-07-24

學習筆記之程式設計達到一個高的境界就是自制指令碼語言(圖)
程式設計達到一個高的境界就是自制指令碼語言,通過這可以精通程式設計裡面的高深的技術,如編譯原理、語言處理器、編譯器與直譯器,這些都是代表一個程式設計師實力的技術。
每個程式設計師都有實現屬於自己程式語言的夢想,說其是夢想,原因是實現的難度很大......這種情況一直持續到《自制程式語言》的出現。
《自制程式語言》鄭鋼著
本書講的是純粹的技術“乾貨”,符合鄭鋼老師一貫的寫作風格,這是他靜心寫出來的東西,內容滿滿,很值得閱讀。滴滴系統部技術高階總監於曉聲說:“很高興能成為本書的首批讀者,也很高興能為本書寫推薦序。”
剛拿到本書手稿時,從書名上我意識到這是對我胃口的書。果然,整書閱讀以後,收穫頗多。如今程式設計師的開發成本已經很低了,專案中有各種成熟的框架和庫可供選擇和使用,但還有人能靜下心來研究編譯器這麼底層的技術,實屬難得。本書猶如一把火炬,點燃了技術人內心對開發的熱情。

學習筆記之程式設計達到一個高的境界就是自制指令碼語言(圖)


依稀記得2010年年初在百度與鄭鋼初次見面的情景,那時他工作之餘的時間基本都用在向各個技術專家請教、討論各類技術問題上,他是我帶過的人中最勤奮的人之一。時間荏苒,一分耕耘一分收穫,看到他今天的成長,尤感欣慰。
本書講述了一門指令碼語言(sparrow)的開發過程,這是一本“步步為營”式的書籍,延續了他編寫《作業系統真象還原》的風格,手把手地教讀者從零實現一門語言,從原理到實踐每一步都有實際的程式碼和詳盡的原理說明,通過執行書中各小節中的程式碼,讀者可以很輕鬆地掌握各個細節,因此本書的學習曲線並不陡峭,甚至很平坦。
另外,值得欣喜的是,本書所編寫的指令碼語言並不是用Java、C++等入門難度略大的語言實現的,而是用C語言,這是我們學習程式設計的基礎語言。也就是說,本書並不需要專業的開發經驗即可上手學習。另外,在實現過程中並未用到複雜的庫函式或系統呼叫,可以負責地說,本書已經將學習成本降到最低。
C語言是一種程式導向的語言,如何用一種程式導向的語言去實現一種物件導向的語言很有意思。另外,PHP和Perl語言雖然也實現了類,但它們其實是一種程式導向的語言,並不是純粹的面嚮物件語言,而sparrow語言是一種純粹的面嚮物件語言,它在設計之初就採用物件的方式來處理指令碼語言中類的成員和方法,這彷彿讓我們看到了物件導向程式語言的基因。
眾所周知,當今最流行的指令碼語言應屬Python,Python也是用C語言實現的,也許你很好奇Python的內部原理,但是想到它有將近 4 萬行的原始碼時,也許甚至不想看它的源程式了。那麼研讀本書中的sparrow語言會是一種更好的選擇,其原始碼不足7100行,閱讀過程輕鬆愉快,但可以學到Python這種語言的實現原理。
對於指令碼語言來說,兩個重要方面就是垃圾回收和執行環境。垃圾回收就是我們平時所說的GC(Garbage Collection)。有了GC,程式設計師不需要手工釋放所分配的物件,可以使精力專注於業務邏輯而不用擔心記憶體洩漏問題。
在sparrow語言中同樣實現了GC,通過此部分程式碼你可以看到GC 的原理,以及哪些物件才能被回收。 執行時環境就是指令碼語言中的虛擬機器,即VM(如Java語言的JVM也是一種VM)。
指令碼語言是通過虛擬機器才能執行的,如何把編譯器生成的操作碼轉換為實際的程式碼行為,這裡面的工作對大多數人來說很神祕。相信各位在原始碼中一探究竟之後會發現:GC和VM這兩個神祕的黑盒子不過如此。
另外,也許程式設計師最感興趣的就是執行緒,關於執行緒在使用者態下是如何實現的、執行緒如何實現排程,本書將告訴你答案。總之,但凡涉獵,開卷有益。
為什麼創作這本書?
很多讀者看了我寫的《作業系統真象還原》(一本一步步編寫作業系統的書)書後,紛紛來信,要求我再寫一本自制程式語言的書。這也在情理之中,對於很多計算機從業者來說,作業系統和編譯器幾乎是兩座無法逾越的大山,其難度之大,令很多人員望而生畏。最終,在讀者的鼓勵下,一衝動就答應了寫作本書,其實我很“後悔”做出這樣的決定。
為什麼後悔呢?因為寫書代價很大。
首先,寫書相當累,佔用很多精力。其次,佔用自己學習的時間,在當今個人進步緩慢就算退步的時代,自己沒有提升技術會很恐慌。
再次,精力全放在寫書上會影響家庭、影響工作。
最後,還要負責解答許多問題,確實很累。而且,這一次可是在創造程式語言,難度係數太高了,不亞於開發一個作業系統,甚至我父母都勸我:小剛,你都多大了還寫書,好好過日子、踏實上班就行了。但是,我最後還是決定寫本書。
下面是我跨過重重攔阻創作本書的動機。
1  有夢想,有遠方
既然寫書代價那麼大,那我為什麼還要“明知山有虎,偏向虎山行”呢?因為我就是奔著“老虎”去的,沒有老虎的山就沒有探險的樂趣。
2  有難度才有價值
每次遇到一件很難的工作時,我先是“痛苦”,然後隨之而來的就是“興奮”,因為這意味著我要進步。也許讀者會說,一定會進步嗎?也許99%會失敗。
同樣一件事,每個人對它的態度都不同,懦夫看到的是:99%會失敗,別幹了。勇士看到的是:還有1%成功的機會,幹吧!
只要不放棄(注意,不是堅持),一定會成功,成功只是時間長短的問題。
3  人生的意義
人生最大的遺憾是“壯志未酬”。如果你是天才,請將自己的才華“揮霍”得一滴不剩,直到觸碰到自己智力上的天花板,這樣才甘心。如果你是大力士,請努力在奧運賽場上為國爭光,直到累得站不起來,這樣才甘心。
這正是我寫本書的信仰。
學習很累並且無止境,但是多知道一些就會有多一些的欣喜。本著“把自己的知識多掏點給大家”的誠意,本書依然從第0章開始,相對《作業系統真象還原》來說,本書的語言不再那麼活潑(囉唆)了,畢竟編譯器的開發難度略小於開發作業系統,沒必要穿插一些“過渡”的話題。
本書一步步地實現了一種稱為sparrow的程式語言,它是用虛擬機器執行的,因此最後還要實現一個虛擬機器。sparrow語言是用C語言編寫的,學習的難度較低,實現的程式碼不長,希望大家在學習的旅途中愉快。
為什麼讀這本書?
本書是一本專門介紹自制程式語言的圖書,書中深入淺出地講述瞭如何開發一門程式語言,以及執行這門程式語言的虛擬機器。
本書主要內容包括:指令碼語言的功能、詞法分析器、類、物件、原生方法、自上而下算符優先、語法分析、語義分析、虛擬機器、內建類、垃圾回收、命令列及除錯等技術。
本書適合程式設計師閱讀,也適合對程式語言原理感興趣的計算機從業人員學習。
成功的基石不是堅持,而是“不放棄”
人們常說,堅持是成功的“前提”。我說,既然只是前提,這說明堅持也未必會成功。要想成功,人們需要的是成功的“基石”,而不是“前提”,這個基石就是3個字:不放棄。
大部分讀者都覺得開發一門程式語言是很難的事,甚至想都不敢想,我擔心你也有這個想法,所以特意用這種方式先和你說說心裡話:這本書你買都買了,多少發揮點價值才對得起買書的錢,誰的錢也不是白來的。
首先,我並不會為了鼓勵大家而大言不慚地說開發語言“其實不難”“很容易”之類的話,相反,這個方向確實很難,而且就應該很難,我想這也正是吸引你的地方,沒有難度哪來的價值,“其實不難、很容易”之類的話是對大家上進心的不尊重。
其次,只有在“我也認為很難”的前提下才能保證大部分的朋友能看懂本書。你看,在普通人眼裡從A到D,需要有B和C的推理過程,一個步驟都不能少,在天才眼裡,A到D是理所應當的事,不需要解釋得太清楚,天才認為B和C都是廢話,明擺著的事不需要解釋。而我不是天才,所以我會把B和C解釋清楚。
回到開頭的話,為什麼說成功的基石不是“堅持”而是“不放棄”呢?這兩個詞有啥區別?也許有讀者說,不放棄就是做著喜歡的事,讓自己愛上學習技術。個人覺得這有點不對了,我覺得我更喜歡吃喝玩樂,因為那是生物的本能,選擇技術的原因只是我沒那麼討厭它,它是我從眾多討厭的事物中選擇的最不討厭的東西。
放棄是為了減少痛苦,堅持是帶著痛苦繼續前行。“堅持”是個痛苦的詞,但凡靠堅持來做的事情必然建立在痛苦之上,而痛苦就會使人產生放棄的念頭,這是生物的本能。用“堅持”來“鼓勵”自己硬著頭皮幹,其實已經輸了一半,自己認為痛苦的事很難幹下去,幹不下去的原因是遇到困難時頭腦裡有“放棄”的念頭,如果把這個念頭去掉,那麼,只要活著,成功無非是時間長短的問題。這個念頭其實就是心理預期,“提前”做好心理預期很重要。
總之,不要給自己“可以放棄”的念頭,不要讓“可以放棄”成為一種選項,把這個選項去掉,那麼,只剩下成功。
你懂程式語言的“心”嗎
先來猜猜這是什麼?
它是一種人人必不可少,擁有多種顏色、多種外形的物品。
它是一種質地柔軟,可使人免受風寒,給予人們溫暖的日常物品。
它是一種使人更加美麗,更受年輕女性歡迎的物品。
它是一種用鈕釦、拉鍊或繩帶繫結到身體上的物品。
猜到了嗎?其實這是對“衣服”的描述。由於我們都知道什麼是衣服,因此我們認為以上4種描述都是正確的,通過“免受風寒”這4個字便有可能想到是衣服。但對於沒見過衣服的人,比如剛出生的小孩兒,他肯定還是不懂,甚至不知道什麼是鈕釦。
什麼是程式語言呢?以下摘自百度百科。
(1)“程式語言"(programming language),是用來定義計算機程式的形式語言。它是一種被標準化的交流技巧,用來向計算機發出指令…… 
(2)程式語言的描述一般可以分為語法及語義。語法是說明程式語言中,哪些符號或文字的組合方式是正確的,語義則是對於程式設計的解釋……
(3)程式語言俗稱“計算機語言”,種類非常多,總的來說可以分成機器語言、組合語言、高階語言三大類。程式是計算機要執行的指令的集合,而程式全部都是用我們所掌握的語言來編寫的……
就像剛才我對衣服的描述,以上的3個概念,懂的人早已經懂了,不懂的人還是不懂,回答顯得很“雞肋”。因為對於程式語言的理解並不在語言本身,而是在編譯器,編譯器是程式語言的“心”,而我們很少有人像瞭解衣服那樣瞭解編譯器,因此對於我們大多數人來說只是熟悉了語言的語法,僅僅是“會用”而已。
那什麼是程式語言呢?無論我用多少文字都不足以表述精準與全面,因為語言的本質就是編譯器,等你瞭解編譯器後,答案自在心中。目前我只能給出同樣“雞肋”的答案—程式語言是編譯器用來“將人類思想轉換為計算機行為”的語法規則。
程式語言的來歷
世界上本沒有程式語言,有的只是編譯器。語言本身只是一系列的語法規則, 這個規則對應的“行為”才是我們程式設計的“意圖”,因此從“規則”到“行為”解析便是語言的本質,這就是編譯器所做的工作。
估計大夥兒都知道,如果想輸出字串,在PHP語言中可以用語句echo,在C語言中使用printf函式,在C++中使用cout,這說明不同的規則對應相同的行為,因此語言規則的多樣性只是迷惑人的外表,而本質的行為都是一樣的,萬變不離其宗。
並不是“列印”功能就一定得是print、out等相關的字眼兒,那是編譯器的設計者為了使用者使用方便(當然也是為了他自己設計方便)而採用了大夥兒有共識的關鍵字,避免不必要的混亂。
語言一定要用更底層的語言來編寫嗎
有這個疑問並不奇怪,比如:
(1)Python是用C寫的,C較Python來說更適合底層執行。
(2)C程式碼在編譯後會轉換為更底層的彙編程式碼給彙編器,再由彙編器將彙編程式碼轉換為機器碼。
因此給人的感覺是,一種語言必須要用更底層的語言來實現,其實這是個誤解。C只是起初是用匯編語言寫的,因為在C語言之前只有組合語言和機器語言。人總是懶惰的,肯定是挑最方便的用,組合語言好歹是機器語言的符號化,因此相對來說更好用一些,所以只好用匯編來編寫C語言,等第一版C語言誕生後,他們就用C語言來寫了。
什麼?用C來編寫C?有些讀者內心就崩潰了,似乎像是陷入了死迴圈。其實這根本不是一回事,因為起作用的並不是C語言,而是C編譯器。語言只是規則,編譯器產生的行為才是最關鍵的,編譯器就是個程式,C程式碼只是它的文字輸入。用C來編寫C,這就是自舉,假如編譯器是用別的語言寫的,也許你心裡就好受一些了。
其實只要所使用的語言具有一定的寫檔案功能就能夠寫編譯器,為什麼這麼說呢?因為編譯器本身是程式,程式本身是由作業系統載入執行的,作業系統識別程式的格式後按照格式讀取程式中的段並載入到記憶體,最後使程式計數器(暫存器pc或ip)跳到程式入口,該程式就執行了。
因此用來編寫編譯器的語言只要具有一定程度的寫檔案的能力即可,比如至少要具有形同seek的檔案定位功能,這可用於按照不同格式的協議在不同的偏移處寫入資料,因此用Python是可以寫出C編譯器的。在這之前我寫過《作業系統真象還原》一書,裡面的第0章第0.17小節“先有的語言還是先有的編譯器,第1個編譯器是怎麼產生的”,詳細地說明C編譯器是如何自舉的,下面我把它貼過來。
首先肯定的是先有的程式語言,哪怕這個語言簡單到只有一個符號。先是設計好語言的規則,然後編寫能夠識別這套規則的編譯器,否則若沒有語言規則作為指導方向,編譯器的編寫將無從下筆。第1個編譯器是怎麼產生的,這個問題我並沒有求證,不過可以談下自己的理解,請大夥兒辯證地看。
這個問題屬於哲學中雞生蛋,蛋生雞的問題,這種思維迴旋性質的本源問題經常讓人產生迷惑。可是現實生活中這樣的例子太多了,具體如下。
(1)英語老師教學生英語,學生成了英語老師後又可以教其他學生英語。
(2)寫新的書需要參考其他舊書,新的書將來又會被更新的書參考,就像本書編寫過程一樣,要參考許多前輩的著作。
(3)用工具可以製造工具,被製造出來的工具將來又可以製造新的工具。
(4)編譯器可以編譯出新的編譯器。
這種自己創造自己的現象,稱為自舉。
自舉?是不是自己把自己舉起來?是的,人是不能把自己舉起來的,這個詞很形象地描述了這類“後果必須有前因”的現象。
以上前3個舉的都是生活例子,似乎比第4個更容易接受。即使這樣,對於前3個例子大家依然會有疑問:
(1)第一個會英語的人是誰教的?
(2)第一本書是怎樣產生的?
(3)第一個工具是如何製造出來的?
其實看到第(2)個例子大家就可能明白了。世界上的第一本書,它的知識來源肯定是人的記憶,通過向個人或群眾打聽,把大家都認同的知識記錄到某個介質上,這樣第一本書就出生了。此後再記錄新的知識時,由於有了這本書的參考,不需要重新再向眾人打聽原有知識了,從此以後便形成了書生書的因果迴圈。
從書的例子可以證明,本源問題中的第一個,都是由其他事物建立出來的,不是自己創造的自己。
就像先有雞還是先有蛋一樣,一定是先有的其他生命體,這個生命體不是今天所說的雞。伴隨這個生命體漫長的進化中,突然有一天具備了生蛋的能力(也許這個蛋在最初並不能孵化成雞,這個生命體又經過漫長的進化,最終可以生出能夠孵化成雞的蛋),於是這個蛋可以生出雞了。過了很久之後,才有的人類。人一開始便接觸的是現在的雞而不知道那個生命體的存在,所以人只知道雞是由蛋生出來的。
很容易讓人混淆的是編譯C語言時,它先是被編譯成彙編程式碼,再由彙編程式碼編譯為機器碼,這樣很容易讓人誤以為一種語言是基於一種更底層的語言的。似乎沒有組合語言,C語言就沒有辦法編譯一樣。拿gcc來說,其內部確實要呼叫匯編器來完成組合語言到機器碼的翻譯工作。因為已經有了組合語言編譯器,那何必浪費這個資源不用,自己非要把C語言直接翻譯成機器碼呢,畢竟彙編器已經無比健壯了,將C直接變成機器碼這個難度比將C語言翻譯為組合語言大多了,這屬於重新造輪子的行為。
曾經我就這樣問過自己,PHP直譯器是用C語言寫的,C編譯器是用匯編語言寫的(這句話不正確),組合語言是誰寫的呢?後來才知道,編譯器gcc其實是用C語言寫的。乍一聽,什麼?用C語言寫C編譯器?自己創造自己,就像電影《超驗駭客》一樣。當時的思維似乎陷入了死迴圈一樣,現在看來這不奇怪。其實編譯器用什麼語言寫是無所謂的,關鍵是能編譯出指令就行了。
編譯出的可執行檔案是要寫到磁碟上的,理論上,某個程式,無論其是不是編譯器,只要其關於讀寫檔案的功能足夠強大,可以往磁碟上寫任意內容,都可以生成可執行檔案,直接讓作業系統載入執行。想象一下,用Python寫一個指令碼,功能是複製一個二進位制可執行檔案,新複製出來的檔案肯定是可以執行的。那Python指令碼直接輸出這樣的一個二進位制可執行檔案,它自然就是可以直接執行的,完全脫離Python直譯器了。
編譯器其實就是語言,因為編譯器在設計之初就是先要規劃好某種語言,根據這個語言規則來寫合適的編譯器。所以說,要發明一種語言,關鍵是得寫出與之配套的編譯器,這兩者是同時出來的。最初的編譯器肯定是簡單、粗糙的,因為當時的程式語言肯定不完善,頂多是幾個符號而已,所以難以稱之為語言。只有功能完善且符合規範,有自己一套體系後才能稱之為語言。
不用說,這個最初的編譯器肯定無法編譯今天的C語言程式碼。程式語言只是文字,文字只是用來看的,沒有執行能力。最初的編譯器肯定是用機器碼寫出來的。這個編譯器能識別文字,可以處理一些符號關鍵字。隨著符號越來越多,不斷地去改進這個編譯器就是了。
以上的符號說的就是程式語言。後來這個編譯器支援的關鍵字越來越多了,也就是這個編譯器支援的程式語言越發強大了,可以寫出一些複雜的功能的時候,乾脆直接用這個語言寫個新的編譯器,這個新的編譯器出生時,還是需要用舊的編譯器編譯出來的。
只要有了新的編譯器,之後就可以和舊的編譯器說拜拜了。發明新的編譯器實際上就是能夠處理更多的符號關鍵字,也就是又有新的開發語言了,這門語言可以是全新的也可以是最初的語言,這取決於編譯器的實現。這個過程不斷持續,不斷進化,逐漸才有了今天的各種語言直譯器,這是個迭代的過程。
圖 0-1

圖0-1在網路上非常火,它常常與勵志類的文字相關。起初看到這個雕像在雕刻自己時,我著實被感動了,感受到的是一種成長之痛。弟子規讀後感(http://www.simayi.net/duhougan/6660.html)心得體會,今天把它貼過來的目的是想告訴大家,起初的編譯器也是功能簡單,不成規範的,然而經過不斷自我“雕刻”,它才有了今天功能的完善。

下面的內容我參考了別人的文章,由於找不到這位大師的署名,只好在此先獻上我真摯的敬意,感謝他對求知者的奉獻。
要說到C編譯器的發展,必須要提到這兩位大神—C語言之父Dennis Ritchie和Ken Thompson。Dennis和Ken在程式語言和作業系統的深遠貢獻讓他們獲得了電腦科學的最高榮譽,Dennis和Ken於1983年贏得了ACM圖靈獎。
編譯器是靠不斷學習,不斷積累才發展起來的,這是自我學習的過程。下面來看看他們是如何讓編譯器長大的。
我們都知道轉義字元,轉義字元是以\開頭的多個字元,通常表示某些控制字元,它們通常是不可鍵入的,也就是這些字元無法在鍵盤上直接輸入,比如\n表示回車換行,\t表示tab。由於以\開頭的字元表示轉義,因此要想表示\字元本身,就約定用\來轉義自己,即\\表示字元\。轉義字元雖然表示的是單個字元的意義,在編譯器眼裡轉義字元是多個字元組成的字串,比如\n是字元\和n組成的字串。
起初的C編譯器中並沒有處理轉義字元,為敘述方便,我們現在稱之為舊編譯器。如果待編譯的程式碼檔案中有字串\\,這在舊編譯器眼裡就是\\字串,並不是轉義後的單個字元\。為了表明編譯器與作為其輸入的程式碼檔案的關係,我們稱“作為輸入的程式碼檔案”為應用程式檔案。儘管被編譯的程式碼檔案是實現了一個編譯器,而在編譯器眼裡,它只是一個應用程式級的角色。例如,gcc –c a.c中,a.c就是應用程式檔案。
現在想在編譯器中新增對轉義字元的支援,那就需要修改舊編譯器的原始碼,假設舊編譯器的原始碼檔名為compile_old.c。被修改後的編譯器程式碼,已不屬於舊編譯器的原始碼,故我們命名其檔名為compile_new_a.c,圖0-2是修改後的內容。

相關文章