Советы по отладке gdb
Отображение переменных Julia
В gdb
любой объект (obj
) jl_value_t*
можно отобразить следующим образом.
(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)
.
Полезные переменные Julia для проверки
Хотя вывод адресов многих переменных, например одинарных экземпляров, может быть полезен при многих сбоях, существует ряд дополнительных переменных (полный список см. в описании 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
Полезные функции Julia для проверки этих переменных
-
jl_gdblookup($rip)
: для поиска текущей функции и строки. (Используйте$eip
на платформах i686.) -
jlbacktrace()
: для сохранения текущего обратного стека Julia в stderr. Используется только после вызоваrecord_backtrace()
. -
jl_dump_llvm_value(Value*)
: для вызова функцииValue->dump()
в gdb, где она не работает изначально. Например,f->linfo->functionObject
,f->linfo->specFunctionObject
иto_function(f->linfo)
. -
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))
Вставка точек останова при определенных условиях
Работа с сигналами
Для корректного функционирования 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 (начальной загрузки)
Ошибки, возникающие во время сборки (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} # <-- сигнатура типа для 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) ... # Здесь находится ваша точка останова
Отладка ошибок предварительной компиляции
Предварительная компиляция модуля порождает отдельный процесс Julia для предварительной компиляции каждого модуля. Для установки точки останова или отслеживания сбоев в рабочей роли предварительной компиляции требуется подключить отладчик. Самым простым подходом является настройка отладчика для наблюдения за запуском новых процессов, соответствующих заданному имени. Пример:
(gdb) attach -w -n julia-debug
или:
(lldb) process attach -w -n julia-debug
Затем выполните скрипт или команду для запуска предварительной компиляции. Как было описано ранее, используйте условные точки останова в родительском процессе для перехвата определенных событий загрузки файлов и сужения окна отладки. (В некоторых операционных системах могут потребоваться альтернативные подходы, например следование за каждой ветвью (fork
) из родительского процесса.)
Платформа записи и воспроизведения (rr) Mozilla
Сейчас Julia напрямую работает с rr, упрощенной платформой записи и детерминированной отладки от Mozilla. Это позволяет детерминированно воспроизводить трассировку выполнения. Адресные пространства, содержимое регистров, данные системных вызовов и т. д. воспроизводимого выполнения в каждом запуске абсолютно одинаковы.
Требуется последняя версия rr (3.1.0 или выше).
Воспроизведение ошибок параллелизма с помощью rr
По умолчанию rr имитирует однопоточный компьютер. Для отладки параллельного кода вы можете использовать rr record --chaos
, что заставит rr работать в смоделированных ситуациях, где количество ядер случайным образом составляет от одного до восьми. Поэтому вы можете установить JULIA_NUM_THREADS=8
и повторно выполнять код в rr, пока ошибка не будет перехвачена.