在上一篇《前端魔法堂——異常不只僅是try/catch》中咱們描述出一副異常及如何捕獲異常的畫像,但僅僅如此而已。試想一下,咱們窮盡一切捕獲異常實例,而後僅僅爲告訴用戶,運維和開發人員頁面報了一個哪一個哪一個類型的錯誤嗎?答案是否認的。咱們的目的是收集剛剛足夠的現場證據,好讓咱們能立刻重現問題,快速修復,提供更優質的用戶體驗。那麼問題就落在「收集足夠的現場證據」,那麼咱們又須要哪些現場證據呢?那就是異常信息,調用棧和棧幀局部狀態。(異常信息咱們已經獲取了)
本文將圍繞上調用棧和棧幀局部狀態敘述,準開開車^_^html
本篇將敘述以下內容:前端
既然咱們要獲取調用棧信息,那麼起碼要弄清楚什麼是調用棧吧!下面咱們分別從兩個層次來理解~git
假若主要工做內容爲應用開發,那麼咱們對調用棧的印象以下就差很少了:github
function funcA (a, b){ return a + b } function funcB (a){ let b = 3 return funcA(a, b) } function main(){ let a = 5 funcB(a) } main()
那麼每次調用函數時就會生成一個棧幀,並壓入調用棧,棧幀中存儲對應函數的局部變量;當該函數執行完成後,其對應的棧幀就會彈出調用棧。
所以調用main()
時,調用棧以下chrome
----------------<--棧頂 |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
調用funcB()
時,調用棧以下架構
----------------<--棧頂 |function:funcB| |let b = 3 | |return funcA()| ---------------- |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
調用funcA()
時,調用棧以下運維
----------------<--棧頂 |function:funcA| |return a + b | ---------------- |function:funcB| |let b = 3 | |return funcA()| ---------------- |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
funcA()
執行完成後,調用棧以下函數
----------------<--棧頂 |function:funcB| |let b = 3 | |return funcA()| ---------------- |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
funcB()
執行完成後,調用棧以下優化
----------------<--棧頂 |function: main| |let a = 5 | |return void(0)| ----------------<--棧底
main()
執行完成後,調用棧以下this
----------------<--棧頂 ----------------<--棧底
如今咱們對調用棧有了大概的印象了,但你們有沒有留意上面記錄"棧幀中存儲對應函數的局部變量",棧幀中僅僅存儲對應函數的局部變量,那麼入參呢?難道會做爲局部變量嗎?這個咱們要從理論的層面才能獲得解答呢。
這裏咱們要引入一個簡單的C程序,透過其對應的彙編指令來說解了。我會盡我所能用通俗易懂的語言描述這一切的,如有錯誤請各位指正!!
ESP/RSP, 暫存棧頂地址 EBP/RBP, 暫存棧幀起始地址 EIP, 暫存下一個CPU指令的內存地址,當CPU執行完當前指令後,從EIP讀取下一條指令的內存地址,而後繼續執行
PUSH <OPRD>,將ESP向低位地址移動操做數所需的空間,而後將操做數壓入調用棧中 POP <OPRD>,從調用棧中讀取數據暫存到操做數指定的寄存器或內存空間中,而後向高位地址移動操做數對應的空間字節數 MOV <SRC>,<DST>,數據傳送指令。用於將一個數據從源地址傳送到目標地址,且不破壞源地址的內容 ADD <OPRD1>,<OPRD2>,兩數相加不帶進位,而後將結果保存到目標地址上 RET,至關於POP EIP。就是從堆棧中出棧,而後將值保存到EIP寄存器中 LEAVE,至關於MOV EBP ESP,而後再POP EBP。就是將棧頂指向當前棧幀地址,而後將調用者的棧幀地址暫存到EBP中
push %rbp ;將調用者的棧幀指針壓入調用棧 mov %rsp,%rbp ;如今棧頂指向剛入棧的RBP內容,要將其設置爲棧幀的起始位置
如今們結合實例來理解吧!
C語言
#include <stdio.h> int add(int a, int b){ return a + b; } int add2(int a){ int sum = add(0, a); return sum + 2; } void main(){ add2(2); }
而後執行如下命令編譯帶調試信息的可執行文件,和dump文件
$ gcc -g -o main main.c $ objdump -d main > main.dump
下面咱們截取main、add2和add對應的彙編指令來說解
main函數對應的彙編指令
0x40050f <main> push %rbp 0x400510 <main+1> mov %rsp,%rbp ;將2暫存到寄存器EDI中 0x400513 <main+4> mov $0x2,%edi ;執行call指令前,EIP寄存器已經存儲下一條指令的地址0x40051d了 ;首先將EIP寄存器的值入棧,當函數返回時用於恢復以前的執行序列 ;而後纔是執行JUMP指令跳轉到add2函數中開始執行其第一條指令 0x400518 <main+9> callq 0x4004ea <add2> ;什麼都不作 0x40051d <main+14> nop ;設置RBP爲指向main函數調用方的棧幀地址 0x40051e <main+15> pop %rbp ;設置EIP指向main函數返回後將要執行的指令的地址 0x40051f <main+16> retq
下面是執行add2函數第一條指令前的調用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數調用方的棧幀地址 <-- EBP +++++++++++++++++ 98 | 0x40051d | -- EIP的值,存放add2返回後將執行的指令的地址 <-- ESP +++++++++++++++++ 低位地址
add2函數對應的彙編指令
0x4004ea <add2> push %rbp 0x4004eb <add2+1> mov %rsp,%rbp 0x4004ee <add2+4> sub $0x18,%rsp ;棧頂向低位移動24個字節,爲後續操做預留堆棧空間 0x4004f2 <add2+8> mov %edi,-0x14(%rbp);從EDI寄存器中讀取參數,並存放到堆棧空間中 0x4004f5 <add2+11> mov -0x14(%rbp),%eax;從堆棧空間中讀取參數,放進EAX寄存器中 0x4004f8 <add2+14> mov %eax,%esi ;從EAX寄存器中讀取參數,存放到ESI寄存器中 0x4004fa <add2+16> mov $0x0,%edi ;將0存放到EDI寄存器中 ;執行call指令前,EIP寄存器已經存儲下一條指令的地址0x400504了 ;首先將EIP寄存器的值入棧,當函數返回時用於恢復以前的執行序列 ;而後纔是執行JUMP指令跳轉到add函數中開始執行其第一條指令 0x4004ff <add2+21> callq 0x4004d6 <add> 0x400504 <add2+26> mov %eax,-0x4(%rbp) ;讀取add的返回值(存儲在EAX寄存器中),存放到堆棧空間中 0x400507 <add2+29> mov -0x4(%rbp),%eax ;又將add的返回值存放到EAX寄存器中(這是有多無聊啊~~) 0x40050a <add2+32> add $0x2,%eax ;讀取EAX寄存器的值與2相加,結果存放到EAX寄存器中 0x40050d <add2+35> leaveq ;讓棧頂指針指向main函數的棧幀地址,而後讓EBP指向main函數的棧幀地址 0x40050e <add2+36> retq ;讓EIP指向add2返回後將執行的指令的地址
下面是執行完add2函數中mov %rsp,%rbp
的調用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數調用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回後將執行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數調用方(即main函數)的棧幀地址<-- ESP,EBP +++++++++++++++++ 低位地址
下面是執行add函數第一條指令前的調用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數調用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回後將執行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數調用方(即main函數)的棧幀地址<-- EBP +++++++++++++++++ 96 | 0xXX | +++++++++++++++++ ................. 76 | 0x02 | -- 這是`mov %edi,-0x14(%rbp)`的執行結果 +++++++++++++++++ ................. +++++++++++++++++ 73 | 0xXX | +++++++++++++++++ 72 | 0x400504 | -- EIP的值,存放add返回後將執行的指令的地址 <-- ESP +++++++++++++++++ 低位地址
add函數對應的彙編指令
0x4004d6 <add> push %rbp 0x4004d7 <add+1> mov %rsp,%rbp 0x4004da <add+4> mov %edi,-0x4(%rbp) 0x4004dd <add+7> mov %esi,-0x8(%rbp) 0x4004e0 <add+10> mov -0x4(%rbp),%edx 0x4004e3 <add+13> mov -0x8(%rbp),%eax 0x4004e6 <add+16> add %edx,%eax 0x4004e8 <add+18> pop %rbp 0x4004e9 <add+19> retq
下面是add函數執行完mov %rsp,%rbp
的調用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數調用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回後將執行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數調用方(即main函數)的棧幀地址 +++++++++++++++++ 96 | 0xXX | +++++++++++++++++ ................. 76 | 0x02 | -- 這是`mov %edi,-0x14(%rbp)`的執行結果 +++++++++++++++++ ................. +++++++++++++++++ 73 | 0xXX | +++++++++++++++++ 72 | 0x400504 | -- EIP的值,存放add返回後將執行的指令的地址 +++++++++++++++++ 71 | 97 | -- 存放add函數調用方(即add函數)的棧幀地址<-- EBP,ESP +++++++++++++++++ 低位地址
下面就是一系列彈出棧幀的過程了
當add函數執行完retq
的調用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數調用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回後將執行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數調用方(即main函數)的棧幀地址 <-- EBP +++++++++++++++++ 96 | 0xXX | +++++++++++++++++ ................. 76 | 0x02 | -- 這是`mov %edi,-0x14(%rbp)`的執行結果 +++++++++++++++++ ................. +++++++++++++++++ 73 | 0xXX | <-- ESP +++++++++++++++++ 低位地址
而後就不斷彈出棧幀了~~~
從上面看到函數入參是先存儲到寄存器中,而後在函數體內讀取到棧幀所在空間中(局部變量、臨時變量)。那麼從調用棧中咱們能獲取函數的調用流和入參信息,從而恢復案發現場^_^
其實函數入參的傳遞方式不止上述這種,還有如下3種
但無論哪一種,最終仍是會在函數體內讀取到當前棧幀空間中。
上面寫的這麼多,但是咱們如今寫的是JavaScript哦,那到底怎麼才能讀取調用棧的信息呢?
IE10+的Error實例中包含一個stack
屬性
示例
function add(a, b){ let sum = a + b throw Error("Capture Call Stack!") return sum } function add2(a){ return 2 + add(0, a) } function main(){ add2(2) } try{ main() } catch (e){ console.log(e.stack) }
Chrome回顯
Error: Capture Call Stack! at add (index.html:16) at add2 (index.html:21) at main (index.html:25) at index.html:29
FireFox回顯
add@file:///home/john/index.html:16:9 add2@file:///home/john/index.html:21:14 main@file:///home/john/index.html:25:3 @file:///home/john/index.html:29:3
Error.captureStackTrace
函數 V8引擎向JavaScript提供了其Stack Trace API中的captureStackTrace
函數,用於獲取調用Error.captureStackTrace
時的調用棧快照。函數簽名以下
@static @method captureStackTrace(targetObject, constructorOpt) @param {Object} targetObject - 爲targetObject添加.stack屬性,該屬性保存調用Error.captureStackTrace時的調用棧快照 @param {Function} constructorOpt= - 調用棧快照不斷做出棧操做,直到constructorOpt所指向的函數恰好出棧爲止,而後保存到targetObject的stack屬性中 @return {undefined}
示例
function add(a, b){ let sum = a + b let targetObj = {} Error.captureStackTrace(targetObj) console.log(targetObj.stack) Error.captureStackTrace(targetObj, add) console.log(targetObj.stack) return sum } function add2(a){ return 2 + add(0, a) } function main(){ add2(2) } main()
Chrome回顯
Error at add (index.html:18) at add2 (index.html:28) at main (index.html:32) at index.html:35 Error at add2 (index.html:28) at main (index.html:32) at index.html:35
console.trace
函數 還有最後一招console.trace
,不過實際用處不大
示例
function add(a, b){ let sum = a + b console.trace() return sum } function add2(a){ return 2 + add(0, a) } function main(){ add2(2) } main()
Chrome回顯
add @ index.html:16 add2 @ index.html:22 main @ index.html:26 (anonymous) @ index.html:29
上述三種方式(實際就兩種可用啦)都只能獲取函數調用流,函數入參、局部變量等信息全都灰飛煙滅了?上面不是說好這些信息調用棧都有嘛,幹嗎不給我呢?其實想一想都知道調用棧中有這麼多信息,其實咱們只需一小部分,全盤托出並非什麼好設計。其實咱們只要再獲取棧幀局部狀態就行了。
所謂棧幀局部狀態其實就是函數入參和局部變量,試想若是咱們獲得add
函數調用時的入參是a=0
、b=2
和sum=2
,那麼不就獲得完整案發現場了嗎?那問題就是如何得到了。要不咱們作個Monkey Patch
function StackTraceError(e, env){ if (this instanceof StackTraceError);else return new StackTraceError(e, env) this.e = e this.env = env } let proto = StackTraceError.prototype = Object.create(Error.prototype) proto.name = "StackTraceError" proto.message = "Internal error." proto.constructor = StackTraceError proto.valueOf = proto.toString = function(){ let curr = this, q = [], files = [] do { if (curr.stack){ let stack = String(curr.stack) let segs = stack.split('\n').map(seg => seg.trim()) files = segs.filter(seg => seg != "Error") } else{ q.unshift({name: curr.name, msg: curr.message, env: curr.env}) } } while (curr = curr.e) let frames = [] let c = files.length, i = 0 while (i < c){ let file = files[i] let e = q[i] let frame = { name: e && e.name, msg: e && e.msg, env: e && e.env, file: file } frames.push(JSON.stringify(frame)) i += 1 } return frames.join("\n") }
try/catch
捕獲棧幀局部狀態function add(a, b){ try{ var sum = a + b throw Error() } catch(e){ throw StackTraceError(e, ["a:", a, "b", b, "sum", sum].join("::")) } return sum } function add2(a){ try{ return 2 + add(0, a) } catch(e){ throw StackTraceError(e, ["a", a].join("::")) } } function main(){ try{ add2(2) } catch(e){ throw StackTraceError(e, "") } } try{ main() } catch(e){ console.log(e+'') }
chrome下
{"name":"StackTraceError","msg":"Internal error.","env":"a::0::b::2::sum::2","file":"at add (file:///home/john/index.html:57:11)"} {"name":"StackTraceError","msg":"Internal error.","env":"a:;2","file":"at add2 (file:///home/john/index.html:67:16)"} {"name":"StackTraceError","msg":"Internal error.","env":"","file":"at main (file:///home/john/index.html:76:5)"} {"file":"at file:///home/john/index.html:84:3"}
上面這種作法有三個問題
try/catch
的函數進行優化,若是每一個函數都包含try/catch
那會嚴重影響執行效率。sum
這種臨時變量其實並不用記錄,由於它能夠被運算出來,只要記錄a
和b
便可。假如咱們寫的全是純函數(就是相同入參一定獲得相同的返回值,函數內部不依賴外部狀態,如加法同樣,1+1永遠等於2),那麼咱們只需捕獲入口/公用函數的入參便可恢復整個案發現場了。
function add(a, b){ var sum = a + b throw Error() return sum } function add2(a){ try{ return 2 + add(0, a) } catch(e){ throw {error:e, env:["a:", a].join("::")}) } } function main(){ add2(2) } try{ main() } catch(e){ console.log(e+'') }
而後咱們就能夠拿着報錯信息從add2
逐步調試到add
中了。假如用ClojureScript咱們還能夠定義個macro簡化一下
;; 私有函數 (defn- add [a b] (let [sum (+ a b)] (throw (Error.)) sum)) ;; 入口/公用函數 (defn-pub add2 [a] (+ 2 (add 0 a))) (defn main [] (add2 2)) (try (main) (catch e (println e)))
defn-pub macro的定義
(defmacro defn-pub [name args & body] (let [e (gensym) arg-names (mapv str args)] `(def ~name (fn ~args (try ~@body (catch js/Object ~e (throw (clj->js {:e ~e, :env (zipmap ~arg-names ~args)}))))))))
寫到這裏其實也沒有一個很好的方式去捕獲案發現場證據,在入口/公用函數中加入try/catch
是我現階段能想到比較可行的方式,請各位多多指點。
尊重原創,轉載請註明轉自:http://www.cnblogs.com/fsjohnhuang/p/7729527.html ^_^肥仔John
http://www.cnblogs.com/exiahan/p/4310010.html http://blog.csdn.net/qiu265843468/article/details/17844419 http://lucasfcosta.com/2017/02/17/JavaScript-Errors-and-Stack-Traces.html http://blog.shaochuancs.com/about-error-capturestacktrace/ https://github.com/v8/v8/wiki/Stack-Trace-API