d2大會 - 如何經過逆向工程從v8進程中復活node的內容記錄與讀後感

ps:其實d2過去很長一段時間了,可是中間一直忙於業務沒時間梳理這個分享的內容~前端

首先,咱們考慮下,就是怎麼逆向工程從v8進程中復活node。其實在不熟悉整個流程的時候,咱們能夠簡化條件,來加深本身對內容的理解,能夠先考慮系統是如何從進程中復活一個程序,由於node運行的進程對系統來講與其餘進程其實無異,這裏就要引入coredump的概念了node

1.進程的運行與coredump

  ,當咱們運行一個可執行文件時,會起一個對應的進程。某個時刻,這個進程碰到了abort而終止。此時,若是你設置好了對應的應對方式,系統根據你的設置寫入文件到你的本地磁盤裏。除此以外,在沒有發生異常的時候能夠經過gcore等方式把進程運行時的鏡像內容寫入磁盤中以便於後續調試。linux上經常使用的是coredump,會把當時的內存狀態以及一些寄存器內容寫入磁盤。windows中經常使用的是minidump,minidump只記載對應的某一個異常狀況,好比說chrome報錯了,就記錄chrome的錯誤信息。除此以外還有不少工具是應用程序本身定位錯誤的工具,好比node-report,他會生成定製的文件,用來記錄對應工具的狀態。過後能夠運用工具來解析當時你程序的運行狀態linux

img

2.Core Dump analysis 舉例

  舉一個coredump analysis的例子,假設有一個虛構的vm.cc用來讀取js文件,這個vm在linux上能夠編譯成一個可執行文件。linux的可執行文件的格式通常都是elf,這個elf上會有一個專門用來記錄debugInfo的section(這個debugInfo格式通常都dwarf)。vm運行過程當中abort了,根據你的設置coredump的時候記錄一份代碼。 chrome

img

  通常經過lldb或者gdb來進行分析你的可執行文件的elf和記錄core dump時內存狀況的elf,獲得具體的報錯內容。可是如咱們所見,這個報錯內容中有一些不可讀的信息,這些就是咱們js拋出的錯誤信息,由於咱們動態語言是用咱們的vm進行執行的,它不必定會把相應的debugInfo寫入,或者寫入也沒法被lldb和gdb進行解讀,因此會呈現爲不可讀狀態,會致使咱們沒法快速定位到錯誤信息,那麼如何去讓這個錯誤信息變成可讀狀態呢?npm

img

  通常狀況下,都是經過在可執行文件加入一額外的hock/additional Metadata,或者裝一些插件,插入一些lldb或gdb可理解的內容,用來還原當時場景windows

  那麼就引入了此次分享的主角llnode,llnode是基於ldb進行開發的。llnode是主要面對v8的一個插件,除了nodejs之外,只要是基於v8開發的引擎其實均可以藉助llnode進行調試api

3.llnode的運行過程簡單介紹

  咱們都知道,v8引擎是node源碼中很小的一塊,v8下面有個腳本,這個腳本會掃描v8的頭文件,輸出一個debug-support的.cc文件,它會把所有metadata放入這個文件中,輸出成全局變量,編譯的時候會放在你的elf文件中,可讓debug找獲得,以便於嵌入一個埋點。實際過程當中,node程序運行了,拿到了生產coredump的可執行文件與coredump文件,而後把這兩個文件給llnode,就能解讀對應內存塊,解讀出你的文件中本來不被是別的js代碼bash

接下來咱們來了解下lldb是如何從內存中獲取對應的js信息的函數

4.從原始內存中重建js值

  假設有一個內存塊,咱們獲取他的內存地址去解析這塊內存上的內容。由於咱們都知道v8的全部內存都是對齊。一個64位機器上,一個內存地址後面就是64位的字段,因此能夠先查看64位的最後一個字段,若是最後一位是0,表明是一個小整數,那麼直接獲取前32位內容就能夠了,按照整數來解讀。若是最後一位是1,那麼表明是一個地址,咱們須要取前面63位,根據這個地址去找到另外一個內存地址,那到第二個地址後就能夠知道heapObject是怎樣的佈局,v8上絕大部分的對象通常都是用heapObject佈局。第二個內存地址第一位通常來講都是個map pointer,根據這個指針能夠去讀取出實際的map是怎麼構成的,讀取真是map的常量得知這個map中存有一個offset叫作instancetype,而後能夠根據instancetype得知這個對象具體是什麼類型的工具

img

  拿一個簡單的例子爲例--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個位置表明一個字符,就能夠一個個讀取直接返回對應的字符串內容。

img
  再舉一個複雜點的例子,對象模型。 js在v8裏有對應的佈局,叫jsObject,JSObject的map中有個字段叫descriptor array,裏面有不少元素解釋對象的佈局 jsObject中還含有不少的pointer,指向不一樣的backing store,裏面存儲着對象裏成員的具體內容 假設,一個對象裏存了一個數據key爲a,value指向另外一個對象的,這樣最快捷的方式會把指針放在對象裏面,放在對象自己後面,這樣存儲內容的內部位置叫作inObjectProps,存儲內容有限。而後a的details是另外一個bitfield,含有對象的信息,這個對象是是否可改變,是否放在inObjectProps,根據descriptor裏的details就能夠去解讀inObjectProps的信息, 若是內部放滿了,就會把指針存在在property backing store 。store中含有對應指針指向具體value,而後在descriptor會寫入一些信息,告訴你當前位置的內容指針在property backing store中,而後就能夠去property backing stone中獲取,而後假如是整數key成員,會存在elements backing store中,由於key爲整數,因此不須要去推導key的內容,而後整數key的位置指向對應指針,而後也會更新descriptor內部對應的property details

img

在瞭解了lldb如何從內存中獲取信息後再瞭解下v8是如何處理js調用函數的流程的

5. 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引發的,調用的時候的參數等等.

img

6.安裝與api調用方式

npm install llnode  --llnode_build_addon=true
複製代碼

llnode_build_addon 暴露的api, 會把對應的api暴露出來,能夠把故障的可執行文件和coredump結合起來,把js的值從新還原到js中,寫入nodejs裏的腳本去

  api使用方式,

  1. 能夠用process threads看全部的線程,每個線程裏的信息。能夠去掃描全部進程,當你知道你是由於內存泄漏掛掉的時候,
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}`)
    })
})
複製代碼
  1. 能夠掃描進程得知coredump的時候哪一個進程最多,而後能夠根據打印出來的class去找尋js中泄露的對象
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進行大升級的。

我的感想

  1. 聽分享的過程當中深入感受到本身不少知識的缺失,不少內容沒有了解對應的前置內容時,會徹底沒法理解
  2. 參考資料中的案發現場還原有比較完整的測試用例,有興趣的同窗能夠嘗試下

參考資料

  1. 第十三屆 D2 前端技術論壇精彩回顧 -- 張秋怡 - Bringing JavaScript Back to Life.pdf
  2. Node 案發現場揭祕 —— Coredump 還原線上異常
  3. 前置知識點 v8引擎的編譯方式與內存存儲方式 coredump的基礎知識 lldb與gdb的做用
相關文章
相關標籤/搜索