V8環境搭建
為什么寫這個主題
Chrome的重要地位不用贅述,V8不僅是chrome的核心組件,還是node.js等眾多軟件的核心組件,V8的重要程度亦不用多言。V8涉及的技術十分廣泛,包括了操作系統、編譯技術、計算機系統結構等多方面的知識,而網上的材料或零散或陳舊,給初學者造成了很大的困難,所以我準備寫一系列文章,從基礎入手,對V8內存分配、Isolate創建、handles概念、builtin、codegen、編譯等每個方面進行詳細講解,力求做到有概念層面的講解、有理論依據,又有代碼層面的說明、有事實論證。
本節內容介紹
本文是這個系列的第一篇,主要講解四部分內容:一、編譯工具鏈、V8編譯和調試;二、如何學習V8代碼最容易;三、以JavaScript元素(Element)為例,開啟V8源碼的學習之旅,起到拋磚引玉的作用;四、元素原理講解。
本文所講的操作過程要求你的網絡能訪問谷歌,還要求你的cmd命令行也能通過http(s)訪問谷歌。
一、下載、編譯、調試
1.系統環境要求
操作系統 win 10 64bit,VS2019社區版,git
windows 10 SDK至少是10.0.19041以上版本,安裝SDK時不推薦用VS installer安裝,應該單獨下載SDK安裝包,原因是installer安裝的SDK文件不全。
2.depot_tools工具
它是v8的編譯工具鏈,下載代碼,編譯代碼都需要用到它。
- 下載地址:https://storage.googleapis.com/chrome-infra/depot_tools.zip
- 壓縮depot_tools.zip,用鼠標右鍵壓縮,注意:不要雙擊打開并從中拖拽出來
- 把depot_tools加入到環境變量PATH中;添加系統變量DEPOT_TOOLS_WIN_TOOLCHAIN=0
- 打開CMD終端(不是powershell,并且能通過http(s)訪問谷歌),執行gclient。它做一些初始化工作,與v8代碼無關,不作深入講解,可自行查閱源碼
- gclient執行完畢,用where python查看depot_tools中的python.bat路徑信息,確保python.bat在環境變量PATH中的位置在系統中原有(如果有)的python環境位置前面
3.下載代碼
git的初始化
> git config --global user.name "Name" > git config --global user.email "address@mail.com" > git config --global core.autocrlf false > git config --global core.filemode false > git config --global branch.autosetuprebase always
下載V8代碼
fetch v8 git pull origin master
源碼不太大,用debug模式編譯后需要7G的硬盤空間,建議用固態硬盤,因為文件數量多,VS2019在掃描文件之間的引用關系時,固態很有優勢。
生成GN工程文件
> cd ~\v8\src #進入v8 src目錄 > gn gen --ide=vs out\default --args="is_component_build = true is_debug = true v8_optimized_debug = false"
gn命令不是本文重點,可自行查閱,參數:is_debug = true 讓v8可以被調試,v8_optimized_debug = false 去掉對調試v8有干擾的代碼優化,這不會影響v8的正確性,只可能對性能有點影響,下文是我的編譯配置文件args.gn。
is_component_build = true is_debug = true v8_optimized_debug = false v8_use_snapshot = false
4.編譯、調試
用VS2019編譯V8
在src\out\default下,能看到all.sln,雙擊打開,如圖-1。

在解決方案資源管理器中,能看到v8_hello_world這個方案,鼠標右擊“設為啟動項目”,再次鼠標右擊“生成”,這樣就開始編譯了,在圖1下方的輸出窗口,能看到編譯過程。編譯時間長短要看機器性能:CPU和內存頻率、硬盤讀寫速度。

上圖是跟蹤hello world的調用堆棧。
總結,環境搭建過程中可能會出現意想不到的問題,多數都和SDK版本、環境變量有關,詳細查看出錯信息往往會得到答案。
二、更容易入手的學習方法
學代碼,肯定是可以調試、跟蹤最容易,V8代碼量大、結構復雜、類引用的層級關系多,要有一個合理的入口才好,v8的源碼都在src目錄下,如下圖。

除了src目錄,我們看到還有一個samples目錄,它就是我們開始學習的地方,圖1中打開的文件(hello-world.cc)正是在這個目錄下,這個hello world程序是用C++編寫的,包括了啟動V8,然后運行一個javascript語言的hello world,還有一個加法運算。準確地說,V8是一個javascript虛擬機,這個hello word.cc中僅有”hello world”和加法算法是一個真正的javascript程序,其它的代碼都是為了運行javascript程序而做的準備工作(啟動V8虛擬機),包括了V8的創建、Isolate創建、handle創建,編譯,輸出hello world,再結束V8的全過程。這里只包括了V8最簡單最必要的功能集,所以,從跟蹤hello-world.cc入手學習V8是最簡單的。
三、元素(Element)的調試跟蹤
V8的啟動全過程涉及很多知識,本篇文章的作用為學習V8代碼起個頭,所以咱們聊聊相對簡單的元素(Element)初始化。通過元素的初始化,了解如何跟蹤V8代碼,以及了解V8的代碼風格。
先來說什么是元素,Element是什么?先來看一個概念,{a:”foo”,b:”bar”},這個對象有兩個名字屬性(name properties),”a”和”b”,它們不能用數組下標索引,有下標索引屬性的,通常被稱為元素(Element),例如:[“foo”,”bar”],0位置是”foo”,1位置是”bar”。這在V8中的處理方式有很大的不同,先通過一張圖說明一下javascript對象在V8中的表現形式。

從圖可以看出,在V8中javascript對象內部是分開做存儲的,name property和元素分開存儲的,也就是圖版中紅色標記的部分。我直接給出了在V8中定位元素代碼和調試跟蹤方法,見下圖。

注意圖中文件的位置,和斷點,調試時就可以在這個位置停下來,這就是跟蹤方法,單步進入分析即可。
四、元素(Element)原理及重要數據結構
1.主要代碼和數據結構
Element中的大量方法,例如pop或者slice方法等,大多都是用來對一段連續的地址空間進行操作的。ElementsAccessor是操作Element的基類,在Element上的每個操作都會對應到ElmentsAccessor。
void ElementsAccessor::InitializeOncePerProcess() { static ElementsAccessor* accessor_array[] = {#define ACCESSOR_ARRAY(Class, Kind, Store) new Class(), ELEMENTS_LIST(ACCESSOR_ARRAY)//這里初化的宏義#undef ACCESSOR_ARRAY }; STATIC_ASSERT((sizeof(accessor_array) / sizeof(*accessor_array)) == kElementsKindCount); elements_accessors_ = accessor_array;}
代碼中的ACCESSOR_ARRAY和ELEMENTS_LIST兩個宏配合完成初始化,ELEMENTS_LIST中也能看到所有種類的Element,也就是Element kinds,如下圖所示。

以FastPackedSmiElemntsAccessor為例進行分析,這個類從名字可以看出來它是連續存儲的Smi類型(V8中規定),連續存儲是指數據地址中沒有空洞,例如:a =[1,2,3],這就是packed(連續) Smi,b = [1,,3],這就是Holey Smi。何為Smi?就是小整數,以32位CPU為例說明,最低的一位(bit)是表示類型的,如果是1,則是指針,是0表示是Smi。
FastPackedSmiElementsAccessor中沒有具體實現的功能, 我們直接看他的父類 FastSmiOrObjectElementsAccessor,這里有一些功能的實現,但目前看完這些功能,是沒有辦法在腦海中構建出一個Element結構來的,現在還為時尚早,因為這里只有Element的初始化,沒有講它是如何使用的,比如:slice方法是如何實現的,這里并沒有詳細說明,這里主要說明了初始化。接著看FastElementsAccessor這個父類,這里定義的功能實現比FastSmiOrObjectElementsAccessor中多,方法名中包含”Normailize”字樣的方法非常重要,因為任何一個Javascript中關于Element的操作方法(比如slice)的執行過程都與Normalize類方法有關,Normalize是Element的操作方法到最終實現之間的必經之路。

NormalizeImpl是Normalize方法的具體實現,再進入下一個父類。

最后給出類型的嵌套關系,如下圖。

看完這些類中的各種方法,感覺方法之間無法關聯起來,這是正常的,因為到目前為止,我們還沒有進入方法的調用過程。
2.從element.length看執行過程
到目前為止,我們還只是看ELement的初始化過程,如果能讓所有的成員動起來,可以提高我們的學習效率,借助debug讓代碼動起來,如下圖。

我們調試的是shell.cc,它的位置在samples\下,我們沒有使用hello world,改為使用shell.cc,只是因為shell的交互性更強,讓你看到”動起來”的效果更明顯,hello world依舊是最簡單的學習方式。調試之前要把這個工程編譯,看圖中右側的紅色標記,可以把這個工程設置為啟動項,然后生成,啟動debug后可以看到下面這個圖。

這時,我們在elements.cc中下斷點,如下圖。

在這樣圖中,能看到斷點,還有調用堆棧,從中能看到函數的調用過程,在shell窗口中執行如下指令。
a=[1,2,3,4]a.length = 7 //特意改變數組長度,為了能觸發斷點
這時就能夠復原上圖的堆棧了,你可以查看附近的相關代碼。
3.數據結構Accessors
我們先來看一下這個結構的部分源代碼。
// Accessors contains all predefined proxy accessors.class Accessors : public AllStatic { public:#define ACCESSOR_GETTER_DECLARATION(_, accessor_name, AccessorName, ...) \ static void AccessorName##Getter( \ v8::Local name, \ const v8::PropertyCallbackInfo& info); ACCESSOR_INFO_LIST_GENERATOR(ACCESSOR_GETTER_DECLARATION, /* not used */)#undef ACCESSOR_GETTER_DECLARATION
#define ACCESSOR_SETTER_DECLARATION(accessor_name) \ static void accessor_name( \ v8::Local name, v8::Local value, \ const v8::PropertyCallbackInfo& info); ACCESSOR_SETTER_LIST(ACCESSOR_SETTER_DECLARATION)#undef ACCESSOR_SETTER_DECLARATION...省略很多代碼... .......
這個Accessors的作用是什么?通過上面的例子來說明:上面定義的a.length=5的執行過程是:需要先找到它的Accessors,然后才能到具體的setlength,這正好和V8的ElementsAccessor說明是對應的,如下圖。
