gdb debugging tips
Displaying Julia variables
В gdb
любой объект jl_value_t*
obj
можно отобразить, используя
(gdb) call jl_(obj)
Объект будет отображаться в сессии julia
, а не в сессии gdb. Это полезный способ узнать типы и значения объектов, которые обрабатываются C-кодом Julia.
Аналогично, если вы отлаживаете некоторые внутренние компоненты Julia (например, compiler.jl
), вы можете вывести obj
, используя
ccall(:jl_, Cvoid, (Any,), obj)
Это хороший способ обойти проблемы, возникающие из-за порядка, в котором инициализируются выходные потоки Julia.
Интерпретатор flisp Julia использует объекты value_t
; их можно отобразить с помощью call fl_print(fl_ctx, ios_stdout, obj)
.
Useful Julia variables for Inspecting
Хотя адреса многих переменных, таких как синглетоны, могут быть полезны для вывода при многих сбоях, существует ряд дополнительных переменных (см. julia.h
для полного списка), которые еще более полезны.
- (когда в
jl_apply_generic
)mfunc
иjl_uncompress_ast(mfunc->def, mfunc->code)
:: для того, чтобы немного разобраться в стеке вызовов jl_lineno
иjl_filename
:: для определения, с какой строки в тесте начинать отладку (или выяснения, насколько далеко в файле был выполнен парсинг)$1
:: не совсем переменная, но все же полезное сокращение для ссылки на результат последней команды gdb (например,print
)jl_options
:: иногда полезно, так как он перечисляет все параметры командной строки, которые были успешно разобраныjl_uv_stderr
:: потому что кто не любит иметь возможность взаимодействовать с stdio
Useful Julia functions for Inspecting those variables
jl_print_task_backtraces(0)
:: Похоже наthread apply all bt
в gdb илиthread backtrace all
в lldb. Запускает все потоки, одновременно выводя трассировки для всех существующих задач.jl_gdblookup($pc)
:: Для поиска текущей функции и строки.jl_gdblookupinfo($pc)
:: Для поиска текущего объекта экземпляра метода.jl_gdbdumpcode(mi)
:: Для дампа всегоcode_typed/code_llvm/code_asm
, когда REPL работает неправильно.jlbacktrace()
:: Для вывода текущего стека обратных вызовов Julia в stderr. Может использоваться только после вызоваrecord_backtrace()
.jl_dump_llvm_value(Value*)
:: Для вызоваValue->dump()
в gdb, где это не работает нативно. Например,f->linfo->functionObject
,f->linfo->specFunctionObject
иto_function(f->linfo)
.jl_dump_llvm_module(Module*)
:: Для вызоваModule->dump()
в gdb, где это не работает нативно.Type->dump()
:: работает только в lldb. Примечание: добавьте что-то вроде;1
, чтобы предотвратить вывод подсказки lldb поверх результата.jl_eval_string("expr")
:: для вызова побочных эффектов для изменения текущего состояния или для поиска символовjl_typeof(jl_value_t*)
:: для извлечения типа тега значения Julia (в gdb сначала вызовитеmacro define jl_typeof jl_typeof
, или выберите что-то короткое, напримерty
, для первого аргумента, чтобы определить сокращение)
Inserting breakpoints for inspection from 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))
Inserting breakpoints upon certain conditions
Loading a particular file
Давайте скажем, что файл называется sysimg.jl
:
(gdb) break jl_load if strcmp(fname, "sysimg.jl")==0
Calling a particular method
(gdb) break jl_apply_generic if strcmp((char*)(jl_symbol_name)(jl_gf_mtable(F)->name), "method_to_break")==0
Поскольку эта функция используется для каждого вызова, вы сделаете все в 1000 раз медленнее, если сделаете это.
Dealing with signals
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), чтобы поймать только фактическую сегментационную ошибку, а не точки синхронизации сборщика мусора.
Debugging during Julia's build process (bootstrap)
Ошибки, которые возникают во время make
, требуют специальной обработки. Julia строится в два этапа, создавая sys0
и sys.ji
. Чтобы увидеть, какие команды выполняются в момент сбоя, используйте make VERBOSE=1
.
На момент написания этой статьи вы можете отлаживать ошибки сборки во время фазы sys0
из директории base
, используя:
julia/base$ gdb --args ../usr/bin/julia-debug -C native --build ../usr/lib/julia/sys0 sysimg.jl
Вам, возможно, потребуется удалить все файлы в usr/lib/julia/
, чтобы это заработало.
Вы можете отладить фазу sys.ji
, используя:
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, поэтому мы можем вернуться туда и посмотреть на AST для функции julia_convert_16886
. Это уникальное имя для некоторого метода 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} # <-- type signature for 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
Наконец, и, возможно, наиболее полезно, мы можем заставить функцию быть перекомпилированной, чтобы пройти через процесс генерации кода. Для этого очистите кэшированный functionObject
из jl_lamdbda_info_t*
:
(gdb) p f->linfo->functionObject
$8 = (void *) 0x1289d070
(gdb) set f->linfo->functionObject = NULL
Затем установите точку останова в каком-нибудь полезном месте (например, emit_function
, emit_expr
, emit_call
и т.д.) и запустите кодогенерацию:
(gdb) p jl_compile(f)
... # your breakpoint here
Debugging precompilation errors
Модульная предкомпиляция запускает отдельный процесс Julia для предкомпиляции каждого модуля. Установка точки останова или перехват ошибок в рабочем процессе предкомпиляции требует подключения отладчика к рабочему процессу. Самый простой способ — установить отладчик для отслеживания новых запусков процессов, соответствующих заданному имени. Например:
(gdb) attach -w -n julia-debug
или:
(lldb) process attach -w -n julia-debug
Затем выполните скрипт/команду для начала предварительной компиляции. Как описано ранее, используйте условные точки останова в родительском процессе, чтобы поймать конкретные события загрузки файлов и сузить окно отладки. (некоторые операционные системы могут требовать альтернативных подходов, таких как отслеживание каждого fork
из родительского процесса)
Mozilla's Record and Replay Framework (rr)
Julia теперь работает из коробки с rr, легковесным фреймворком для записи и детерминированной отладки от Mozilla. Это позволяет вам воспроизводить трассировку выполнения детерминированно. Адресные пространства, содержимое регистров, данные системных вызовов и т. д. воспроизведенного выполнения точно такие же в каждом запуске.
Требуется последняя версия rr (3.1.0 или выше).
Reproducing concurrency bugs with rr
rr по умолчанию симулирует однопоточную машину. Чтобы отлаживать конкурентный код, вы можете использовать rr record --chaos
, что заставит rr симулировать от одного до восьми ядер, выбранных случайным образом. Поэтому вы можете установить JULIA_NUM_THREADS=8
и повторно запустить ваш код под rr, пока не поймаете вашу ошибку.