Документация Engee

Советы по отладке 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))

Вставка точек останова при определенных условиях

Загрузка конкретного файла

Допустим, используется файл 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 (начальной загрузки)

Ошибки, возникающие во время сборки (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, пока ошибка не будет перехвачена.