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, пока не поймаете вашу ошибку.