Some question about the llvm.dbg

Hello everybody,

I’m working on adding debug info support for julia and have a few questions about the llvm.dbg intrinsics. Sorry if this is a little long. If any of the things I’m asking about aren’t currently possible in LLVM, I’d be more than happy to work on them, if I can get some advice on the general direction (e.g. can everything be retrofitted onto the existing intrinsics or do I need new ones, etc.)

My first question is as follows. Consider the following llvm IR (exerted from a Julia try/catch block)

call void @llvm.dbg.value(metadata !{%jl_value_t* %16}, i64 0, metadata !19)
%17 = getelementptr %jl_value_t** %1, i64 1, !dbg !15
%18 = load %jl_value_t** %17, !dbg !15
call void @llvm.dbg.value(metadata !{%jl_value_t* %18}, i64 0, metadata !20)
store i1 false, i1* %“#s55”, !dbg !21
%19 = getelementptr i8* %14, i64 0, !dbg !21
call void @jl_enter_handler(i8* %19), !dbg !21
%20 = call i32 @sigsetjmp(i8* %19, i32 0), !dbg !21
%21 = icmp eq i32 %20, 0, !dbg !21
br i1 %21, label %try, label %L, !dbg !21

try: ; preds = %top
call void inttoptr (i64 4295956000 to void ())(), !dbg !22
%22 = getelementptr %jl_value_t** %4, i64 1, !dbg !22
store %jl_value_t
%18, %jl_value_t** %22, !dbg !22
call void @llvm.dbg.declare(metadata !{%jl_value_t** %22}, metadata !20)

The generated corresponding DWARF info is (corresponding to !20):

AT_location( 0x0000005f
0x00000001142f4000 - 0x00000001142f40b2: rsi
0x00000001142f40b2 - 0x00000001142f40c3: rsp+104
0x00000001142f4115 - 0x00000001142f4153: rax
0x00000001142f4153 - 0x00000001142f4155: rsp+56 )

Now 0x00000001142f40c3 is the location of the call void @jl_enter_handler(i8* %19). So there’s a gap in the debug info from there until the next declare. I would like to understand why this is the case and what to do about it (note that %18 is still used later, so it’s not the case that it goes out of scope, the stack location is still valid).

My second question is about variables that live at multiple locations. The DWARF standard supports overlapping address ranges and in Julia a variable might be in several locations at once and I’d love to just be able to declare them all and have llvm figure out the appropriate ranges. Some examples of where the variable might live are:

  • In the argument array passed to a function (for certain functions we pass an array
    of boxed pointers on the stack)
  • In a GC slot
  • Loaded into registers

Sometimes these overlap, so I feel like I ought to just put all of that information into the debug info and have the debugger figure it out. Is there anyway to declare overlapping ranges?

My third question is related to the argument array. In particular, it should be always possible for the debugger to find out where the argument array is by looking at the gc frame of the calling function and then taking an appropriate offset. I realize this is probably very fancy, but it seems to me that it should be possible to encode this into a DWARF expression and have it just work in the debugger. I’m not sure it is feasible to express this directly using LLVM intrinsics, but it should be fairly straightforward to write myself. Do the necessary hooks exist in LLVM to do something like this (ideally llvm would determine the appropriate address ranges and I’d just provide the single location information a a DWARF expression). If this capability doesn’t exist yet, I’d appreciate some pointer about where to add it.

Thanks for reading and I apologize that this has gotten so long. At some point I might have to move some of the more fancy functionality to the custom debugger, side but I’d prefer to encode as much as possible directly into DWARF, so I want to see how far I can get just with that.

Thanks,
Keno

Hello everybody,

I'm working on adding debug info support for julia and have a few questions
about the llvm.dbg intrinsics. Sorry if this is a little long. If any of the
things I'm asking about aren't currently possible in LLVM, I'd be more than
happy to work on them, if I can get some advice on the general direction
(e.g. can everything be retrofitted onto the existing intrinsics or do I
need new ones, etc.)

My first question is as follows. Consider the following llvm IR (exerted
from a Julia try/catch block)

  call void @llvm.dbg.value(metadata !{%jl_value_t* %16}, i64 0, metadata
!19)
  %17 = getelementptr %jl_value_t** %1, i64 1, !dbg !15
  %18 = load %jl_value_t** %17, !dbg !15
  call void @llvm.dbg.value(metadata !{%jl_value_t* %18}, i64 0, metadata
!20)
  store i1 false, i1* %"#s55", !dbg !21
  %19 = getelementptr i8* %14, i64 0, !dbg !21
  call void @jl_enter_handler(i8* %19), !dbg !21
  %20 = call i32 @sigsetjmp(i8* %19, i32 0), !dbg !21
  %21 = icmp eq i32 %20, 0, !dbg !21
  br i1 %21, label %try, label %L, !dbg !21

try: ; preds = %top
  call void inttoptr (i64 4295956000 to void ()*)(), !dbg !22
  %22 = getelementptr %jl_value_t** %4, i64 1, !dbg !22
  store %jl_value_t* %18, %jl_value_t** %22, !dbg !22
  call void @llvm.dbg.declare(metadata !{%jl_value_t** %22}, metadata !20)

The generated corresponding DWARF info is (corresponding to !20):

                     AT_location( 0x0000005f
                        0x00000001142f4000 - 0x00000001142f40b2: rsi
                        0x00000001142f40b2 - 0x00000001142f40c3: rsp+104
                        0x00000001142f4115 - 0x00000001142f4153: rax
                        0x00000001142f4153 - 0x00000001142f4155: rsp+56 )

Now 0x00000001142f40c3 is the location of the `call void
@jl_enter_handler(i8* %19)`. So there's a gap in the debug info from there
until the next declare. I would like to understand why this is the case and
what to do about it (note that %18 is still used later, so it's not the case
that it goes out of scope, the stack location is still valid).

Location tracking is pretty simplistic and brittle at the moment. LLVM
can manage the allocas that Clang generates and produce debug info
describing the location of the variable as being in that stack slot
for the entire range of the function. Beyond that it's very
hit-and-miss. Notably, LLVM only describes the range of non-alloca
variables as from their definition (where the register is initialized,
or the register offset is written to, etc) to the end of that basic
block. This completely kills lots of perfectly good debug info.

There's a review underway to partially fix this (
⚙ D3933 Generate better location ranges for some register-described variables. ) though it's still fairly restrictive.
I've made comments on there about where I /think/ the fix should be
(it looks like there's code already there to handle it and it's not
working at the moment?), maybe.

My second question is about variables that live at multiple locations. The
DWARF standard supports overlapping address ranges and in Julia a variable
might be in several locations at once and I'd love to just be able to
declare them all and have llvm figure out the appropriate ranges. Some
examples of where the variable might live are:

- In the argument array passed to a function (for certain functions we pass
an array
of boxed pointers on the stack)
- In a GC slot
- Loaded into registers

Sometimes these overlap, so I feel like I ought to just put all of that
information into the debug info and have the debugger figure it out. Is
there anyway to declare overlapping ranges?

Nope, I don't think LLVM has /any/ concept/support for describing
multiple locations simultaneously. It could be added. Shouldn't be
totally heinous to do so, but given the limitations described above,
it's certainly not the first priority, especially if the variable is
live in something like a stack slot forever... it's easy just to
describe that location & skip the rest. Such support could potentially
be added in the DbgValueHistoryCalculator being worked on/modified in
the above review.