llvm / llvm/llvm-project

[DebugInfo][SimplifyCFG] Salvaged `ge`/`gt`/`le`/`lt` expressions zero-extend signed integer operands

Open
#219,412 3 comments 0 reactions 0 assignees View on GitHub
debuginfo llvm:transforms wrong-debug
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Description

SimplifyCFG folds a signed `icmp sge i32` instruction into the `DIExpression` for the source variable `cond2`. In the generated DWARF, the two signed `int` operands are read from registers as address-sized values with their upper 32 bits cleared. They are not sign-extended before `DW_OP_ge` evaluates the comparison.

With `a = -1` and `b = 0`, the source comparison `a >= b` is false. However, the DWARF expression evaluates `a` as the 64-bit value `0x00000000ffffffff`, so LLDB reports `cond2` as true at line 12 while the program is executing the false branch.

I believe this exposes two separate bugs. First, in this example, SimplifyCFG should replace the location operand of the `#dbg_value` for `cond2` with a constant `false` rather than invoking `salvageDebugInfo`, because `cond2` is known to be false on the surviving path. Second, when `salvageDebugInfo` is used, the 32-bit values of `a` and `b` are represented incorrectly in the final DWARF expression.

I am not sure whether the latter issue belongs to the middle end or the backend.

## Reproducer

Godbolt link containing the complete `src.ll` and `tgt.ll`: [Godbolt](https://godbolt.org/z/W7zoj6Yfj).

`case.c`:

```c
#include

volatile bool sink;

__attribute__((noinline)) void is(bool value) { sink = value; }

__attribute__((always_inline)) static inline void check_sge(int a, int b) {
bool cond2 = a >= b;
if (cond2)
is(true);
else
is(false);
}

__attribute__((noinline))
void test_sge_sge(int a, int b) {
bool cond1 = a >= b;
if (!cond1)
check_sge(a, b);
}

int main(void) {
test_sge_sge(-1, 0);
return 0;
}
```

Build pipeline:

```sh
clang -g -O1 -Xclang -disable-llvm-passes -fno-discard-value-names -S -emit-llvm case.c -o case.ll
opt -passes='function(mem2reg,instsimplify),inline' -S case.ll -o src.ll
opt -passes=simplifycfg -S src.ll -o tgt.ll
llc -O2 --dwarf-version=5 -relocation-model=pic -filetype=obj tgt.ll -o tgt.o
clang tgt.o -o tgt.out
```

`lldb-commands.txt`:

```text
breakpoint set --file case.c --line 12
run
frame variable cond2
quit
```

Run LLDB on the optimized binary:

```sh
lldb tgt.out -s lldb-commands.txt
```

## DWARF Debug Information

The DIEs for `cond2` can be inspected with:

```sh
llvm-dwarfdump --debug-info --name=cond2 tgt.out
```

The generated location expression is:

```text
0x0000006b: DW_TAG_variable
DW_AT_name ("cond2")
DW_AT_decl_file ("case.c")
DW_AT_decl_line (8)
DW_AT_type (0x00000078 "_Bool")

0x000000ba: DW_TAG_variable
DW_AT_location (indexed (0x4) loclist = 0x00000048:
[0x0000000000001145, 0x0000000000001147): DW_OP_breg5 RDI+0, DW_OP_constu 0xffffffff, DW_OP_and, DW_OP_breg4 RSI+0, DW_OP_constu 0xffffffff, DW_OP_and, DW_OP_ge, DW_OP_convert (0x00000026) "DW_ATE_unsigned_1", DW_OP_convert (0x0000002a) "DW_ATE_unsigned_8", DW_OP_stack_value)
DW_AT_abstract_origin (0x0000006b "cond2")
```

`RDI` and `RSI` hold the `int` arguments `a` and `b`. The `DW_OP_and 0xffffffff` operations clear the upper 32 bits, producing address-sized values without sign-extending the original signed 32-bit operands. Consequently, `a = -1` is evaluated as `0x00000000ffffffff`, which compares greater than or equal to zero.

## Observed Behavior

Before SimplifyCFG, `cond2` is represented by a signed `icmp` and a direct debug value:

```llvm
%cmp.i = icmp sge i32 %a, %b, !dbg !48
#dbg_value(i1 %cmp.i, !45, !DIExpression(DW_OP_LLVM_convert, 1, DW_ATE_unsigned, DW_OP_LLVM_convert, 8, DW_ATE_unsigned, DW_OP_stack_value), !46)
```

After SimplifyCFG, the comparison is removed and reconstructed with `DW_OP_ge`:

```llvm
#dbg_value(!DIArgList(i32 %a, i32 %b), !45, !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_arg, 1, DW_OP_ge, DW_OP_LLVM_convert, 1, DW_ATE_unsigned, DW_OP_LLVM_convert, 8, DW_ATE_unsigned, DW_OP_stack_value), !46)
call void @is(i1 noundef zeroext false), !dbg !48
```

The call to `is(false)` confirms that the source-level value of `cond2` is false. LLDB instead reports:

```text
(lldb) frame variable cond2
(bool) cond2 = true
```

## Expected Behavior

The generated debug information should preserve the signed 32-bit semantics of the original `icmp sge i32`. At line 12, LLDB should report:

```text
(bool) cond2 = false
```

## Environment

```text
clang version 24.0.0git
llvm-project revision: d35d0e69980f11c2acbd3670c65fec3cf574224a

LLVM version 24.0.0git
lldb version 24.0.0git
```

Contributor guide

Open the contributing guide

Research direction

Start with case.c and run the listed clang, opt, and llc pipeline, comparing src.ll with tgt.ll around SimplifyCFG. Inspect tgt.o using llvm-dwarfdump and the provided LLDB commands; done means the generated debug information preserves signed 32-bit comparison semantics and LLDB reports cond2 as false.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
compilers, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.