Skip to content

Variable locations for C++ structured bindings broken with optimisations #61981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
OCHyams opened this issue Apr 6, 2023 · 1 comment
Open

Comments

@OCHyams
Copy link
Contributor

OCHyams commented Apr 6, 2023

Clang/LLVM version: 1a0653a (5th April 2023).

Variable locations for structured binding are incorrect for promoted variables
after SROA.

Consider this test case:

$ cat test.cpp -n
     1	struct two { int a, b; };
     2	__attribute__((optnone)) two get() { return {5, 10}; }
     3	
     4	int main() {
     5	  auto [x, y] = get();
     6	  return x + y;
     7	}

Build:

$ clang -g test.cpp  -O2  -Xclang -fexperimental-assignment-tracking=disabled -o test

Debug:

$ gdb test
GNU gdb (GDB) 10.2
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from test...
(gdb) b 6
Breakpoint 1 at 0x5ed: file test.cpp, line 6.
(gdb) run
Starting program: test 

Breakpoint 1, main () at test.cpp:6
6	  return x + y;
(gdb) p x
$1 = 5
(gdb) p y
Cannot access memory at address 0xa00000009
(gdb) info addr x
Symbol "x" is multi-location:
  Range 0x5555555545e6-0x5555555545f1: a variable in $rax
.
(gdb) info addr y
Symbol "y" is multi-location:
  Range 0x5555555545e6-0x5555555545f1: a complex DWARF expression:
     0: DW_OP_breg0 4 [$rax]

Variable x is in $rax with value 5 (correct). Variable y is reportedly at address $rax+4 which is clearly incorrect. It should be in $rcx with value 10 (see below).

Objdump:

$ llvm-objdump test --disassemble-symbols=main

test:	file format elf64-x86-64

Disassembly of section .text:

00000000000005e0 <main>:
     5e0: 50                           	pushq	%rax
     5e1: e8 da ff ff ff               	callq	0x5c0 <_Z3getv>
     5e6: 48 89 c1                     	movq	%rax, %rcx
     5e9: 48 c1 e9 20                  	shrq	$0x20, %rcx
     5ed: 01 c1                        	addl	%eax, %ecx
     5ef: 89 c8                        	movl	%ecx, %eax
     5f1: 59                           	popq	%rcx
     5f2: c3                           	retq
     5f3: 66 2e 0f 1f 84 00 00 00 00 00	nopw	%cs:(%rax,%rax)
     5fd: 0f 1f 00                     	nopl	(%rax)

It's SROA that is introducing this error. See below - the DW_OP_plus_uconst, 4 offset in y's (!31) dbg.declare is incorrectly carried over to its dbg.value, which also incorrectly uses the value %.sroa.0.0.extract.trunc (which is the value of the lower 32 bits of the original alloca - i.e. variable x).

$ clang -g test.cpp  -O2  -Xclang -fexperimental-assignment-tracking=disabled -o test -mllvm -print-before=sroa -mllvm -print-after=sroa

*** IR Dump Before SROAPass on main ***
; Function Attrs: mustprogress norecurse nounwind uwtable
define dso_local noundef i32 @main() #1 !dbg !26 {
entry:
  %retval = alloca i32, align 4
  %0 = alloca %struct.two, align 4
  store i32 0, ptr %retval, align 4
  call void @llvm.dbg.declare(metadata ptr %0, metadata !30, metadata !DIExpression()), !dbg !34
  call void @llvm.dbg.declare(metadata ptr %0, metadata !31, metadata !DIExpression(DW_OP_plus_uconst, 4)), !dbg !35
  call void @llvm.dbg.declare(metadata ptr %0, metadata !32, metadata !DIExpression()), !dbg !36
  %call = call i64 @_Z3getv(), !dbg !37
  store i64 %call, ptr %0, align 4, !dbg !37
  %a = getelementptr inbounds %struct.two, ptr %0, i32 0, i32 0, !dbg !34
  %1 = load i32, ptr %a, align 4, !dbg !38, !tbaa !19
  %b = getelementptr inbounds %struct.two, ptr %0, i32 0, i32 1, !dbg !35
  %2 = load i32, ptr %b, align 4, !dbg !39, !tbaa !24
  %add = add nsw i32 %1, %2, !dbg !40
  ret i32 %add, !dbg !42
}
*** IR Dump After SROAPass on main ***
; Function Attrs: mustprogress norecurse nounwind uwtable
define dso_local noundef i32 @main() #1 !dbg !26 {
entry:
  %call = call i64 @_Z3getv(), !dbg !33
  %.sroa.0.0.extract.trunc = trunc i64 %call to i32, !dbg !33
  call void @llvm.dbg.value(metadata i32 %.sroa.0.0.extract.trunc, metadata !30, metadata !DIExpression()), !dbg !34
  call void @llvm.dbg.value(metadata i32 %.sroa.0.0.extract.trunc, metadata !31, metadata !DIExpression(DW_OP_plus_uconst, 4)), !dbg !34
  call void @llvm.dbg.value(metadata i32 %.sroa.0.0.extract.trunc, metadata !32, metadata !DIExpression(DW_OP_LLVM_fragment, 0, 32)), !dbg !34
  %.sroa.4.0.extract.shift = lshr i64 %call, 32, !dbg !33
  %.sroa.4.0.extract.trunc = trunc i64 %.sroa.4.0.extract.shift to i32, !dbg !33
  call void @llvm.dbg.value(metadata i32 %.sroa.4.0.extract.trunc, metadata !32, metadata !DIExpression(DW_OP_LLVM_fragment, 32, 32)), !dbg !34
  %add = add nsw i32 %.sroa.0.0.extract.trunc, %.sroa.4.0.extract.trunc, !dbg !35
  ret i32 %add, !dbg !36
}
@llvmbot
Copy link
Member

llvmbot commented Apr 6, 2023

@llvm/issue-subscribers-debuginfo

OCHyams added a commit to OCHyams/llvm-project that referenced this issue Oct 13, 2023
Addresses one part of llvm#61981: prior to this patch, upon splitting an alloca,
SROA copied the dbg.declare onto the new allocas, updating the fragment info
for each accordingly. This isn't enough; if there's an offset from the alloca
in the DIExpression then it gets blindly copied to the new dbg.declare (i.e.
the offset from the original alloca is copied over and applied to the new alloca).

This patch fixes that by building on and using a utility funciton from
Assignment Tracking called calculateFragmentIntersect to determine:

  a) The new fragment (existing code already did this),
  b) The new offset from the new alloca.

Those parts are put together to create the new expression with a correct offset
and fragment. Herein lies an key limitation of this approach too:

calculateFragmentIntersect uses DIExression::extractIfOffset to grab an
existing offset from the expression. extractIfOffset, and therefore
calculateFragmentIntersect fails, if the expression contains anything other
than an optional trivial offset and optional fragment. In that fail-state,
debug-info for that variable fragment is dropped completely.

That is to say, using this new approach, dbg.declares describing variable
fragments that contain any non-offset non-fragment opcodes backed by allocas
that are split are now dropped.

If this is not an acceptable trade off, I suggest we simple undef/kill split
alloca dbg.declares with offsets untill a more complete solution can be cooked
up. That would probably involve interpreting the DIExpression and splicing the
non-offset non-fragment part into the new expression. I'm not sure how
difficult this is (it could be simple, but will probably be a little fiddly).

Note: this fix doesn't address another similar issue. If two variables are
stuffed into one alloca, then that alloca doesn't get split and mem2reg can
promote it to a single (unsplit) value, the variable in the upper bits needs to
have the offset in its DIExpression converted to a shift or a DW_OP_bit_piece
(or otherwise describe that the variable starts at an offset into the value).
I've called this out in the new test.
OCHyams added a commit to OCHyams/llvm-project that referenced this issue Oct 13, 2023
Addresses one part of llvm#61981: prior to this patch, upon splitting an alloca,
SROA copied the dbg.declare onto the new allocas, updating the fragment info
for each accordingly. This isn't enough; if there's an offset from the alloca
in the DIExpression then it gets blindly copied to the new dbg.declare (i.e.
the offset from the original alloca is copied over and applied to the new alloca).

This patch fixes that by building on and using a utility funciton from
Assignment Tracking called calculateFragmentIntersect to determine:

  a) The new fragment (existing code already did this),
  b) The new offset from the new alloca.

Those parts are put together to create the new expression with a correct offset
and fragment. Herein lies an key limitation of this approach too:

calculateFragmentIntersect uses DIExression::extractIfOffset to grab an
existing offset from the expression. extractIfOffset, and therefore
calculateFragmentIntersect fails, if the expression contains anything other
than an optional trivial offset and optional fragment. In that fail-state,
debug-info for that variable fragment is dropped completely.

That is to say, using this new approach, dbg.declares describing variable
fragments that contain any non-offset non-fragment opcodes backed by allocas
that are split are now dropped.

If this is not an acceptable trade off, I suggest we simple undef/kill split
alloca dbg.declares with offsets untill a more complete solution can be cooked
up. That would probably involve interpreting the DIExpression and splicing the
non-offset non-fragment part into the new expression. I'm not sure how
difficult this is (it could be simple, but will probably be a little fiddly).

Note: this fix doesn't address another similar issue. If two variables are
stuffed into one alloca, then that alloca doesn't get split and mem2reg can
promote it to a single (unsplit) value, the variable in the upper bits needs to
have the offset in its DIExpression converted to a shift or a DW_OP_bit_piece
(or otherwise describe that the variable starts at an offset into the value).
I've called this out in the new test.
OCHyams added a commit to OCHyams/llvm-project that referenced this issue Jul 4, 2024
Fixes llvm#61981 by adjusting variable location offsets (in the DIExpression) when
splittign allocas.

AKA Fix debug locations for C++ structured bindings in SROA.

NOTE: There's still a bug in mem2reg which generates incorrect locations in some
situations: if the variable fragment has an offset into the new (split) alloca,
mem2reg will fail to convert that into a bit shift (the location contains a
garbage offset). That's not addressed here.

insertNewDbgInst - Now takes the address-expression and FragmentInfo as
  serperate parameters because unlike dbg_declares dbg_assigns want those to go
  to different places. dbg_assign records put the variable fragment info in the
  value expression only (whereas dbg_declare has only one expression so puts it
  there - ideally this information wouldn't live in DIExpression, but that's
  another issue).

MigrateOne - Modified to correctly compute the necessary offsets and fragment
  adjustments. The previous implementation produced bogus locations for variables
  with non-zero offsets. The changes replace most of the body of this lambda, so
  it might be easier to review in a split-diff view and focus on the change as a
  whole than to compare it to the old implementation.

  This uses calculateFragmentIntersect and extractLeadingOffset added in previous
  patches in this series, and createOrReplaceFragment described below.

createOrReplaceFragment - Similar to DIExpression::createFragmentExpression
  except for 3 important distinctions:

    1. The new fragment isn't relative to an existing fragment.
    2. There are no checks on the the operation types because it is assumed
       the location this expression is computing is not implicit (i.e., it's
       always safe to create a fragment because arithmetic operations apply
       to the address computation, not to an implicit value computation).
    3. Existing extract_bits are modified independetly of fragment changes
       using \p BitExtractOffset. A change to the fragment offset or size
       may affect a bit extract. But a bit extract offset can change
       independently of the fragment dimensions.

  Returns the new expression, or nullptr if one couldn't be created.  Ideally
  this is only used to signal that a bit-extract has become zero-sized (and thus
  the new debug record has no size and can be dropped), however, it fails for
  other reasons too - see the FIXME below.

  FIXME: To keep this patch NFC the function bails in situations that
  DIExpression::createFragmentExpression fails in.E.g. when fragment and bit
  extract sizes differ. These limitations can be removed in the future.
OCHyams added a commit to OCHyams/llvm-project that referenced this issue Jul 15, 2024
Fixes llvm#61981 by adjusting variable location offsets (in the DIExpression) when
splittign allocas.

AKA Fix debug locations for C++ structured bindings in SROA.

NOTE: There's still a bug in mem2reg which generates incorrect locations in some
situations: if the variable fragment has an offset into the new (split) alloca,
mem2reg will fail to convert that into a bit shift (the location contains a
garbage offset). That's not addressed here.

insertNewDbgInst - Now takes the address-expression and FragmentInfo as
  serperate parameters because unlike dbg_declares dbg_assigns want those to go
  to different places. dbg_assign records put the variable fragment info in the
  value expression only (whereas dbg_declare has only one expression so puts it
  there - ideally this information wouldn't live in DIExpression, but that's
  another issue).

MigrateOne - Modified to correctly compute the necessary offsets and fragment
  adjustments. The previous implementation produced bogus locations for variables
  with non-zero offsets. The changes replace most of the body of this lambda, so
  it might be easier to review in a split-diff view and focus on the change as a
  whole than to compare it to the old implementation.

  This uses calculateFragmentIntersect and extractLeadingOffset added in previous
  patches in this series, and createOrReplaceFragment described below.

createOrReplaceFragment - Similar to DIExpression::createFragmentExpression
  except for 3 important distinctions:

    1. The new fragment isn't relative to an existing fragment.
    2. There are no checks on the the operation types because it is assumed
       the location this expression is computing is not implicit (i.e., it's
       always safe to create a fragment because arithmetic operations apply
       to the address computation, not to an implicit value computation).
    3. Existing extract_bits are modified independetly of fragment changes
       using \p BitExtractOffset. A change to the fragment offset or size
       may affect a bit extract. But a bit extract offset can change
       independently of the fragment dimensions.

  Returns the new expression, or nullptr if one couldn't be created.  Ideally
  this is only used to signal that a bit-extract has become zero-sized (and thus
  the new debug record has no size and can be dropped), however, it fails for
  other reasons too - see the FIXME below.

  FIXME: To keep this patch NFC the function bails in situations that
  DIExpression::createFragmentExpression fails in.E.g. when fragment and bit
  extract sizes differ. These limitations can be removed in the future.
OCHyams added a commit that referenced this issue Jul 18, 2024
Fixes issue #61981 by adjusting variable location offsets (in the DIExpression)
when splitting allocas.

Patch [4/4] to fix structured bindings in SROA.

NOTE: There's still a bug in mem2reg which generates incorrect locations in some
situations: if the variable fragment has an offset into the new (split) alloca,
mem2reg will fail to convert that into a bit shift (the location contains a
garbage offset). That's not addressed here.

insertNewDbgInst - Now takes the address-expression and FragmentInfo as
  separate parameters because unlike dbg_declares dbg_assigns want those to go
  to different places. dbg_assign records put the variable fragment info in the
  value expression only (whereas dbg_declare has only one expression so puts it
  there - ideally this information wouldn't live in DIExpression, but that's
  another issue).

MigrateOne - Modified to correctly compute the necessary offsets and fragment
  adjustments. The previous implementation produced bogus locations for variables
  with non-zero offsets. The changes replace most of the body of this lambda, so
  it might be easier to review in a split-diff view and focus on the change as a
  whole than to compare it to the old implementation.

  This uses calculateFragmentIntersect and extractLeadingOffset added in previous
  patches in this series, and createOrReplaceFragment described below.

createOrReplaceFragment - Similar to DIExpression::createFragmentExpression
  except for 3 important distinctions:

    1. The new fragment isn't relative to an existing fragment.
    2. There are no checks on the the operation types because it is assumed
       the location this expression is computing is not implicit (i.e., it's
       always safe to create a fragment because arithmetic operations apply
       to the address computation, not to an implicit value computation).
    3. Existing extract_bits are modified independetly of fragment changes
       using \p BitExtractOffset. A change to the fragment offset or size
       may affect a bit extract. But a bit extract offset can change
       independently of the fragment dimensions.

  Returns the new expression, or nullptr if one couldn't be created.  Ideally
  this is only used to signal that a bit-extract has become zero-sized (and thus
  the new debug record has no size and can be dropped), however, it fails for
  other reasons too - see the FIXME below.

  FIXME: To keep the scope of this change focused on non-bitfield structured
  bindings the function bails in situations that
  DIExpression::createFragmentExpression fails. E.g. when fragment and bit
  extract sizes differ. These limitations can be removed in the future.
yuxuanchen1997 pushed a commit that referenced this issue Jul 25, 2024
Summary:
Fixes issue #61981 by adjusting variable location offsets (in the DIExpression)
when splitting allocas.

Patch [4/4] to fix structured bindings in SROA.

NOTE: There's still a bug in mem2reg which generates incorrect locations in some
situations: if the variable fragment has an offset into the new (split) alloca,
mem2reg will fail to convert that into a bit shift (the location contains a
garbage offset). That's not addressed here.

insertNewDbgInst - Now takes the address-expression and FragmentInfo as
  separate parameters because unlike dbg_declares dbg_assigns want those to go
  to different places. dbg_assign records put the variable fragment info in the
  value expression only (whereas dbg_declare has only one expression so puts it
  there - ideally this information wouldn't live in DIExpression, but that's
  another issue).

MigrateOne - Modified to correctly compute the necessary offsets and fragment
  adjustments. The previous implementation produced bogus locations for variables
  with non-zero offsets. The changes replace most of the body of this lambda, so
  it might be easier to review in a split-diff view and focus on the change as a
  whole than to compare it to the old implementation.

  This uses calculateFragmentIntersect and extractLeadingOffset added in previous
  patches in this series, and createOrReplaceFragment described below.

createOrReplaceFragment - Similar to DIExpression::createFragmentExpression
  except for 3 important distinctions:

    1. The new fragment isn't relative to an existing fragment.
    2. There are no checks on the the operation types because it is assumed
       the location this expression is computing is not implicit (i.e., it's
       always safe to create a fragment because arithmetic operations apply
       to the address computation, not to an implicit value computation).
    3. Existing extract_bits are modified independetly of fragment changes
       using \p BitExtractOffset. A change to the fragment offset or size
       may affect a bit extract. But a bit extract offset can change
       independently of the fragment dimensions.

  Returns the new expression, or nullptr if one couldn't be created.  Ideally
  this is only used to signal that a bit-extract has become zero-sized (and thus
  the new debug record has no size and can be dropped), however, it fails for
  other reasons too - see the FIXME below.

  FIXME: To keep the scope of this change focused on non-bitfield structured
  bindings the function bails in situations that
  DIExpression::createFragmentExpression fails. E.g. when fragment and bit
  extract sizes differ. These limitations can be removed in the future.

Test Plan: 

Reviewers: 

Subscribers: 

Tasks: 

Tags: 


Differential Revision: https://phabricator.intern.facebook.com/D60250875
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants