ps:其實d2過去很長一段時間了,可是中間一直忙於業務沒時間梳理這個分享的內容~前端
首先,咱們考慮下,就是怎麼逆向工程從v8進程中復活node。其實在不熟悉整個流程的時候,咱們能夠簡化條件,來加深本身對內容的理解,能夠先考慮系統是如何從進程中復活一個程序,由於node運行的進程對系統來講與其餘進程其實無異,這裏就要引入coredump的概念了node
,當咱們運行一個可執行文件時,會起一個對應的進程。某個時刻,這個進程碰到了abort而終止。此時,若是你設置好了對應的應對方式,系統根據你的設置寫入文件到你的本地磁盤裏。除此以外,在沒有發生異常的時候能夠經過gcore等方式把進程運行時的鏡像內容寫入磁盤中以便於後續調試。linux上經常使用的是coredump,會把當時的內存狀態以及一些寄存器內容寫入磁盤。windows中經常使用的是minidump,minidump只記載對應的某一個異常狀況,好比說chrome報錯了,就記錄chrome的錯誤信息。除此以外還有不少工具是應用程序本身定位錯誤的工具,好比node-report,他會生成定製的文件,用來記錄對應工具的狀態。過後能夠運用工具來解析當時你程序的運行狀態linux
舉一個coredump analysis的例子,假設有一個虛構的vm.cc用來讀取js文件,這個vm在linux上能夠編譯成一個可執行文件。linux的可執行文件的格式通常都是elf,這個elf上會有一個專門用來記錄debugInfo的section(這個debugInfo格式通常都dwarf)。vm運行過程當中abort了,根據你的設置coredump的時候記錄一份代碼。 chrome
通常經過lldb或者gdb來進行分析你的可執行文件的elf和記錄core dump時內存狀況的elf,獲得具體的報錯內容。可是如咱們所見,這個報錯內容中有一些不可讀的信息,這些就是咱們js拋出的錯誤信息,由於咱們動態語言是用咱們的vm進行執行的,它不必定會把相應的debugInfo寫入,或者寫入也沒法被lldb和gdb進行解讀,因此會呈現爲不可讀狀態,會致使咱們沒法快速定位到錯誤信息,那麼如何去讓這個錯誤信息變成可讀狀態呢?npm
通常狀況下,都是經過在可執行文件加入一額外的hock/additional Metadata,或者裝一些插件,插入一些lldb或gdb可理解的內容,用來還原當時場景windows
那麼就引入了此次分享的主角llnode,llnode是基於ldb進行開發的。llnode是主要面對v8的一個插件,除了nodejs之外,只要是基於v8開發的引擎其實均可以藉助llnode進行調試api
咱們都知道,v8引擎是node源碼中很小的一塊,v8下面有個腳本,這個腳本會掃描v8的頭文件,輸出一個debug-support的.cc文件,它會把所有metadata放入這個文件中,輸出成全局變量,編譯的時候會放在你的elf文件中,可讓debug找獲得,以便於嵌入一個埋點。實際過程當中,node程序運行了,拿到了生產coredump的可執行文件與coredump文件,而後把這兩個文件給llnode,就能解讀對應內存塊,解讀出你的文件中本來不被是別的js代碼bash
接下來咱們來了解下lldb是如何從內存中獲取對應的js信息的函數
假設有一個內存塊,咱們獲取他的內存地址去解析這塊內存上的內容。由於咱們都知道v8的全部內存都是對齊。一個64位機器上,一個內存地址後面就是64位的字段,因此能夠先查看64位的最後一個字段,若是最後一位是0,表明是一個小整數,那麼直接獲取前32位內容就能夠了,按照整數來解讀。若是最後一位是1,那麼表明是一個地址,咱們須要取前面63位,根據這個地址去找到另外一個內存地址,那到第二個地址後就能夠知道heapObject是怎樣的佈局,v8上絕大部分的對象通常都是用heapObject佈局。第二個內存地址第一位通常來講都是個map pointer,根據這個指針能夠去讀取出實際的map是怎麼構成的,讀取真是map的常量得知這個map中存有一個offset叫作instancetype,而後能夠根據instancetype得知這個對象具體是什麼類型的工具
拿一個簡單的例子爲例--js的string模型,不包含中文字符且底層表示爲序列的string模型。假設這樣一個字符串,你拿到了對應的map,map字後面兩位分別是是字符串的hash和length,而後根據map去查看他的instanceType ,instaceType是於一個bit field ,去讀取這個bit field中表明encoding的byte,若是這個byte是1,表明是string模型下面的字符都是一個字節的,沒有中文字符這些,而後讀取representation對應的byte ,若是這個byte爲0,表明這個模型是序列的,不存在樹形結構。得知解讀方式後就能夠解讀這個模型,知道後面的都是字符,encoding得知1個位置表明一個字符,就能夠一個個讀取直接返回對應的字符串內容。
在瞭解了lldb如何從內存中獲取信息後再瞭解下v8是如何處理js調用函數的流程的
首先debugger unwind the stack(展開當前的堆棧), 會有不少 frame pointer(幀指針) 告訴你每幀對應的佈局是怎麼樣的, 從上到下 分別是context(js當前做用域上下文),jsfunction,而後是各類framepointer,以後是返回地址, 這個時候讀一下 frame pointer 前面的64位字段,指向的是jsfunction的地址,jsfunction對象中也存在一個字段 sharedFunctionInfo,讀取shareFunctionInfo,裏面有個script字段,script字段後面還有一個formal params count的字段,表明調用函數有多少個參數, 能夠按照layout繼續解讀存放在frame上的函數名上參數所對應的數值,function name,對應名字,script 裏面有源代碼,shared name表明文件名, offset表明行列的變化,
根據以上的信息,能夠還原出你出錯的時候,對應是調用什麼function引發的,調用的時候的參數等等.
npm install llnode --llnode_build_addon=true
複製代碼
llnode_build_addon 暴露的api, 會把對應的api暴露出來,能夠把故障的可執行文件和coredump結合起來,把js的值從新還原到js中,寫入nodejs裏的腳本去
api使用方式,
const llnode =require('llnode').fromCoredump('/path/to/core','/path/to/node');
const process =llnode.getProcessObject();
console.log(`Process ${process.pid}: ${process.state}`);
process.threads.forEach((thread)=>{
console.log(`Thread ${thread.threadId}`);
thread.frames.forEach((frame,index)=>{
console.log(`#${index} ${frame.function}`)
})
})
複製代碼
llnode.getHeapTypes().forEach((type)=>{
console.log(`${type.typeName}: ${type.totalSize}`)
console.log(`${type.instanceCount} instances`)
for( const instance of type.instances){
console.log(`0x${instance.address} instance.value`)
}
})
複製代碼
由於是逆向工程,因此嚴重依賴當前版本的執行流程,因此每次v8換一個版本的時候,基本都須要針對新版本v8進行變更,可是nodejs的每一個LTS的版本內部不會對v8進行大升級的。