gdb debugging tips
Displaying Julia variables
Innerhalb von gdb
kann jedes jl_value_t*
-Objekt obj
angezeigt werden mit
(gdb) call jl_(obj)
Das Objekt wird in der julia
-Sitzung angezeigt, nicht in der gdb-Sitzung. Dies ist eine nützliche Möglichkeit, die Typen und Werte von Objekten zu entdecken, die von Julias C-Code manipuliert werden.
Ähnlich, wenn Sie einige von Julias Interna (z. B. compiler.jl
) debuggen, können Sie obj
mit folgendem Befehl ausgeben:
ccall(:jl_, Cvoid, (Any,), obj)
Dies ist eine gute Möglichkeit, Probleme zu umgehen, die aus der Reihenfolge entstehen, in der Julias Ausgabeströme initialisiert werden.
Julias Flisp-Interpreter verwendet value_t
-Objekte; diese können mit call fl_print(fl_ctx, ios_stdout, obj)
angezeigt werden.
Useful Julia variables for Inspecting
Während die Adressen vieler Variablen, wie Singletons, nützlich sein können, um viele Fehler zu drucken, gibt es eine Reihe zusätzlicher Variablen (siehe julia.h
für eine vollständige Liste), die noch nützlicher sind.
- (bei
jl_apply_generic
)mfunc
undjl_uncompress_ast(mfunc->def, mfunc->code)
:: um ein wenig über den Aufrufstapel herauszufinden jl_lineno
undjl_filename
:: um herauszufinden, von welcher Zeile in einem Test man mit dem Debuggen beginnen soll (oder um herauszufinden, wie weit in eine Datei geparst wurde)$1
:: nicht wirklich eine Variable, aber dennoch eine nützliche Abkürzung, um auf das Ergebnis des letzten gdb-Befehls (wieprint
) zu verweisen.jl_options
:: manchmal nützlich, da es alle erfolgreich geparsten Befehlszeilenoptionen auflistetjl_uv_stderr
:: denn wer mag es nicht, mit stdio interagieren zu können
Useful Julia functions for Inspecting those variables
jl_print_task_backtraces(0)
:: Ähnlich wie gdbsthread apply all bt
oder lldbsthread backtrace all
. Führt alle Threads aus und druckt Backtraces für alle vorhandenen Aufgaben.jl_gdblookup($pc)
:: Zum Nachschlagen der aktuellen Funktion und Zeile.jl_gdblookupinfo($pc)
:: Zum Nachschlagen des aktuellen Methodeninstanzobjekts.jl_gdbdumpcode(mi)
:: Zum Dumpen allercode_typed/code_llvm/code_asm
, wenn die REPL nicht richtig funktioniert.jlbacktrace()
:: Zum Dumpen des aktuellen Julia-Backtrace-Stacks nach stderr. Nur verwendbar, nachdemrecord_backtrace()
aufgerufen wurde.jl_dump_llvm_value(Value*)
:: Zum Aufrufen vonValue->dump()
in gdb, wo es nicht nativ funktioniert. Zum Beispielf->linfo->functionObject
,f->linfo->specFunctionObject
undto_function(f->linfo)
.jl_dump_llvm_module(Module*)
:: Zum Aufrufen vonModule->dump()
in gdb, wo es nicht nativ funktioniert.Type->dump()
:: funktioniert nur in lldb. Hinweis: Fügen Sie etwas wie;1
hinzu, um zu verhindern, dass lldb seinen Prompt über die Ausgabe druckt.jl_eval_string("expr")
:: zum Auslösen von Nebenwirkungen, um den aktuellen Zustand zu ändern oder um Symbole nachzuschlagen.jl_typeof(jl_value_t*)
:: zum Extrahieren des Typ-Tags eines Julia-Werts (in gdb, rufen Sie zuerstmacro define jl_typeof jl_typeof
auf, oder wählen Sie etwas Kurzes wiety
für das erste Argument, um eine Abkürzung zu definieren)
Inserting breakpoints for inspection from gdb
In deiner gdb
-Sitzung setze einen Haltepunkt in jl_breakpoint
wie folgt:
(gdb) break jl_breakpoint
Dann fügen Sie in Ihren Julia-Code einen Aufruf zu jl_breakpoint
hinzu, indem Sie
ccall(:jl_breakpoint, Cvoid, (Any,), obj)
wo obj
kann jede Variable oder jedes Tupel sein, das Sie im Haltepunkt zugänglich machen möchten.
Es ist besonders hilfreich, zum jl_apply
-Rahmen zurückzukehren, von dem aus Sie die Argumente einer Funktion anzeigen können, z. B.
(gdb) call jl_(args[0])
Ein weiteres nützliches Frame ist to_function(jl_method_instance_t *li, bool cstyle)
. Das Argument jl_method_instance_t*
ist eine Struktur mit einem Verweis auf den endgültigen AST, der an den Compiler gesendet wird. Zu diesem Zeitpunkt wird der AST jedoch normalerweise komprimiert sein; um den AST anzuzeigen, rufen Sie jl_uncompress_ast
auf und übergeben Sie das Ergebnis an 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
Angenommen, die Datei heißt 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
Da diese Funktion für jeden Aufruf verwendet wird, machst du alles 1000x langsamer, wenn du das tust.
Dealing with signals
Julia benötigt einige Signale, um ordnungsgemäß zu funktionieren. Der Profiler verwendet SIGUSR2
für das Sampling und der Garbage Collector verwendet SIGSEGV
zur Synchronisierung von Threads. Wenn Sie Code debuggen, der den Profiler oder mehrere Threads verwendet, möchten Sie möglicherweise, dass der Debugger diese Signale ignoriert, da sie während normaler Operationen sehr häufig ausgelöst werden können. Der Befehl, um dies in GDB zu tun, lautet (ersetzen Sie SIGSEGV
durch SIGUSR2
oder andere Signale, die Sie ignorieren möchten):
(gdb) handle SIGSEGV noprint nostop pass
Der entsprechende LLDB-Befehl ist (nachdem der Prozess gestartet wurde):
(lldb) pro hand -p true -s false -n false SIGSEGV
Wenn Sie einen Segfault mit mehrteiliger Code debuggen, können Sie einen Haltepunkt auf jl_critical_error
setzen (auch sigdie_handler
sollte unter Linux und BSD funktionieren), um nur den tatsächlichen Segfault zu erfassen und nicht die GC-Synchronisationspunkte.
Debugging during Julia's build process (bootstrap)
Fehler, die während make
auftreten, benötigen eine spezielle Behandlung. Julia wird in zwei Phasen gebaut, wobei sys0
und sys.ji
erstellt werden. Um zu sehen, welche Befehle zum Zeitpunkt des Fehlers ausgeführt werden, verwenden Sie make VERBOSE=1
.
Zum Zeitpunkt dieses Schreibens können Sie Build-Fehler während der sys0
-Phase aus dem base
-Verzeichnis mit folgendem Befehl debuggen:
julia/base$ gdb --args ../usr/bin/julia-debug -C native --build ../usr/lib/julia/sys0 sysimg.jl
Sie müssen möglicherweise alle Dateien in usr/lib/julia/
löschen, damit dies funktioniert.
Sie können die sys.ji
-Phase debuggen, indem Sie:
julia/base$ gdb --args ../usr/bin/julia-debug -C native --build ../usr/lib/julia/sys -J ../usr/lib/julia/sys0.ji sysimg.jl
Standardmäßig führt jeder Fehler dazu, dass Julia beendet wird, selbst unter gdb. Um einen Fehler "in flagranti" zu erfassen, setzen Sie einen Haltepunkt in jl_error
(es gibt mehrere andere nützliche Stellen für spezifische Arten von Fehlern, einschließlich: jl_too_few_args
, jl_too_many_args
und jl_throw
).
Sobald ein Fehler erkannt wird, ist eine nützliche Technik, den Stack nach oben zu durchlaufen und die Funktion zu untersuchen, indem der zugehörige Aufruf von jl_apply
inspiziert wird. Um ein Beispiel aus der realen Welt zu nehmen:
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
...
Der aktuellste jl_apply
befindet sich im Frame #3, also können wir dorthin zurückgehen und den AST für die Funktion julia_convert_16886
betrachten. Dies ist der eindeutige Name für eine Methode von convert
. f
in diesem Frame ist ein jl_function_t*
, also können wir die Typsignatur, falls vorhanden, aus dem Feld specTypes
betrachten:
(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
Dann können wir uns den AST für diese Funktion ansehen:
(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
Schließlich, und vielleicht am nützlichsten, können wir die Funktion zwingen, neu kompiliert zu werden, um den Codegenerierungsprozess Schritt für Schritt zu durchlaufen. Dazu löschen Sie das zwischengespeicherte functionObject
aus dem jl_lamdbda_info_t*
:
(gdb) p f->linfo->functionObject
$8 = (void *) 0x1289d070
(gdb) set f->linfo->functionObject = NULL
Setzen Sie dann einen Haltepunkt an einer nützlichen Stelle (z. B. emit_function
, emit_expr
, emit_call
usw.) und führen Sie die Codegenerierung aus:
(gdb) p jl_compile(f)
... # your breakpoint here
Debugging precompilation errors
Modul-Vorcompilation startet einen separaten Julia-Prozess, um jedes Modul vorzukompilieren. Einen Haltepunkt zu setzen oder Fehler in einem Vorcompilierungsarbeiter zu erfassen, erfordert das Anheften eines Debuggers an den Arbeiter. Der einfachste Ansatz besteht darin, den Debugger so einzustellen, dass er auf neue Prozessstarts mit einem bestimmten Namen achtet. Zum Beispiel:
(gdb) attach -w -n julia-debug
oder:
(lldb) process attach -w -n julia-debug
Führen Sie dann ein Skript/einen Befehl aus, um die Vorcompilierung zu starten. Wie zuvor beschrieben, verwenden Sie bedingte Haltepunkte im übergeordneten Prozess, um spezifische Datei-Ladevorgänge zu erfassen und das Debugging-Fenster einzugrenzen. (Einige Betriebssysteme erfordern möglicherweise alternative Ansätze, wie das Verfolgen jedes fork
vom übergeordneten Prozess aus.)
Mozilla's Record and Replay Framework (rr)
Julia funktioniert jetzt direkt mit rr, dem leichten Aufzeichnungs- und deterministischen Debugging-Framework von Mozilla. Dies ermöglicht es Ihnen, den Verlauf einer Ausführung deterministisch wiederzugeben. Die Adressräume, Registerinhalte, Syscall-Daten usw. der wiedergegebenen Ausführung sind in jedem Durchlauf genau gleich.
Eine aktuelle Version von rr (3.1.0 oder höher) ist erforderlich.
Reproducing concurrency bugs with rr
rr simuliert standardmäßig eine einsträngige Maschine. Um parallelen Code zu debuggen, können Sie rr record --chaos
verwenden, was dazu führt, dass rr zwischen einem und acht Kernen simuliert, die zufällig ausgewählt werden. Sie möchten daher möglicherweise JULIA_NUM_THREADS=8
setzen und Ihren Code unter rr erneut ausführen, bis Sie Ihren Fehler gefunden haben.