在上一篇《前端魔法堂——异常不只仅是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