自我介紹
相關背景請參考個人部落格中的關於我頁面,參賽動機下面會寫到,目前規劃這個系列文會有八篇:
- Day00:V8 bytecode 系列文介紹(基本知識、產生 bytecode)
- Day01:從變數看 bytecode
- Day02:從判斷式看 bytecode
- Day03:從迴圈看 bytecode
- Day04:從函式看 bytecode
- Day05:從 class 看 bytecode
- Day06:從經典案例看 bytecode
- Day07:V8 bytecode 系列文總結
前言
我理解 bytecode 的啟蒙是這一篇文章:Understanding V8’s Bytecode,中譯版:理解 V8 的字節碼,這篇文章裡面有提到 V8 在解析以及運行程式碼時的流程,大致上是這樣的:
- 解析 JavaScript 程式碼,產生 AST(Abstract Syntax Tree)
- 透過 Ignition 產生 bytecode
- 透過 TurboFan 從 bytecode 產生 machine code
上文中的這張圖片,很清楚地說明了 bytecode 的定位:
那會看 bytecode 有什麼好處呢?你可以更接近底層一點,知道 V8 如何去處理某一段程式碼,有時候你會發現,跟你想像中的不太一樣,這就是看 bytecode 的好玩之處!
舉例來說,我以前寫過的三篇文章都有看 bytecode 的段落,透過 bytecode 去看一些底層的東西:
在看這些 bytecode 時有一個問題,那就是參考資料太少,中文的不用說,連英文的都不多,而且有些文章並不是那麼好懂,深度有點太深。因此,才有了這個系列文的想法,希望能以一些簡單的範例帶著跟我一樣對 bytecode 有興趣的人來入門,然後來看懂 bytecode。
產生 bytecode
想要看 bytecode,第一步就是要先產生 bytecode。
產生 bytecode 的方法很簡單,透過 Node.js 搭配 --print-bytecode
這個 flag 就行了。但要注意的是 Node.js 內建的東西不少,所以會產生一大堆的 bytecode,這邊推薦的方法是把你想看的程式碼用一個特別的函式名稱包住,搭配 --print-bytecode-filter
來篩選函式名稱,就會容易很多,例如說以下程式碼:
function find_me_test() {
var a = 1
console.log(a)
}
find_me_test() // V8 很智慧,記得要呼叫 function,否則不會產生程式碼
存檔成a.js
,然後執行:node --print-bytecode --print-bytecode-filter="find_me*" a.js > byte_code.txt
,接著打開byte_code.txt
,就可以看到產生的內容:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
83 E> 0x26e2ec856652 @ 0 : a0 StackCheck
301 S> 0x26e2ec856653 @ 1 : 0c 01 LdaSmi [1]
0x26e2ec856655 @ 3 : 26 fb Star r0
305 S> 0x26e2ec856657 @ 5 : 13 00 00 LdaGlobal [0], [0]
0x26e2ec85665a @ 8 : 26 f9 Star r2
313 E> 0x26e2ec85665c @ 10 : 28 f9 01 02 LdaNamedProperty r2, [1], [2]
0x26e2ec856660 @ 14 : 26 fa Star r1
313 E> 0x26e2ec856662 @ 16 : 57 fa f9 fb 04 CallProperty1 r1, r2, r0, [4]
0x26e2ec856667 @ 21 : 0d LdaUndefined
320 S> 0x26e2ec856668 @ 22 : a4 Return
Constant pool (size = 2)
Handler Table (size = 0)
1
以上程式碼其實沒有那麼好懂,那是因為少了一個關鍵的資訊:constant pool,有一些常數會放到這個裡面,例如說 console.log
的 console
跟 log
你會發現在程式碼裡面都沒有出現,因為這兩個關鍵字都在 constant pool 裡面。
在一般的 Node.js 裡面,你只會看到:Constant pool (size = 2)
,卻看不到裡面的內容。
想要看到裡面的內容的話,必須要自己 build debug 版本的 Node.js,或是直接去 build debug 版本的 V8,因為我想說反正也沒編譯過 V8,就決定來編譯看看了。
(參考資料:Node.js Bytecode Constant Pool Output)
編譯 V8
想要編譯 V8,官方文件提供了滿完整的說明,以下是一些相關文件:
但基本上步驟滿簡單的,我的作業系統是 macOS Mojave 10.14.4,步驟如下:
- 安裝 depot_tools,步驟如下
- 下載 depot_tools:
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
- 設置環境變數:
export PATH=$PATH:/path/to/depot_tools
- 下載 v8:
fetch v8
- 接著我在編譯時碰到問題,猜測是需要新版的作業系統,因此我編譯的是比較舊版的 V8:
git checkout -b 7.3 -t branch-heads/7.3
- 安裝必要套件:
gclient sync
- build debug version:
tools/dev/gm.py x64.debug
編譯跟下載套件都需要一段時間,需要耐心等待。編譯完成以後,就可以在 out/x64.debug
底下找到一個執行檔 d8
,以後都用這個 d8 即可。
我們用同樣的指令,只是執行檔改成 d8,去編譯上面那一段程式碼看看:./d8 --print-bytecode --print-bytecode-filter="find_me*" a.js > byte_code.txt
,結果為:
[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
21 E> 0x10715a29dcaa @ 0 : a5 StackCheck
239 S> 0x10715a29dcab @ 1 : 0c 01 LdaSmi [1]
0x10715a29dcad @ 3 : 26 fb Star r0
243 S> 0x10715a29dcaf @ 5 : 13 00 00 LdaGlobal [0], [0]
0x10715a29dcb2 @ 8 : 26 f9 Star r2
251 E> 0x10715a29dcb4 @ 10 : 28 f9 01 02 LdaNamedProperty r2, [1], [2]
0x10715a29dcb8 @ 14 : 26 fa Star r1
251 E> 0x10715a29dcba @ 16 : 59 fa f9 fb 04 CallProperty1 r1, r2, r0, [4]
0x10715a29dcbf @ 21 : 0d LdaUndefined
258 S> 0x10715a29dcc0 @ 22 : a9 Return
Constant pool (size = 2)
0x10715a29dc31: [FixedArray] in OldSpace
- map: 0x1071768807b1 <Map>
- length: 2
0: 0x1071ec1100e9 <String[#7]: console>
1: 0x1071ec10fbe9 <String[#3]: log>
Handler Table (size = 0)
1
會發現 constant pool 的內容顯示出來了。至此,前置作業已經都準備完成。
結語
在這篇文章中簡單介紹了 bytecode 的定位以及系列文未來的方向,在之後的文章裡面會慢慢補齊一些基礎知識,也會從各個基礎的語法來學習 bytecode。