Skip to content

Fixing cpptrace JIT frame support #332

Description

@jeaye

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. ❤️

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions