[DebugInfo] dereferencing pointer to local variable shows uninitialized data due to DSE
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
cc @jmorse
When Dead Store Elimination (`DSEPass`) eliminates stores into an object, this is tracked in DWARF debuginfo such that the debugger can still show the logical state of the object; but when the debugger looks at a pointer to the object (in particular, when a pointer to a local variable is passed to an inlined subroutine), when the debugger follows this pointer, it will potentially see uninitialized memory.
This might be related to https://github.com/llvm/llvm-project/issues/38112#issuecomment-981001844 , which is also about DSE causing issues with debugging.
Reproducer:
```
jannh@horn:~/test$ cat debug-dse-pointer-test.c
struct s {
int a;
int b;
};
int blah();
int bar(struct s*);
__attribute__((always_inline)) inline int baz(struct s *s) {
blah();
s->a = s->a + 1;
s->b = s->b - 1;
return bar(s);
}
__attribute__((noinline)) int foo(int a, int b) {
struct s s = {.a = a, .b = b};
return baz(&s);
}
int main(void) {
foo(1, 2);
return 0;
}
jannh@horn:~/test$ cat debug-dse-pointer-test2.c
struct s {
int a;
int b;
};
int blah(){return 0;}
int bar(struct s *s){return 0;}
jannh@horn:~/test$ /usr/local/google/home/jannh/git/foreign/llvmp-build-debug/bin/clang-12 -g -O2 -o debug-dse-pointer-test debug-dse-pointer-test.c debug-dse-pointer-test2.c
jannh@horn:~/test$ gdb ./debug-dse-pointer-test
[...]
(gdb) break baz
Breakpoint 1 at 0x401117: file debug-dse-pointer-test.c, line 10.
(gdb) run
[...]
Breakpoint 1, baz (s=0x7fffffffd6f0) at debug-dse-pointer-test.c:10
10 blah();
(gdb) print *s
$1 = {a = 4198720, b = 0}
(gdb) bt
#0 baz (s=0x7fffffffd6f0) at debug-dse-pointer-test.c:10
#1 foo (a=a@entry=1, b=b@entry=2) at debug-dse-pointer-test.c:18
#2 0x0000000000401150 in main () at debug-dse-pointer-test.c:22
(gdb) frame 1
#1 foo (a=a@entry=1, b=b@entry=2) at debug-dse-pointer-test.c:18
18 return baz(&s);
(gdb) print s
$2 = {a = 1, b = 2}
(gdb)
```
**Note that the outer function `foo` can correctly display the struct contents, wrong data only shows up when following the pointer argument in the inlined callee.**
Relevant DWARF:
```
0x00000059: DW_TAG_subprogram
DW_AT_low_pc (0x0000000000401110)
DW_AT_high_pc (0x0000000000401138)
DW_AT_frame_base (DW_OP_reg7 RSP)
DW_AT_call_all_calls (true)
DW_AT_name ("foo")
DW_AT_decl_file ("/usr/local/google/home/jannh/test/debug-dse-pointer-test.c")
DW_AT_decl_line (16)
DW_AT_prototyped (true)
DW_AT_type (0x00000038 "int")
DW_AT_external (true)
[...]
0x0000007a: DW_TAG_variable
DW_AT_location (indexed (0x2) loclist = 0x0000003e:
[0x0000000000401117, 0x0000000000401120): DW_OP_reg6 RBP, DW_OP_piece 0x4, DW_OP_reg3 RBX, DW_OP_piece 0x4
[0x0000000000401120, 0x0000000000401123): DW_OP_piece 0x4, DW_OP_reg3 RBX, DW_OP_piece 0x4
[0x0000000000401123, 0x0000000000401125): DW_OP_breg7 RSP+0, DW_OP_piece 0x4, DW_OP_reg3 RBX, DW_OP_piece 0x4
[0x0000000000401125, 0x0000000000401129): DW_OP_breg7 RSP+0, DW_OP_piece 0x4
[0x0000000000401129, 0x0000000000401138): DW_OP_breg7 RSP+0)
DW_AT_name ("s")
DW_AT_decl_file ("/usr/local/google/home/jannh/test/debug-dse-pointer-test.c")
DW_AT_decl_line (17)
DW_AT_type (0x00000041 "s")
0x00000083: DW_TAG_inlined_subroutine
DW_AT_abstract_origin (0x00000027 "baz")
DW_AT_low_pc (0x0000000000401117)
DW_AT_high_pc (0x0000000000401131)
DW_AT_call_file ("/usr/local/google/home/jannh/test/debug-dse-pointer-test.c")
DW_AT_call_line (18)
DW_AT_call_column (10)
0x00000090: DW_TAG_formal_parameter
DW_AT_location (DW_OP_reg7 RSP)
DW_AT_abstract_origin (0x0000002f "s")
```
Disassembly:
```
$ objdump --disassemble=foo -Mintel ./debug-dse-pointer-test
[...]
0000000000401110 :
401110: 55 push rbp
401111: 53 push rbx
401112: 50 push rax
401113: 89 f3 mov ebx,esi
401115: 89 fd mov ebp,edi
401117: 31 c0 xor eax,eax
401119: e8 42 00 00 00 call 401160
40111e: ff c5 inc ebp
401120: 89 2c 24 mov DWORD PTR [rsp],ebp
401123: ff cb dec ebx
401125: 89 5c 24 04 mov DWORD PTR [rsp+0x4],ebx
401129: 48 89 e7 mov rdi,rsp
40112c: e8 3f 00 00 00 call 401170
401131: 48 83 c4 08 add rsp,0x8
401135: 5b pop rbx
401136: 5d pop rbp
401137: c3 ret
```
Tested with:
```
clang version 22.0.0git (https://github.com/llvm/llvm-project a9b8dfe7b5f224e2d442352979cf2e0c1c0b539b)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/google/home/jannh/git/foreign/llvmp-build-debug/bin
Build config: +unoptimized, +assertions
```
When building with ` -mllvm --print-after-all`, you can see that the initial two `store` instructions in `foo` disappear during `DSEPass`:
```
[...]
; *** IR Dump After MemCpyOptPass on foo ***
; Function Attrs: noinline nounwind uwtable
define dso_local i32 @foo(i32 noundef %a, i32 noundef %b) local_unnamed_addr #0 !dbg !8 {
entry:
%s = alloca %struct.s, align 4, !DIAssignID !20
#dbg_assign(i1 poison, !15, !DIExpression(), !20, ptr %s, !DIExpression(), !21)
#dbg_value(i32 %a, !13, !DIExpression(), !21)
#dbg_value(i32 %b, !14, !DIExpression(), !21)
call void @llvm.lifetime.start.p0(ptr nonnull %s) #4, !dbg !22
store i32 %a, ptr %s, align 4, !dbg !23, !tbaa !24, !DIAssignID !29
#dbg_assign(i32 %a, !15, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !29, ptr %s, !DIExpression(), !21)
%b2 = getelementptr inbounds nuw i8, ptr %s, i64 4, !dbg !30
store i32 %b, ptr %b2, align 4, !dbg !23, !tbaa !31, !DIAssignID !32
#dbg_assign(i32 %b, !15, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !32, ptr %b2, !DIExpression(), !21)
#dbg_value(ptr %s, !33, !DIExpression(), !39)
%call.i = tail call i32 (...) @blah() #4, !dbg !41
%add.i = add nsw i32 %a, 1, !dbg !42
store i32 %add.i, ptr %s, align 4, !dbg !43, !tbaa !24, !DIAssignID !44
#dbg_assign(i32 %add.i, !15, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !44, ptr %s, !DIExpression(), !21)
%sub.i = add nsw i32 %b, -1, !dbg !45
store i32 %sub.i, ptr %b2, align 4, !dbg !46, !tbaa !31, !DIAssignID !47
#dbg_assign(i32 %sub.i, !15, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !47, ptr %b2, !DIExpression(), !21)
%call3.i = call i32 @bar(ptr noundef nonnull %s) #4, !dbg !48
call void @llvm.lifetime.end.p0(ptr nonnull %s) #4, !dbg !49
ret i32 %call3.i, !dbg !50
}
; *** IR Dump After DSEPass on foo ***
; Function Attrs: noinline nounwind uwtable
define dso_local i32 @foo(i32 noundef %a, i32 noundef %b) local_unnamed_addr #0 !dbg !8 {
entry:
%s = alloca %struct.s, align 4, !DIAssignID !20
#dbg_assign(i1 poison, !15, !DIExpression(), !20, ptr %s, !DIExpression(), !21)
#dbg_value(i32 %a, !13, !DIExpression(), !21)
#dbg_value(i32 %b, !14, !DIExpression(), !21)
call void @llvm.lifetime.start.p0(ptr nonnull %s) #4, !dbg !22
#dbg_assign(i32 %a, !15, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !23, ptr %s, !DIExpression(), !21)
%b2 = getelementptr inbounds nuw i8, ptr %s, i64 4, !dbg !24
#dbg_assign(i32 %b, !15, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !25, ptr %b2, !DIExpression(), !21)
#dbg_value(ptr %s, !26, !DIExpression(), !32)
%call.i = tail call i32 (...) @blah() #4, !dbg !34
%add.i = add nsw i32 %a, 1, !dbg !35
store i32 %add.i, ptr %s, align 4, !dbg !36, !tbaa !37, !DIAssignID !42
#dbg_assign(i32 %add.i, !15, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !42, ptr %s, !DIExpression(), !21)
%sub.i = add nsw i32 %b, -1, !dbg !43
store i32 %sub.i, ptr %b2, align 4, !dbg !44, !tbaa !45, !DIAssignID !46
#dbg_assign(i32 %sub.i, !15, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !46, ptr %b2, !DIExpression(), !21)
%call3.i = call i32 @bar(ptr noundef nonnull %s) #4, !dbg !47
call void @llvm.lifetime.end.p0(ptr nonnull %s) #4, !dbg !48
ret i32 %call3.i, !dbg !49
}
[...]
```
Contributor guide
Research direction
Start by reproducing the issue with debug-dse-pointer-test.c and debug-dse-pointer-test2.c using clang -g -O2, then compare the IR before and after DSEPass. Inspect the emitted DWARF and the GDB results for foo and the inlined baz frame. Done means dereferencing the pointer in the inlined subroutine no longer displays uninitialized data while preserving the outer frame's logical values.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100