Engee 文档

GDB调试技巧

显示Julia变量

在’gdb`中,任何对象(obj)`jl_value_t*'都可以显示如下。

(gdb) call jl_(obj)

对象将显示在’julia’会话中,而不是gdb会话中。 这是确定由Julia的C代码操作的对象的类型和值的好方法。

此外,如果您正在调试一些内部Julia组件(例如,compiler.jl),可以使用函数输出一个对象(obj)

ccall(:jl_, Cvoid, (Any,), obj)

这是解决由于julia输出流的初始化顺序而出现的问题的最佳选择。

Julia中的flisp解释器使用’value_t’对象。 它们可以使用`call fl_print(fl_ctx,ios_stdout,obj)'显示。

Julia用于检查的有用变量

虽然输出许多变量的地址,例如单个实例,对于许多故障可能很有用,但还有许多其他变量(有关完整列表,请参阅`julia’的描述。h')更相关。

  • (when in`jl_apply_generic')`mfunc`and'jl_uncompress_ast(mfunc->def,mfunc->code)':确定调用栈的一些信息

  • 'jl_lineno`和’jl_filename':确定测试中应该从哪个行开始调试(或确定文件被分析的程度)

  • '$1':不完全是一个变量,但仍然是引用最后一个gdb命令结果的有用简写(例如,'print'`

  • 'jl_options':有时很有用,因为它列出了已成功分析的所有命令行参数

  • 'jl_uv_stderr':用作与stdio交互的替代方法

Julia用于检查这些变量的有用函数

  • 'jl_print_task_backtraces(0)`:类似于gdb中的`thread apply all bt`或lldb中的`thread backtrace all'。

  • 'jl_gdblookup(pc pc)':搜索当前函数和字符串。

  • 'jl_gdblookupinfo`pc pc)':搜索当前方法实例对象。

  • 'jl_gdbdumpcode`mi)`:当REPL不能正常工作时重置整个`code_typed/code_llvm/code_asm'。

  • 'jlbacktrace()':将当前Julia backtrace堆栈保存到stderr。 它仅在调用`record_backtrace()'后使用。

  • 'jl_dump_llvm_value(Value*):在gdb中调用函数+Value→dump()',它最初不起作用。 例如`'+f->linfo->functionObject',+f→linfo→specFunctionObject+`和+to_function(f→linfo)+'。

  • jl_dump_llvm_module(Module*):在gdb中调用函数'Module->dump()',它最初不起作用。

  • 'Type->dump():仅在lldb中工作。 注意。 添加类似;1’的内容以防止lldb在输出期间显示其窗口。

  • `jl_eval_string("expr")':调用副作用以更改当前状态或搜索字符。

  • 'jl_typeof(jl_value_t*)':要提取Julia值类型标签(在gdb中,首先调用`macro define jl_typeof jl_typeof`或选择像`ty`这样的简短内容作为定义缩写的第一个参数)。

从gdb插入验证的断点

在’gdb’会话中,在`jl_breakpoint`函数中设置断点,如下所示。

(gdb) break jl_breakpoint

然后,在Julia的代码中,通过添加插入对`jl_breakpoint`函数的调用:

ccall(:jl_breakpoint, Cvoid, (Any,), obj)

'obj’可以是任何应该在断点处可用的变量或元组。

返回’jl_apply’框架特别有用,从中可以显示函数的参数,例如如下所示。

(gdb) call jl_(args[0])

另一个有用的帧是’to_function(jl_method_instance_t*li,bool cstyle)'。 `Jl_method_instance_t*'参数是一个结构,引用发送到编译器的最终AST树。 然而,AST通常在该阶段被压缩。 要查看AST,请调用’jl_uncompress_ast',然后将结果传递给’jl_'。

#2  0x00007ffff7928bf7 in to_function (li=0x2812060, cstyle=false) at codegen.cpp:584
584          abort();
(gdb) p jl_(jl_uncompress_ast(li, li->ast))

在某些条件下插入断点

上传特定文件

假设文件’sysimg。jl’被使用。

(gdb) break jl_load if strcmp(fname, "sysimg.jl")==0

调用特定方法

(gdb) break jl_apply_generic if strcmp((char*)(jl_symbol_name)(jl_gf_mtable(F)->name), "method_to_break")==0

由于此功能用于每次调用,如果您这样做,事情会显着减慢。

使用信号

Julia正常工作需要几个信号。 探查器使用`SIGUSR2’进行采样,垃圾收集器使用’SIGSEGV’进行线程同步。 如果您正在调试由探查器或多个线程使用的代码,则可以允许调试器忽略这些信号,因为它们可以在正常操作期间经常被激活。 为此,GDB使用以下命令(将`SIGSEGV`替换为`SIGUSR2`或其他需要忽略的信号)。

(gdb) handle SIGSEGV noprint nostop pass

相应的LLDB命令(在进程启动后执行)如下所示。

(lldb) pro hand -p true -s false -n false SIGSEGV

如果您正在使用流式代码调试紧急情况,则可以在`jl_critical_error`(`sigdie_handler`也应该在Linux和BSD上工作)中设置断点,以仅拦截实际的紧急情况,而不是垃圾收集器的同步点。

在Julia构建期间进行调试(bootstrap)

构建(make)期间发生的错误需要特殊处理。 Julia分两个阶段构建`sys0’和’sys。纪'。 要查看崩溃时正在运行的命令,请使用’make VERBOSE=1'。

在编写本文档时,您可以从基本目录调试sys0阶段的构建错误,如下所示。

julia/base$ gdb --args ../usr/bin/julia-debug -C native --build ../usr/lib/julia/sys0 sysimg.jl

您可能需要删除`usr/lib/julia/`中的所有文件才能使其工作。

你可以调试’sys。纪’如下。

julia/base$ gdb --args ../usr/bin/julia-debug -C native --build ../usr/lib/julia/sys -J ../usr/lib/julia/sys0.ji sysimg.jl

默认情况下,任何错误都会导致Julia关闭,即使在gdb中也是如此。 要捕获过程中的错误,请将断点设置为’jl_error`(对于特定类型的故障,有几个更有用的断点,包括`jl_too_few_args`,`jl_too_many_args`和’jl_throw')。

在捕获错误后,建议通过检查关联的函数调用`jl_apply`来向上堆栈并检查函数。 让我们举一个真实的例子。

Breakpoint 1, jl_throw (e=0x7ffdf42de400) at task.c:802
802 {
(gdb) p jl_(e)
ErrorException("auto_unbox: unable to determine argument type")
$2 = void
(gdb) bt 10
#0  jl_throw (e=0x7ffdf42de400) at task.c:802
#1  0x00007ffff65412fe in jl_error (str=0x7ffde56be000 <_j_str267> "auto_unbox:
   unable to determine argument type")
   at builtins.c:39
#2  0x00007ffde56bd01a in julia_convert_16886 ()
#3  0x00007ffff6541154 in jl_apply (f=0x7ffdf367f630, args=0x7fffffffc2b0, nargs=2) at julia.h:1281
...

最后一个函数’jl_apply’在第3帧中,所以我们可以回到那里并查看函数`julia_convert_16886’的AST。 这是某些转换方法(`convert')的唯一名称。 此帧中的’f’是’jl_function_t’的函数,因此您可以从’specTypes’字段查看类型签名(如果有的话)。

(gdb) f 3
#3  0x00007ffff6541154 in jl_apply (f=0x7ffdf367f630, args=0x7fffffffc2b0, nargs=2) at julia.h:1281
1281            return f->fptr((jl_value_t*)f, args, nargs);
(gdb) p f->linfo->specTypes
$4 = (jl_tupletype_t *) 0x7ffdf39b1030
(gdb) p jl_( f->linfo->specTypes )
Tuple{Type{Float32}, Float64}           # <-- сигнатура типа для julia_convert_16886

然后我们可以看看这个函数的AST。

(gdb) p jl_( jl_uncompress_ast(f->linfo, f->linfo->ast) )
Expr(:lambda, Array{Any, 1}[:#s29, :x], Array{Any, 1}[Array{Any, 1}[], Array{Any, 1}[Array{Any, 1}[:#s29, :Any, 0], Array{Any, 1}[:x, :Any, 0]], Array{Any, 1}[], 0], Expr(:body,
Expr(:line, 90, :float.jl)::Any,
Expr(:return, Expr(:call, :box, :Float32, Expr(:call, :fptrunc, :Float32, :x)::Any)::Any)::Any)::Any)::Any

最后,您可以对函数执行强制重新编译以完成代码生成过程。 为此,请从`jl_lamdbda_info_t*`中清除缓存对象`functionObject'。

(gdb) p f->linfo->functionObject
$8 = (void *) 0x1289d070
(gdb) set f->linfo->functionObject = NULL

然后在正确的位置设置断点(例如,emit_functionemit_expr,`emit_call`等。)并开始代码生成。

(gdb) p jl_compile(f)
... # Здесь находится ваша точка останова

调试预编译错误

预编译模块会生成单独的Julia进程,用于预编译每个模块。 要在预编译工作角色中设置断点或跟踪故障,需要连接调试器。 最简单的方法是设置调试器来监视与给定名称匹配的新进程的启动。 例如:

(gdb) attach -w -n julia-debug

或:

(lldb) process attach -w -n julia-debug

然后运行脚本或命令来运行预编译。 如前所述,在父进程中使用条件断点来拦截某些文件上传事件并缩小调试窗口。 (某些操作系统可能需要替代方法,例如遵循父进程中的每个分支(fork)。)

Mozilla录制和播放平台(rr)

朱莉娅目前正在直接与https://rr-project.org /[rr],Mozilla的简化记录和确定性调试平台。 这允许您确定性地再现执行跟踪。 地址空间、寄存器内容、系统调用数据等. 再现的执行在每次运行中完全相同。

需要最新版本的rr(3.1.0或更高版本)。

使用rr重现并发错误

默认情况下,rr模拟单线程计算机。 要调试并行代码,您可以使用`rr record—​chaos`,这将使rr在模拟情况下工作,其中内核数量随机范围从一到八。 因此,您可以设置’JULIA_NUM_THREADS=8’并重新执行rr中的代码,直到捕获错误。