現代有不少程式語言,在編譯過後產生位元組碼(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或自創的位元組碼,實用價值完全取決於開發者對位元組碼的熟悉度!

專欄作者

熱門新聞

Advertisement