gdb debugging tips

Displaying Julia variables

داخل gdb، يمكن عرض أي كائن jl_value_t* obj باستخدام

(gdb) call jl_(obj)

سيتم عرض الكائن في جلسة julia، وليس في جلسة gdb. هذه طريقة مفيدة لاكتشاف الأنواع والقيم للكائنات التي يتم التعامل معها بواسطة كود C الخاص بـ Julia.

بالمثل، إذا كنت تقوم بتصحيح بعض من تفاصيل جوليّا (مثل compiler.jl)، يمكنك طباعة obj باستخدام

ccall(:jl_, Cvoid, (Any,), obj)

هذه طريقة جيدة لتجاوز المشكلات التي تنشأ من ترتيب تهيئة تدفقات الإخراج في جوليا.

يستخدم مترجم فليسب لجوليا كائنات 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() :: لطباعة مكدس تتبع الأخطاء الحالي في جوليا إلى 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*) :: لاستخراج علامة نوع قيمة جوليا (في gdb، قم باستدعاء macro define jl_typeof jl_typeof أولاً، أو اختر شيئًا قصيرًا مثل ty كأول وسيط لتعريف اختصار)

Inserting breakpoints for inspection from gdb

في جلسة gdb الخاصة بك، قم بتعيين نقطة توقف في jl_breakpoint على النحو التالي:

(gdb) break jl_breakpoint

ثم داخل كود جوليا الخاص بك، أضف استدعاء إلى 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* هي بنية تحتوي على مرجع إلى شجرة التركيب النهائية المرسلة إلى المترجم. ومع ذلك، ستكون شجرة التركيب في هذه المرحلة عادةً مضغوطة؛ لمشاهدة شجرة التركيب، قم باستدعاء 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

تتطلب جوليا بعض الإشارات لتعمل بشكل صحيح. يستخدم المحلل SIGUSR2 لأغراض أخذ العينات، ويستخدم جامع القمامة SIGSEGV لمزامنة الخيوط. إذا كنت تقوم بتصحيح بعض الأكواد التي تستخدم المحلل أو عدة خيوط، قد ترغب في السماح للمصحح بتجاهل هذه الإشارات لأنها يمكن أن تُ triggered بشكل متكرر أثناء العمليات العادية. الأمر للقيام بذلك في GDB هو (استبدل SIGSEGV بـ SIGUSR2 أو أي إشارات أخرى تريد تجاهلها):

(gdb) handle SIGSEGV noprint nostop pass

الأمر المقابل لـ LLDB هو (بعد بدء العملية):

(lldb) pro hand -p true -s false -n false SIGSEGV

إذا كنت تقوم بتصحيح خطأ segfault مع كود متعدد الخيوط، يمكنك تعيين نقطة توقف على jl_critical_error (يجب أن تعمل sigdie_handler أيضًا على Linux و BSD) من أجل التقاط خطأ segfault الفعلي فقط بدلاً من نقاط تزامن GC.

Debugging during Julia's build process (bootstrap)

الأخطاء التي تحدث أثناء make تحتاج إلى معالجة خاصة. يتم بناء جوليا على مرحلتين، حيث يتم إنشاء 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

بشكل افتراضي، ستؤدي أي أخطاء إلى خروج جوليا، حتى تحت 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

تقوم عملية ما قبل التجميع بإنشاء عملية جوليا منفصلة لتجميع كل وحدة. يتطلب تعيين نقطة توقف أو التقاط الأخطاء في عامل ما قبل التجميع توصيل مصحح الأخطاء بالعامل. أسهل طريقة هي تعيين مصحح الأخطاء لمراقبة إطلاق العمليات الجديدة التي تتطابق مع اسم معين. على سبيل المثال:

(gdb) attach -w -n julia-debug

أو:

(lldb) process attach -w -n julia-debug

ثم قم بتشغيل نص/أمر لبدء ما قبل التجميع. كما هو موضح سابقًا، استخدم نقاط التوقف الشرطية في العملية الأصلية لالتقاط أحداث تحميل الملفات المحددة وتضييق نافذة التصحيح. (قد تتطلب بعض أنظمة التشغيل طرقًا بديلة، مثل متابعة كل fork من العملية الأصلية)

Mozilla's Record and Replay Framework (rr)

تعمل جوليا الآن خارج الصندوق مع rr، وهو إطار عمل خفيف للتسجيل وتصحيح الأخطاء الحتمي من موزيلا. يتيح لك ذلك إعادة تشغيل تتبع تنفيذ بشكل حتمي. إن مساحات عنوان التنفيذ المعاد تشغيله، ومحتويات السجلات، وبيانات استدعاء النظام، إلخ، هي بالضبط نفسها في كل تشغيل.

يتطلب إصدار حديث من rr (3.1.0 أو أعلى).

Reproducing concurrency bugs with rr

rr يحاكي آلة ذات خيط واحد بشكل افتراضي. من أجل تصحيح الشيفرة المتزامنة، يمكنك استخدام rr record --chaos الذي سيسبب لـ rr بمحاكاة ما بين نواة واحدة إلى ثماني نوى، يتم اختيارها عشوائيًا. لذلك قد ترغب في تعيين JULIA_NUM_THREADS=8 وإعادة تشغيل الشيفرة الخاصة بك تحت rr حتى تتمكن من التقاط الخطأ.