摘要:前言在上一篇前端魔法堂異常不僅僅是中我們描述出一副異常及如何捕獲異常的畫像,但僅僅如此而已。調用方從右到左的順序將參數壓入棧中,在被調用方執行完成后,由被調用方負責清理棧中的參數也稱為棧平衡。
前言
?在上一篇《前端魔法堂——異常不僅僅是try/catch》中我們描述出一副異常及如何捕獲異常的畫像,但僅僅如此而已。試想一下,我們窮盡一切捕獲異常實例,然后僅僅為告訴用戶,運維和開發人員頁面報了一個哪個哪個類型的錯誤嗎?答案是否定的。我們的目的是收集剛剛足夠的現場證據,好讓我們能馬上重現問題,快速修復,提供更優質的用戶體驗。那么問題就落在“收集足夠的現場證據”,那么我們又需要哪些現場證據呢?那就是異常信息,調用棧和棧幀局部狀態。(異常信息我們已經獲取了)
?本文將圍繞上調用棧和棧幀局部狀態敘述,準開開車^_^
?本篇將敘述如下內容:
什么是調用棧?
如何獲取調用棧?
什么是棧幀局部狀態?又如何獲取呢?
一.什么是調用棧??既然我們要獲取調用棧信息,那么起碼要弄清楚什么是調用棧吧!下面我們分別從兩個層次來理解~
印象派?倘若主要工作內容為應用開發,那么我們對調用棧的印象如下就差不多了:
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()時,調用棧如下
----------------<--棧頂 |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()執行完成后,調用棧如下
----------------<--棧頂 ----------------<--棧底
?現在我們對調用棧有了大概的印象了,但大家有沒有留意上面記錄"棧幀中存儲對應函數的局部變量",棧幀中僅僅存儲對應函數的局部變量,那么入參呢?難道會作為局部變量嗎?這個我們要從理論的層面才能得到解答呢。
理論派?這里我們要引入一個簡單的C程序,透過其對應的匯編指令來講解了。我會盡我所能用通俗易懂的語言描述這一切的,若有錯誤請各位指正??!
前提知識Intel X86架構中調用棧的棧底位于高位地址,而棧頂位于低位地址。(和印象派中示意圖的方向剛好相反)
調用棧涉及的寄存器有
ESP/RSP, 暫存棧頂地址 EBP/RBP, 暫存棧幀起始地址 EIP, 暫存下一個CPU指令的內存地址,當CPU執行完當前指令后,從EIP讀取下一條指令的內存地址,然后繼續執行
操作指令
PUSH,將ESP向低位地址移動操作數所需的空間,然后將操作數壓入調用棧中 POP ,從調用棧中讀取數據暫存到操作數指定的寄存器或內存空間中,然后向高位地址移動操作數對應的空間字節數 MOV , ,數據傳送指令。用于將一個數據從源地址傳送到目標地址,且不破壞源地址的內容 ADD , ,兩數相加不帶進位,然后將結果保存到目標地址上 RET,相當于POP EIP。就是從堆棧中出棧,然后將值保存到EIP寄存器中 LEAVE,相當于MOV EBP ESP,然后再POP EBP。就是將棧頂指向當前棧幀地址,然后將調用者的棧幀地址暫存到EBP中
每個函數調用前匯編器都會加入以下前言(Prolog),用于保存棧幀和返回地址
push %rbp ;將調用者的棧幀指針壓入調用棧 mov %rsp,%rbp ;現在棧頂指向剛入棧的RBP內容,要將其設置為棧幀的起始位置
?現在們結合實例來理解吧!
C語言
#includeint 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函數對應的匯編指令
0x40050fpush %rbp 0x400510 mov %rsp,%rbp ;將2暫存到寄存器EDI中 0x400513 mov $0x2,%edi ;執行call指令前,EIP寄存器已經存儲下一條指令的地址0x40051d了 ;首先將EIP寄存器的值入棧,當函數返回時用于恢復之前的執行序列 ;然后才是執行JUMP指令跳轉到add2函數中開始執行其第一條指令 0x400518 callq 0x4004ea ;什么都不做 0x40051d nop ;設置RBP為指向main函數調用方的棧幀地址 0x40051e pop %rbp ;設置EIP指向main函數返回后將要執行的指令的地址 0x40051f retq
下面是執行add2函數第一條指令前的調用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數調用方的棧幀地址 <-- EBP +++++++++++++++++ 98 | 0x40051d | -- EIP的值,存放add2返回后將執行的指令的地址 <-- ESP +++++++++++++++++ 低位地址
add2函數對應的匯編指令
0x4004eapush %rbp 0x4004eb mov %rsp,%rbp 0x4004ee sub $0x18,%rsp ;棧頂向低位移動24個字節,為后續操作預留堆??臻g 0x4004f2 mov %edi,-0x14(%rbp);從EDI寄存器中讀取參數,并存放到堆??臻g中 0x4004f5 mov -0x14(%rbp),%eax;從堆??臻g中讀取參數,放進EAX寄存器中 0x4004f8 mov %eax,%esi ;從EAX寄存器中讀取參數,存放到ESI寄存器中 0x4004fa mov $0x0,%edi ;將0存放到EDI寄存器中 ;執行call指令前,EIP寄存器已經存儲下一條指令的地址0x400504了 ;首先將EIP寄存器的值入棧,當函數返回時用于恢復之前的執行序列 ;然后才是執行JUMP指令跳轉到add函數中開始執行其第一條指令 0x4004ff callq 0x4004d6 0x400504 mov %eax,-0x4(%rbp) ;讀取add的返回值(存儲在EAX寄存器中),存放到堆??臻g中 0x400507 mov -0x4(%rbp),%eax ;又將add的返回值存放到EAX寄存器中(這是有多無聊啊~~) 0x40050a add $0x2,%eax ;讀取EAX寄存器的值與2相加,結果存放到EAX寄存器中 0x40050d leaveq ;讓棧頂指針指向main函數的棧幀地址,然后讓EBP指向main函數的棧幀地址 0x40050e retq ;讓EIP指向add2返回后將執行的指令的地址
下面是執行完add2函數中mov %rsp,%rbp的調用棧快照
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數調用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回后將執行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數調用方(即main函數)的棧幀地址<-- ESP,EBP +++++++++++++++++ 低位地址
下面是執行add函數第一條指令前的調用??煺?/p>
+++++++++++++++++ 高位地址 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函數對應的匯編指令
0x4004d6push %rbp 0x4004d7 mov %rsp,%rbp 0x4004da mov %edi,-0x4(%rbp) 0x4004dd mov %esi,-0x8(%rbp) 0x4004e0 mov -0x4(%rbp),%edx 0x4004e3 mov -0x8(%rbp),%eax 0x4004e6 add %edx,%eax 0x4004e8 pop %rbp 0x4004e9 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的調用??煺?/p>
+++++++++++++++++ 高位地址 99 | 110 | -- 存放main函數調用方的棧幀地址 +++++++++++++++++ 98 | 0x40051d | -- 存放EIP的值,add2返回后將執行的指令的地址 +++++++++++++++++ 97 | 99 | -- 存放add2函數調用方(即main函數)的棧幀地址 <-- EBP +++++++++++++++++ 96 | 0xXX | +++++++++++++++++ ................. 76 | 0x02 | -- 這是`mov %edi,-0x14(%rbp)`的執行結果 +++++++++++++++++ ................. +++++++++++++++++ 73 | 0xXX | <-- ESP +++++++++++++++++ 低位地址
然后就不斷彈出棧幀了~~~
?從上面看到函數入參是先存儲到寄存器中,然后在函數體內讀取到棧幀所在空間中(局部變量、臨時變量)。那么從調用棧中我們能獲取函數的調用流和入參信息,從而恢復案發現場^_^
?其實函數入參的傳遞方式不止上述這種,還有以下3種
cdecl調用約定
?調用方從右到左的順序將參數壓入棧中,在被調用方執行完成后,由調用方負責清理棧中的參數(也稱為棧平衡)。
stdcall調用約定
?巨硬自稱的一種調用約定,并不是實際上的標準調用約定。調用方從右到左的順序將參數壓入棧中,在被調用方執行完成后,由被調用方負責清理棧中的參數(也稱為棧平衡)。
fastcall調用約定
?是stdcall的變體,調用方從右到左的順序將參數壓入棧中,最右邊的兩個參數則不壓入棧中,而是分別存儲在ECX和EDX寄存器中,在被調用方執行完成后,由被調用方負責清理棧中的參數(也稱為棧平衡)。
?但不管哪種,最終還是會在函數體內讀取到當前棧幀空間中。
二. 如何獲取調用棧??上面寫的這么多,可是我們現在寫的是JavaScript哦,那到底怎么才能讀取調用棧的信息呢?
拋個異??纯?/b>?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:3V8的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(" ").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(" ") }
每個函數定義都通過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"}
?上面這種做法有三個問題
V8引擎不會對包含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/fsjohn... ^_^肥仔John
http://www.cnblogs.com/exiaha...
http://blog.csdn.net/qiu26584...
http://lucasfcosta.com/2017/0...
http://blog.shaochuancs.com/a...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89298.html
摘要:我打算分成前端魔法堂異常不僅僅是和前端魔法堂調用棧,異常實例中的寶藏兩篇分別敘述內置自定義異常類,捕獲運行時異常語法異常網絡請求異常事件,什么是調用棧和如何獲取調用棧的相關信息。 前言 ?編程時我們往往拿到的是業務流程正確的業務說明文檔或規范,但實際開發中卻布滿荊棘和例外情況,而這些例外中包含業務用例的例外,也包含技術上的例外。對于業務用例的例外我們別無它法,必須要求實施人員與用戶共同...
摘要:前端日報精選淺談前端和移動端的事件機制字符串轉數字陷阱前端魔法堂調用棧,異常實例中的寶藏一份完美的前端清單專為現代網站和極致的開發者打造居中辦法學習筆記中文開發如何在里面優雅的解決跨域,路由沖突問題超詳細前端學習譯響應式腦電波如何使 2017-10-26 前端日報 精選 淺談前端和移動端的事件機制JavaScript 字符串轉數字:陷阱前端魔法堂——調用棧,異常實例中的寶藏一份完美的前...
摘要:一前言圖片上傳是一個普通不過的功能,而圖片預覽就是就是上傳功能中必不可少的子功能了。偶然從上找到純前端圖片預覽的相關資料,經過整理后記錄下來以便日后查閱。類型為,表示在讀取文件時發生的錯誤,只讀。 一、前言 圖片上傳是一個普通不過的功能,而圖片預覽就是就是上傳功能中必不可少的子功能了。在這之前,我曾經通過訂閱input[type=file]元素的onchange事件,一旦更改路徑...
摘要:五的子類對象會返回一個集合對象,集合內存儲類型的元素。七的子類初看很有可能以為集合元素就是單選表單元素,其實可以存儲任意類型的表單元素。八的子類開始,將返回子類的對象,其行為特征和一致。但在前,我們應該先了解清楚的類型的特征。 一、前言 大家先看看下面的js,猜猜結果會怎樣吧! 可選答案: ①. 獲取id屬性值為id的節點元素 ②...
摘要:而同步和異步則是描述另一個方面。異步將數據從內核空間拷貝到用戶空間的操作由系統自動處理,然后通知應用程序直接使用數據即可。 前言 ?上周5在公司作了關于JS異步編程模型的技術分享,可能是內容太干的緣故吧,最后從大家的表情看出這條粉腸到底在說啥?的結果:(下面是PPT的講義,具體的PPT和示例代碼在https://github.com/fsjohnhuan...上,有興趣就上去看看吧! ...
閱讀 663·2021-11-23 09:51
閱讀 3291·2021-10-11 10:58
閱讀 15456·2021-09-29 09:47
閱讀 3555·2021-09-01 11:42
閱讀 1289·2019-08-29 16:43
閱讀 1838·2019-08-29 15:37
閱讀 2103·2019-08-29 12:56
閱讀 1726·2019-08-28 18:21