Debugging a function that can be inlined is problematic because its callsite is eliminated and there is no longer an instruction that corresponds to the source location of the call.
GCC (starting from v8.1) generates at least two .loc
directives for the first instruction of an inlined function (meaning multiple records in .debug_line
for the same address). The first .loc
corresponds to the source location of the callsite while the second one refers to the source location of the instruction itself. Thus the debugger is able to stop on the first instruction of the inlined function if itâs asked to stop on the call.
Consider the following example:
$ cat test.cpp
01 void bar();
02
03 inline __attribute__((always_inline))
04 void foo() {
05 bar();
06 }
07
08 void test() {
09 foo();
10 }
$ gcc-11.2 -g -O3 -S -o -
...
test():
.LFB1:
.file 1 "test.cpp"
.loc 1 8 13 view -0
.cfi_startproc
.loc 1 9 3 view .LVU1 <- callsite
.LBB4:
.LBI4:
.loc 1 4 6 view .LVU2
.loc 1 5 3 view .LVU3
.loc 1 5 6 is_stmt 0 view .LVU4
jmp bar()
...
$ bin/llvm-dwarfdump -debug-line test.o
Address Line Column File ISA Discriminator Flags
------------------ ------ ------ ------ --- ------------- -------------
0x0000000000000000 8 13 1 0 0 is_stmt
0x0000000000000004 9 3 1 0 0 is_stmt
0x0000000000000004 4 6 1 0 0 is_stmt
0x0000000000000004 5 3 1 0 0 is_stmt
0x0000000000000004 5 6 1 0 0
0x0000000000000009 5 6 1 0 0 end_sequence
As you can see, bar()
call which was a part of inlined function foo()
corresponds to multiple .loc
directives, one of them points to line 9 which is the callsite of foo()
.
gdb handles this as expected, one can set a breakpoint on line 9 and run step-in/step-out/finish for foo()
. A breakpoint on line 05 also works as it should.
The only issue that can be observed is in some cases gdb shows the inlined function instead of its callsite:
(gdb) b test.cpp:9
Breakpoint 1 at 0x401116: file test.cpp, line 9.
(gdb) r
Starting program: a.out
Breakpoint 1, foo () at test.cpp:5
5 bar();
Clang-based toolchain ignores the issue whatsoever: neither the compiler produces .loc
directives for an inlined callsite, nor lldb is able to handle multiple .loc
for a single instruction. It ignores all .loc
but the last one.
$ clang-tot -g -O3 -S -o -
test(): # @test()
.Lfunc_begin0:
.file 1 "test.cpp"
.loc 1 8 0 # test.cpp:8:0
.cfi_startproc
.loc 1 5 3 prologue_end # test.cpp:5:3
jmp bar() # TAILCALL
.Ltmp0:
.Lfunc_end0:
$ bin/llvm-dwarfdump -debug-line test.o
Address Line Column File ISA Discriminator Flags
------------------ ------ ------ ------ --- ------------- -------------
0x0000000000000000 8 0 0 0 0 is_stmt
0x0000000000000000 5 3 0 0 0 is_stmt prologue_end
0x0000000000000005 5 3 0 0 0 is_stmt end_sequence
Basically, having multiple .debug_line
entries`for the same address seems to be a bit off standard:
6.2.5 The Line Number Program
As stated before, the goal of a line number program is to build a matrix
representing one compilation unit, which may have produced multiple
sequences of target machine instructions. Within a sequence, addresses and
operation pointers may only increase. (Line numbers may decrease in cases of
pipeline scheduling or other optimization.)
this is why lldb gives up if it faces multiple .debug_line
entries for the same address (see the comment in lldb/source/Symbol/LineTable.cpp
):
// Replace the last entry if the address is the same, otherwise append it. If
// we have multiple line entries at the same address, this indicates illegal
// DWARF so this âfixesâ the line table to be correct. If not fixed this can
// cause a line entryâs address that when resolved back to a symbol context,
// could resolve to a different line entry. We really want a
// 1 to 1 mapping
// here to avoid these kinds of inconsistencies. We will need or revisit
// this if the DWARF line tables are updated to allow multiple entries at the
// same address legally.
On the other side, clang still may produce multiple .loc
(thus multiple line info records) for the same address in other circumstances that are not connected with the issue we are considering.
My personal opinion is that the feature GCC/GDB provides is useful in engineering practice despite itâs a bit off the standard, but Iâd like to ask the community to express their opinion on the subject. Does it make any sense to implement the same behavior for llvm/lldb?