栈跟踪

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. [...]

异常栈与catch_stack

Julia 1.1

异常栈需要 Julia 1.1 及以上版本。

在处理一个异常时,后续的异常同样可能被抛出。观察这些异常对定位问题的源头极有帮助。Julia runtime 支持将每个异常发生后推入一个内部的异常栈。当代码正常退出一个catch语句,可认为所有被推入栈中的异常在相应的try语句中被成功处理并已从栈中移除。

存放当前异常的栈可通过测试函数 Base.catch_stack 获取,例如

  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

在本例中,根源异常(A)排在栈头,其后放置着延伸异常(B)。 在正常退出(例如,不抛出新异常)两个 catch 块后,所有异常都被移除出栈,无法访问。

异常栈被存放于发生异常的 Task 处。当某个任务失败,出现意料外的异常时,catch_stack(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!