栈跟踪

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

    调用 stacktrace() 会返回一个 数组。为了使用方便,可以用 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. [...]

    注意,在调用 的时,通常会出现 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

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

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

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

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

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

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

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

    Julia 1.1

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

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

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

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

    异常栈被存放于发生异常的 Task 处。当某个任务失败,出现意料外的异常时,current_exceptions(task) 可被用于观察该任务的异常栈。

    调用 会返回一个 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

    需要注意的是, 返回的向量有 18 个元素,而 stacktrace 返回的向量只包含6 个元素。这是因为 在默认情况下会移除所有底层 C 函数的栈信息。如果你想显示 C 函数调用的栈帧,可以这样做:

    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)!")