栈跟踪

StackTraces 模块提供了简单的栈跟踪功能,这些栈跟踪信息既可读又易于编程使用。

查看栈跟踪

获取栈跟踪信息的主要函数是 stacktrace

  1. 6-element Array{Base.StackTraces.StackFrame,1}:
  2. top-level scope
  3. eval at boot.jl:317 [inlined]
  4. eval(::Module, ::Expr) at REPL.jl:5
  5. eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
  6. macro expansion at REPL.jl:116 [inlined]
  7. (::getfield(REPL, Symbol("##28#29")){REPL.REPLBackend})() at event.jl:92

调用 stacktrace() 会返回一个 StackTraces.StackFrame 数组。为了使用方便,可以用 StackTraces.StackTrace 来代替 Vector{StackFrame}。下面例子中 […] 的意思是这部分输出的内容可能会根据代码的实际执行情况而定。

  1. julia> example() = stacktrace()
  2. example (generic function with 1 method)
  3. julia> example()
  4. 7-element Array{Base.StackTraces.StackFrame,1}:
  5. example() at REPL[1]:1
  6. top-level scope
  7. eval at boot.jl:317 [inlined]
  8. [...]
  9. julia> @noinline child() = stacktrace()
  10. child (generic function with 1 method)
  11. julia> @noinline parent() = child()
  12. parent (generic function with 1 method)
  13. julia> grandparent() = parent()
  14. grandparent (generic function with 1 method)
  15. julia> grandparent()
  16. 9-element Array{Base.StackTraces.StackFrame,1}:
  17. child() at REPL[3]:1
  18. parent() at REPL[4]:1
  19. grandparent() at REPL[5]:1
  20. [...]

注意,在调用 stacktrace() 的时,通常会出现 eval at boot.jl 这帧。 当从 REPL 里调用 stacktrace() 的时候,还会显示 REPL.jl 里的一些额外帧,就像下面一样:

  1. julia> example() = stacktrace()
  2. example (generic function with 1 method)
  3. julia> example()
  4. 7-element Array{Base.StackTraces.StackFrame,1}:
  5. example() at REPL[1]:1
  6. top-level scope
  7. eval at boot.jl:317 [inlined]
  8. eval(::Module, ::Expr) at REPL.jl:5
  9. eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
  10. macro expansion at REPL.jl:116 [inlined]
  11. (::getfield(REPL, Symbol("##28#29")){REPL.REPLBackend})() at event.jl:92

抽取有用信息

每个 StackTraces.StackFrame 都会包含函数名,文件名,代码行数,lambda 信息,一个用于确认此帧是否被内联的标帜,一个用于确认函数是否为 C 函数的标帜(在默认的情况下 C 函数不会出现在栈跟踪信息中)以及一个用整数表示的指针,它是由 backtrace 返回的:

  1. julia> frame = stacktrace()[3]
  2. eval(::Module, ::Expr) at REPL.jl:5
  3. julia> frame.func
  4. :eval
  5. julia> frame.file
  6. Symbol("~/julia/usr/share/julia/stdlib/v0.7/REPL/src/REPL.jl")
  7. julia> frame.line
  8. 5
  9. julia> top_frame.linfo
  10. MethodInstance for eval(::Module, ::Expr)
  11. julia> top_frame.inlined
  12. false
  13. julia> top_frame.from_c
  14. false
  1. julia> top_frame.pointer
  2. 0x00007f92d6293171

这使得我们可以通过编程的方式将栈跟踪信息用于打印日志,处理错误以及其它更多用途。

错误处理

能够轻松地获取当前调用栈的状态信息在许多场景下都很有用,但最直接的应用是错误处理和调试。

  1. julia> @noinline bad_function() = undeclared_variable
  2. bad_function (generic function with 1 method)
  3. julia> @noinline example() = try
  4. bad_function()
  5. catch
  6. stacktrace()
  7. end
  8. example (generic function with 1 method)
  9. julia> example()
  10. 7-element Array{Base.StackTraces.StackFrame,1}:
  11. example() at REPL[2]:4
  12. top-level scope
  13. eval at boot.jl:317 [inlined]
  14. [...]

你可能已经注意到了,上述例子中第一个栈帧指向了 stacktrace 被调用的第 4 行,而不是 bad_function 被调用的第 2 行,且完全没有出现 bad_function 的栈帧。这是也是可以理解的,因为 stacktrace 是在 catch 的上下文中被调用的。虽然在这个例子中很容易查找到错误的真正源头,但在复杂的情况下查找错误源并不是一件容易的事。

为了补救,我们可以将 catch_backtrace 的输出传递给 stacktracecatch_backtrace 会返回最近发生异常的上下文中的栈信息,而不是返回当前上下文中的调用栈信息。

  1. julia> @noinline bad_function() = undeclared_variable
  2. bad_function (generic function with 1 method)
  3. julia> @noinline example() = try
  4. bad_function()
  5. catch
  6. stacktrace(catch_backtrace())
  7. end
  8. example (generic function with 1 method)
  9. julia> example()
  10. 8-element Array{Base.StackTraces.StackFrame,1}:
  11. bad_function() at REPL[1]:1
  12. example() at REPL[2]:2
  13. [...]

可以看到,现在栈跟踪会显示正确的行号以及之前缺失的栈帧。

  1. julia> @noinline child() = error("Whoops!")
  2. child (generic function with 1 method)
  3. julia> @noinline parent() = child()
  4. parent (generic function with 1 method)
  5. julia> @noinline function grandparent()
  6. try
  7. parent()
  8. catch err
  9. println("ERROR: ", err.msg)
  10. stacktrace(catch_backtrace())
  11. end
  12. end
  13. grandparent (generic function with 1 method)
  14. julia> grandparent()
  15. ERROR: Whoops!
  16. 10-element Array{Base.StackTraces.StackFrame,1}:
  17. error at error.jl:33 [inlined]
  18. child() at REPL[1]:1
  19. parent() at REPL[2]:1
  20. grandparent() at REPL[3]:3
  21. [...]

Exception stacks and catch_stack

Julia 1.1

Exception stacks requires at least Julia 1.1.

While handling an exception further exceptions may be thrown. It can be useful to inspect all these exceptions to identify the root cause of a problem. The julia runtime supports this by pushing each exception onto an internal exception stack as it occurs. When the code exits a catch normally, any exceptions which were pushed onto the stack in the associated try are considered to be successfully handled and are removed from the stack.

The stack of current exceptions can be accessed using the experimental Base.catch_stack function. For example,

  1. julia> try
  2. error("(A) The root cause")
  3. catch
  4. try
  5. error("(B) An exception while handling the exception")
  6. catch
  7. for (exc, bt) in Base.catch_stack()
  8. showerror(stdout, exc, bt)
  9. println()
  10. end
  11. end
  12. end
  13. (A) The root cause
  14. Stacktrace:
  15. [1] error(::String) at error.jl:33
  16. [2] top-level scope at REPL[7]:2
  17. [3] eval(::Module, ::Any) at boot.jl:319
  18. [4] eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
  19. [5] macro expansion at REPL.jl:117 [inlined]
  20. [6] (::getfield(REPL, Symbol("##26#27")){REPL.REPLBackend})() at task.jl:259
  21. (B) An exception while handling the exception
  22. Stacktrace:
  23. [1] error(::String) at error.jl:33
  24. [2] top-level scope at REPL[7]:5
  25. [3] eval(::Module, ::Any) at boot.jl:319
  26. [4] eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
  27. [5] macro expansion at REPL.jl:117 [inlined]
  28. [6] (::getfield(REPL, Symbol("##26#27")){REPL.REPLBackend})() at task.jl:259

In this example the root cause exception (A) is first on the stack, with a further exception (B) following it. After exiting both catch blocks normally (i.e., without throwing a further exception) all exceptions are removed from the stack and are no longer accessible.

The exception stack is stored on the Task where the exceptions occurred. When a task fails with uncaught exceptions, catch_stack(task) may be used to inspect the exception stack for that task.

stacktrace 与 backtrace 的比较

调用 backtrace 会返回一个 Union{Ptr{Nothing}, Base.InterpreterIP} 的数组,可以将其传给 stacktrace 函数进行转化:

  1. julia> trace = backtrace()
  2. 18-element Array{Union{Ptr{Nothing}, Base.InterpreterIP},1}:
  3. Ptr{Nothing} @0x00007fd8734c6209
  4. Ptr{Nothing} @0x00007fd87362b342
  5. Ptr{Nothing} @0x00007fd87362c136
  6. Ptr{Nothing} @0x00007fd87362c986
  7. Ptr{Nothing} @0x00007fd87362d089
  8. Base.InterpreterIP(CodeInfo(:(begin
  9. Core.SSAValue(0) = backtrace()
  10. trace = Core.SSAValue(0)
  11. return Core.SSAValue(0)
  12. end)), 0x0000000000000000)
  13. Ptr{Nothing} @0x00007fd87362e4cf
  14. [...]
  15. julia> stacktrace(trace)
  16. 6-element Array{Base.StackTraces.StackFrame,1}:
  17. top-level scope
  18. eval at boot.jl:317 [inlined]
  19. eval(::Module, ::Expr) at REPL.jl:5
  20. eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
  21. macro expansion at REPL.jl:116 [inlined]
  22. (::getfield(REPL, Symbol("##28#29")){REPL.REPLBackend})() at event.jl:92

需要注意的是,backtrace 返回的数组有 18 个元素,而经过 stacktrace 转化后仅剩 6 个。这是因为 stacktrace 在默认情况下会移除所有底层 C 函数的栈信息。如果你想显示 C 函数调用的栈帧,可以这样做:

  1. julia> stacktrace(trace, true)
  2. 21-element Array{Base.StackTraces.StackFrame,1}:
  3. jl_apply_generic at gf.c:2167
  4. do_call at interpreter.c:324
  5. eval_value at interpreter.c:416
  6. eval_body at interpreter.c:559
  7. jl_interpret_toplevel_thunk_callback at interpreter.c:798
  8. top-level scope
  9. jl_interpret_toplevel_thunk at interpreter.c:807
  10. jl_toplevel_eval_flex at toplevel.c:856
  11. jl_toplevel_eval_in at builtins.c:624
  12. eval at boot.jl:317 [inlined]
  13. eval(::Module, ::Expr) at REPL.jl:5
  14. jl_apply_generic at gf.c:2167
  15. eval_user_input(::Any, ::REPL.REPLBackend) at REPL.jl:85
  16. jl_apply_generic at gf.c:2167
  17. macro expansion at REPL.jl:116 [inlined]
  18. (::getfield(REPL, Symbol("##28#29")){REPL.REPLBackend})() at event.jl:92
  19. jl_fptr_trampoline at gf.c:1838
  20. jl_apply_generic at gf.c:2167
  21. jl_apply at julia.h:1540 [inlined]
  22. start_task at task.c:268
  23. ip:0xffffffffffffffff

我们也可以将 backtrace 返回的单个指针传递给StackTraces.lookup 来转化成 StackTraces.StackFrame

  1. julia> pointer = backtrace()[1];
  2. julia> frame = StackTraces.lookup(pointer)
  3. 1-element Array{Base.StackTraces.StackFrame,1}:
  4. jl_apply_generic at gf.c:2167
  5. julia> println("The top frame is from $(frame[1].func)!")
  6. The top frame is from jl_apply_generic!