Hey Jeremy. :) It's been over a year since you tackled #226 to help with jank and here to follow up and say that this is unfortunately no longer working on LLVM 22+. I've asked some LLVM folks about this, to see what may have changed, but I'm not optimistic about getting a clear answer. So instead I've been trying to break this apart myself. I'm hoping that these pieces I've gathered will make some sense to you, if you have the time and interest to take a look. :)
Recap and overview
As a recap, we added a hook into the jank runtime to call this function after giving any new code or object files to LLVM's JIT runtime:
static void register_jit_stack_frames()
{
if(auto *entry = cpptrace::detail::__jit_debug_descriptor.relevant_entry)
{
cpptrace::register_jit_object(entry->symfile_addr, entry->symfile_size);
}
}
I have a single .jank file which I'm running. It JIT compiles a clojure_core_foo_1_0 function which throws and then calls it immediately. I've upgraded to cpptrace::try_catch and I'm using that to catch the exception and print the stacktrace.
((fn foo [] (throw 'meow)))
The trace comes back with the very first frame not having a symbol:
Stack trace (most recent call first):
#0 0x00007ffff3286092
#1 0x0000555555e3790a in jank::runtime::obj::jit_function::call() const at jit_function.cpp:66
#2 0x00005555557e4b5c in jank::runtime::oref<jank::runtime::object>::call() const at oref.hpp:542
#3 0x00005555565d4d93 in jank::evaluate::eval(jtl::ref<jank::analyze::expr::call>) at evaluate.cpp:336
#4 0x0000555556623ee5 in auto jank::evaluate::eval(jtl::ref<jank::analyze::expression>)::$_0::operator()<jtl::ref<jank::analyze::expr::call> >(jtl::ref<jank::analyze::expr::call>) const at evaluate.cpp:271
#5 0x00005555565d3c3a in auto jank::analyze::visit_expr<jank::evaluate::eval(jtl::ref<jank::analyze::expression>)::$_0>(jank::evaluate::eval(jtl::ref<jank::analyze::expression>)::$_0 const&, jtl::ref<jank::analyze::expression>) at visit.hpp:55
#6 0x00005555565d3a46 in jank::evaluate::eval(jtl::ref<jank::analyze::expression>) at evaluate.cpp:271
#7 0x00005555566fd320 in jank::runtime::context::eval_string(jtl::immutable_string const&, jank::read::source_position const&) const at context_dynamic.cpp:77
#8 0x00005555566fcee0 in jank::runtime::context::eval_string(jtl::immutable_string const&) const at context_dynamic.cpp:150
#9 0x00005555566fcdbf in jank::runtime::context::eval_file(jtl::immutable_string const&) at context_dynamic.cpp:52
#10 0x000055555570c179 in jank::run() at main.cpp:78
#11 0x000055555570c01c in main::$_0::operator()(int, char const**) const at main.cpp:486
#12 0x000055555570b7eb in main::$_0::__invoke(int, char const**) at main.cpp:390
#13 0x00005555566ee92b in jank_init_dynamic::$_0::operator()() const at c_api_dynamic.cpp:45
#25 0x00005555566ee205 in jank_init_dynamic at c_api_dynamic.cpp:34
#26 0x000055555570b770 in main at main.cpp:384
However, we can see from the 0x00007ffff3286092 address space that it's different from the normal 0x0000555555e3790a range for AOT compiled functions.
Troubleshooting
I've put a breakpoint on __jit_debug_register_code to ensure it's being called. It is. I have hacked in a simple print to show if we're registering any entries for this JIT compiled foo function and we are. We're registering just one entry, which makes sense for the one JIT function we're adding.
void register_jit_object(const char* ptr, std::size_t size) {
std::printf("register 0x%lx, %zu\n", ptr, size);
detail::register_jit_object(ptr, size);
}
Here's the output I get from running the .jank file:
register 0x7fffe7884000, 113296
Uncaught exception: meow
<stack trace from above>
So I wrote that entry out to an object file, starting at 0x7fffe7884000, for 113296 bytes. Inside, we can find the clojure_core_foo_1_0 symbol.
❯ objdump -t /tmp/jit_debug_obj.o | grep clojure
00007ffff3286000 g F .ltext 0000000000000093 clojure_core_foo_1_0
We can see from this output that the symbol is at 00007ffff3286000 and is 0000000000000093 bytes. That perfectly matches the first range I'm seeing from llvm-dwarfdump.
❯ llvm-dwarfdump --debug-info /tmp/jit_debug_obj.o | head -40
/tmp/jit_debug_obj.o: file format elf64-x86-64
.debug_info contents:
0x00000000: Compile Unit: length = 0x000045df, format = DWARF32, version = 0x0005, unit_type = DW_UT_compile, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x000045e3)
0x0000000c: DW_TAG_compile_unit
DW_AT_producer ("clang version 23.0.0git")
DW_AT_language (DW_LANG_C_plus_plus_14)
DW_AT_name ("<<< inputs >>>")
DW_AT_str_offsets_base (0x00000008)
DW_AT_stmt_list (0x00000000)
DW_AT_comp_dir ("/home/jeaye/projects/jank/compiler+runtime")
DW_AT_low_pc (0x0000000000000000)
DW_AT_ranges (indexed (0x0) rangelist = 0x00000010
[0x00007ffff3286000, 0x00007ffff3286093) ### Right here.
Summary of findings
When I presented all of this to Claude to see if it thought it was an LLVM issue or cpptrace issue, Claude suggested this is a cpptrace issue.
The throw PC falls exactly inside both the symbol table entry's range and the first DWARF range. Both objdump and llvm-dwarfdump are correctly resolving this unlinked ET_REL object's addresses — objdump by combining the symbol's section-relative st_value with the (already-patched-by-ELFDebugObjectPlugin) section sh_addr, and llvm-dwarfdump by applying the pending ELF relocations against .debug_addr/.debug_rnglists on the fly as it reads.
So: LLVM is emitting correct debug info, ELFDebugObjectPlugin is patching section addresses correctly, and the object is genuinely resolvable by tooling that knows how to process ET_REL relocations at read time. The remaining gap is squarely in cpptrace's own resolver not doing that relocation step when it reads the object you hand it via register_jit_object.
I'm still investigating why this would have changed, but as jank is much more mature now than it was a year ago, I intend on continuously testing that this doesn't regress, once we can get it working again. That way any future issues become clear immediately.
I've attached the object file I've been using, in case that's helpful (had to gzip it to upload).
jit_debug_obj.o.gz
If it's easier to chat on Discord, feel free to reach out! Thanks for any time or attention to this. ❤️
Hey Jeremy. :) It's been over a year since you tackled #226 to help with jank and here to follow up and say that this is unfortunately no longer working on LLVM 22+. I've asked some LLVM folks about this, to see what may have changed, but I'm not optimistic about getting a clear answer. So instead I've been trying to break this apart myself. I'm hoping that these pieces I've gathered will make some sense to you, if you have the time and interest to take a look. :)
Recap and overview
As a recap, we added a hook into the jank runtime to call this function after giving any new code or object files to LLVM's JIT runtime:
I have a single
.jankfile which I'm running. It JIT compiles aclojure_core_foo_1_0function which throws and then calls it immediately. I've upgraded tocpptrace::try_catchand I'm using that to catch the exception and print the stacktrace.The trace comes back with the very first frame not having a symbol:
However, we can see from the
0x00007ffff3286092address space that it's different from the normal0x0000555555e3790arange for AOT compiled functions.Troubleshooting
I've put a breakpoint on
__jit_debug_register_codeto ensure it's being called. It is. I have hacked in a simple print to show if we're registering any entries for this JIT compiledfoofunction and we are. We're registering just one entry, which makes sense for the one JIT function we're adding.Here's the output I get from running the
.jankfile:So I wrote that entry out to an object file, starting at
0x7fffe7884000, for113296bytes. Inside, we can find theclojure_core_foo_1_0symbol.❯ objdump -t /tmp/jit_debug_obj.o | grep clojure 00007ffff3286000 g F .ltext 0000000000000093 clojure_core_foo_1_0We can see from this output that the symbol is at
00007ffff3286000and is0000000000000093bytes. That perfectly matches the first range I'm seeing fromllvm-dwarfdump.Summary of findings
When I presented all of this to Claude to see if it thought it was an LLVM issue or cpptrace issue, Claude suggested this is a cpptrace issue.
I'm still investigating why this would have changed, but as jank is much more mature now than it was a year ago, I intend on continuously testing that this doesn't regress, once we can get it working again. That way any future issues become clear immediately.
I've attached the object file I've been using, in case that's helpful (had to gzip it to upload).
jit_debug_obj.o.gz
If it's easier to chat on Discord, feel free to reach out! Thanks for any time or attention to this. ❤️