Debugging JIT-compiled code with LLVM

Hey list,

I know similar questions have been asked before, but I have been
unable to find a clear answer to what approach would be recommended
for this use case (the LLDB documentation is sparse, to put it
generously).

My situation: The scripting language is JIT-compiled by LLVM. As is
the language runtime, written in C and precompiled to bitcode with
Clang. The runtime is hosted by a driver application that loads and
initializes LLVM, then hands over responsibilities to the runtime.

Currently, this is hard to debug, because neither gdb nor LLDB seem to
know about the symbols and debug information emitted by the JIT
compiler. The driver application sets llvm::JITEmitDebugInfo = true at
launch.

According to the LLVM documentation, gdb 7.0 and later should support
debugging of JIT'ed code, but this does not appear to be true
(backtraces from JIT'ed functions are nonsensical, the eh_frame looks
wrong, and of course breakpoints don't work). What surprises me is
that LLDB doesn't seem to catch the JIT'ed debug information either.

The previously proposed solution of compiling code to dylibs and using
dlopen()/dladdr() to call functions, instead of using the on-the-fly
JIT compiler in LLVM, is not only pretty inconvenient, it's also
incorrect, since my code and language takes advantage of quite a few
opportunities for run-time-optimization (and so the code used for
debugging would poorly reflect the actual code).

I have been attaching LLDB/gdb to the main driver executable, but
perhaps this is the wrong way to go — would it be more advisable to
host the debugger within my own driver process using LLDB.framework?

Please advise. :slight_smile:

Platform: Mac OS X 10.6.5, GCC 4.5, gdb 7.1, LLVM 2.8, LLDB from SVN trunk.

Sincerely,
Simon

According to the LLVM documentation, gdb 7.0 and later should support
debugging of JIT'ed code, but this does not appear to be true
(backtraces from JIT'ed functions are nonsensical, the eh_frame looks
wrong, and of course breakpoints don't work). What surprises me is
that LLDB doesn't seem to catch the JIT'ed debug information either.

...

Platform: Mac OS X 10.6.5, GCC 4.5, gdb 7.1, LLVM 2.8, LLDB from SVN trunk.

Ah, I wouldn't be surprised, I don't think anyone has really succeeded
with gdb off of Linux x86 or x86_64, and I wasn't able to build FSF
gdb on OS X when I was working on this.

Reid

The main issue is that there is no central place right now where JIT'ed code is registered that allows anyone to register newly generated code that would be known to debuggers. I know the current dyld has some way to add JIT's code to its all_image_infos structure, but none of our debuggers use this, so even if you did add your jitted code to this structure, no darwin debuggers would currently see this anyway.

I beleive that GDB has some hooks for loading JIT'ed ELF .o files on the fly, but I don't believe this is implemented for mach-o .o files on MacOSX.

LLDB currently doesn't yet have any support for JIT'ed code, though I would be happy to work with you if you wanted to get that working in LLDB.

Greg Clayton

This was my fear. But I'll need some kind of debugger for my own
language anyway, so I'd be happy to implement this in LLDB.

Could you briefly outline the general steps necessary for adding
support for JIT'ed code? As you mention, I would expect the procedure
to look similar to how dlopen()ed dylibs are registered, but I might
be wrong.

GDB has a hook for JIT'ed code, but you are right that it only works
for ELF binaries. The approach there is that GDB sets a breakpoint in
an extern function, which LLVM calls when emitting code, giving GDB a
chance to load the symbols. Would a different approach in LLDB be
desirable, or does that seem OK to you?

According to the LLVMdev list, LLVM did not emit DWARF data for JIT'ed
code as of March 2009 — I'm not sure if this has changed, though I
suspect it hasn't, so to get this to work I guess there is also a bit
of work to be done on the LLVM side of things.

- Simon

2010/11/18 Greg Clayton <gclayton@apple.com>:

LLDB currently doesn’t yet have any support for JIT’ed code, though I would be happy to work with you if you wanted to get that working in LLDB.

This was my fear. But I’ll need some kind of debugger for my own
language anyway, so I’d be happy to implement this in LLDB.

Great!

Could you briefly outline the general steps necessary for adding
support for JIT’ed code? As you mention, I would expect the procedure
to look similar to how dlopen()ed dylibs are registered, but I might
be wrong.

A few questions on how this would be debugged (not worrying about the JIT yet):
1 - When you are debugging this, are you going to want to step through your new source code files or generated C/C++ sources?
2 - If you want to debug sources that you produce, will this be like debugging lex/yacc code where a bunch of #line and #file directives are used to map C/C++ code to your proprietary source code?

If you are going to be debugging standard i386/x86_64 code, then you won’t need to subclass lldb_private::Process.

In order to support JIT’ed code, we just need a way to communicate between a running program and the debugger. Setting a breakpoint, like is done with the JIT support in GDB, is quite ok for this as this is how the dynamic loader plug-in for macosx currently works. We can probably get away with being able to register additional dynamic loader plug-ins with the current Process. To elaborate a bit lets look at how the dynamic loaders work for shared libraries. Currently each process has a pluggable dynamic loader plug-in that gets loaded prior to launch by the Process subclasses in “Process::WillLaunch()”, or prior to attaching in “Process::WillAttachToProcessWithID (lldb::pid_t pid)” and “Process::WillAttachToProcessWithName (const char *process_name, bool wait_for_launch)”. So any process can re-use an abstract dynamic loader plugin. The pseudo code looks like:

class Process
{

std::auto_ptr m_dynamic_loader_ap;
};

When the WillLaunch, or WillAttach functions are overridden in the Process subclasses (see ProcessGDBRemote for an example), it will find a dynamic loader by the plug-in name:

m_dynamic_loader_ap.reset(DynamicLoader::FindPlugin(this, “dynamic-loader.macosx-dyld”));

Since the ProcessGDBRemote plug-in is currently for MacOSX debugging, we know to lookup the dynamic loader using a specific name.

After a dynamic loader plug-in is installed, it will get a callback after attaching or launching:

void
ProcessGDBRemote::DidLaunch ()
{
DidLaunchOrAttach ();
if (m_dynamic_loader_ap.get())
m_dynamic_loader_ap->DidLaunch();
}

This gives the dynamic loader plug-in a chance to install its breakpoint and assign a callback to that breakpoint. When breakpoints have callbacks associated with them, the callbacks get called synchronously when the breakpoint is hit and this allows you to load/unload shared libaries (See DynamicLoaderMacOSXDYLD for example code).

We could allow the Process class to have more than one dynamic loader plug-in since loading JIT code is very similar to loading shared libraries:

class Process
{

std::vector m_dynamic_loaders;
};

where DynamicLoaderSP is a shared pointer typedef…

This would allow us to have a standard system dynamic loader, and one or more JIT dynamic loader plug-ins.

The JIT’ed dynamic loader plug-in would do the same kind of thing the macosx one does: it will set a breakpoint, install a callback and react to that breakpoint callback as needed.

Inside LLDB we will need to think about how we want to represent JIT’ed code. There are a few options, but first lets look at how shared libraries are represented. Any executable or shared library is represented by a Module. Module objects have ObjectFile objects (abstracted object file readers (ELF and mach-o)), and a SymbolFile for reading debug symbols. We will want to repesent JIT’ed code by making a new Module that might be a special module that might own all of the JIT’ed code in a process from a specific JIT. So the clang JIT’ed code might require us to make a DynamicLoaderClangJIT DynamicLoader subclass, which would create a new module named with a fake name “” that we could add any information to. As new JIT’ed code gets added, new functions and data would get added to the “” object file (symbol table symbols and new sections) and symbol file (if we have debug info for the JIT’ed code). Another way would be let the JIT define logical modules in case you want to organize your JIT’ed code a bit more so that you can create many different Clang JIT modules. Either way, all of this work will be done by the DynamicLoaderClangJIT class.

GDB has a hook for JIT’ed code, but you are right that it only works
for ELF binaries. The approach there is that GDB sets a breakpoint in
an extern function, which LLVM calls when emitting code, giving GDB a
chance to load the symbols. Would a different approach in LLDB be
desirable, or does that seem OK to you?

That should work, see above comments.

According to the LLVMdev list, LLVM did not emit DWARF data for JIT’ed
code as of March 2009 — I’m not sure if this has changed, though I
suspect it hasn’t, so to get this to work I guess there is also a bit
of work to be done on the LLVM side of things.

Agreed, it would be great to be able to get DWARF for JIT’ed code.

  • Simon

Let me know if you need any explanation on anything mentioned above.

Greg

Thank you for your elaborate reply! I have a few small questions:

A few questions on how this would be debugged (not worrying about the JIT
yet):
1 - When you are debugging this, are you going to want to step through your
new source code files or generated C/C++ sources?
2 - If you want to debug sources that you produce, will this be like
debugging lex/yacc code where a bunch of #line and #file directives are used
to map C/C++ code to your proprietary source code?
If you are going to be debugging standard i386/x86_64 code, then you won't
need to subclass lldb_private::Process.

1) Both, ideally, but the first step will definitely be to be able to
debug JIT'ed C/C++ code.
2) For my language, there is a "proper" codegen that targets the LLVM
IR directly — no intermediate C/C++. So that's great! In the long run,
I suppose it would be nice to support the other way as well, but
that'll be up to the first person who needs that. :wink:

In order to support JIT'ed code, we just need a way to communicate between a
running program and the debugger. Setting a breakpoint, like is done with
the JIT support in GDB, is quite ok for this as this is how the dynamic
loader plug-in for macosx currently works. We can probably get away with
being able to register additional dynamic loader plug-ins with the current
Process.

(… examples and elaboration omitted for brevity …)

Since I'm not deeply familiar with the architecture of LLDB yet, it's
not clear to me what the exact relationship between the classes
Process, ProcessGDBRemote, and ProcessMacOSX is. Would it be necessary
to implement the JIT support in both ProcessGDBRemote and
ProcessMacOSX to get things working on Darwin? The way the two use the
DynamicLoader plugin is currently almost identical, so I'm unsure
whether that's a temporary thing (ProcessGDBRemote should at some
point become cross-platform, or what?).

Which one is used under normal debugging circumstances on Mac OS X?

The rest makes sense, and I have a pretty clear idea where to go from here.

Let me know if you need any explanation on anything mentioned above.
Greg

Again, thank you for being so helpful. :slight_smile:

Simon

Thank you for your elaborate reply! I have a few small questions:

A few questions on how this would be debugged (not worrying about the JIT
yet):
1 - When you are debugging this, are you going to want to step through your
new source code files or generated C/C++ sources?
2 - If you want to debug sources that you produce, will this be like
debugging lex/yacc code where a bunch of #line and #file directives are used
to map C/C++ code to your proprietary source code?
If you are going to be debugging standard i386/x86_64 code, then you won't
need to subclass lldb_private::Process.

1) Both, ideally, but the first step will definitely be to be able to
debug JIT'ed C/C++ code.
2) For my language, there is a "proper" codegen that targets the LLVM
IR directly — no intermediate C/C++. So that's great! In the long run,
I suppose it would be nice to support the other way as well, but
that'll be up to the first person who needs that. :wink:

In order to support JIT'ed code, we just need a way to communicate between a
running program and the debugger. Setting a breakpoint, like is done with
the JIT support in GDB, is quite ok for this as this is how the dynamic
loader plug-in for macosx currently works. We can probably get away with
being able to register additional dynamic loader plug-ins with the current
Process.

(… examples and elaboration omitted for brevity …)

Since I'm not deeply familiar with the architecture of LLDB yet, it's
not clear to me what the exact relationship between the classes
Process, ProcessGDBRemote, and ProcessMacOSX is.

Process is the main class that has pure virtual functions that base classes (ProcessGDBRemote and ProcessMacOSX) would need to implement (DoLaunch, DoAttach, DoHalt, etc).

Would it be necessary to implement the JIT support in both ProcessGDBRemote and
ProcessMacOSX to get things working on Darwin?

We aren't supporting ProcessMacOSX right now, but later when we do, you won't need to do anything different. If we make a new dynamic loader plug-in, they are process os/vendor/arch agnostic and can be plugged into any abstract process class.

The way the two use the DynamicLoader plugin is currently almost identical, so I'm unsure
whether that's a temporary thing (ProcessGDBRemote should at some
point become cross-platform, or what?).

This is hard coded for now, but we do plan on looking at the os/vendor/arch and finding an appropriate dynamic loader plug-in for the triple. In fact I can make that change soon since we already do get the triple from the process during launch or attach.

Which one is used under normal debugging circumstances on Mac OS X?

ProcessGDBRemote (again ProcessMacOSX is currently disabled).

The rest makes sense, and I have a pretty clear idea where to go from here.

Let me know if you need any explanation on anything mentioned above.
Greg

Again, thank you for being so helpful. :slight_smile:

No problem, let me know if you have more questions!

Greg Clayton