現代有不少程式語言,在編譯過後產生位元組碼(byte code),真正執行時,才會直譯為各平臺上可執行形式。因此,認識位元組碼是有價值的,然而翻開位元組碼相關文件,總是令人昏昏欲睡,多半也只能想像?那麼,試著從實際操作堆疊來操作位元組碼如何?
從堆疊開始
在實作語言時,必須經過語法剖析與詞法剖析,以產生對應的語法樹節點,所以,語言越複雜,這個階段處理也就越麻煩而耗時。
若語法樹節點的執行流程,能以某種指令形式儲存下來,下次執行時,直接載入、依序執行指令就好了,這省略了剖析上的相關階段,理論上,能使語言執行時獲得更好的效率,而且,由於位元組碼指令與格式是公開規範,不同平臺可以各自實作直譯位元組碼的程式,或者不同語言可以將執行流程轉換特定位元組碼,這使得位元組碼就類似於原生指令,而直譯位元組碼的程式就像是個虛擬機器。
編譯後產生位元組碼,最經典的代表語言,有Java與Python,不少文件討論介紹語言的位元組碼格式與指令,明瞭運作原理後,透過更高階的程式來閱讀,或是程式庫來操作位元組碼,也有實用價值。
雖然手寫位元組碼非常麻煩,然而試著自行實作簡單的位元組碼,對於各種位元組碼的理解,會有非常大的助益。這沒想像中難,第一步是先用語法樹寫些簡單的程式,像是1+2、3+4*5這類的算式,在能夠運行語法樹得到正確的執行結果後,試著將操作限定在堆疊上,寫些函式來封裝相關操作,例如數字節點Num執行時,可呼叫函式push(stack, self.value),而函式內容為stack.append(value),也就是在堆疊頂端放置一個數字。
相加的Add節點執行時,可呼叫self.left.evaluate(stack)、self.right.evaluate(stack)與add(stack),add函式會從堆疊頂端依序取出兩個值,在相加後置入堆疊:
def add(stack): r = stack.pop() l = stack.pop() stack.append(l + r)
因此,對於1+2,對應的語法節點Add(Num(1), Num(2)).evaluate(stack)執行後,stack頂端的值會是3,類似地,可用相同的方式,實作出減、乘、除等在堆疊上對應的操作,這可以參考我在〈Byte Code ABC - 1〉(https://goo.gl/qHdNCZ)的簡單實作。
記憶體中的位元組碼
到目前為止,我們所進行的,是基於堆疊架構的操作,這也是大多數位元組碼指令與虛擬機的基礎,在記憶體中對堆疊資料結構進行操作,將相關的操作封裝為函式,是為了熟悉堆疊導向(Stack-oriented)的程式設計思維。當然,在目前的程式執行結束後,堆疊就會消失,因而下一步是必須將操作化為位元組碼指令,以便有機會將之儲存下來。
方才談到的push、add函式名稱,就可以是個位元組碼指令,對於push(stack, self.value)來說,若self.value實際是1,可以設計簡單的位元組碼指令push 1,add雖然不需要引數,然而還設計為add _指令,_是為了讓位元組碼指令有固定的格式,後續在執行位元組碼時,就不會有錯綜複雜文法的剖析負擔,這也是為何位元組碼都規範有特定格式的原因。
設計出來的指令,並不用急著儲存到檔案去,我們可以先儲存在記憶體中,例如儲存在有序的list資料結構之中,這省去了一開始就得考慮檔案編碼,以及檔案中位元組碼格式等問題。
例如,若bytecodes是個list,數字節點Num要生成位元組碼時,可以執行bytecodes.append(f'push {self.value}'),相加的Add節點執行時,會執行self.left.bc_instructn(bytecodes)、self.right.bc_instructn(bytecodes),接著才是bytecodes.append('add _')。
因此,對於5+2*3,也就是Add(Num(5), Mul(Num(2), Num(3))).bc_instructn(bytecodes)來說,bytecodes就會儲存push 5、push 2、push 3、mul _、add _的位元組碼,若要執行這些位元組碼,可以寫個簡單的vm函式:
def vm(bytecodes): stack = [] for code in bytecodes: instruction, value = code.split(' ') instructions[instruction](stack, value) return stack
instructions是個dict,值的部份是push、add等函式,操作都是在堆疊上進行,因而傳回的stack就包含了操作結果,這可以參考我在〈Byte Code ABC - 2〉(https://goo.gl/7BCbK5)的簡單實作。
Python的dis模組
在能夠實作簡單的算式之後,或許開發者會開始加入變數或者更多語法節點,以及相對應的位元組碼指令,而且會需要使用多個堆疊。如果對於該採用哪個位元組碼指令有困惑,此時,可以參考其他語言的位元組碼指令清單。以Python為例,方才的push指令類似於Python的LOAD_CONST,add指令類似於BINARY_ADD等。
若想觀察Python程式碼的位元組指令,可用標準程式庫提供的dis模組,位元組碼指令清單也可在dis模組的文件中找到。舉例來說,若定義add函式:
def add(a, b): return a + b
若想探查add函式的定義內容,可以透過add的__code__特性,它是個code物件,擁有co_varnames(區域變數)等特性,代表函式上定義的各元素,而co_code是函式的位元組碼,然而它是以bytes傳回,可以透過dis的dis函式,將位元組碼翻譯為可讀指令並顯示出來,例如Python 3.7中,顯示的訊息會包含七個欄位,而不同指令會顯示的欄位資訊不同,dis.dis(add.__code__.co_code)的話會顯示:
2 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4 BINARY_ADD 6 RETURN_VALUE
就上述欄位來說,第一行開頭的2代表原始碼第2行,亦即return a + b,0表示位元組指令位移,自Python 3.6起,每個位元組碼指令是使用兩個位元組儲存,因此會看到0、2、4、6的指令位移,LOAD_FAST是指令名稱,之後的0是指令需要的引數,就LOAD_FAST來說就是區域變數的編號,(a)則是參數名稱。有興趣更進一步認識Python的位元組碼,也可以參考PyCon 2016中〈Playing with Python Bytecode〉(https://goo.gl/oZdpL8)的演講內容。
堆疊導向的設計風格
除了從位元組碼上直接熟悉這種基於堆疊的操作,也可以借助wabt這類工具,來熟悉堆疊導向的設計風格,開發者可以使用方便閱讀的文字格式,具體來說,是使用S運算式來撰寫程式,再透過wat2wasm轉換為WebAssembly,有興趣的話,可以參考〈以WasmVM向WebAssembly說哈囉〉(https://goo.gl/uVcvq5),當中有詳細說明。
認識位元組碼的好處,也是認識程式碼寫法對效率的影響,就像修改位元組碼來改變程式行為,甚至自造語言時,編譯為特定VM或自創的位元組碼,實用價值完全取決於開發者對位元組碼的熟悉度!
專欄作者
熱門新聞
2024-12-27
2024-12-24
2024-11-29
2024-12-22
2024-12-20