diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aabfd96..a8185ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,13 +14,13 @@ jobs: - name: Install LLVM run: | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - sudo add-apt-repository "deb http://apt.llvm.org/focal/ llvm-toolchain-focal-15 main" + sudo add-apt-repository "deb http://apt.llvm.org/focal/ llvm-toolchain-focal-17 main" sudo apt-get update - sudo apt-get install --no-install-recommends --yes llvm-15 llvm-15-tools + sudo apt-get install --no-install-recommends --yes llvm-17 llvm-17-tools - name: Install lit run: pip install lit - name: Run lit testsuite - run: lit -v --path "$PWD/target/release/:/usr/lib/llvm-15/bin/" ./tests + run: lit -v --path "$PWD/target/release/:/usr/lib/llvm-17/bin/" ./tests fmt: name: rustfmt diff --git a/.gitignore b/.gitignore index de7687d..85fc308 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /tests/.lit_test_times.txt +*~ diff --git a/Cargo.lock b/Cargo.lock index 5952679..df562c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "equivalent" version = "1.0.2" @@ -170,12 +176,12 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "gimli" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +checksum = "1033caf0b349c518623b5396bfb2cf0bddf44f0306d543a250e5743297aafd10" dependencies = [ "fnv", - "hashbrown", + "hashbrown 0.17.1", "indexmap", "stable_deref_trait", ] @@ -185,6 +191,15 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", @@ -204,7 +219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -213,6 +228,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -282,7 +306,7 @@ checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" dependencies = [ "crc32fast", "flate2", - "hashbrown", + "hashbrown 0.16.1", "indexmap", "memchr", "ruzstd", @@ -419,7 +443,8 @@ name = "thorin-dwp" version = "0.10.0" dependencies = [ "gimli", - "hashbrown", + "hashbrown 0.17.1", + "itertools", "object", "tracing", ] diff --git a/tests/columns.test b/tests/columns.test index cc0c788..310d57b 100644 --- a/tests/columns.test +++ b/tests/columns.test @@ -2,6 +2,6 @@ RUN: thorin %p/inputs/columns-a.dwo %p/inputs/columns-b.dwo -o - \ RUN: | llvm-dwarfdump -v - | FileCheck --check-prefixes=CHECK %s CHECK-LABEL: .debug_cu_index contents: -CHECK: Index Signature INFO ABBREV LINE STR_OFFSETS -CHECK: 2 0x{{.*}} [0x0000002d, 0x00000052) [0x0000002c, 0x00000056) [0x00000000, 0x00000000) [0x00000008, 0x00000018) -CHECK: 3 0x{{.*}} [0x00000000, 0x0000002d) [0x00000000, 0x0000002c) [0x00000000, 0x00000025) [0x00000000, 0x00000008) +CHECK: Index Signature INFO ABBREV LINE STR_OFFSETS +CHECK: 2 0x{{.*}} [0x000000000000002d, 0x0000000000000052) [0x0000002c, 0x00000056) [0x00000000, 0x00000000) [0x00000008, 0x00000018) +CHECK: 3 0x{{.*}} [0x0000000000000000, 0x000000000000002d) [0x00000000, 0x0000002c) [0x00000000, 0x00000025) [0x00000000, 0x00000008) diff --git a/tests/debug-macro-v5.s b/tests/debug-macro-v5.s index 073fc56..e2f9787 100644 --- a/tests/debug-macro-v5.s +++ b/tests/debug-macro-v5.s @@ -11,8 +11,8 @@ # CHECK-DAG: .debug_cu_index contents: # CHECK-NEXT: version = 5, units = 1, slots = 2 -# CHECK: Index Signature INFO ABBREV STR_OFFSETS MACRO -# CHECK: 1 0x0000000000000000 [0x00000000, 0x00000019) [0x00000000, 0x00000008) [0x00000000, 0x0000000c) [0x00000000, 0x0000000b) +# CHECK: Index Signature INFO ABBREV STR_OFFSETS MACRO +# CHECK: 1 0x0000000000000000 [0x0000000000000000, 0x0000000000000019) [0x00000000, 0x00000008) [0x00000000, 0x0000000c) [0x00000000, 0x0000000b) .section .debug_info.dwo,"e",@progbits .long .Ldebug_info_dwo_end0-.Ldebug_info_dwo_start0 # Length of Unit diff --git a/tests/gc-all-dead.test b/tests/gc-all-dead.test new file mode 100644 index 0000000..77b88f4 --- /dev/null +++ b/tests/gc-all-dead.test @@ -0,0 +1,28 @@ +# Test that GC does not remove an entirely dead CU. The skeleton CU in the +# executable references this CU by DwoId; dropping it would leave a dangling +# reference that we cannot fix without modifying the executable. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-all-dead-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-all-dead-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: both subprograms survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprograms are removed but the CU survives. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# The compile unit must always be present. +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# Without GC both dead subprograms are present. +# NOGC: DW_TAG_subprogram +# NOGC: DW_TAG_subprogram + +# After GC both subprograms are removed but the CU itself survives. +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-compact-gnu-str-index.test b/tests/gc-compact-gnu-str-index.test new file mode 100644 index 0000000..d7ec77f --- /dev/null +++ b/tests/gc-compact-gnu-str-index.test @@ -0,0 +1,67 @@ +# Test that GC compacts the .debug_str_offsets.dwo table when string attributes +# use DW_FORM_GNU_str_index (DWARF4 split-DWARF extension), by removing entries +# for strings only referenced by dead DIEs, and that surviving DIEs' string +# attributes are correctly remapped to the new indices. +# +# The .dwo has 4 string offset entries (indices 0-3). The dead subprogram +# uses index 2 ("dead_func") and the live subprogram uses index 3 +# ("live_func"). After GC the dead entry at index 2 is removed, so the live +# subprogram's string must be remapped from index 3 to index 2. Without the +# DW_FORM_GNU_str_index fix, the old index is copied verbatim and the string +# resolves incorrectly. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-compact-gnu-str-index-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-compact-gnu-str-index-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all subprograms and strings survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-str-offsets -debug-str %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed and str_offsets compacted. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-str-offsets -debug-str %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# -- debug_info checks -- +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit +# CHECK: DW_AT_name ("test.c") +# CHECK: DW_AT_comp_dir ("/home") + +# Without GC, the dead subprogram comes first. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_name ("dead_func") + +# The live subprogram must always be present with correct name. +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("live_func") + +# After GC there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram + +# -- debug_str checks (appears before str_offsets in output) -- +# After GC the dead string should not be in .debug_str.dwo. +# GC: .debug_str.dwo contents: +# GC: "test.c" +# GC: "live_func" +# GC-NOT: "dead_func" + +# -- str_offsets checks -- +# CHECK: .debug_str_offsets.dwo contents: + +# Without GC the table has 4 entries (16 bytes, no header in DWARF4). +# NOGC: Contribution size = 16 +# NOGC: "test.c" +# NOGC: "/home" +# NOGC: "dead_func" +# NOGC: "live_func" + +# After GC the table has 3 entries (12 bytes, no header in DWARF4). +# GC: Contribution size = 12 +# GC: "test.c" +# GC: "/home" +# GC: "live_func" +# GC-NOT: "dead_func" diff --git a/tests/gc-compact-loclists.test b/tests/gc-compact-loclists.test new file mode 100644 index 0000000..4e83fdb --- /dev/null +++ b/tests/gc-compact-loclists.test @@ -0,0 +1,74 @@ +# Test that GC compacts the .debug_loclists.dwo offset table by removing +# entries for location lists only referenced by dead subprograms, and that +# surviving DIEs' loclistx attributes are correctly renumbered. +# +# The .dwo has 3 location lists (loclistx 0, 1, 2). The live subprogram's +# variable uses loclistx 1. After GC the dead entries at indices 0 and 2 +# are removed, so the surviving variable's loclistx must be renumbered +# from 1 to 0. Without the fix, the old index is copied verbatim and the +# location list resolves incorrectly. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-compact-loclists-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-compact-loclists-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all subprograms and location lists survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-loclists %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprograms are removed and loclists compacted. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-loclists %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# -- debug_info checks -- +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# Without GC, dead_func1 comes first. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_name ("dead_func1") + +# The live subprogram must always be present. +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("live_func") + +# Its variable with a location list must survive. +# Without GC the original loclistx index 1 is preserved. +# NOGC: DW_TAG_variable +# NOGC: DW_AT_location (indexed (0x1) loclist = +# NOGC: : DW_OP_reg1 RDX) + +# After GC the loclistx must be renumbered from 1 to 0. +# GC: DW_TAG_variable +# GC: DW_AT_location (indexed (0x0) loclist = +# GC: : DW_OP_reg1 RDX) + +# Without GC, dead_func2 is also present. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_name ("dead_func2") + +# After GC there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram + +# -- loclists checks -- +# CHECK: .debug_loclists.dwo contents: + +# Without GC the offset table has 3 entries. +# NOGC: offset_entry_count = 0x00000003 +# NOGC: offsets: [ +# NOGC: 0x{{[0-9a-f]+}} +# NOGC-NEXT: 0x{{[0-9a-f]+}} +# NOGC-NEXT: 0x{{[0-9a-f]+}} +# NOGC-NEXT: ] + +# After GC the offset table has only 1 entry (compacted). +# GC: offset_entry_count = 0x00000001 +# GC: offsets: [ +# GC: 0x{{[0-9a-f]+}} +# GC-NEXT: ] + +# The surviving location list data is correct. +# GC: : DW_OP_reg1 RDX diff --git a/tests/gc-compact-rnglists.test b/tests/gc-compact-rnglists.test new file mode 100644 index 0000000..33c08e5 --- /dev/null +++ b/tests/gc-compact-rnglists.test @@ -0,0 +1,69 @@ +# Test that GC compacts the .debug_rnglists.dwo offset table by removing +# entries for range lists only referenced by dead subprograms, and that +# surviving DIEs' rnglistx attributes are correctly renumbered. +# +# The .dwo has 3 range lists (rnglistx 0, 1, 2). The live subprogram uses +# rnglistx 1. After GC the dead entries at indices 0 and 2 are removed, +# so the surviving subprogram's rnglistx must be renumbered from 1 to 0. +# Without the fix, the old index is copied verbatim and the range list +# resolves incorrectly. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-compact-rnglists-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-compact-rnglists-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all subprograms and range lists survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-rnglists %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprograms are removed and rnglists compacted. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-rnglists %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# -- debug_info checks -- +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# Without GC, dead_func1 comes first. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_name ("dead_func1") + +# The live subprogram must always be present. +# CHECK: DW_TAG_subprogram + +# Without GC the original rnglistx index 1 is preserved. +# NOGC: DW_AT_ranges (indexed (0x1) rangelist = +# After GC the rnglistx must be renumbered from 1 to 0. +# GC: DW_AT_ranges (indexed (0x0) rangelist = + +# CHECK: DW_AT_name ("live_func") + +# Without GC, dead_func2 is also present. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_name ("dead_func2") + +# After GC there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram + +# -- rnglists checks -- +# CHECK: .debug_rnglists.dwo contents: + +# Without GC the offset table has 3 entries. +# NOGC: offset_entry_count = 0x00000003 +# NOGC: offsets: [ +# NOGC: 0x{{[0-9a-f]+}} +# NOGC-NEXT: 0x{{[0-9a-f]+}} +# NOGC-NEXT: 0x{{[0-9a-f]+}} +# NOGC-NEXT: ] + +# After GC the offset table has only 1 entry (compacted). +# GC: offset_entry_count = 0x00000001 +# GC: offsets: [ +# GC: 0x{{[0-9a-f]+}} +# GC-NEXT: ] + +# The surviving range list data for live_func is correct. +# GC: [0x{{[0-9a-f]+}}, 0x{{[0-9a-f]+}}) diff --git a/tests/gc-compact-str-offsets.test b/tests/gc-compact-str-offsets.test new file mode 100644 index 0000000..e8e796f --- /dev/null +++ b/tests/gc-compact-str-offsets.test @@ -0,0 +1,67 @@ +# Test that GC compacts the .debug_str_offsets.dwo table when string attributes +# use DW_FORM_strx3 (DWARF5), by removing entries for strings only referenced +# by dead DIEs, and that surviving DIEs' string attributes are correctly +# remapped to the new indices. +# +# The .dwo has 4 string offset entries (indices 0-3). The dead subprogram +# uses index 2 ("dead_func") and the live subprogram uses index 3 +# ("live_func"). After GC the dead entry at index 2 is removed, so the live +# subprogram's string must be remapped from index 3 to index 2. Without the +# DW_FORM_strx3 fix, the old index is copied verbatim and the string +# resolves incorrectly. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-compact-str-offsets-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-compact-str-offsets-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all subprograms and strings survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-str-offsets -debug-str %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed and str_offsets compacted. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-str-offsets -debug-str %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# -- debug_info checks -- +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit +# CHECK: DW_AT_name ("test.c") +# CHECK: DW_AT_comp_dir ("/home") + +# Without GC, the dead subprogram comes first. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_name ("dead_func") + +# The live subprogram must always be present with correct name. +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("live_func") + +# After GC there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram + +# -- debug_str checks (appears before str_offsets in output) -- +# After GC the dead string should not be in .debug_str.dwo. +# GC: .debug_str.dwo contents: +# GC: "test.c" +# GC: "live_func" +# GC-NOT: "dead_func" + +# -- str_offsets checks -- +# CHECK: .debug_str_offsets.dwo contents: + +# Without GC the table has 4 entries (contribution size = 4 header + 4*4 = 20). +# NOGC: Contribution size = 20 +# NOGC: "test.c" +# NOGC: "/home" +# NOGC: "dead_func" +# NOGC: "live_func" + +# After GC the table has 3 entries (contribution size = 4 header + 3*4 = 16). +# GC: Contribution size = 16 +# GC: "test.c" +# GC: "/home" +# GC: "live_func" +# GC-NOT: "dead_func" diff --git a/tests/gc-convert-first-op.test b/tests/gc-convert-first-op.test new file mode 100644 index 0000000..a9a688c --- /dev/null +++ b/tests/gc-convert-first-op.test @@ -0,0 +1,46 @@ +# Test that a DW_OP_convert that is the FIRST opcode of an exprloc (byte 0 of the +# expression == 0xa8) is patched correctly after GC removes a dead subprogram and +# shifts the referenced base_type to a smaller CU-relative offset. +# +# This exercises the ULEB128 operand patcher: the operand starts at byte 1 of the +# expression, so its width must be measured from r.pos == 1, not from byte 0. Byte +# 0 is the 0xa8 opcode, whose high (continuation) bit is set; measuring width from +# byte 0 wrongly returns 2 and corrupts the rewritten expression. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-convert-first-op-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-convert-first-op-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed and offsets shift. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) + +# live_func (addrx 1, live) must always be present with its variable child. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# CHECK: DW_TAG_variable +# CHECK-NEXT: DW_AT_location [DW_FORM_exprloc] (DW_OP_convert (0x{{[0-9a-f]+}} -> {{0x[0-9a-f]+}}) "ct") + +# The base_type "ct" (conv_type) must survive, kept alive by DW_OP_convert, and +# the DW_OP_convert operand above must resolve to its offset/name. With the bug +# the operand is a corrupted 2-byte ULEB128 and dwarfdump decodes a wrong offset, +# so the "ct" reference above fails to resolve. +# CHECK: DW_TAG_base_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("ct") + +# After GC there must be no extra subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-dead-subprogram.test b/tests/gc-dead-subprogram.test new file mode 100644 index 0000000..7537880 --- /dev/null +++ b/tests/gc-dead-subprogram.test @@ -0,0 +1,35 @@ +# Test that GC removes a dead subprogram (tombstoned low_pc) while keeping a live +# one and its referenced base_type, with the DW_AT_type ref4 patched correctly. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-dead-subprogram-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-dead-subprogram-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: both subprograms survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx4] (indexed (00000000) + +# The live subprogram (addrx 1) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx4] (indexed (00000001) +# CHECK-NEXT: DW_AT_type [DW_FORM_ref4] (cu + 0x{{[0-9a-f]+}} => {[[BTOFF:0x[0-9a-f]+]]} + +# The base_type must survive at the offset referenced by DW_AT_type. +# CHECK: [[BTOFF]]: DW_TAG_base_type + +# After garbage collection there must be no second subprogram. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-deep-nesting.test b/tests/gc-deep-nesting.test new file mode 100644 index 0000000..76598b8 --- /dev/null +++ b/tests/gc-deep-nesting.test @@ -0,0 +1,64 @@ +# Test that GC handles multi-level DIE nesting deeper than compile_unit > namespace > subprogram. +# Two patterns are tested: +# 1. compile_unit > structure_type > subprogram (class with member functions) +# 2. compile_unit > subprogram > subprogram (nested/local function) +# In both cases the parent is Retained (tombstoned but ancestor of a live child), +# the dead sibling subprogram is pruned, and the live one survives. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-deep-nesting-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-deep-nesting-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprograms are removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# --- Pattern 1: structure_type with member functions --- + +# The structure_type must always be present (retained as ancestor of live_method). +# CHECK: DW_TAG_structure_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("MyClass") + +# The dead method (addrx2 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx2] (indexed (00000000) +# NOGC-NEXT: DW_AT_name [DW_FORM_string] ("dead_method") + +# The live method (addrx2 1) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx2] (indexed (00000001) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("live_method") + +# After GC, end of structure_type children — no more subprograms inside. +# GC: NULL +# GC-NOT: DW_TAG_subprogram + +# --- Pattern 2: subprogram containing nested subprograms --- + +# The outer subprogram must always be present (retained as ancestor of live_inner). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx3] (indexed (00000002) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("outer") + +# The dead inner subprogram (addrx2 3, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx2] (indexed (00000003) +# NOGC-NEXT: DW_AT_name [DW_FORM_string] ("dead_inner") + +# The live inner subprogram (addrx2 4) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx2] (indexed (00000004) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("live_inner") + +# After GC, end of outer's children — no more subprograms inside. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-duplicate-dwoid.test b/tests/gc-duplicate-dwoid.test new file mode 100644 index 0000000..8589059 --- /dev/null +++ b/tests/gc-duplicate-dwoid.test @@ -0,0 +1,50 @@ +# Test that when two executables reference the same .dwo (same DWO ID), +# liveness is computed as the union across both executables. A DIE that +# is live in *either* executable must survive GC. +# +# Setup: one .dwo with two subprograms (func_a at addrx 0, func_b at +# addrx 1). Exec1 has func_a live and func_b tombstoned; exec2 has the +# reverse. Both subprograms should survive. +# +# Bug #8: put_data_for_dwo is last-wins, so only the second executable's +# addresses are used. func_a (live only in exec1) gets incorrectly GC'd. + +# Assemble the .dwo. +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-duplicate-dwoid-dwo.s -o %t.dwo + +# Assemble both executables, each pointing to the same .dwo. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-duplicate-dwoid-exec1.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t-exec1.o +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-duplicate-dwoid-exec2.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t-exec2.o + +# With only exec1: func_a survives, func_b is GC'd. +# RUN: thorin --gc -e %t-exec1.o -o %t-exec1.dwp +# RUN: llvm-dwarfdump -debug-info %t-exec1.dwp | FileCheck --check-prefix=EXEC1 %s + +# EXEC1: .debug_info.dwo contents: +# EXEC1: DW_TAG_compile_unit +# EXEC1: DW_TAG_subprogram +# EXEC1: DW_AT_name ("func_a") +# EXEC1: NULL +# EXEC1-NOT: DW_TAG_subprogram + +# With only exec2: func_b survives, func_a is GC'd. +# RUN: thorin --gc -e %t-exec2.o -o %t-exec2.dwp +# RUN: llvm-dwarfdump -debug-info %t-exec2.dwp | FileCheck --check-prefix=EXEC2 %s + +# EXEC2: .debug_info.dwo contents: +# EXEC2: DW_TAG_compile_unit +# EXEC2: DW_TAG_subprogram +# EXEC2: DW_AT_name ("func_b") +# EXEC2: NULL +# EXEC2-NOT: DW_TAG_subprogram + +# With both executables: both subprograms must survive (union of liveness). +# RUN: thorin --gc -e %t-exec1.o -e %t-exec2.o -o %t-both.dwp +# RUN: llvm-dwarfdump -debug-info %t-both.dwp | FileCheck --check-prefix=BOTH %s + +# BOTH: .debug_info.dwo contents: +# BOTH: DW_TAG_compile_unit +# BOTH: DW_TAG_subprogram +# BOTH: DW_AT_name ("func_a") +# BOTH: DW_TAG_subprogram +# BOTH: DW_AT_name ("func_b") diff --git a/tests/gc-entry-value.test b/tests/gc-entry-value.test new file mode 100644 index 0000000..2138c8a --- /dev/null +++ b/tests/gc-entry-value.test @@ -0,0 +1,41 @@ +# Test that walk_expression recurses into DW_OP_entry_value sub-expressions +# and finds CU-relative references (DW_OP_call4) there, keeping the target +# subprogram alive even though its low_pc is tombstoned. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-entry-value-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-entry-value-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) + +# The live subprogram (addrx 1) must always be present (has children). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) + +# The variable with DW_OP_entry_value containing DW_OP_call4 must survive. +# CHECK: DW_TAG_variable +# CHECK-NEXT: DW_AT_location [DW_FORM_exprloc] + +# The target subprogram (addrx 2, tombstoned but kept alive by DW_OP_call4 +# inside the entry_value sub-expression). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000002) + +# After GC there must be no extra subprograms beyond the two live ones. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-expr-ops.test b/tests/gc-expr-ops.test new file mode 100644 index 0000000..59d2d7e --- /dev/null +++ b/tests/gc-expr-ops.test @@ -0,0 +1,51 @@ +# Test that DW_OP_call2 and DW_OP_GNU_parameter_ref inside exprloc attributes +# keep their target DIEs alive (even though those DIEs have tombstoned low_pc) +# and that the expression operands are patched to the correct new CU-relative +# offsets after GC removes a dead DIE. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-expr-ops-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-expr-ops-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) + +# The caller subprogram (addrx 1, live) must always be present with DW_OP_call2. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# CHECK-NEXT: DW_AT_frame_base [DW_FORM_exprloc] (DW_OP_call2 0x{{[0-9a-f]+}}) + +# The callee subprogram (addrx 2, tombstoned but kept alive by DW_OP_call2). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000002) + +# The paramfn subprogram (addrx 3, live, has children). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000003) + +# The formal_parameter with DW_OP_GNU_parameter_ref (0xfa). +# llvm-dwarfdump does not decode this GNU extension; it appears as raw bytes. +# CHECK: DW_TAG_formal_parameter +# CHECK-NEXT: DW_AT_location [DW_FORM_exprloc] ({{.*}}fa + +# The paramtgt subprogram (addrx 4, tombstoned but kept alive by DW_OP_GNU_parameter_ref). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000004) + +# After GC only the compile_unit NULL terminator follows paramtgt. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-exprloc-call.test b/tests/gc-exprloc-call.test new file mode 100644 index 0000000..ddfa9ff --- /dev/null +++ b/tests/gc-exprloc-call.test @@ -0,0 +1,39 @@ +# Test that a DW_OP_call4 inside a DW_AT_frame_base exprloc keeps the callee +# DIE alive (even though its low_pc is tombstoned) and that the DW_OP_call4 +# operand is patched to the callee's new CU-relative offset after the dead +# base_type is removed. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-exprloc-call-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-exprloc-call-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead base_type is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The caller subprogram (addrx 0, live) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) +# In the baseline the callee is at 0x1e; after GC removes the base_type it moves to 0x1d. +# NOGC-NEXT: DW_AT_frame_base [DW_FORM_exprloc] (DW_OP_call4 0x1e) +# GC-NEXT: DW_AT_frame_base [DW_FORM_exprloc] (DW_OP_call4 0x1d) + +# The dead base_type sits between caller and callee; present only in the baseline. +# NOGC: DW_TAG_base_type + +# The callee subprogram (addrx 1, tombstoned but kept alive by DW_OP_call4). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) + +# After GC the base_type must be gone -- only NULL follows the callee. +# GC: NULL +# GC-NOT: DW_TAG_base_type diff --git a/tests/gc-indirect-ref.test b/tests/gc-indirect-ref.test new file mode 100644 index 0000000..41b24a4 --- /dev/null +++ b/tests/gc-indirect-ref.test @@ -0,0 +1,37 @@ +# Test that a reference attribute encoded via DW_FORM_indirect (which embeds the +# actual form as a ULEB128 prefix in the DIE stream) is correctly traced as a +# liveness edge and patched after GC. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-indirect-ref-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-indirect-ref-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) + +# The live subprogram (addrx 1) must always be present. +# llvm-dwarfdump resolves DW_FORM_indirect transparently, showing the inner form. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# CHECK-NEXT: DW_AT_type [DW_FORM_ref4] (cu + 0x{{[0-9a-f]+}} => {[[BTOFF:0x[0-9a-f]+]]} + +# The base_type must survive at the offset referenced by DW_AT_type. +# CHECK: [[BTOFF]]: DW_TAG_base_type + +# After garbage collection there must be no second subprogram. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-loclist-sec-offset.test b/tests/gc-loclist-sec-offset.test new file mode 100644 index 0000000..948fd74 --- /dev/null +++ b/tests/gc-loclist-sec-offset.test @@ -0,0 +1,50 @@ +# Test that a DW_OP_call4 inside a location list entry (referenced via +# DW_AT_location sec_offset into .debug_loclists.dwo with offset_entry_count +# == 0, GCC-style) has its CU-relative offset correctly patched after GC +# removes a dead DIE. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-loclist-sec-offset-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-loclist-sec-offset-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) +# NOGC-NEXT: DW_AT_name [DW_FORM_string] ("dead_func") + +# The live subprogram (addrx 1) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("live_func") + +# The variable inside the live subprogram. The loclist expression contains +# a DW_OP_call4 whose operand must point to the target subprogram. +# In the baseline the target is at CU-relative 0x33; after GC removes +# dead_func the target moves to 0x27 and the operand must be patched. +# CHECK: DW_TAG_variable +# NOGC-NEXT: DW_AT_location [DW_FORM_sec_offset] (0x0000000c: +# NOGC-NEXT: : DW_OP_call4 0x33) +# GC-NEXT: DW_AT_location [DW_FORM_sec_offset] (0x0000000c: +# GC-NEXT: : DW_OP_call4 0x27) + +# The target subprogram (addrx 2, tombstoned but kept alive by DW_OP_call4 in loclist). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000002) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("target") + +# After garbage collection there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-loclist-v4.test b/tests/gc-loclist-v4.test new file mode 100644 index 0000000..0478105 --- /dev/null +++ b/tests/gc-loclist-v4.test @@ -0,0 +1,51 @@ +# Test that a DW_OP_call4 inside a location list entry in .debug_loc.dwo +# (DWARF4 split DWARF, GNU extension) keeps the target subprogram alive even +# though its low_pc is tombstoned, and that a dead subprogram placed before +# the live one is correctly removed. The DW_OP_call4 operand must be patched +# to the target's new CU-relative offset after GC. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-loclist-v4-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-loclist-v4-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (GNU_addr_index 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_GNU_addr_index] (indexed (00000000) +# NOGC-NEXT: DW_AT_name [DW_FORM_string] ("dead_func") + +# The live subprogram (GNU_addr_index 1) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_GNU_addr_index] (indexed (00000001) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("live_func") + +# The variable inside the live subprogram. The loclist expression contains +# a DW_OP_call4 whose operand must point to the target subprogram. +# In the baseline the target is at CU-relative 0x32; after GC removes +# dead_func the target moves to 0x26 and the operand must be patched. +# CHECK: DW_TAG_variable +# NOGC-NEXT: DW_AT_location [DW_FORM_sec_offset] (0x{{[0-9a-f]+}}: +# NOGC: DW_OP_call4 0x32) +# GC-NEXT: DW_AT_location [DW_FORM_sec_offset] (0x{{[0-9a-f]+}}: +# GC: DW_OP_call4 0x26) + +# The target subprogram (GNU_addr_index 2, tombstoned but kept alive by DW_OP_call4 in loclist). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_GNU_addr_index] (indexed (00000002) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("target") + +# After garbage collection there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-loclist.test b/tests/gc-loclist.test new file mode 100644 index 0000000..ff984fd --- /dev/null +++ b/tests/gc-loclist.test @@ -0,0 +1,50 @@ +# Test that a DW_OP_call4 inside a location list entry (referenced via +# DW_AT_location loclistx into .debug_loclists.dwo) keeps the target +# subprogram alive even though its low_pc is tombstoned, and that a dead +# subprogram placed before the live one is correctly removed. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-loclist-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-loclist-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) +# NOGC-NEXT: DW_AT_name [DW_FORM_string] ("dead_func") + +# The live subprogram (addrx 1) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("live_func") + +# The variable inside the live subprogram. The loclist expression contains +# a DW_OP_call4 whose operand must point to the target subprogram. +# In the baseline the target is at CU-relative 0x34; after GC removes +# dead_func the target moves to 0x28 and the operand must be patched. +# CHECK: DW_TAG_variable +# NOGC-NEXT: DW_AT_location [DW_FORM_loclistx] (indexed (0x0) loclist = 0x{{[0-9a-f]+}}: +# NOGC-NEXT: : DW_OP_call4 0x34) +# GC-NEXT: DW_AT_location [DW_FORM_loclistx] (indexed (0x0) loclist = 0x{{[0-9a-f]+}}: +# GC-NEXT: : DW_OP_call4 0x28) + +# The target subprogram (addrx 2, tombstoned but kept alive by DW_OP_call4 in loclist). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000002) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("target") + +# After garbage collection there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-multi-dwp-v4.test b/tests/gc-multi-dwp-v4.test new file mode 100644 index 0000000..28c518f --- /dev/null +++ b/tests/gc-multi-dwp-v4.test @@ -0,0 +1,61 @@ +# Test garbage collection of a multi-CU `.dwp` input (DWARF 4 GNU extension). +# +# Two single-CU `.dwo` files are first packaged into one `.dwp`, which is then +# re-packaged with `--gc`. The shared `.debug_loc.dwo` section of the `.dwp` is +# the concatenation of both CUs' contributions, so each CU's location list must +# be patched against its OWN sub-range using its OWN offset remap. +# +# CU 0xdd (gc-loclist-v4-dwo.s) and CU 0xde (gc-multi-dwp-v4-b-dwo.s) each have +# a location list with a DW_OP_call4 that keeps a tombstoned `target` +# subprogram alive. The two CUs have deliberately different layouts, so the +# patched CU-relative call offsets differ (0x26 vs 0x28). If one CU's remap +# bled into the other, the patched offset would be wrong. + +# Assemble the two distinct `.dwo` files. +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-loclist-v4-dwo.s -o %t-dd.dwo +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-multi-dwp-v4-b-dwo.s -o %t-de.dwo + +# Package them into a single multi-CU `.dwp` (no GC). +# RUN: thorin %t-dd.dwo %t-de.dwo -o %t-multi.dwp + +# Build the executable that supplies the GC address data for both CUs. +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-multi-dwp-v4-exec.s -o %t.o + +# Re-package the `.dwp` with garbage collection. +# RUN: thorin --gc -e %t.o %t-multi.dwp -o %t-gc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-loc %t-gc.dwp | FileCheck %s +# RUN: llvm-dwarfdump -v -debug-cu-index %t-gc.dwp | FileCheck --check-prefix=INDEX %s + +# -- debug_info: dead DIEs removed in both CUs; live func + call4 target kept -- +# CHECK: .debug_info.dwo contents: +# CHECK-NOT: dead_func + +# CU 0xdd: its call keeps `target` alive, patched to a CU 0xdd-relative offset. +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("live_func") +# CHECK: DW_TAG_variable +# CHECK: DW_AT_location +# CHECK: DW_LLE_startx_length {{.*}} DW_OP_call4 0x26 +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("target") + +# CU 0xde: its call keeps `target_b` alive, patched to a DIFFERENT +# CU 0xde-relative offset (proving no cross-CU remap bleed). +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("live_func_b") +# CHECK: DW_TAG_variable +# CHECK: DW_AT_location +# CHECK: DW_LLE_startx_length {{.*}} DW_OP_call4 0x28 +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("target_b") + +# -- .debug_loc: each CU's list patched to its own call offset -- +# CHECK: .debug_loc.dwo contents: +# CHECK: DW_OP_call4 0x26 +# CHECK: DW_OP_call4 0x28 + +# -- cu_index: the two CUs' LOC contributions are contiguous and disjoint -- +# INDEX: .debug_cu_index contents: +# INDEX: LOC +# INDEX-DAG: 0x00000000000000dd {{.*}} [0x00000000, 0x0000000e) +# INDEX-DAG: 0x00000000000000de {{.*}} [0x0000000e, 0x0000001c) diff --git a/tests/gc-multi-dwp-v5.test b/tests/gc-multi-dwp-v5.test new file mode 100644 index 0000000..6880622 --- /dev/null +++ b/tests/gc-multi-dwp-v5.test @@ -0,0 +1,67 @@ +# Test garbage collection of a multi-CU `.dwp` input (DWARF 5). +# +# Two single-CU `.dwo` files are first packaged into one `.dwp`, which is then +# re-packaged with `--gc`. The shared `.debug_loclists.dwo` section of the +# `.dwp` is the concatenation of both CUs' contributions, so each CU must be +# rewritten against its OWN sub-range: its referenced loclist indices and its +# CU-relative expression references must not bleed into the other CU. +# +# CU 0xbb (gc-compact-loclists-dwo.s): 3 location lists, only loclistx 1 is +# live. After GC the offset table compacts to 1 entry and the surviving +# variable's loclistx is renumbered 1 -> 0. +# CU 0xcc (gc-loclist-dwo.s): 1 location list whose expression is +# DW_OP_call4 . The call keeps the tombstoned `target` subprogram +# alive, and the CU-relative call offset must be patched using 0xcc's own +# remap (not 0xbb's). + +# Assemble the two distinct `.dwo` files. +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-compact-loclists-dwo.s -o %t-bb.dwo +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-loclist-dwo.s -o %t-cc.dwo + +# Package them into a single multi-CU `.dwp` (no GC). +# RUN: thorin %t-bb.dwo %t-cc.dwo -o %t-multi.dwp + +# Build the executable that supplies the GC address data for both CUs. +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-multi-dwp-v5-exec.s -o %t.o + +# Re-package the `.dwp` with garbage collection. +# RUN: thorin --gc -e %t.o %t-multi.dwp -o %t-gc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-loclists %t-gc.dwp | FileCheck %s +# RUN: llvm-dwarfdump -v -debug-cu-index %t-gc.dwp | FileCheck --check-prefix=INDEX %s + +# -- debug_info: dead DIEs removed in both CUs, live ones kept -- +# CHECK: .debug_info.dwo contents: + +# CU 0xbb: only live_func survives, its loclistx renumbered to 0. +# CHECK: DWO_id = 0x00000000000000bb +# CHECK-NOT: dead_func +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("live_func") +# CHECK: DW_TAG_variable +# CHECK: DW_AT_location (indexed (0x0) loclist = + +# CU 0xcc: live_func survives; the DW_OP_call4 keeps `target` alive and its +# CU-relative offset is patched to point within CU 0xcc. +# CHECK: DWO_id = 0x00000000000000cc +# CHECK-NOT: dead_func +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("live_func") +# CHECK: DW_TAG_variable +# CHECK: DW_AT_location (indexed (0x0) loclist = +# CHECK: : DW_OP_call4 0x28 +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("target") + +# -- loclists: each CU keeps its own compacted, single-entry offset table -- +# CHECK: .debug_loclists.dwo contents: +# CHECK: offset_entry_count = 0x00000001 +# CHECK: : DW_OP_reg1 RDX +# CHECK: offset_entry_count = 0x00000001 +# CHECK: : DW_OP_call4 0x28 + +# -- cu_index: the two CUs' LOCLISTS contributions are contiguous and disjoint +# (sizes recomputed from the rewritten per-CU bytes) -- +# INDEX: .debug_cu_index contents: +# INDEX: LOCLISTS +# INDEX-DAG: 0x00000000000000cc {{.*}} [0x00000014, 0x0000002c) +# INDEX-DAG: 0x00000000000000bb {{.*}} [0x00000000, 0x00000014) diff --git a/tests/gc-nothing-dead-orphan-list.test b/tests/gc-nothing-dead-orphan-list.test new file mode 100644 index 0000000..71af18c --- /dev/null +++ b/tests/gc-nothing-dead-orphan-list.test @@ -0,0 +1,37 @@ +# Test that when all DIEs are live, GC does NOT compact rnglists or loclists +# offset tables, even if there are orphan (unreferenced) list entries. +# +# The .dwo has 3 range lists and 3 location lists. Index 0 of each is +# orphaned (not referenced by any DIE). The two subprograms use indices +# 1 and 2. Since nothing is dead, debug_info passes through unchanged, +# so the offset tables must also be preserved to keep the indices valid. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-nothing-dead-orphan-list-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-nothing-dead-orphan-list-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# With garbage collection both subprograms survive and offset tables are NOT compacted. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-rnglists -debug-loclists %t-gc.dwp | FileCheck %s + +# -- debug_info checks: both subprograms survive with original indices -- +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_ranges (indexed (0x1) rangelist = +# CHECK: DW_AT_name ("func_a") +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_ranges (indexed (0x2) rangelist = +# CHECK: DW_AT_name ("func_b") +# CHECK: NULL +# CHECK-NOT: DW_TAG_subprogram + +# -- loclists: offset table is NOT compacted (still 3 entries) -- +# CHECK: .debug_loclists.dwo contents: +# CHECK: offset_entry_count = 0x00000003 + +# -- rnglists: offset table is NOT compacted (still 3 entries) -- +# CHECK: .debug_rnglists.dwo contents: +# CHECK: offset_entry_count = 0x00000003 diff --git a/tests/gc-nothing-dead.test b/tests/gc-nothing-dead.test new file mode 100644 index 0000000..e1b3092 --- /dev/null +++ b/tests/gc-nothing-dead.test @@ -0,0 +1,30 @@ +# Test that when nothing is dead, GC leaves the output unchanged — both +# subprograms survive with and without --gc. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-nothing-dead-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-nothing-dead-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Both with and without garbage collection the output should be identical. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck %s + +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# First subprogram (addrx 0, live). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx1] (indexed (00000000) + +# Second subprogram (addrx 1, live). +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx1] (indexed (00000001) + +# After the last subprogram, the compile_unit's children end. +# CHECK: NULL +# CHECK-NOT: DW_TAG_subprogram diff --git a/tests/gc-ranges-dead-v4.test b/tests/gc-ranges-dead-v4.test new file mode 100644 index 0000000..e747a5f --- /dev/null +++ b/tests/gc-ranges-dead-v4.test @@ -0,0 +1,56 @@ +# Test that a DW_TAG_subprogram whose DW_AT_ranges (DW_FORM_sec_offset) +# points to a range list in .debug_ranges where every entry is tombstoned +# is correctly identified as dead and removed by GC. +# +# DWARF4 split DWARF (GNU extension). The range data is in the skeleton +# executable's .debug_ranges section, referenced by sec_offset from the .dwo. +# +# The skeleton sets DW_AT_GNU_ranges_base, so the .dwo's sec_offsets are relative +# to that base: GC must add ranges_base before reading .debug_ranges. Without that +# adjustment the offsets land in leading padding and mixed_func is wrongly dropped. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-ranges-dead-v4-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-ranges-dead-v4-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# llvm-dwarfdump exits non-zero because .debug_ranges is not in the .dwp, +# making the sec_offset values unresolvable. Ignore that exit code. +# RUN: (llvm-dwarfdump -v %t-nogc.dwp 2>/dev/null || true) | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprograms should be removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: (llvm-dwarfdump -v %t-gc.dwp 2>/dev/null || true) | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# dead_func (all ranges tombstoned) -- dead, removed by GC. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_ranges [DW_FORM_sec_offset] +# NOGC: DW_AT_name [DW_FORM_string] ("dead_func") + +# dead_base_func (base address tombstoned + offset pair) -- dead, removed by GC. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_ranges [DW_FORM_sec_offset] +# NOGC: DW_AT_name [DW_FORM_string] ("dead_base_func") + +# In the GC case, dead_func and dead_base_func must not appear. +# The GC-NOT range extends from compile_unit to the mixed_func name match. +# GC-NOT: ("dead_func") +# GC-NOT: ("dead_base_func") + +# mixed_func (one tombstoned + one live range entry) -- survives GC. +# CHECK: DW_AT_name [DW_FORM_string] ("mixed_func") + +# live_func (DW_AT_low_pc, live) -- always present. +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_low_pc [DW_FORM_GNU_addr_index] (indexed (00000000) +# CHECK: DW_AT_name [DW_FORM_string] ("live_func") + +# After garbage collection there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-ranges-dead.test b/tests/gc-ranges-dead.test new file mode 100644 index 0000000..1c5d3c0 --- /dev/null +++ b/tests/gc-ranges-dead.test @@ -0,0 +1,53 @@ +# Test that a DW_TAG_subprogram whose DW_AT_ranges points to a range list +# where every entry is tombstoned is correctly identified as dead and removed +# by GC. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-ranges-dead-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-ranges-dead-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram should be removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# dead_func (all ranges tombstoned via startx_length) -- dead, removed by GC. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_ranges [DW_FORM_rnglistx] +# NOGC: DW_AT_name [DW_FORM_string] ("dead_func") + +# dead_base_func (base_addressx tombstoned + offset_pair) -- dead, removed by GC. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_ranges [DW_FORM_rnglistx] +# NOGC: DW_AT_name [DW_FORM_string] ("dead_base_func") + +# After GC, dead_func and dead_base_func must not appear. The GC-NOT region +# spans from compile_unit to the next GC/CHECK positive match (mixed_func's +# name below), so no CHECK lines for mixed_func's tag can appear before this. +# GC-NOT: "dead_func" +# GC-NOT: "dead_base_func" + +# mixed_func (one tombstoned + one live range entry) -- survives GC. +# Use separate NOGC/GC checks so the shared CHECK lines don't truncate the +# GC-NOT region above. +# NOGC: DW_TAG_subprogram +# NOGC: DW_AT_ranges [DW_FORM_rnglistx] +# NOGC: DW_AT_name [DW_FORM_string] ("mixed_func") +# GC: DW_AT_name [DW_FORM_string] ("mixed_func") + +# live_func (DW_AT_low_pc, live) -- always present. +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# CHECK: DW_AT_name [DW_FORM_string] ("live_func") + +# After garbage collection there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-ref-forms.test b/tests/gc-ref-forms.test new file mode 100644 index 0000000..37bf47a --- /dev/null +++ b/tests/gc-ref-forms.test @@ -0,0 +1,82 @@ +# Test that GC correctly handles CU-relative reference forms other than ref4: +# DW_FORM_ref1, DW_FORM_ref2, and DW_FORM_ref_udata. Each form is used as a +# DW_AT_type reference from a live subprogram to a base_type. A dead subprogram +# placed before them forces all offsets to shift when GC removes it, exercising +# the patching logic for each form. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-ref-forms-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-ref-forms-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all subprograms and base_types survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# Verify that after GC, the patched ref values point to the correct base_types. +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefix=GCPATCH %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) + +# live_func (addrx 1) with DW_AT_type ref1 -> t_r1. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# CHECK-NEXT: DW_AT_type [DW_FORM_ref1] + +# The base_type "t_r1" must survive. +# CHECK: DW_TAG_base_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("t_r1") + +# live_func2 (addrx 2) with DW_AT_type ref2 -> t_r2. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000002) +# CHECK-NEXT: DW_AT_type [DW_FORM_ref2] + +# The base_type "t_r2" must survive. +# CHECK: DW_TAG_base_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("t_r2") + +# live_func3 (addrx 3) with DW_AT_type ref_udata -> t_ru. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000003) +# CHECK-NEXT: DW_AT_type [DW_FORM_ref_udata] + +# The base_type "t_ru" must survive. +# CHECK: DW_TAG_base_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("t_ru") + +# After garbage collection there must be no further subprograms. +# GC: NULL +# GC-NOT: DW_TAG_subprogram + +# Verify that after GC, each ref form points to the correct base_type. +# Capture the ref target from each live subprogram and verify the base_type is there. +# GCPATCH: .debug_info.dwo contents: + +# GCPATCH: DW_TAG_subprogram +# GCPATCH-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# GCPATCH-NEXT: DW_AT_type [DW_FORM_ref1] (cu + 0x{{[0-9a-f]+}} => {[[R1OFF:0x[0-9a-f]+]]} +# GCPATCH: [[R1OFF]]: DW_TAG_base_type +# GCPATCH-NEXT: DW_AT_name [DW_FORM_string] ("t_r1") + +# GCPATCH: DW_TAG_subprogram +# GCPATCH-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000002) +# GCPATCH-NEXT: DW_AT_type [DW_FORM_ref2] (cu + 0x{{[0-9a-f]+}} => {[[R2OFF:0x[0-9a-f]+]]} +# GCPATCH: [[R2OFF]]: DW_TAG_base_type +# GCPATCH-NEXT: DW_AT_name [DW_FORM_string] ("t_r2") + +# GCPATCH: DW_TAG_subprogram +# GCPATCH-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000003) +# GCPATCH-NEXT: DW_AT_type [DW_FORM_ref_udata] (cu + 0x{{[0-9a-f]+}} => {[[RUOFF:0x[0-9a-f]+]]} +# GCPATCH: [[RUOFF]]: DW_TAG_base_type +# GCPATCH-NEXT: DW_AT_name [DW_FORM_string] ("t_ru") diff --git a/tests/gc-retained-edge.test b/tests/gc-retained-edge.test new file mode 100644 index 0000000..9f741f5 --- /dev/null +++ b/tests/gc-retained-edge.test @@ -0,0 +1,40 @@ +# Test that a Retained DIE's reference-form attribute (DW_AT_type on a namespace) +# keeps its target alive even when that target is not reachable from any Live DIE +# directly, and that the ref4 is patched to the correct new offset after GC. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-retained-edge-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-retained-edge-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: everything survives when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The namespace must be retained (ancestor of the live subprogram). +# Its DW_AT_type ref4 must point to the base_type. +# CHECK: DW_TAG_namespace +# CHECK-NEXT: DW_AT_type [DW_FORM_ref4] (cu + 0x{{[0-9a-f]+}} => {[[BTOFF:0x[0-9a-f]+]]} + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) + +# The live subprogram (addrx 1) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) + +# The base_type must survive at the offset referenced by namespace's DW_AT_type. +# CHECK: [[BTOFF]]: DW_TAG_base_type + +# After garbage collection there must be no second subprogram. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-retained-namespace.test b/tests/gc-retained-namespace.test new file mode 100644 index 0000000..407ea7a --- /dev/null +++ b/tests/gc-retained-namespace.test @@ -0,0 +1,34 @@ +# Test that GC retains a namespace as ancestor of a live child, but removes the +# dead subprogram inside it, keeping only the live one. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-retained-namespace-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-retained-namespace-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: both subprograms survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The namespace must always be present (retained as ancestor of live child). +# CHECK: DW_TAG_namespace + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) + +# The live subprogram (addrx 1) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) + +# After garbage collection there must be no second subprogram. +# GC: NULL +# GC-NOT: DW_TAG_subprogram diff --git a/tests/gc-sibling.test b/tests/gc-sibling.test new file mode 100644 index 0000000..60c1da2 --- /dev/null +++ b/tests/gc-sibling.test @@ -0,0 +1,100 @@ +# Tests for DW_AT_sibling handling during garbage collection. +# 1. DW_AT_sibling pointing to a DIE does not keep that DIE alive. +# 2. DW_AT_sibling is adjusted correctly when the target DIE is removed +# (live_func's sibling pointed to dead_func; after removal it must point to survivor). +# 3. DW_AT_sibling is not adjusted to point across a parent boundary +# (kept_method's sibling pointed to removed_method inside MyStruct; after removal +# the sibling must become null, not point to the Neighbor structure_type). + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-sibling-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-sibling-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all subprograms survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprograms are removed and siblings patched. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# Verify sibling patching in the GC output (needs its own pass to capture offsets). +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefix=GCSIBLING %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# live_func (addrx 0, live) must always be present with its sibling attribute. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) +# CHECK-NEXT: DW_AT_sibling [DW_FORM_ref4] +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("live_func") + +# The base_type "int" child of live_func must always be present. +# CHECK: DW_TAG_base_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("int") + +# dead_func (addrx 1, tombstoned) is present only in the baseline (case 1: +# live_func's DW_AT_sibling pointing to dead_func does not keep it alive). +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# NOGC-NEXT: DW_AT_sibling +# NOGC-NEXT: DW_AT_name [DW_FORM_string] ("dead_func") + +# survivor (addrx 2, live) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000002) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("survivor") + +# MyStruct must always be present (retained as ancestor of kept_method). +# CHECK: DW_TAG_structure_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("MyStruct") + +# kept_method (addrx 3, live) must always be present with its sibling attribute. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000003) +# CHECK-NEXT: DW_AT_sibling [DW_FORM_ref4] +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("kept_method") + +# The base_type "char" child of kept_method must always be present. +# CHECK: DW_TAG_base_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("char") + +# removed_method (addrx 4, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000004) +# NOGC-NEXT: DW_AT_name [DW_FORM_string] ("removed_method") + +# After removal, removed_method must be gone. +# GC-NOT: removed_method + +# Neighbor must always be present (retained as ancestor of neighbor_method). +# CHECK: DW_TAG_structure_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("Neighbor") + +# neighbor_method (addrx 5, live) must always be present. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000005) +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("neighbor_method") + +# --- Sibling patching verification (separate pass over GC output) --- + +# Verify live_func's sibling offset points to survivor after removal (case 2). +# GCSIBLING: .debug_info.dwo contents: +# GCSIBLING: DW_TAG_subprogram +# GCSIBLING-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) +# GCSIBLING-NEXT: DW_AT_sibling [DW_FORM_ref4] (cu + 0x{{[0-9a-f]+}} => {[[SIBOFF:0x[0-9a-f]+]]} +# GCSIBLING: [[SIBOFF]]: DW_TAG_subprogram +# GCSIBLING-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000002) +# GCSIBLING-NEXT: DW_AT_name [DW_FORM_string] ("survivor") + +# Verify kept_method's sibling points to the parent's null terminator +# after removal (case 3). It must NOT point to the Neighbor structure_type +# which is not a sibling. +# GCSIBLING: DW_TAG_subprogram +# GCSIBLING-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000003) +# GCSIBLING-NEXT: DW_AT_sibling [DW_FORM_ref4] (cu + 0x{{[0-9a-f]+}} => {[[NULLOFF:0x[0-9a-f]+]]} +# GCSIBLING-NEXT: DW_AT_name [DW_FORM_string] ("kept_method") +# GCSIBLING: [[NULLOFF]]: NULL diff --git a/tests/gc-type-unit-no-compact-v4.test b/tests/gc-type-unit-no-compact-v4.test new file mode 100644 index 0000000..acfa599 --- /dev/null +++ b/tests/gc-type-unit-no-compact-v4.test @@ -0,0 +1,34 @@ +# Test that the presence of a type unit in a separate .debug_types.dwo section +# (DWARF4) disables compaction of the .debug_str_offsets.dwo table, since the +# type unit shares that section with the compilation unit. +# +# Dead DIEs should still be removed from .debug_info.dwo. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-type-unit-no-compact-v4-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-type-unit-no-compact-v4-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# With garbage collection the dead subprogram is removed but str_offsets are NOT compacted. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-str-offsets -debug-str %t-gc.dwp | FileCheck %s + +# -- debug_info checks: dead DIE is still removed -- +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit +# CHECK: DW_AT_name ("test.c") +# CHECK: DW_AT_comp_dir ("/home") +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("live_func") +# CHECK: NULL +# CHECK-NOT: DW_TAG_subprogram + +# -- str_offsets: table is NOT compacted (still 5 entries, 5*4 = 20 bytes, no header in DWARF4) -- +# CHECK: .debug_str_offsets.dwo contents: +# CHECK: Contribution size = 20 +# CHECK: "test.c" +# CHECK: "/home" +# CHECK: "live_func" +# CHECK: "dead_func" +# CHECK: "int" diff --git a/tests/gc-type-unit-no-compact.test b/tests/gc-type-unit-no-compact.test new file mode 100644 index 0000000..4df7006 --- /dev/null +++ b/tests/gc-type-unit-no-compact.test @@ -0,0 +1,65 @@ +# Test that the presence of a type unit in .debug_info.dwo disables +# compaction of .debug_rnglists.dwo, .debug_loclists.dwo, and +# .debug_str_offsets.dwo offset tables, since the type unit shares +# those sections with the compilation unit. +# +# Dead DIEs should still be removed from .debug_info.dwo. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-type-unit-no-compact-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-type-unit-no-compact-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# With garbage collection the dead subprogram is removed but offset tables are NOT compacted. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -debug-info -debug-str-offsets -debug-str -debug-rnglists -debug-loclists %t-gc.dwp | FileCheck %s + +# Verify the shared sections are not duplicated (emitted once, not once per unit). +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefix=INDEX %s + +# -- debug_info checks: dead DIE is still removed -- +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit +# CHECK: DW_AT_name ("test.c") +# CHECK: DW_AT_comp_dir ("/home") +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("live_func") +# CHECK: NULL +# CHECK-NOT: DW_TAG_subprogram + +# -- loclists: offset table is NOT compacted (still 2 entries) -- +# CHECK: .debug_loclists.dwo contents: +# CHECK: offset_entry_count = 0x00000002 +# CHECK: offsets: [ +# CHECK: 0x{{[0-9a-f]+}} +# CHECK-NEXT: 0x{{[0-9a-f]+}} +# CHECK-NEXT: ] + +# -- rnglists: offset table is NOT compacted (still 2 entries) -- +# CHECK: .debug_rnglists.dwo contents: +# CHECK: offset_entry_count = 0x00000002 +# CHECK: offsets: [ +# CHECK: 0x{{[0-9a-f]+}} +# CHECK-NEXT: 0x{{[0-9a-f]+}} +# CHECK-NEXT: ] + +# -- str_offsets: table is NOT compacted (still 5 entries, contribution size = 4 header + 5*4 = 24) -- +# CHECK: .debug_str_offsets.dwo contents: +# CHECK: Contribution size = 24 +# CHECK: "test.c" +# CHECK: "/home" +# CHECK: "live_func" +# CHECK: "dead_func" +# CHECK: "int" + +# -- CU and TU index entries should share the same section contributions -- +# -- (sections emitted once, not duplicated per unit) -- +# INDEX: .debug_cu_index contents: +# INDEX: LOCLISTS +# INDEX-NEXT: ----- +# INDEX-NEXT: {{.*}} [0x00000000, 0x0000001c) [0x00000000, 0x0000001c) [0x00000000, 0x0000001c) +# INDEX: .debug_tu_index contents: +# INDEX: LOCLISTS +# INDEX-NEXT: ----- +# INDEX-NEXT: {{.*}} [0x00000000, 0x0000001c) [0x00000000, 0x0000001c) [0x00000000, 0x0000001c) diff --git a/tests/gc-typed-expr.test b/tests/gc-typed-expr.test new file mode 100644 index 0000000..03c7d50 --- /dev/null +++ b/tests/gc-typed-expr.test @@ -0,0 +1,53 @@ +# Test that DW_OP_convert and DW_OP_const_type ULEB128 CU-relative references +# to base_types are traced as liveness edges and patched correctly after GC +# removes a dead subprogram and shifts offsets. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-typed-expr-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-typed-expr-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: all DIEs survive when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram is removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) + +# The dead base_type "dt" (only reachable from dead_func) is present only in the baseline. +# NOGC: DW_TAG_base_type +# NOGC-NEXT: DW_AT_name [DW_FORM_string] ("dt") + +# conv_func (addrx 1, live) must always be present with its variable child. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) +# CHECK: DW_TAG_variable +# CHECK-NEXT: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_convert (0x{{[0-9a-f]+}} -> 0x{{[0-9a-f]+}}) "it", DW_OP_stack_value) + +# The base_type "it" (conv_type) must survive, kept alive by DW_OP_convert. +# CHECK: DW_TAG_base_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("it") + +# ct_func (addrx 2, live) must always be present with its variable child. +# CHECK: DW_TAG_subprogram +# CHECK-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000002) +# CHECK: DW_TAG_variable +# CHECK-NEXT: DW_AT_location [DW_FORM_exprloc] ({{.*}}a4 + +# The base_type "ft" (ct_type) must survive, kept alive by DW_OP_const_type. +# CHECK: DW_TAG_base_type +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("ft") + +# After GC there must be no extra subprograms and "dt" must be gone. +# GC: NULL +# GC-NOT: DW_TAG_subprogram +# GC-NOT: "dt" diff --git a/tests/gc-unreferenced-dwo.test b/tests/gc-unreferenced-dwo.test new file mode 100644 index 0000000..04bd1f6 --- /dev/null +++ b/tests/gc-unreferenced-dwo.test @@ -0,0 +1,49 @@ +# Test that a .dwo passed as a positional input with --gc whose DWO ID +# does not match any executable still has its sections (str_offsets, etc.) +# preserved in the output .dwp. +# +# Before the emit_gc_sections fix, maybe_gc returned early for +# unreferenced dwo_ids, silently dropping saved sections (str_offsets, +# rnglists, loclists) even though the debug_info was still appended. + +# Assemble the matching .dwo (DWO ID 0xaa, referenced by the executable). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-unreferenced-dwo-matching-dwo.s -o %t-matching.dwo + +# Assemble the unreferenced .dwo (DWO ID 0xbb, NOT referenced by any executable). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-unreferenced-dwo-dwo.s -o %t-unreferenced.dwo + +# Generate the exec assembly with the matching .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t-matching.dwo|' %p/inputs/gc-unreferenced-dwo-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Run thorin with --gc, passing the unreferenced .dwo as a positional input. +# RUN: thorin --gc -e %t.o %t-unreferenced.dwo -o %t.dwp + +# Verify both CUs are present in the output with their sections. +# RUN: llvm-dwarfdump -debug-info -debug-str-offsets -debug-str %t.dwp | FileCheck %s + +# Positional inputs are processed first, so unreferenced CU (0xbb) comes first. +# CHECK: .debug_info.dwo contents: + +# CHECK: DW_TAG_compile_unit +# CHECK: DW_AT_name ("other.c") +# CHECK: DW_TAG_subprogram +# CHECK: DW_AT_name ("some_func") + +# The matching CU (0xaa) follows. +# CHECK: DW_TAG_compile_unit +# CHECK: DW_AT_name ("main.c") + +# Both CUs' strings must be in .debug_str.dwo. +# CHECK: .debug_str.dwo contents: +# CHECK-DAG: "other.c" +# CHECK-DAG: "some_func" +# CHECK-DAG: "main.c" + +# Both CUs' str_offsets contributions must be present. +# CHECK: .debug_str_offsets.dwo contents: +# CHECK: Contribution size = 16 +# CHECK: "other.c" +# CHECK: "/src" +# CHECK: "some_func" +# CHECK: Contribution size = 8 +# CHECK: "main.c" diff --git a/tests/gc-variable-root.test b/tests/gc-variable-root.test new file mode 100644 index 0000000..5cca22b --- /dev/null +++ b/tests/gc-variable-root.test @@ -0,0 +1,50 @@ +# Test that GC correctly handles DW_TAG_variable DIEs as roots based on their +# DW_AT_location exprloc containing DW_OP_addrx: a variable whose address +# resolves to a non-tombstoned value is live, while one whose address is +# tombstoned is dead. The live variable's DW_AT_type ref4 keeps the +# base_type alive, and the ref4 is patched correctly after GC. + +# Assemble the .dwo (split debug info). +# RUN: llvm-mc --triple=x86_64-unknown-linux --filetype=obj %p/inputs/gc-variable-root-dwo.s -o %t.dwo + +# Generate the exec assembly with the .dwo path substituted in, then assemble it. +# RUN: sed 's|DWO_PATH|%t.dwo|' %p/inputs/gc-variable-root-exec.s | llvm-mc --triple=x86_64-unknown-linux --filetype=obj -o %t.o + +# Baseline: everything survives when not garbage-collecting. +# RUN: thorin -e %t.o -o %t-nogc.dwp +# RUN: llvm-dwarfdump -v %t-nogc.dwp | FileCheck --check-prefixes=CHECK,NOGC %s + +# With garbage collection the dead subprogram and dead variable are removed. +# RUN: thorin --gc -e %t.o -o %t-gc.dwp +# RUN: llvm-dwarfdump -v %t-gc.dwp | FileCheck --check-prefixes=CHECK,GC %s + +# CHECK: .debug_info.dwo contents: +# CHECK: DW_TAG_compile_unit + +# The dead subprogram (addrx 0, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_subprogram +# NOGC-NEXT: DW_AT_low_pc [DW_FORM_addrx] (indexed (00000000) + +# After GC, the dead subprogram must not appear before the live variable. +# GC-NOT: DW_TAG_subprogram + +# The live variable (DW_OP_addrx 1, non-tombstoned) must always be present. +# CHECK: DW_TAG_variable +# CHECK-NEXT: DW_AT_name [DW_FORM_string] ("live_var") +# CHECK-NEXT: DW_AT_location [DW_FORM_exprloc] +# CHECK-NEXT: DW_AT_type [DW_FORM_ref4] (cu + 0x{{[0-9a-f]+}} => {[[BTOFF:0x[0-9a-f]+]]} + +# The dead variable (DW_OP_addrx 2, tombstoned) is present only in the baseline. +# NOGC: DW_TAG_variable +# NOGC-NEXT: DW_AT_name [DW_FORM_string] ("dead_var") + +# After GC, the dead variable must not appear before the base_type. +# GC-NOT: "dead_var" + +# The base_type must survive at the offset referenced by live_var's DW_AT_type. +# CHECK: [[BTOFF]]: DW_TAG_base_type + +# After garbage collection there must be no extra subprograms or variables. +# GC: NULL +# GC-NOT: DW_TAG_subprogram +# GC-NOT: DW_TAG_variable diff --git a/tests/info-v5.s b/tests/info-v5.s index fade3fa..31a3fd4 100644 --- a/tests/info-v5.s +++ b/tests/info-v5.s @@ -8,8 +8,8 @@ # CHECK-DAG: .debug_cu_index contents: # CHECK: version = 5, units = 1, slots = 2 -# CHECK: Index Signature INFO ABBREV -# CHECK: 1 [[DWOID]] [0x00000000, 0x00000054) [0x00000000, 0x0000002a) +# CHECK: Index Signature INFO ABBREV +# CHECK: 1 [[DWOID]] [0x0000000000000000, 0x0000000000000054) [0x00000000, 0x0000002a) .section .debug_info.dwo,"e",@progbits .long .Ldebug_info_dwo_end0-.Ldebug_info_dwo_start0 # Length of Unit diff --git a/tests/inputs/gc-all-dead-dwo.s b/tests/inputs/gc-all-dead-dwo.s new file mode 100644 index 0000000..393d1d8 --- /dev/null +++ b/tests/inputs/gc-all-dead-dwo.s @@ -0,0 +1,53 @@ +# .dwo file for gc-all-dead.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram: DW_AT_low_pc = addrx 0 (will be tombstoned -> dead) +# subprogram: DW_AT_low_pc = addrx 1 (will be tombstoned -> dead) +# +# Every subprogram is dead, so GC removes all children. The compile_unit +# itself must survive because the skeleton CU in the executable references it. +# +# The DWO ID is 0xdeadbeef. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeadbeef # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # Abbrev 2: DW_TAG_subprogram (dead - addrx 0 is tombstoned) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + + # Abbrev 2: DW_TAG_subprogram (dead - addrx 1 is also tombstoned) + .byte 2 + .byte 1 # DW_AT_low_pc: addrx index 1 + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-all-dead-exec.s b/tests/inputs/gc-all-dead-exec.s new file mode 100644 index 0000000..a810e68 --- /dev/null +++ b/tests/inputs/gc-all-dead-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-all-dead.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xdeadbeef, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses, both tombstoned: +# index 0: 0xffffffffffffffff (tombstone) +# index 1: 0xffffffffffffffff (tombstone) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeadbeef # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone + .quad 0xffffffffffffffff # Index 1: tombstone +.Laddr_end: diff --git a/tests/inputs/gc-compact-gnu-str-index-dwo.s b/tests/inputs/gc-compact-gnu-str-index-dwo.s new file mode 100644 index 0000000..0732d61 --- /dev/null +++ b/tests/inputs/gc-compact-gnu-str-index-dwo.s @@ -0,0 +1,98 @@ +# .dwo file for gc-compact-gnu-str-index.test +# +# DWARF4 split-compile unit (GNU extension) containing: +# compile_unit (children, DW_AT_name = GNU_str_index 0 -> "test.c", +# DW_AT_comp_dir = GNU_str_index 1 -> "/home", +# DW_AT_GNU_dwo_id = 0xee) +# subprogram "dead_func" (GNU_addr_index 0, tombstoned): +# DW_AT_name = GNU_str_index 2 -> "dead_func" +# subprogram "live_func" (GNU_addr_index 1, live): +# DW_AT_name = GNU_str_index 3 -> "live_func" +# +# .debug_str_offsets.dwo has 4 entries (indices 0-3), NO header (DWARF4). +# .debug_str.dwo has 4 strings: "test.c", "/home", "dead_func", "live_func". +# +# After GC, dead_func is removed. Its string "dead_func" at str index 2 +# should be removed from the str_offsets table, compacting it from 4 to 3 +# entries. Crucially, "live_func" moves from index 3 to index 2, so the +# DW_FORM_GNU_str_index attribute for the surviving subprogram must be +# remapped. Without the fix, the old index 3 is copied verbatim and points +# to garbage. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + # DW_AT_name: GNU_str_index, ULEB128 index 0 -> "test.c" + .byte 0 + # DW_AT_comp_dir: GNU_str_index, ULEB128 index 1 -> "/home" + .byte 1 + # DW_AT_GNU_dwo_id: data8 = 0xee + .quad 0xee + + # Abbrev 2: DW_TAG_subprogram "dead_func" (GNU_addr_index 0, tombstoned) + .byte 2 + .byte 0 # DW_AT_low_pc: GNU_addr_index index 0 + # DW_AT_name: GNU_str_index, ULEB128 index 2 -> "dead_func" + .byte 2 + + # Abbrev 2: DW_TAG_subprogram "live_func" (GNU_addr_index 1, live) + .byte 2 + .byte 1 # DW_AT_low_pc: GNU_addr_index index 1 + # DW_AT_name: GNU_str_index, ULEB128 index 3 -> "live_func" + .byte 3 + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_str.dwo,"eMS",@progbits,1 +.Lstr0: + .asciz "test.c" +.Lstr1: + .asciz "/home" +.Lstr2: + .asciz "dead_func" +.Lstr3: + .asciz "live_func" + + .section .debug_str_offsets.dwo,"e",@progbits + # DWARF4: no header, just raw offset entries (4 bytes each, DWARF32) + .long .Lstr0-.debug_str.dwo # Index 0: "test.c" + .long .Lstr1-.debug_str.dwo # Index 1: "/home" + .long .Lstr2-.debug_str.dwo # Index 2: "dead_func" + .long .Lstr3-.debug_str.dwo # Index 3: "live_func" + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, + # DW_AT_name(GNU_str_index) + DW_AT_comp_dir(GNU_str_index) + + # DW_AT_GNU_dwo_id(data8) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .uleb128 3 # DW_AT_name + .uleb128 0x1f02 # DW_FORM_GNU_str_index + .uleb128 0x1b # DW_AT_comp_dir + .uleb128 0x1f02 # DW_FORM_GNU_str_index + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, + # DW_AT_low_pc(GNU_addr_index) + DW_AT_name(GNU_str_index) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .uleb128 17 # DW_AT_low_pc + .uleb128 0x1f01 # DW_FORM_GNU_addr_index + .uleb128 3 # DW_AT_name + .uleb128 0x1f02 # DW_FORM_GNU_str_index + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-compact-gnu-str-index-exec.s b/tests/inputs/gc-compact-gnu-str-index-exec.s new file mode 100644 index 0000000..0f03989 --- /dev/null +++ b/tests/inputs/gc-compact-gnu-str-index-exec.s @@ -0,0 +1,45 @@ +# Fake executable for gc-compact-gnu-str-index.test +# +# DWARF4 skeleton CU (GNU extension) containing: +# .debug_info: DW_TAG_compile_unit with DW_AT_GNU_dwo_id = 0xee, +# DW_AT_GNU_addr_base, and DW_AT_GNU_dwo_name +# .debug_addr: Two 8-byte addresses (no header, DWARF4 style): +# index 0: 0xffffffffffffffff (tombstone, dead_func) +# index 1: 0x1000 (live, live_func) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + + # [0x0b] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .quad 0xee # DW_AT_GNU_dwo_id + .long .Laddr_table_base # DW_AT_GNU_addr_base + .asciz "DWO_PATH" # DW_AT_GNU_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .uleb128 0x2133 # DW_AT_GNU_addr_base + .uleb128 23 # DW_FORM_sec_offset + .uleb128 0x2130 # DW_AT_GNU_dwo_name + .uleb128 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF4: no header, just raw address entries. + # DW_AT_GNU_addr_base points directly to the start of entries. +.Laddr_table_base: + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func) + .quad 0x1000 # Index 1: live address (live_func) diff --git a/tests/inputs/gc-compact-loclists-dwo.s b/tests/inputs/gc-compact-loclists-dwo.s new file mode 100644 index 0000000..b9a32a2 --- /dev/null +++ b/tests/inputs/gc-compact-loclists-dwo.s @@ -0,0 +1,131 @@ +# .dwo file for gc-compact-loclists.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children, DW_AT_loclists_base) +# subprogram "dead_func1" (addrx 0, tombstoned, has children) +# variable "w": DW_AT_location = loclistx 0 (dead) +# subprogram "live_func" (addrx 1, live, has children) +# variable "v": DW_AT_location = loclistx 1 (live) +# subprogram "dead_func2" (addrx 2, tombstoned, has children) +# variable "x": DW_AT_location = loclistx 2 (dead) +# +# .debug_loclists.dwo has 3 location lists in the offset table. +# After GC, dead_func1 and dead_func2 are removed, so only loclistx 1 +# survives. The offset table should be compacted from 3 entries to 1, +# and the surviving variable's loclistx must be renumbered from 1 to 0. +# +# DWO ID is 0xbb. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbb # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children, DW_AT_loclists_base) + .byte 1 + .long .Lloclists_table_base-.Lloclists_section_start # DW_AT_loclists_base + + # Abbrev 2: DW_TAG_subprogram "dead_func1" (addrx 0, tombstoned, has children) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + .asciz "dead_func1" # DW_AT_name + + # Abbrev 3: DW_TAG_variable "w" (DW_AT_location = loclistx 0) + .byte 3 + .byte 0 # DW_AT_location: loclistx index 0 + + .byte 0 # End of dead_func1 children + + # Abbrev 2: DW_TAG_subprogram "live_func" (addrx 1, live, has children) + .byte 2 + .byte 1 # DW_AT_low_pc: addrx index 1 + .asciz "live_func" # DW_AT_name + + # Abbrev 3: DW_TAG_variable "v" (DW_AT_location = loclistx 1) + .byte 3 + .byte 1 # DW_AT_location: loclistx index 1 + + .byte 0 # End of live_func children + + # Abbrev 2: DW_TAG_subprogram "dead_func2" (addrx 2, tombstoned, has children) + .byte 2 + .byte 2 # DW_AT_low_pc: addrx index 2 + .asciz "dead_func2" # DW_AT_name + + # Abbrev 3: DW_TAG_variable "x" (DW_AT_location = loclistx 2) + .byte 3 + .byte 2 # DW_AT_location: loclistx index 2 + + .byte 0 # End of dead_func2 children + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_loclists.dwo,"e",@progbits +.Lloclists_section_start: + .long .Lloclists_end-.Lloclists_start # Unit length +.Lloclists_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size + .long 3 # Offset entry count +.Lloclists_table_base: + # Offset table (3 entries, DWARF32 -> 4 bytes each) + .long .Lloclist0-.Lloclists_table_base # Offset of location list 0 + .long .Lloclist1-.Lloclists_table_base # Offset of location list 1 + .long .Lloclist2-.Lloclists_table_base # Offset of location list 2 +.Lloclist0: + # Location list 0 (w in dead_func1): DW_LLE_default_location with DW_OP_reg0 + .byte 0x05 # DW_LLE_default_location + .byte 1 # Expression length (ULEB128): 1 byte + .byte 0x50 # DW_OP_reg0 + .byte 0x00 # DW_LLE_end_of_list +.Lloclist1: + # Location list 1 (v in live_func): DW_LLE_default_location with DW_OP_reg1 + .byte 0x05 # DW_LLE_default_location + .byte 1 # Expression length (ULEB128): 1 byte + .byte 0x51 # DW_OP_reg1 + .byte 0x00 # DW_LLE_end_of_list +.Lloclist2: + # Location list 2 (x in dead_func2): DW_LLE_default_location with DW_OP_reg2 + .byte 0x05 # DW_LLE_default_location + .byte 1 # Expression length (ULEB128): 1 byte + .byte 0x52 # DW_OP_reg2 + .byte 0x00 # DW_LLE_end_of_list +.Lloclists_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, DW_AT_loclists_base(sec_offset) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0x8c, 0x01 # DW_AT_loclists_base (ULEB128: 140) + .byte 23 # DW_FORM_sec_offset + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, has children, DW_AT_low_pc(addrx) + DW_AT_name(string) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_variable, no children, DW_AT_location(loclistx) + .byte 3 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 0x22 # DW_FORM_loclistx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-compact-loclists-exec.s b/tests/inputs/gc-compact-loclists-exec.s new file mode 100644 index 0000000..798f68d --- /dev/null +++ b/tests/inputs/gc-compact-loclists-exec.s @@ -0,0 +1,52 @@ +# Fake executable for gc-compact-loclists.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xbb, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Three 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone, dead_func1) +# index 1: 0x1000 (live, live_func) +# index 2: 0xffffffffffffffff (tombstone, dead_func2) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbb # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func1) + .quad 0x1000 # Index 1: live address (live_func) + .quad 0xffffffffffffffff # Index 2: tombstone (dead_func2) +.Laddr_end: diff --git a/tests/inputs/gc-compact-rnglists-dwo.s b/tests/inputs/gc-compact-rnglists-dwo.s new file mode 100644 index 0000000..e8756f7 --- /dev/null +++ b/tests/inputs/gc-compact-rnglists-dwo.s @@ -0,0 +1,101 @@ +# .dwo file for gc-compact-rnglists.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children, DW_AT_rnglists_base) +# subprogram "dead_func1": DW_AT_ranges = rnglistx 0 (tombstoned) +# subprogram "live_func": DW_AT_ranges = rnglistx 1 (live range) +# subprogram "dead_func2": DW_AT_ranges = rnglistx 2 (tombstoned) +# +# .debug_rnglists.dwo has 3 range lists in the offset table. +# After GC, dead_func1 and dead_func2 are removed, so only rnglistx 1 +# survives. The offset table should be compacted from 3 entries to 1, +# and the surviving subprogram's rnglistx must be renumbered from 1 to 0. +# +# DWO ID is 0xaa. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xaa # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children, DW_AT_rnglists_base) + .byte 1 + .long .Lrnglists_table_base-.Lrnglists_section_start # DW_AT_rnglists_base + + # Abbrev 2: DW_TAG_subprogram "dead_func1" (DW_AT_ranges = rnglistx 0) + .byte 2 + .byte 0 # DW_AT_ranges: rnglistx index 0 + .asciz "dead_func1" # DW_AT_name + + # Abbrev 2: DW_TAG_subprogram "live_func" (DW_AT_ranges = rnglistx 1) + .byte 2 + .byte 1 # DW_AT_ranges: rnglistx index 1 + .asciz "live_func" # DW_AT_name + + # Abbrev 2: DW_TAG_subprogram "dead_func2" (DW_AT_ranges = rnglistx 2) + .byte 2 + .byte 2 # DW_AT_ranges: rnglistx index 2 + .asciz "dead_func2" # DW_AT_name + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_rnglists.dwo,"e",@progbits +.Lrnglists_section_start: + .long .Lrnglists_end-.Lrnglists_start # Unit length +.Lrnglists_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size + .long 3 # Offset entry count +.Lrnglists_table_base: + # Offset table (3 entries, DWARF32 -> 4 bytes each) + .long .Lrangelist0-.Lrnglists_table_base # Offset of range list 0 + .long .Lrangelist1-.Lrnglists_table_base # Offset of range list 1 + .long .Lrangelist2-.Lrnglists_table_base # Offset of range list 2 +.Lrangelist0: + # Range list 0 (dead_func1): DW_RLE_startx_length addrx 0 (tombstoned), length 0x10 + .byte 0x03 # DW_RLE_startx_length + .byte 0 # Start address index (ULEB128): addrx 0 + .byte 0x10 # Length (ULEB128): 16 bytes + .byte 0x00 # DW_RLE_end_of_list +.Lrangelist1: + # Range list 1 (live_func): DW_RLE_startx_length addrx 1 (live), length 0x20 + .byte 0x03 # DW_RLE_startx_length + .byte 1 # Start address index (ULEB128): addrx 1 + .byte 0x20 # Length (ULEB128): 32 bytes + .byte 0x00 # DW_RLE_end_of_list +.Lrangelist2: + # Range list 2 (dead_func2): DW_RLE_startx_length addrx 2 (tombstoned), length 0x30 + .byte 0x03 # DW_RLE_startx_length + .byte 2 # Start address index (ULEB128): addrx 2 + .byte 0x30 # Length (ULEB128): 48 bytes + .byte 0x00 # DW_RLE_end_of_list +.Lrnglists_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, DW_AT_rnglists_base(sec_offset) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0x74 # DW_AT_rnglists_base + .byte 23 # DW_FORM_sec_offset + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_ranges(rnglistx) + DW_AT_name(string) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 0x55 # DW_AT_ranges + .byte 0x23 # DW_FORM_rnglistx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-compact-rnglists-exec.s b/tests/inputs/gc-compact-rnglists-exec.s new file mode 100644 index 0000000..909e8ff --- /dev/null +++ b/tests/inputs/gc-compact-rnglists-exec.s @@ -0,0 +1,52 @@ +# Fake executable for gc-compact-rnglists.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xaa, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Three 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone, dead_func1) +# index 1: 0x1000 (live, live_func range) +# index 2: 0xffffffffffffffff (tombstone, dead_func2) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xaa # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func1) + .quad 0x1000 # Index 1: live address (live_func) + .quad 0xffffffffffffffff # Index 2: tombstone (dead_func2) +.Laddr_end: diff --git a/tests/inputs/gc-compact-str-offsets-dwo.s b/tests/inputs/gc-compact-str-offsets-dwo.s new file mode 100644 index 0000000..f1f750d --- /dev/null +++ b/tests/inputs/gc-compact-str-offsets-dwo.s @@ -0,0 +1,95 @@ +# .dwo file for gc-compact-str-offsets.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children, DW_AT_name = strx1 0 -> "test.c", +# DW_AT_comp_dir = strx1 1 -> "/home") +# subprogram "dead_func" (addrx 0, tombstoned): DW_AT_name = strx3 2 -> "dead_func" +# subprogram "live_func" (addrx 1, live): DW_AT_name = strx3 3 -> "live_func" +# +# .debug_str_offsets.dwo has 4 entries (indices 0-3). +# .debug_str.dwo has 4 strings: "test.c", "/home", "dead_func", "live_func". +# +# After GC, dead_func is removed. Its string "dead_func" at strx index 2 +# is removed from the str_offsets table, compacting it from 4 to 3 entries. +# Crucially, "live_func" moves from index 3 to index 2, so the +# DW_FORM_strx3 attribute for the surviving subprogram must be remapped. +# Without the fix, the old index 3 is copied verbatim and points to garbage. +# +# DWO ID is 0xee. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xee # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + .byte 0 # DW_AT_name: strx1 index 0 -> "test.c" + .byte 1 # DW_AT_comp_dir: strx1 index 1 -> "/home" + + # Abbrev 2: DW_TAG_subprogram "dead_func" (addrx 0, tombstoned) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + .byte 2, 0, 0 # DW_AT_name: strx3 index 2 -> "dead_func" + + # Abbrev 2: DW_TAG_subprogram "live_func" (addrx 1, live) + .byte 2 + .byte 1 # DW_AT_low_pc: addrx index 1 + .byte 3, 0, 0 # DW_AT_name: strx3 index 3 -> "live_func" + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_str.dwo,"eMS",@progbits,1 +.Lstr0: + .asciz "test.c" +.Lstr1: + .asciz "/home" +.Lstr2: + .asciz "dead_func" +.Lstr3: + .asciz "live_func" + + .section .debug_str_offsets.dwo,"e",@progbits + # DWARF5 str_offsets header + .long .Lstr_offsets_end-.Lstr_offsets_start # Unit length +.Lstr_offsets_start: + .short 5 # Version + .short 0 # Padding + # Offset table entries (4 entries, DWARF32 -> 4 bytes each) + .long .Lstr0-.debug_str.dwo # Index 0: "test.c" + .long .Lstr1-.debug_str.dwo # Index 1: "/home" + .long .Lstr2-.debug_str.dwo # Index 2: "dead_func" + .long .Lstr3-.debug_str.dwo # Index 3: "live_func" +.Lstr_offsets_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, + # DW_AT_name(strx1) + DW_AT_comp_dir(strx1) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 3 # DW_AT_name + .byte 0x25 # DW_FORM_strx1 + .byte 0x1b # DW_AT_comp_dir + .byte 0x25 # DW_FORM_strx1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, + # DW_AT_low_pc(addrx) + DW_AT_name(strx3) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 3 # DW_AT_name + .byte 0x27 # DW_FORM_strx3 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-compact-str-offsets-exec.s b/tests/inputs/gc-compact-str-offsets-exec.s new file mode 100644 index 0000000..b440a3d --- /dev/null +++ b/tests/inputs/gc-compact-str-offsets-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-compact-str-offsets.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xee, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone, dead_func) +# index 1: 0x1000 (live, live_func) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xee # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func) + .quad 0x1000 # Index 1: live address (live_func) +.Laddr_end: diff --git a/tests/inputs/gc-convert-first-op-dwo.s b/tests/inputs/gc-convert-first-op-dwo.s new file mode 100644 index 0000000..26eb0c6 --- /dev/null +++ b/tests/inputs/gc-convert-first-op-dwo.s @@ -0,0 +1,107 @@ +# .dwo file for gc-convert-first-op.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram "dead_func": DW_AT_low_pc = addrx 0 (tombstoned -> dead), +# no children (removed by GC, shifts later offsets) +# subprogram "live_func": DW_AT_low_pc = addrx 1 (live), has children +# variable "v": DW_AT_location = exprloc { DW_OP_convert } +# The expression is exactly 2 bytes: [0xa8, conv_type_offset]. +# 0xa8 (DW_OP_convert) is the FIRST byte of the expression, +# which is what distinguishes this test from gc-typed-expr. +# null +# base_type "conv_type": DW_AT_name="ct", byte_size=4, encoding=DW_ATE_float +# (kept alive by DW_OP_convert) +# null +# +# DWO ID is 0x42. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x42 # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # Abbrev 2: DW_TAG_subprogram (dead_func - addrx 0 is tombstoned, no children) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + + # Abbrev 3: DW_TAG_subprogram (live_func - addrx 1, live, has children) + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + + # Abbrev 4: DW_TAG_variable (v) + # DW_AT_location: exprloc { DW_OP_convert } + # The expression is exactly 2 bytes; byte 0 is the 0xa8 opcode. + .byte 4 + .byte 2 # exprloc length = 2 bytes + .byte 0xa8 # DW_OP_convert (byte 0 of expression) + .byte .Lconv_type-.Ldebug_info_dwo_start+4 # ULEB128 CU-relative offset of conv_type + + .byte 0 # End Of Children Mark (live_func) + + # Abbrev 5: DW_TAG_base_type (conv_type, kept alive by DW_OP_convert) +.Lconv_type: + .byte 5 + .asciz "ct" # DW_AT_name + .byte 4 # DW_AT_byte_size + .byte 4 # DW_AT_encoding: DW_ATE_float + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, has children, DW_AT_low_pc(addrx) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_variable, no children, DW_AT_location(exprloc) + .byte 4 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 24 # DW_FORM_exprloc + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 5: DW_TAG_base_type, no children, DW_AT_name(string) + DW_AT_byte_size(data1) + DW_AT_encoding(data1) + .byte 5 # Abbreviation Code + .byte 36 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 11 # DW_AT_byte_size + .byte 11 # DW_FORM_data1 + .byte 62 # DW_AT_encoding + .byte 11 # DW_FORM_data1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-convert-first-op-exec.s b/tests/inputs/gc-convert-first-op-exec.s new file mode 100644 index 0000000..af5c75a --- /dev/null +++ b/tests/inputs/gc-convert-first-op-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-convert-first-op.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0x42, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone for dead_func) +# index 1: 0x1000 (live for live_func) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x42 # DWO ID + + # Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func) + .quad 0x1000 # Index 1: live address (live_func) +.Laddr_end: diff --git a/tests/inputs/gc-dead-subprogram-dwo.s b/tests/inputs/gc-dead-subprogram-dwo.s new file mode 100644 index 0000000..47ffae1 --- /dev/null +++ b/tests/inputs/gc-dead-subprogram-dwo.s @@ -0,0 +1,75 @@ +# .dwo file for gc-dead-subprogram.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram: DW_AT_low_pc = addrx4 0 (will be tombstoned -> dead) +# subprogram: DW_AT_low_pc = addrx4 1 (live), DW_AT_type = ref4 -> base_type +# base_type (kept alive by the live subprogram's DW_AT_type) +# +# The DWO ID is 0xdeadbeef. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeadbeef # DWO ID + # DIE tree begins at offset 0x14 (20 bytes into unit) + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # [0x15] Abbrev 2: DW_TAG_subprogram (dead - addrx4 0 is tombstoned) + .byte 2 + .long 0 # DW_AT_low_pc: addrx4 index 0 + + # [0x1a] Abbrev 3: DW_TAG_subprogram (live - addrx4 1 is real) + .byte 3 + .long 1 # DW_AT_low_pc: addrx4 index 1 + .long .Lbase_type-.Ldebug_info_dwo_start+4 # DW_AT_type: ref4 to base_type + + # base_type (referenced by live subprogram) +.Lbase_type: + .byte 4 # Abbrev 4: DW_TAG_base_type + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx4) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 0x2c # DW_FORM_addrx4 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx4) + DW_AT_type(ref4) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 0x2c # DW_FORM_addrx4 + .byte 73 # DW_AT_type + .byte 19 # DW_FORM_ref4 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_base_type, no children, no attrs + .byte 4 # Abbreviation Code + .byte 36 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-dead-subprogram-exec.s b/tests/inputs/gc-dead-subprogram-exec.s new file mode 100644 index 0000000..6e94d07 --- /dev/null +++ b/tests/inputs/gc-dead-subprogram-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-dead-subprogram.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xdeadbeef, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone) +# index 1: 0x1000 (live) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeadbeef # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone + .quad 0x1000 # Index 1: live address +.Laddr_end: diff --git a/tests/inputs/gc-deep-nesting-dwo.s b/tests/inputs/gc-deep-nesting-dwo.s new file mode 100644 index 0000000..bd21f5d --- /dev/null +++ b/tests/inputs/gc-deep-nesting-dwo.s @@ -0,0 +1,107 @@ +# .dwo file for gc-deep-nesting.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# structure_type "MyClass" (children, byte_size=1) +# subprogram "dead_method": DW_AT_low_pc = addrx2 0 (tombstoned -> dead) +# subprogram "live_method": DW_AT_low_pc = addrx2 1 (live) +# null (end structure_type) +# subprogram "outer": DW_AT_low_pc = addrx3 2 (tombstoned -> Retained), has children +# subprogram "dead_inner": DW_AT_low_pc = addrx2 3 (tombstoned -> dead) +# subprogram "live_inner": DW_AT_low_pc = addrx2 4 (live) +# null (end outer) +# null (end compile_unit) +# +# The DWO ID is 0xdeed. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeed # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # Abbrev 2: DW_TAG_structure_type "MyClass" (has children, byte_size=1) + .byte 2 + .asciz "MyClass" # DW_AT_name + .byte 1 # DW_AT_byte_size + + # Abbrev 3: DW_TAG_subprogram "dead_method" (dead - addrx2 0 is tombstoned) + .byte 3 + .short 0 # DW_AT_low_pc: addrx2 index 0 + .asciz "dead_method" # DW_AT_name + + # Abbrev 3: DW_TAG_subprogram "live_method" (live - addrx2 1 is real) + .byte 3 + .short 1 # DW_AT_low_pc: addrx2 index 1 + .asciz "live_method" # DW_AT_name + + .byte 0 # End Of Children Mark (structure_type) + + # Abbrev 4: DW_TAG_subprogram "outer" (has children, addrx3 2 is tombstoned -> Retained) + .byte 4 + .byte 2 # DW_AT_low_pc: addrx3 index 2 (3-byte little-endian) + .short 0 + .asciz "outer" # DW_AT_name + + # Abbrev 3: DW_TAG_subprogram "dead_inner" (dead - addrx2 3 is tombstoned) + .byte 3 + .short 3 # DW_AT_low_pc: addrx2 index 3 + .asciz "dead_inner" # DW_AT_name + + # Abbrev 3: DW_TAG_subprogram "live_inner" (live - addrx2 4 is real) + .byte 3 + .short 4 # DW_AT_low_pc: addrx2 index 4 + .asciz "live_inner" # DW_AT_name + + .byte 0 # End Of Children Mark (outer) + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_structure_type, has children, DW_AT_name(string) + DW_AT_byte_size(data1) + .byte 2 # Abbreviation Code + .byte 19 # DW_TAG_structure_type + .byte 1 # DW_CHILDREN_yes + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 11 # DW_AT_byte_size + .byte 11 # DW_FORM_data1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx2) + DW_AT_name(string) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 0x2a # DW_FORM_addrx2 + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_subprogram, has children, DW_AT_low_pc(addrx3) + DW_AT_name(string) + .byte 4 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .byte 0x2b # DW_FORM_addrx3 + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-deep-nesting-exec.s b/tests/inputs/gc-deep-nesting-exec.s new file mode 100644 index 0000000..6373bf9 --- /dev/null +++ b/tests/inputs/gc-deep-nesting-exec.s @@ -0,0 +1,56 @@ +# Fake executable for gc-deep-nesting.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xdeed, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Five 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone for dead_method) +# index 1: 0x1000 (live for live_method) +# index 2: 0xffffffffffffffff (tombstone for outer -> Retained) +# index 3: 0xffffffffffffffff (tombstone for dead_inner) +# index 4: 0x3000 (live for live_inner) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeed # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_method) + .quad 0x1000 # Index 1: live (live_method) + .quad 0xffffffffffffffff # Index 2: tombstone (outer -> Retained) + .quad 0xffffffffffffffff # Index 3: tombstone (dead_inner) + .quad 0x3000 # Index 4: live (live_inner) +.Laddr_end: diff --git a/tests/inputs/gc-duplicate-dwoid-dwo.s b/tests/inputs/gc-duplicate-dwoid-dwo.s new file mode 100644 index 0000000..2bef9cb --- /dev/null +++ b/tests/inputs/gc-duplicate-dwoid-dwo.s @@ -0,0 +1,78 @@ +# .dwo file for gc-duplicate-dwoid.test +# +# DWARF5 split-compile unit with DWO ID 0xdd containing two +# subprograms. Each uses a different addrx index for DW_AT_low_pc: +# subprogram "func_a": addrx4 index 0 +# subprogram "func_b": addrx4 index 1 +# +# Two separate executables reference this .dwo. Exec1 has index 0 live +# and index 1 tombstoned; exec2 has the reverse. Both subprograms +# should survive GC because each is live in at least one executable. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdd # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + .byte 0 # DW_AT_name: strx1 index 0 -> "test.c" + + # Abbrev 2: DW_TAG_subprogram "func_a" (addrx4 index 0) + .byte 2 + .long 0 # DW_AT_low_pc: addrx4 index 0 + .byte 1 # DW_AT_name: strx1 index 1 -> "func_a" + + # Abbrev 2: DW_TAG_subprogram "func_b" (addrx4 index 1) + .byte 2 + .long 1 # DW_AT_low_pc: addrx4 index 1 + .byte 2 # DW_AT_name: strx1 index 2 -> "func_b" + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_str.dwo,"eMS",@progbits,1 +.Lstr0: + .asciz "test.c" +.Lstr1: + .asciz "func_a" +.Lstr2: + .asciz "func_b" + + .section .debug_str_offsets.dwo,"e",@progbits + .long .Lstr_offsets_end-.Lstr_offsets_start # Unit length +.Lstr_offsets_start: + .short 5 # Version + .short 0 # Padding + .long .Lstr0-.debug_str.dwo # Index 0: "test.c" + .long .Lstr1-.debug_str.dwo # Index 1: "func_a" + .long .Lstr2-.debug_str.dwo # Index 2: "func_b" +.Lstr_offsets_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, DW_AT_name(strx1) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 3 # DW_AT_name + .byte 0x25 # DW_FORM_strx1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, + # DW_AT_low_pc(addrx4) + DW_AT_name(strx1) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 0x2c # DW_FORM_addrx4 + .byte 3 # DW_AT_name + .byte 0x25 # DW_FORM_strx1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-duplicate-dwoid-exec1.s b/tests/inputs/gc-duplicate-dwoid-exec1.s new file mode 100644 index 0000000..ec27d44 --- /dev/null +++ b/tests/inputs/gc-duplicate-dwoid-exec1.s @@ -0,0 +1,42 @@ +# Executable 1 for gc-duplicate-dwoid.test +# +# Skeleton CU with DWO ID 0xdd. Address table: +# index 0: 0x1000 (live — func_a is here) +# index 1: 0xffffffffffffffff (tombstone — func_b not in this binary) + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdd # DWO ID + + .byte 1 # Abbrev 1: DW_TAG_compile_unit + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + .quad 0x1000 # Index 0: live (func_a) + .quad 0xffffffffffffffff # Index 1: tombstone (func_b) +.Laddr_end: diff --git a/tests/inputs/gc-duplicate-dwoid-exec2.s b/tests/inputs/gc-duplicate-dwoid-exec2.s new file mode 100644 index 0000000..1741c64 --- /dev/null +++ b/tests/inputs/gc-duplicate-dwoid-exec2.s @@ -0,0 +1,42 @@ +# Executable 2 for gc-duplicate-dwoid.test +# +# Skeleton CU with DWO ID 0xdd. Address table: +# index 0: 0xffffffffffffffff (tombstone — func_a not in this binary) +# index 1: 0x2000 (live — func_b is here) + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdd # DWO ID + + .byte 1 # Abbrev 1: DW_TAG_compile_unit + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + .quad 0xffffffffffffffff # Index 0: tombstone (func_a) + .quad 0x2000 # Index 1: live (func_b) +.Laddr_end: diff --git a/tests/inputs/gc-entry-value-dwo.s b/tests/inputs/gc-entry-value-dwo.s new file mode 100644 index 0000000..b35c0bf --- /dev/null +++ b/tests/inputs/gc-entry-value-dwo.s @@ -0,0 +1,95 @@ +# .dwo file for gc-entry-value.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram (dead_func): DW_AT_low_pc = addrx 0 (tombstoned -> dead) +# subprogram (live_func): DW_AT_low_pc = addrx 1 (live), has children +# variable "v": DW_AT_location = exprloc { +# DW_OP_entry_value(5, DW_OP_call4 ), DW_OP_stack_value +# } +# subprogram (target): DW_AT_low_pc = addrx 2 (tombstoned, kept alive by +# DW_OP_call4 inside entry_value) +# +# DWO ID is 0xbbbb. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbbbb # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # [0x15] Abbrev 2: DW_TAG_subprogram (dead_func - addrx 0 is tombstoned) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + + # [0x17] Abbrev 3: DW_TAG_subprogram (live_func - addrx 1 is real, has children) + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + + # [0x19] Abbrev 4: DW_TAG_variable + # DW_AT_location = exprloc, total expression length = 8 bytes: + # DW_OP_entry_value (0xa3) 1 byte + # ULEB128 sub-expression length (5) 1 byte + # DW_OP_call4 (0x99) 1 byte + # 4-byte LE CU-relative offset 4 bytes + # DW_OP_stack_value (0x9f) 1 byte + .byte 4 + .byte 8 # exprloc length = 8 + .byte 0xa3 # DW_OP_entry_value + .byte 5 # sub-expression length = 5 + .byte 0x99 # DW_OP_call4 + .long .Ltarget-.Ldebug_info_dwo_start+4 # 4-byte CU-relative offset of target + .byte 0x9f # DW_OP_stack_value + + .byte 0 # End Of Children Mark (live_func) + + # [target] Abbrev 2: DW_TAG_subprogram (target, kept alive by DW_OP_call4) +.Ltarget: + .byte 2 + .byte 2 # DW_AT_low_pc: addrx index 2 + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, has children, DW_AT_low_pc(addrx) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_variable, no children, DW_AT_location(exprloc) + .byte 4 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 24 # DW_FORM_exprloc + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-entry-value-exec.s b/tests/inputs/gc-entry-value-exec.s new file mode 100644 index 0000000..62df6ff --- /dev/null +++ b/tests/inputs/gc-entry-value-exec.s @@ -0,0 +1,52 @@ +# Fake executable for gc-entry-value.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xbbbb, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Three 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone, dead_func) +# index 1: 0x1000 (live, live_func) +# index 2: 0xffffffffffffffff (tombstone, target - kept alive by ref) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbbbb # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func) + .quad 0x1000 # Index 1: live address (live_func) + .quad 0xffffffffffffffff # Index 2: tombstone (target, kept alive by ref) +.Laddr_end: diff --git a/tests/inputs/gc-expr-ops-dwo.s b/tests/inputs/gc-expr-ops-dwo.s new file mode 100644 index 0000000..b034046 --- /dev/null +++ b/tests/inputs/gc-expr-ops-dwo.s @@ -0,0 +1,111 @@ +# .dwo file for gc-expr-ops.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram "dead1": DW_AT_low_pc = addrx 0 (tombstoned -> dead, removed by GC) +# subprogram "caller": DW_AT_low_pc = addrx 1 (live), +# DW_AT_frame_base = exprloc { DW_OP_call2 } +# subprogram "callee": DW_AT_low_pc = addrx 2 (tombstoned, kept alive by DW_OP_call2) +# subprogram "paramfn": DW_AT_low_pc = addrx 3 (live), has children +# formal_parameter: DW_AT_location = exprloc { DW_OP_GNU_parameter_ref } +# subprogram "paramtgt": DW_AT_low_pc = addrx 4 (tombstoned, kept alive by DW_OP_GNU_parameter_ref) +# +# DWO ID is 0x66. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x66 # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # Abbrev 2: DW_TAG_subprogram "dead1" (addrx 0, tombstoned -> dead) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + + # Abbrev 3: DW_TAG_subprogram "caller" (addrx 1, live) + # DW_AT_frame_base = exprloc { DW_OP_call2 } + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + .byte 3 # exprloc length = 3 (DW_OP_call2 + 2-byte offset) + .byte 0x98 # DW_OP_call2 + .short .Lcallee-.Ldebug_info_dwo_start+4 # 2-byte CU-relative offset of callee + + # Abbrev 2: DW_TAG_subprogram "callee" (addrx 2, tombstoned, kept alive) +.Lcallee: + .byte 2 + .byte 2 # DW_AT_low_pc: addrx index 2 + + # Abbrev 4: DW_TAG_subprogram "paramfn" (addrx 3, live, has children) + .byte 4 + .byte 3 # DW_AT_low_pc: addrx index 3 + + # Abbrev 5: DW_TAG_formal_parameter + # DW_AT_location = exprloc { DW_OP_GNU_parameter_ref } + .byte 5 + .byte 5 # exprloc length = 5 (opcode + 4-byte offset) + .byte 0xfa # DW_OP_GNU_parameter_ref + .long .Lparamtgt-.Ldebug_info_dwo_start+4 # 4-byte CU-relative offset of paramtgt + + .byte 0 # End Of Children Mark (paramfn) + + # Abbrev 2: DW_TAG_subprogram "paramtgt" (addrx 4, tombstoned, kept alive) +.Lparamtgt: + .byte 2 + .byte 4 # DW_AT_low_pc: addrx index 4 + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_frame_base(exprloc) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 64 # DW_AT_frame_base + .byte 24 # DW_FORM_exprloc + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_subprogram, has children, DW_AT_low_pc(addrx) + .byte 4 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 5: DW_TAG_formal_parameter, no children, DW_AT_location(exprloc) + .byte 5 # Abbreviation Code + .byte 5 # DW_TAG_formal_parameter + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 24 # DW_FORM_exprloc + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-expr-ops-exec.s b/tests/inputs/gc-expr-ops-exec.s new file mode 100644 index 0000000..68c7c19 --- /dev/null +++ b/tests/inputs/gc-expr-ops-exec.s @@ -0,0 +1,56 @@ +# Fake executable for gc-expr-ops.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0x66, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Five 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone, dead1) +# index 1: 0x1000 (live, caller) +# index 2: 0xffffffffffffffff (tombstone, callee - kept alive by DW_OP_call2) +# index 3: 0x2000 (live, paramfn) +# index 4: 0xffffffffffffffff (tombstone, paramtgt - kept alive by DW_OP_GNU_parameter_ref) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x66 # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead1) + .quad 0x1000 # Index 1: live address (caller) + .quad 0xffffffffffffffff # Index 2: tombstone (callee) + .quad 0x2000 # Index 3: live address (paramfn) + .quad 0xffffffffffffffff # Index 4: tombstone (paramtgt) +.Laddr_end: diff --git a/tests/inputs/gc-exprloc-call-dwo.s b/tests/inputs/gc-exprloc-call-dwo.s new file mode 100644 index 0000000..b2fadea --- /dev/null +++ b/tests/inputs/gc-exprloc-call-dwo.s @@ -0,0 +1,81 @@ +# .dwo file for gc-exprloc-call.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram (caller): DW_AT_low_pc = addrx 0 (live), +# DW_AT_frame_base = exprloc { DW_OP_call4 } +# base_type (dead, never referenced — placed before callee so removing +# it shifts the callee's offset, exercising operand patching) +# subprogram (callee): DW_AT_low_pc = addrx 1 (tombstoned, not a root, +# kept alive by DW_OP_call4 reference) +# +# DWO ID is 0x55. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x55 # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # Abbrev 2: DW_TAG_subprogram (caller) + # DW_AT_low_pc: addrx index 0 (live) + # DW_AT_frame_base: exprloc { DW_OP_call4 } + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + .byte 5 # exprloc length = 5 + .byte 0x99 # DW_OP_call4 + .long .Lcallee-.Ldebug_info_dwo_start+4 # 4-byte CU-relative offset of callee + + # Abbrev 3: DW_TAG_base_type (dead, never referenced) + .byte 3 + + # Abbrev 4: DW_TAG_subprogram (callee, kept alive by DW_OP_call4) +.Lcallee: + .byte 4 + .byte 1 # DW_AT_low_pc: addrx index 1 + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_frame_base(exprloc) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 64 # DW_AT_frame_base + .byte 24 # DW_FORM_exprloc + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_base_type, no children, no attrs + .byte 3 # Abbreviation Code + .byte 36 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 4 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-exprloc-call-exec.s b/tests/inputs/gc-exprloc-call-exec.s new file mode 100644 index 0000000..2d975c0 --- /dev/null +++ b/tests/inputs/gc-exprloc-call-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-exprloc-call.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0x55, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0x1000 (live, the caller) +# index 1: 0xffffffffffffffff (tombstone, the callee) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x55 # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0x1000 # Index 0: live address (caller) + .quad 0xffffffffffffffff # Index 1: tombstone (callee) +.Laddr_end: diff --git a/tests/inputs/gc-indirect-ref-dwo.s b/tests/inputs/gc-indirect-ref-dwo.s new file mode 100644 index 0000000..c2c02ed --- /dev/null +++ b/tests/inputs/gc-indirect-ref-dwo.s @@ -0,0 +1,80 @@ +# .dwo file for gc-indirect-ref.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram: DW_AT_low_pc = addrx 0 (will be tombstoned -> dead) +# subprogram: DW_AT_low_pc = addrx 1 (live), +# DW_AT_type = DW_FORM_indirect { DW_FORM_ref4, } +# base_type (kept alive by the indirect ref from live subprogram) +# +# The DWO ID is 0xaaaa. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xaaaa # DWO ID + # DIE tree begins at offset 0x14 (20 bytes into unit) + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # [0x15] Abbrev 2: DW_TAG_subprogram (dead - addrx 0 is tombstoned) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + + # [0x17] Abbrev 3: DW_TAG_subprogram (live - addrx 1 is real) + # DW_AT_type uses DW_FORM_indirect in abbrev table + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + # DW_FORM_indirect value: first a ULEB128 of the real form (DW_FORM_ref4 = 0x13), + # then the 4-byte CU-relative offset. + .byte 0x13 # Inner form: DW_FORM_ref4 + .long .Lbase_type-.Ldebug_info_dwo_start+4 # DW_AT_type: ref4 to base_type + + # base_type (referenced by live subprogram via indirect ref) +.Lbase_type: + .byte 4 # Abbrev 4: DW_TAG_base_type + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_type(indirect) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 73 # DW_AT_type + .byte 22 # DW_FORM_indirect + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_base_type, no children, no attrs + .byte 4 # Abbreviation Code + .byte 36 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-indirect-ref-exec.s b/tests/inputs/gc-indirect-ref-exec.s new file mode 100644 index 0000000..dbf9d73 --- /dev/null +++ b/tests/inputs/gc-indirect-ref-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-indirect-ref.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xaaaa, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone) +# index 1: 0x1000 (live) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xaaaa # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone + .quad 0x1000 # Index 1: live address +.Laddr_end: diff --git a/tests/inputs/gc-loclist-dwo.s b/tests/inputs/gc-loclist-dwo.s new file mode 100644 index 0000000..ed87b1f --- /dev/null +++ b/tests/inputs/gc-loclist-dwo.s @@ -0,0 +1,120 @@ +# .dwo file for gc-loclist.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram "dead_func": DW_AT_low_pc = addrx 0 (tombstoned -> dead) +# subprogram "live_func": DW_AT_low_pc = addrx 1 (live), has children +# variable "v": DW_AT_location = loclistx 0 -> loclist in .debug_loclists.dwo +# subprogram "target": DW_AT_low_pc = addrx 2 (tombstoned, kept alive by +# DW_OP_call4 in the location list expression) +# +# The location list in .debug_loclists.dwo contains a DW_LLE_default_location +# entry whose expression is { DW_OP_call4 }. +# +# DWO ID is 0xcc. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xcc # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children, DW_AT_loclists_base) + .byte 1 + .long .Lloclists_table_base-.Lloclists_section_start # DW_AT_loclists_base + + # Abbrev 2: DW_TAG_subprogram "dead_func" (addrx 0, tombstoned -> dead) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + .asciz "dead_func" # DW_AT_name + + # Abbrev 3: DW_TAG_subprogram "live_func" (addrx 1, live, has children) + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + .asciz "live_func" # DW_AT_name + + # Abbrev 4: DW_TAG_variable "v" (DW_AT_location = loclistx 0) + .byte 4 + .byte 0 # DW_AT_location: loclistx index 0 + + # End of live_func children + .byte 0 + + # Abbrev 2: DW_TAG_subprogram "target" (addrx 2, tombstoned, kept alive by loclist ref) +.Ltarget: + .byte 2 + .byte 2 # DW_AT_low_pc: addrx index 2 + .asciz "target" # DW_AT_name + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_loclists.dwo,"e",@progbits +.Lloclists_section_start: + # DWARF5 .debug_loclists header + .long .Lloclist_end-.Lloclist_start # Unit length +.Lloclist_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size + .long 1 # Offset entry count +.Lloclists_table_base: + .long .Lloclist0-.Lloclists_table_base # Offset of location list 0 +.Lloclist0: + # DW_LLE_default_location + .byte 0x05 # DW_LLE_default_location + .byte 5 # Expression length (ULEB128) + # NB: It obviously doesn't make any real sense to DW_OP_call4 a + # DW_TAG_subprogram. The point here is just to test the mark-and-sweep + # of the DIEs. + .byte 0x99 # DW_OP_call4 + .long .Ltarget-.Ldebug_info_dwo_start+4 # 4-byte CU-relative offset of target + # DW_LLE_end_of_list + .byte 0x00 +.Lloclist_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, DW_AT_loclists_base(sec_offset) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0x8c, 0x01 # DW_AT_loclists_base (ULEB128: 140) + .byte 23 # DW_FORM_sec_offset + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_name(string) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, has children, DW_AT_low_pc(addrx) + DW_AT_name(string) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_variable, no children, DW_AT_location(loclistx) + .byte 4 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 0x22 # DW_FORM_loclistx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-loclist-exec.s b/tests/inputs/gc-loclist-exec.s new file mode 100644 index 0000000..17691df --- /dev/null +++ b/tests/inputs/gc-loclist-exec.s @@ -0,0 +1,52 @@ +# Fake executable for gc-loclist.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xcc, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Three 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone, dead_func) +# index 1: 0x1000 (live, live_func) +# index 2: 0xffffffffffffffff (tombstone, target) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xcc # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func) + .quad 0x1000 # Index 1: live address (live_func) + .quad 0xffffffffffffffff # Index 2: tombstone (target) +.Laddr_end: diff --git a/tests/inputs/gc-loclist-sec-offset-dwo.s b/tests/inputs/gc-loclist-sec-offset-dwo.s new file mode 100644 index 0000000..f3a9edb --- /dev/null +++ b/tests/inputs/gc-loclist-sec-offset-dwo.s @@ -0,0 +1,117 @@ +# .dwo file for gc-loclist-sec-offset.test +# +# DWARF5 split-compile unit with DW_FORM_sec_offset loclist reference +# (offset_entry_count == 0, GCC-style). Contains: +# compile_unit (children) +# subprogram "dead_func": DW_AT_low_pc = addrx 0 (tombstoned -> dead) +# subprogram "live_func": DW_AT_low_pc = addrx 1 (live), has children +# variable "v": DW_AT_location = sec_offset -> loclist in .debug_loclists.dwo +# subprogram "target": DW_AT_low_pc = addrx 2 (tombstoned, kept alive by +# DW_OP_call4 in the location list expression) +# +# The location list in .debug_loclists.dwo has offset_entry_count == 0 and +# contains a DW_LLE_default_location entry whose expression is +# { DW_OP_call4 }. +# +# DWO ID is 0xdd. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdd # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # Abbrev 2: DW_TAG_subprogram "dead_func" (addrx 0, tombstoned -> dead) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + .asciz "dead_func" # DW_AT_name + + # Abbrev 3: DW_TAG_subprogram "live_func" (addrx 1, live, has children) + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + .asciz "live_func" # DW_AT_name + + # Abbrev 4: DW_TAG_variable "v" (DW_AT_location = sec_offset) + .byte 4 + .long .Lloclist0-.Lloclists_section_start # DW_AT_location: sec_offset to loclist + + # End of live_func children + .byte 0 + + # Abbrev 2: DW_TAG_subprogram "target" (addrx 2, tombstoned, kept alive by loclist ref) +.Ltarget: + .byte 2 + .byte 2 # DW_AT_low_pc: addrx index 2 + .asciz "target" # DW_AT_name + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_loclists.dwo,"e",@progbits +.Lloclists_section_start: + # DWARF5 .debug_loclists header + .long .Lloclist_end-.Lloclist_start # Unit length +.Lloclist_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size + .long 0 # Offset entry count (0 = no offset table) +.Lloclist0: + # DW_LLE_default_location + .byte 0x05 # DW_LLE_default_location + .byte 5 # Expression length (ULEB128) + # NB: It obviously doesn't make any real sense to DW_OP_call4 a + # DW_TAG_subprogram. The point here is just to test the mark-and-sweep + # of the DIEs. + .byte 0x99 # DW_OP_call4 + .long .Ltarget-.Ldebug_info_dwo_start+4 # 4-byte CU-relative offset of target + # DW_LLE_end_of_list + .byte 0x00 +.Lloclist_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children (no attributes) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_name(string) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, has children, DW_AT_low_pc(addrx) + DW_AT_name(string) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_variable, no children, DW_AT_location(sec_offset) + .byte 4 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 23 # DW_FORM_sec_offset + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-loclist-sec-offset-exec.s b/tests/inputs/gc-loclist-sec-offset-exec.s new file mode 100644 index 0000000..26201f7 --- /dev/null +++ b/tests/inputs/gc-loclist-sec-offset-exec.s @@ -0,0 +1,52 @@ +# Fake executable for gc-loclist-sec-offset.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xdd, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Three 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone, dead_func) +# index 1: 0x1000 (live, live_func) +# index 2: 0xffffffffffffffff (tombstone, target) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdd # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func) + .quad 0x1000 # Index 1: live address (live_func) + .quad 0xffffffffffffffff # Index 2: tombstone (target) +.Laddr_end: diff --git a/tests/inputs/gc-loclist-v4-dwo.s b/tests/inputs/gc-loclist-v4-dwo.s new file mode 100644 index 0000000..3f50b38 --- /dev/null +++ b/tests/inputs/gc-loclist-v4-dwo.s @@ -0,0 +1,110 @@ +# .dwo file for gc-loclist-v4.test +# +# DWARF4 split-compile unit (GNU extension) containing: +# compile_unit (children, DW_AT_GNU_dwo_id = 0xdd) +# subprogram "dead_func": DW_AT_low_pc = GNU_addr_index 0 (tombstoned -> dead) +# subprogram "live_func": DW_AT_low_pc = GNU_addr_index 1 (live), has children +# variable "v": DW_AT_location = sec_offset -> loclist in .debug_loc.dwo +# subprogram "target": DW_AT_low_pc = GNU_addr_index 2 (tombstoned, kept alive by +# DW_OP_call4 in the location list expression) +# +# The location list in .debug_loc.dwo contains a DW_LLE_startx_length entry +# (GNU extension encoding) whose expression is { DW_OP_call4 }. +# +# DWO ID is 0xdd. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + .quad 0xdd # DW_AT_GNU_dwo_id + + # Abbrev 2: DW_TAG_subprogram "dead_func" (GNU_addr_index 0, tombstoned -> dead) + .byte 2 + .byte 0 # DW_AT_low_pc: GNU_addr_index index 0 + .asciz "dead_func" # DW_AT_name + + # Abbrev 3: DW_TAG_subprogram "live_func" (GNU_addr_index 1, live, has children) + .byte 3 + .byte 1 # DW_AT_low_pc: GNU_addr_index index 1 + .asciz "live_func" # DW_AT_name + + # Abbrev 4: DW_TAG_variable "v" (DW_AT_location = sec_offset -> loclist) + .byte 4 + .long 0 # DW_AT_location: sec_offset (byte 0 of .debug_loc.dwo) + + # End of live_func children + .byte 0 + + # Abbrev 2: DW_TAG_subprogram "target" (GNU_addr_index 2, tombstoned, kept alive by loclist ref) +.Ltarget: + .byte 2 + .byte 2 # DW_AT_low_pc: GNU_addr_index index 2 + .asciz "target" # DW_AT_name + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_loc.dwo,"e",@progbits + # GNU split DWARF uses DW_LLE entry types even in DWARF4. + # DW_LLE_startx_length + .byte 0x03 # DW_LLE_startx_length + .byte 1 # begin: GNU_addr_index 1 (ULEB128) + .long 0x10 # length (4 bytes) + .short 5 # Expression length (2 bytes) + # NB: It obviously doesn't make any real sense to DW_OP_call4 a + # DW_TAG_subprogram. The point here is just to test the mark-and-sweep + # of the DIEs. + .byte 0x99 # DW_OP_call4 + .long .Ltarget-.Ldebug_info_dwo_start+4 # 4-byte CU-relative offset of target + # DW_LLE_end_of_list + .byte 0x00 + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, + # DW_AT_GNU_dwo_id(data8) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(GNU_addr_index) + DW_AT_name(string) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .uleb128 0x1f01 # DW_FORM_GNU_addr_index + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, has children, DW_AT_low_pc(GNU_addr_index) + DW_AT_name(string) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .uleb128 0x1f01 # DW_FORM_GNU_addr_index + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_variable, no children, DW_AT_location(sec_offset) + .byte 4 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 23 # DW_FORM_sec_offset + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-loclist-v4-exec.s b/tests/inputs/gc-loclist-v4-exec.s new file mode 100644 index 0000000..b50184d --- /dev/null +++ b/tests/inputs/gc-loclist-v4-exec.s @@ -0,0 +1,47 @@ +# Fake executable for gc-loclist-v4.test +# +# DWARF4 skeleton CU (GNU extension) containing: +# .debug_info: DW_TAG_compile_unit with DW_AT_GNU_dwo_id = 0xdd, +# DW_AT_GNU_addr_base, and DW_AT_GNU_dwo_name +# .debug_addr: Three 8-byte addresses (no header, DWARF4 style): +# index 0: 0xffffffffffffffff (tombstone, dead_func) +# index 1: 0x1000 (live, live_func) +# index 2: 0xffffffffffffffff (tombstone, target) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + + # [0x0b] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .quad 0xdd # DW_AT_GNU_dwo_id + .long .Laddr_table_base # DW_AT_GNU_addr_base + .asciz "DWO_PATH" # DW_AT_GNU_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .uleb128 0x2133 # DW_AT_GNU_addr_base + .uleb128 23 # DW_FORM_sec_offset + .uleb128 0x2130 # DW_AT_GNU_dwo_name + .uleb128 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF4: no header, just raw address entries. + # DW_AT_GNU_addr_base points directly to the start of entries. +.Laddr_table_base: + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func) + .quad 0x1000 # Index 1: live address (live_func) + .quad 0xffffffffffffffff # Index 2: tombstone (target) diff --git a/tests/inputs/gc-multi-dwp-v4-b-dwo.s b/tests/inputs/gc-multi-dwp-v4-b-dwo.s new file mode 100644 index 0000000..d5d5c5d --- /dev/null +++ b/tests/inputs/gc-multi-dwp-v4-b-dwo.s @@ -0,0 +1,108 @@ +# Second `.dwo` for gc-multi-dwp-v4.test (DWARF4 GNU extension). +# +# Structurally similar to gc-loclist-v4-dwo.s, but with DWO ID 0xde and a longer +# leading subprogram name so that the `target` DIE sits at a DIFFERENT +# CU-relative offset. The DW_OP_call4 offset in its location list therefore +# differs from the other CU's, so if one CU's offset remap bled into the other +# the patched call offset would be visibly wrong. +# +# compile_unit (children, DW_AT_GNU_dwo_id = 0xde) +# subprogram "dead_function_b": GNU_addr_index 0 (tombstoned -> dead) +# subprogram "live_func_b": GNU_addr_index 1 (live), has children +# variable "v": DW_AT_location = sec_offset -> loclist in .debug_loc.dwo +# subprogram "target_b": GNU_addr_index 2 (tombstoned, kept alive by the +# DW_OP_call4 in the location list expression) +# +# DWO ID is 0xde. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + .quad 0xde # DW_AT_GNU_dwo_id + + # Abbrev 2: DW_TAG_subprogram "dead_function_b" (GNU_addr_index 0, tombstoned -> dead) + .byte 2 + .byte 0 # DW_AT_low_pc: GNU_addr_index index 0 + .asciz "dead_function_b" # DW_AT_name (deliberately long) + + # Abbrev 3: DW_TAG_subprogram "live_func_b" (GNU_addr_index 1, live, has children) + .byte 3 + .byte 1 # DW_AT_low_pc: GNU_addr_index index 1 + .asciz "live_func_b" # DW_AT_name + + # Abbrev 4: DW_TAG_variable "v" (DW_AT_location = sec_offset -> loclist) + .byte 4 + .long 0 # DW_AT_location: sec_offset (byte 0 of .debug_loc.dwo) + + # End of live_func_b children + .byte 0 + + # Abbrev 2: DW_TAG_subprogram "target_b" (GNU_addr_index 2, tombstoned, kept alive by loclist ref) +.Ltarget: + .byte 2 + .byte 2 # DW_AT_low_pc: GNU_addr_index index 2 + .asciz "target_b" # DW_AT_name + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_loc.dwo,"e",@progbits + # GNU split DWARF uses DW_LLE entry types even in DWARF4. + # DW_LLE_startx_length + .byte 0x03 # DW_LLE_startx_length + .byte 1 # begin: GNU_addr_index 1 (ULEB128) + .long 0x10 # length (4 bytes) + .short 5 # Expression length (2 bytes) + .byte 0x99 # DW_OP_call4 + .long .Ltarget-.Ldebug_info_dwo_start+4 # 4-byte CU-relative offset of target_b + # DW_LLE_end_of_list + .byte 0x00 + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, DW_AT_GNU_dwo_id(data8) + .byte 1 + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .byte 0 + .byte 0 + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(GNU_addr_index) + DW_AT_name(string) + .byte 2 + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .uleb128 0x1f01 # DW_FORM_GNU_addr_index + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 + .byte 0 + + # Abbrev 3: DW_TAG_subprogram, has children, DW_AT_low_pc(GNU_addr_index) + DW_AT_name(string) + .byte 3 + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .uleb128 0x1f01 # DW_FORM_GNU_addr_index + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 + .byte 0 + + # Abbrev 4: DW_TAG_variable, no children, DW_AT_location(sec_offset) + .byte 4 + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 23 # DW_FORM_sec_offset + .byte 0 + .byte 0 + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-multi-dwp-v4-exec.s b/tests/inputs/gc-multi-dwp-v4-exec.s new file mode 100644 index 0000000..5d31ff5 --- /dev/null +++ b/tests/inputs/gc-multi-dwp-v4-exec.s @@ -0,0 +1,63 @@ +# Fake executable for gc-multi-dwp-v4.test (DWARF4 GNU extension). +# +# Provides garbage-collection address data for two compilation units that are +# re-packaged from a multi-CU `.dwp` input: +# DWO ID 0xdd (from gc-loclist-v4-dwo.s) +# DWO ID 0xde (from gc-multi-dwp-v4-b-dwo.s) +# +# Each skeleton CU has its own DW_AT_GNU_addr_base into a per-CU address table. +# The `.dwo` references are never loaded: the `.dwp` is processed first, so the +# units are already contained and the executable only contributes addr data. +# +# Both address tables: index 0 tombstone, index 1 live, index 2 tombstone. + + .section .debug_info,"",@progbits + # Skeleton CU for DWO ID 0xdd + .long .Ldebug_info_dd_end-.Ldebug_info_dd_start # Length of Unit +.Ldebug_info_dd_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + .byte 1 # Abbrev 1: DW_TAG_compile_unit + .quad 0xdd # DW_AT_GNU_dwo_id + .long .Laddr_table_dd # DW_AT_GNU_addr_base + .asciz "ignored-dd.dwo" # DW_AT_GNU_dwo_name (never loaded) +.Ldebug_info_dd_end: + + # Skeleton CU for DWO ID 0xde + .long .Ldebug_info_de_end-.Ldebug_info_de_start # Length of Unit +.Ldebug_info_de_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + .byte 1 # Abbrev 1: DW_TAG_compile_unit + .quad 0xde # DW_AT_GNU_dwo_id + .long .Laddr_table_de # DW_AT_GNU_addr_base + .asciz "ignored-de.dwo" # DW_AT_GNU_dwo_name (never loaded) +.Ldebug_info_de_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .uleb128 0x2133 # DW_AT_GNU_addr_base + .uleb128 23 # DW_FORM_sec_offset + .uleb128 0x2130 # DW_AT_GNU_dwo_name + .uleb128 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF4: no header, just raw address entries. +.Laddr_table_dd: + .quad 0xffffffffffffffff # Index 0: tombstone + .quad 0x1000 # Index 1: live + .quad 0xffffffffffffffff # Index 2: tombstone +.Laddr_table_de: + .quad 0xffffffffffffffff # Index 0: tombstone + .quad 0x1000 # Index 1: live + .quad 0xffffffffffffffff # Index 2: tombstone diff --git a/tests/inputs/gc-multi-dwp-v5-exec.s b/tests/inputs/gc-multi-dwp-v5-exec.s new file mode 100644 index 0000000..2dc29c7 --- /dev/null +++ b/tests/inputs/gc-multi-dwp-v5-exec.s @@ -0,0 +1,74 @@ +# Fake executable for gc-multi-dwp-v5.test +# +# Provides garbage-collection address data for two compilation units that are +# re-packaged from a multi-CU `.dwp` input: +# DWO ID 0xbb (from gc-compact-loclists-dwo.s) +# DWO ID 0xcc (from gc-loclist-dwo.s) +# +# Each skeleton CU has its own DW_AT_addr_base into a per-CU address table. +# The `.dwo` references are never loaded: the `.dwp` is processed first, so the +# units are already contained and the executable only contributes addr data. +# +# Both address tables use the same pattern: +# index 0: 0xffffffffffffffff (tombstone) +# index 1: 0x1000 (live) +# index 2: 0xffffffffffffffff (tombstone) + + .section .debug_info,"",@progbits + # Skeleton CU for DWO ID 0xbb + .long .Ldebug_info_bb_end-.Ldebug_info_bb_start # Length of Unit +.Ldebug_info_bb_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbb # DWO ID + .byte 1 # Abbrev 1: DW_TAG_compile_unit + .long .Laddr_table_bb # DW_AT_addr_base + .asciz "ignored-bb.dwo" # DW_AT_dwo_name (never loaded) +.Ldebug_info_bb_end: + + # Skeleton CU for DWO ID 0xcc + .long .Ldebug_info_cc_end-.Ldebug_info_cc_start # Length of Unit +.Ldebug_info_cc_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xcc # DWO ID + .byte 1 # Abbrev 1: DW_TAG_compile_unit + .long .Laddr_table_cc # DW_AT_addr_base + .asciz "ignored-cc.dwo" # DW_AT_dwo_name (never loaded) +.Ldebug_info_cc_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_bb: + # Address table for DWO ID 0xbb + .quad 0xffffffffffffffff # Index 0: tombstone + .quad 0x1000 # Index 1: live + .quad 0xffffffffffffffff # Index 2: tombstone +.Laddr_table_cc: + # Address table for DWO ID 0xcc + .quad 0xffffffffffffffff # Index 0: tombstone + .quad 0x1000 # Index 1: live + .quad 0xffffffffffffffff # Index 2: tombstone +.Laddr_end: diff --git a/tests/inputs/gc-nothing-dead-dwo.s b/tests/inputs/gc-nothing-dead-dwo.s new file mode 100644 index 0000000..11fc180 --- /dev/null +++ b/tests/inputs/gc-nothing-dead-dwo.s @@ -0,0 +1,52 @@ +# .dwo file for gc-nothing-dead.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram: DW_AT_low_pc = addrx1 0 (live) +# subprogram: DW_AT_low_pc = addrx1 1 (live) +# +# Both subprograms are live, so nothing should be pruned. +# The DWO ID is 0x1234. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x1234 # DWO ID + # DIE tree begins at offset 0x14 (20 bytes into unit) + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # [0x15] Abbrev 2: DW_TAG_subprogram (live - addrx1 0) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx1 index 0 + + # [0x17] Abbrev 2: DW_TAG_subprogram (live - addrx1 1) + .byte 2 + .byte 1 # DW_AT_low_pc: addrx1 index 1 + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx1) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 0x29 # DW_FORM_addrx1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-nothing-dead-exec.s b/tests/inputs/gc-nothing-dead-exec.s new file mode 100644 index 0000000..8060bb3 --- /dev/null +++ b/tests/inputs/gc-nothing-dead-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-nothing-dead.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0x1234, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0x1000 (live) +# index 1: 0x2000 (live) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x1234 # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0x1000 # Index 0: live address + .quad 0x2000 # Index 1: live address +.Laddr_end: diff --git a/tests/inputs/gc-nothing-dead-orphan-list-dwo.s b/tests/inputs/gc-nothing-dead-orphan-list-dwo.s new file mode 100644 index 0000000..5c1e198 --- /dev/null +++ b/tests/inputs/gc-nothing-dead-orphan-list-dwo.s @@ -0,0 +1,140 @@ +# .dwo file for gc-nothing-dead-orphan-list.test +# +# DWARF5 split-compile unit where all DIEs are live, but there is an +# orphan (unreferenced) range list at index 0 in .debug_rnglists.dwo +# and an orphan location list at index 0 in .debug_loclists.dwo. +# +# Compile unit (DW_UT_split_compile, DWO ID 0xbb): +# compile_unit (children, DW_AT_rnglists_base, DW_AT_loclists_base) +# subprogram "func_a" (rnglistx 1, loclistx 1, live) +# subprogram "func_b" (rnglistx 2, loclistx 2, live) +# +# .debug_rnglists.dwo has 3 range lists (indices 0, 1, 2). +# Index 0 is orphaned (not referenced by any DIE). +# .debug_loclists.dwo has 3 location lists (indices 0, 1, 2). +# Index 0 is orphaned (not referenced by any DIE). +# +# Since all DIEs are live, GC should NOT rewrite debug_info, and +# must NOT compact the rnglists/loclists offset tables. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbb # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + .long .Lrnglists_table_base-.Lrnglists_section_start # DW_AT_rnglists_base + .long .Lloclists_table_base-.Lloclists_section_start # DW_AT_loclists_base + + # Abbrev 2: DW_TAG_subprogram "func_a" (rnglistx 1, loclistx 1, live) + .byte 2 + .byte 1 # DW_AT_ranges: rnglistx index 1 + .byte 1 # DW_AT_location: loclistx index 1 + .asciz "func_a" # DW_AT_name + + # Abbrev 2: DW_TAG_subprogram "func_b" (rnglistx 2, loclistx 2, live) + .byte 2 + .byte 2 # DW_AT_ranges: rnglistx index 2 + .byte 2 # DW_AT_location: loclistx index 2 + .asciz "func_b" # DW_AT_name + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_rnglists.dwo,"e",@progbits +.Lrnglists_section_start: + .long .Lrnglists_end-.Lrnglists_start # Unit length +.Lrnglists_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size + .long 3 # Offset entry count +.Lrnglists_table_base: + .long .Lrangelist0-.Lrnglists_table_base # Offset of range list 0 + .long .Lrangelist1-.Lrnglists_table_base # Offset of range list 1 + .long .Lrangelist2-.Lrnglists_table_base # Offset of range list 2 +.Lrangelist0: + # Range list 0 (orphan): DW_RLE_startx_length addrx 0, length 0x08 + .byte 0x03 # DW_RLE_startx_length + .byte 0 # Start address index (ULEB128): addrx 0 + .byte 0x08 # Length (ULEB128): 8 bytes + .byte 0x00 # DW_RLE_end_of_list +.Lrangelist1: + # Range list 1 (func_a): DW_RLE_startx_length addrx 0, length 0x20 + .byte 0x03 # DW_RLE_startx_length + .byte 0 # Start address index (ULEB128): addrx 0 + .byte 0x20 # Length (ULEB128): 32 bytes + .byte 0x00 # DW_RLE_end_of_list +.Lrangelist2: + # Range list 2 (func_b): DW_RLE_startx_length addrx 1, length 0x30 + .byte 0x03 # DW_RLE_startx_length + .byte 1 # Start address index (ULEB128): addrx 1 + .byte 0x30 # Length (ULEB128): 48 bytes + .byte 0x00 # DW_RLE_end_of_list +.Lrnglists_end: + + .section .debug_loclists.dwo,"e",@progbits +.Lloclists_section_start: + .long .Lloclists_end-.Lloclists_start # Unit length +.Lloclists_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size + .long 3 # Offset entry count +.Lloclists_table_base: + .long .Lloclist0-.Lloclists_table_base # Offset of location list 0 + .long .Lloclist1-.Lloclists_table_base # Offset of location list 1 + .long .Lloclist2-.Lloclists_table_base # Offset of location list 2 +.Lloclist0: + # Location list 0 (orphan): DW_LLE_default_location with DW_OP_reg2 + .byte 0x05 # DW_LLE_default_location + .byte 1 # Expression length (ULEB128): 1 byte + .byte 0x52 # DW_OP_reg2 + .byte 0x00 # DW_LLE_end_of_list +.Lloclist1: + # Location list 1 (func_a): DW_LLE_default_location with DW_OP_reg0 + .byte 0x05 # DW_LLE_default_location + .byte 1 # Expression length (ULEB128): 1 byte + .byte 0x50 # DW_OP_reg0 + .byte 0x00 # DW_LLE_end_of_list +.Lloclist2: + # Location list 2 (func_b): DW_LLE_default_location with DW_OP_reg1 + .byte 0x05 # DW_LLE_default_location + .byte 1 # Expression length (ULEB128): 1 byte + .byte 0x51 # DW_OP_reg1 + .byte 0x00 # DW_LLE_end_of_list +.Lloclists_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, + # DW_AT_rnglists_base(sec_offset) + DW_AT_loclists_base(sec_offset) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0x74 # DW_AT_rnglists_base + .byte 23 # DW_FORM_sec_offset + .byte 0x8c, 0x01 # DW_AT_loclists_base (ULEB128: 140) + .byte 23 # DW_FORM_sec_offset + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, + # DW_AT_ranges(rnglistx) + DW_AT_location(loclistx) + DW_AT_name(string) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 0x55 # DW_AT_ranges + .byte 0x23 # DW_FORM_rnglistx + .byte 2 # DW_AT_location + .byte 0x22 # DW_FORM_loclistx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-nothing-dead-orphan-list-exec.s b/tests/inputs/gc-nothing-dead-orphan-list-exec.s new file mode 100644 index 0000000..6d55939 --- /dev/null +++ b/tests/inputs/gc-nothing-dead-orphan-list-exec.s @@ -0,0 +1,49 @@ +# Fake executable for gc-nothing-dead-orphan-list.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xbb, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses (both live): +# index 0: 0x1000 (func_a) +# index 1: 0x2000 (func_b) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbb # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + .quad 0x1000 # Index 0: live address (func_a) + .quad 0x2000 # Index 1: live address (func_b) +.Laddr_end: diff --git a/tests/inputs/gc-ranges-dead-dwo.s b/tests/inputs/gc-ranges-dead-dwo.s new file mode 100644 index 0000000..7412554 --- /dev/null +++ b/tests/inputs/gc-ranges-dead-dwo.s @@ -0,0 +1,129 @@ +# .dwo file for gc-ranges-dead.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children, DW_AT_rnglists_base) +# subprogram "dead_func": DW_AT_ranges = rnglistx 0 (all entries tombstoned via startx_length) +# subprogram "dead_base_func": DW_AT_ranges = rnglistx 1 (all entries tombstoned via base_addressx + offset_pair) +# subprogram "mixed_func": DW_AT_ranges = rnglistx 2 (mix of tombstoned and live entries) +# subprogram "live_func": DW_AT_low_pc = addrx 1 (live) +# +# .debug_rnglists.dwo contains three range lists: +# index 0: single DW_RLE_startx_length referencing addrx 0 (tombstoned) +# index 1: DW_RLE_base_addressx(addrx 2, tombstoned) + DW_RLE_offset_pair +# index 2: DW_RLE_startx_length(addrx 0, tombstoned) + DW_RLE_startx_length(addrx 3, live) +# +# DWO ID is 0xdd. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdd # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children, DW_AT_rnglists_base) + .byte 1 + .long .Lrnglists_table_base-.Lrnglists_section_start # DW_AT_rnglists_base + + # Abbrev 2: DW_TAG_subprogram "dead_func" (DW_AT_ranges = rnglistx 0) + .byte 2 + .byte 0 # DW_AT_ranges: rnglistx index 0 + .asciz "dead_func" # DW_AT_name + + # Abbrev 2: DW_TAG_subprogram "dead_base_func" (DW_AT_ranges = rnglistx 1) + .byte 2 + .byte 1 # DW_AT_ranges: rnglistx index 1 + .asciz "dead_base_func" # DW_AT_name + + # Abbrev 2: DW_TAG_subprogram "mixed_func" (DW_AT_ranges = rnglistx 2) + .byte 2 + .byte 2 # DW_AT_ranges: rnglistx index 2 + .asciz "mixed_func" # DW_AT_name + + # Abbrev 3: DW_TAG_subprogram "live_func" (DW_AT_low_pc = addrx 1) + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + .asciz "live_func" # DW_AT_name + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_rnglists.dwo,"e",@progbits +.Lrnglists_section_start: + .long .Lrnglists_end-.Lrnglists_start # Unit length +.Lrnglists_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size + .long 3 # Offset entry count +.Lrnglists_table_base: + # Offset table (3 entries, DWARF32 -> 4 bytes each) + # Offsets are relative to the start of the offset table (= DW_AT_rnglists_base). + .long .Lrangelist0-.Lrnglists_table_base # Offset of range list 0 + .long .Lrangelist1-.Lrnglists_table_base # Offset of range list 1 + .long .Lrangelist2-.Lrnglists_table_base # Offset of range list 2 +.Lrangelist0: + # DW_RLE_startx_length: addrx 0 (tombstoned), length 0x10 + .byte 0x03 # DW_RLE_startx_length + .byte 0 # Start address index (ULEB128): addrx 0 + .byte 0x10 # Length (ULEB128): 16 bytes + # DW_RLE_end_of_list + .byte 0x00 +.Lrangelist1: + # DW_RLE_base_addressx: addrx 2 (tombstoned) + .byte 0x01 # DW_RLE_base_addressx + .byte 2 # Address index (ULEB128): addrx 2 + # DW_RLE_offset_pair: offset 0, length 0x20 + .byte 0x04 # DW_RLE_offset_pair + .byte 0 # Start offset (ULEB128) + .byte 0x20 # End offset (ULEB128) + # DW_RLE_end_of_list + .byte 0x00 +.Lrangelist2: + # DW_RLE_startx_length: addrx 0 (tombstoned), length 0x10 + .byte 0x03 # DW_RLE_startx_length + .byte 0 # Start address index (ULEB128): addrx 0 + .byte 0x10 # Length (ULEB128): 16 bytes + # DW_RLE_startx_length: addrx 3 (live), length 0x10 + .byte 0x03 # DW_RLE_startx_length + .byte 3 # Start address index (ULEB128): addrx 3 + .byte 0x10 # Length (ULEB128): 16 bytes + # DW_RLE_end_of_list + .byte 0x00 +.Lrnglists_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, DW_AT_rnglists_base(sec_offset) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0x74 # DW_AT_rnglists_base + .byte 23 # DW_FORM_sec_offset + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_ranges(rnglistx) + DW_AT_name(string) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 0x55 # DW_AT_ranges + .byte 0x23 # DW_FORM_rnglistx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_name(string) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-ranges-dead-exec.s b/tests/inputs/gc-ranges-dead-exec.s new file mode 100644 index 0000000..184eb84 --- /dev/null +++ b/tests/inputs/gc-ranges-dead-exec.s @@ -0,0 +1,52 @@ +# Fake executable for gc-ranges-dead.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xdd, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone, dead_func range) +# index 1: 0x1000 (live, live_func) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdd # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func range) + .quad 0x1000 # Index 1: live address (live_func) + .quad 0xffffffffffffffff # Index 2: tombstone (dead_base_func base address) + .quad 0x2000 # Index 3: live address (mixed_func range) +.Laddr_end: diff --git a/tests/inputs/gc-ranges-dead-v4-dwo.s b/tests/inputs/gc-ranges-dead-v4-dwo.s new file mode 100644 index 0000000..f2ed130 --- /dev/null +++ b/tests/inputs/gc-ranges-dead-v4-dwo.s @@ -0,0 +1,84 @@ +# .dwo file for gc-ranges-dead-v4.test +# +# DWARF4 split-compile unit (GNU extension) containing: +# compile_unit (children, DW_AT_GNU_dwo_id = 0xdd) +# subprogram "dead_func": DW_AT_ranges = sec_offset 0x00 (all entries tombstoned) +# subprogram "dead_base_func": DW_AT_ranges = sec_offset 0x20 (base tombstoned + offset pair) +# subprogram "mixed_func": DW_AT_ranges = sec_offset 0x50 (one tombstoned + one live) +# subprogram "live_func": DW_AT_low_pc = GNU_addr_index 0 (live) +# +# These sec_offsets are relative to the skeleton's DW_AT_GNU_ranges_base; the absolute +# offsets into the executable's .debug_ranges are ranges_base (0x40) plus these values. +# +# The range data lives in the skeleton executable's .debug_ranges section, +# not in a .dwo section (DWARF4 has no .debug_ranges.dwo). +# +# DWO ID is 0xdd. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + .quad 0xdd # DW_AT_GNU_dwo_id + + # Abbrev 2: DW_TAG_subprogram "dead_func" (DW_AT_ranges = sec_offset 0x00) + .byte 2 + .long 0x00 # DW_AT_ranges: sec_offset into .debug_ranges + .asciz "dead_func" # DW_AT_name + + # Abbrev 2: DW_TAG_subprogram "dead_base_func" (DW_AT_ranges = sec_offset 0x20) + .byte 2 + .long 0x20 # DW_AT_ranges: sec_offset into .debug_ranges + .asciz "dead_base_func" # DW_AT_name + + # Abbrev 2: DW_TAG_subprogram "mixed_func" (DW_AT_ranges = sec_offset 0x50) + .byte 2 + .long 0x50 # DW_AT_ranges: sec_offset into .debug_ranges + .asciz "mixed_func" # DW_AT_name + + # Abbrev 3: DW_TAG_subprogram "live_func" (DW_AT_low_pc = GNU_addr_index 0) + .byte 3 + .byte 0 # DW_AT_low_pc: GNU_addr_index index 0 + .asciz "live_func" # DW_AT_name + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, DW_AT_GNU_dwo_id(data8) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_ranges(sec_offset) + DW_AT_name(string) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 0x55 # DW_AT_ranges + .byte 23 # DW_FORM_sec_offset + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, no children, DW_AT_low_pc(GNU_addr_index) + DW_AT_name(string) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .uleb128 0x1f01 # DW_FORM_GNU_addr_index + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-ranges-dead-v4-exec.s b/tests/inputs/gc-ranges-dead-v4-exec.s new file mode 100644 index 0000000..a6efe22 --- /dev/null +++ b/tests/inputs/gc-ranges-dead-v4-exec.s @@ -0,0 +1,103 @@ +# Fake executable for gc-ranges-dead-v4.test +# +# DWARF4 skeleton CU (GNU extension) containing: +# .debug_info: DW_TAG_compile_unit with DW_AT_GNU_dwo_id = 0xdd, +# DW_AT_GNU_addr_base, DW_AT_GNU_ranges_base, and DW_AT_GNU_dwo_name +# .debug_addr: One 8-byte address (no header, DWARF4 style): +# index 0: 0x1000 (live, live_func) +# .debug_ranges: 0x40 bytes of leading padding, then three range lists. The .dwo's +# DW_AT_ranges sec_offsets are relative to DW_AT_GNU_ranges_base (= 0x40, +# the offset of .Lranges_base), exercising the ranges_base adjustment: +# base+0x00: dead_func (all entries use tombstoned addresses) +# base+0x20: dead_base_func (base address tombstoned + offset pair) +# base+0x50: mixed_func (one tombstoned + one live entry) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + + # [0x0b] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .quad 0xdd # DW_AT_GNU_dwo_id + .long .Laddr_table_base # DW_AT_GNU_addr_base + .long .Lranges_base-.Lranges_sec_start # DW_AT_GNU_ranges_base (offset of this CU's + # range lists within .debug_ranges) + .asciz "DWO_PATH" # DW_AT_GNU_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .uleb128 0x2133 # DW_AT_GNU_addr_base + .uleb128 23 # DW_FORM_sec_offset + .uleb128 0x2132 # DW_AT_GNU_ranges_base + .uleb128 23 # DW_FORM_sec_offset + .uleb128 0x2130 # DW_AT_GNU_dwo_name + .uleb128 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF4: no header, just raw address entries. + # DW_AT_GNU_addr_base points directly to the start of entries. +.Laddr_table_base: + .quad 0x1000 # Index 0: live address (live_func) + + .section .debug_ranges,"",@progbits + # DWARF4 .debug_ranges: pairs of (start, end) addresses, 8 bytes each. + # (0xffffffffffffffff, addr) = base address selector. + # (0, 0) = end of list. +.Lranges_sec_start: + + # --- Leading padding: 0x40 bytes simulating a prior CU's range lists. --- + # This CU's lists begin at .Lranges_base, pointed to by DW_AT_GNU_ranges_base, + # so the .dwo's sec_offsets are relative to here, not the section start. Without + # applying ranges_base, GC would misread these padding bytes (all (0,0) = empty + # lists) and wrongly drop mixed_func. + .quad 0x0 + .quad 0x0 + .quad 0x0 + .quad 0x0 + .quad 0x0 + .quad 0x0 + .quad 0x0 + .quad 0x0 +.Lranges_base: + + # --- Range list 0 at base+0x00: dead_func (all entries tombstoned) --- + # Single range with GNU ld tombstoned start address (0x0). + .quad 0x0 # Start: 0 (GNU ld tombstone) + .quad 0x10 # End + .quad 0x0 # End of list + .quad 0x0 # End of list + + # --- Range list 1 at base+0x20: dead_base_func (base tombstoned) --- + # Base address selector: set base to 0 (GNU ld tombstone). + .quad 0xffffffffffffffff # Base address selector sentinel + .quad 0x0 # New base: 0 (GNU ld tombstone) + # Offset pair under the tombstoned base. + .quad 0x0 # Start offset + .quad 0x20 # End offset + # End of list. + .quad 0x0 + .quad 0x0 + + # --- Range list 2 at base+0x50: mixed_func (one dead + one live) --- + # Entry 1: tombstoned. + .quad 0x0 # Start: 0 (GNU ld tombstone) + .quad 0x10 # End + # Entry 2: live. + .quad 0x2000 # Start: live address + .quad 0x2010 # End + # End of list. + .quad 0x0 + .quad 0x0 diff --git a/tests/inputs/gc-ref-forms-dwo.s b/tests/inputs/gc-ref-forms-dwo.s new file mode 100644 index 0000000..419f4f7 --- /dev/null +++ b/tests/inputs/gc-ref-forms-dwo.s @@ -0,0 +1,134 @@ +# .dwo file for gc-ref-forms.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram "dead": DW_AT_low_pc = addrx 0 (tombstoned -> dead) +# subprogram "live_func": DW_AT_low_pc = addrx 1 (live), DW_AT_type = ref1 -> t_r1 +# base_type "t_r1": name, byte_size=4, encoding=DW_ATE_signed +# subprogram "live_func2": DW_AT_low_pc = addrx 2 (live), DW_AT_type = ref2 -> t_r2 +# base_type "t_r2": name, byte_size=4, encoding=DW_ATE_unsigned +# subprogram "live_func3": DW_AT_low_pc = addrx 3 (live), DW_AT_type = ref_udata -> t_ru +# base_type "t_ru": name, byte_size=8, encoding=DW_ATE_float +# +# The DWO ID is 0xbeefcafe. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbeefcafe # DWO ID + # DIE tree begins at offset 0x14 (20 bytes into unit) + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # [0x15] Abbrev 2: DW_TAG_subprogram (dead - addrx 0 is tombstoned) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + + # [0x17] Abbrev 3: DW_TAG_subprogram (live - addrx 1), DW_AT_type = ref1 + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + .byte .Lt_r1-.Ldebug_info_dwo_start+4 # DW_AT_type: ref1 to t_r1 + + # [0x1a] base_type "t_r1" (referenced by live_func via ref1) +.Lt_r1: + .byte 4 # Abbrev 4: DW_TAG_base_type + .asciz "t_r1" # DW_AT_name + .byte 4 # DW_AT_byte_size + .byte 5 # DW_AT_encoding (DW_ATE_signed) + + # [0x22] Abbrev 5: DW_TAG_subprogram (live - addrx 2), DW_AT_type = ref2 + .byte 5 + .byte 2 # DW_AT_low_pc: addrx index 2 + .short .Lt_r2-.Ldebug_info_dwo_start+4 # DW_AT_type: ref2 to t_r2 + + # [0x26] base_type "t_r2" (referenced by live_func2 via ref2) +.Lt_r2: + .byte 4 # Abbrev 4: DW_TAG_base_type + .asciz "t_r2" # DW_AT_name + .byte 4 # DW_AT_byte_size + .byte 7 # DW_AT_encoding (DW_ATE_unsigned) + + # [0x2e] Abbrev 6: DW_TAG_subprogram (live - addrx 3), DW_AT_type = ref_udata + .byte 6 + .byte 3 # DW_AT_low_pc: addrx index 3 + .byte .Lt_ru-.Ldebug_info_dwo_start+4 # DW_AT_type: ref_udata (ULEB128) to t_ru + + # [0x31] base_type "t_ru" (referenced by live_func3 via ref_udata) +.Lt_ru: + .byte 4 # Abbrev 4: DW_TAG_base_type + .asciz "t_ru" # DW_AT_name + .byte 8 # DW_AT_byte_size + .byte 4 # DW_AT_encoding (DW_ATE_float) + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_type(ref1) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 73 # DW_AT_type + .byte 17 # DW_FORM_ref1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_base_type, no children, DW_AT_name(string) + DW_AT_byte_size(data1) + DW_AT_encoding(data1) + .byte 4 # Abbreviation Code + .byte 36 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 11 # DW_AT_byte_size + .byte 11 # DW_FORM_data1 + .byte 62 # DW_AT_encoding + .byte 11 # DW_FORM_data1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 5: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_type(ref2) + .byte 5 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 73 # DW_AT_type + .byte 18 # DW_FORM_ref2 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 6: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_type(ref_udata) + .byte 6 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 73 # DW_AT_type + .byte 21 # DW_FORM_ref_udata + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-ref-forms-exec.s b/tests/inputs/gc-ref-forms-exec.s new file mode 100644 index 0000000..71c92f0 --- /dev/null +++ b/tests/inputs/gc-ref-forms-exec.s @@ -0,0 +1,54 @@ +# Fake executable for gc-ref-forms.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xbeefcafe, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Four 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone - dead) +# index 1: 0x1000 (live - live_func) +# index 2: 0x2000 (live - live_func2) +# index 3: 0x3000 (live - live_func3) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbeefcafe # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead) + .quad 0x1000 # Index 1: live (live_func) + .quad 0x2000 # Index 2: live (live_func2) + .quad 0x3000 # Index 3: live (live_func3) +.Laddr_end: diff --git a/tests/inputs/gc-retained-edge-dwo.s b/tests/inputs/gc-retained-edge-dwo.s new file mode 100644 index 0000000..4f76706 --- /dev/null +++ b/tests/inputs/gc-retained-edge-dwo.s @@ -0,0 +1,79 @@ +# .dwo file for gc-retained-edge.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# namespace: DW_AT_type = ref4 -> base_type (Retained, ancestor of live subprogram) +# subprogram: DW_AT_low_pc = addrx 0 (tombstoned -> dead) +# subprogram: DW_AT_low_pc = addrx 1 (live) +# base_type (only reachable via namespace's DW_AT_type) +# +# The DWO ID is 0xcafebabe. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xcafebabe # DWO ID + # DIE tree begins at offset 0x14 (20 bytes into unit) + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # [0x15] Abbrev 2: DW_TAG_namespace (has children), DW_AT_type = ref4 -> base_type + .byte 2 + .long .Lbase_type-.Ldebug_info_dwo_start+4 # DW_AT_type: CU-relative ref4 to base_type + + # [0x1a] Abbrev 3: DW_TAG_subprogram (dead - addrx 0 is tombstoned) + .byte 3 + .byte 0 # DW_AT_low_pc: addrx index 0 + + # [0x1c] Abbrev 3: DW_TAG_subprogram (live - addrx 1 is real) + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + + .byte 0 # End Of Children Mark (namespace) + + # base_type (referenced by namespace's DW_AT_type) +.Lbase_type: + .byte 4 # Abbrev 4: DW_TAG_base_type + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_namespace, has children, DW_AT_type(ref4) + .byte 2 # Abbreviation Code + .byte 57 # DW_TAG_namespace + .byte 1 # DW_CHILDREN_yes + .byte 73 # DW_AT_type + .byte 19 # DW_FORM_ref4 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_base_type, no children, no attrs + .byte 4 # Abbreviation Code + .byte 36 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-retained-edge-exec.s b/tests/inputs/gc-retained-edge-exec.s new file mode 100644 index 0000000..c8e6292 --- /dev/null +++ b/tests/inputs/gc-retained-edge-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-retained-edge.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xcafebabe, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone) +# index 1: 0x8000 (live) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xcafebabe # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone + .quad 0x8000 # Index 1: live address +.Laddr_end: diff --git a/tests/inputs/gc-retained-namespace-dwo.s b/tests/inputs/gc-retained-namespace-dwo.s new file mode 100644 index 0000000..49a75f8 --- /dev/null +++ b/tests/inputs/gc-retained-namespace-dwo.s @@ -0,0 +1,64 @@ +# .dwo file for gc-retained-namespace.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# namespace (children, structurally retained as ancestor of live child) +# subprogram: DW_AT_low_pc = addrx 0 (will be tombstoned -> dead) +# subprogram: DW_AT_low_pc = addrx 1 (live) +# null (end namespace) +# null (end compile_unit) +# +# The DWO ID is 0xabcd. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xabcd # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # Abbrev 2: DW_TAG_namespace (has children) + .byte 2 + + # Abbrev 3: DW_TAG_subprogram (dead - addrx 0 is tombstoned) + .byte 3 + .byte 0 # DW_AT_low_pc: addrx index 0 + + # Abbrev 3: DW_TAG_subprogram (live - addrx 1 is real) + .byte 3 + .byte 1 # DW_AT_low_pc: addrx index 1 + + .byte 0 # End Of Children Mark (namespace) + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_namespace, has children, no attrs + .byte 2 # Abbreviation Code + .byte 57 # DW_TAG_namespace + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 3 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-retained-namespace-exec.s b/tests/inputs/gc-retained-namespace-exec.s new file mode 100644 index 0000000..5ecfebe --- /dev/null +++ b/tests/inputs/gc-retained-namespace-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-retained-namespace.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xabcd, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone) +# index 1: 0x2000 (live) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xabcd # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone + .quad 0x2000 # Index 1: live address +.Laddr_end: diff --git a/tests/inputs/gc-sibling-dwo.s b/tests/inputs/gc-sibling-dwo.s new file mode 100644 index 0000000..e0383d3 --- /dev/null +++ b/tests/inputs/gc-sibling-dwo.s @@ -0,0 +1,163 @@ +# .dwo file for gc-sibling.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram "live_func": DW_AT_low_pc = addrx 0 (live), DW_AT_sibling -> dead_func (children) +# base_type "int" +# subprogram "dead_func": DW_AT_low_pc = addrx 1 (tombstoned), DW_AT_sibling -> survivor (children) +# base_type "float" +# subprogram "survivor": DW_AT_low_pc = addrx 2 (live), no children +# structure_type "MyStruct" (children) +# subprogram "kept_method": DW_AT_low_pc = addrx 3 (live), DW_AT_sibling -> removed_method (children) +# base_type "char" +# subprogram "removed_method": DW_AT_low_pc = addrx 4 (tombstoned), no children +# structure_type "Neighbor" (children) +# subprogram "neighbor_method": DW_AT_low_pc = addrx 5 (live), no children +# +# The DWO ID is 0xdeadc0de. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeadc0de # DWO ID + # DIE tree begins at offset 0x14 (20 bytes into unit) + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # [0x15] Abbrev 2: DW_TAG_subprogram "live_func" (has children) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + .long .Ldead_func-.Ldebug_info_dwo_start+4 # DW_AT_sibling: ref4 -> dead_func + .asciz "live_func" # DW_AT_name + + # child: base_type "int" +.Lint: + .byte 3 # Abbrev 3: DW_TAG_base_type + .asciz "int" # DW_AT_name + .byte 4 # DW_AT_byte_size + .byte 5 # DW_AT_encoding (DW_ATE_signed) + + .byte 0 # End Of Children Mark (live_func) + + # [dead_func] Abbrev 2: DW_TAG_subprogram "dead_func" (has children) +.Ldead_func: + .byte 2 + .byte 1 # DW_AT_low_pc: addrx index 1 + .long .Lsurvivor-.Ldebug_info_dwo_start+4 # DW_AT_sibling: ref4 -> survivor + .asciz "dead_func" # DW_AT_name + + # child: base_type "float" +.Lfloat: + .byte 3 # Abbrev 3: DW_TAG_base_type + .asciz "float" # DW_AT_name + .byte 4 # DW_AT_byte_size + .byte 4 # DW_AT_encoding (DW_ATE_float) + + .byte 0 # End Of Children Mark (dead_func) + + # [survivor] Abbrev 4: DW_TAG_subprogram "survivor" (no children) +.Lsurvivor: + .byte 4 + .byte 2 # DW_AT_low_pc: addrx index 2 + .asciz "survivor" # DW_AT_name + + # Abbrev 5: DW_TAG_structure_type "MyStruct" (has children) + .byte 5 + .asciz "MyStruct" # DW_AT_name + + # Abbrev 2: DW_TAG_subprogram "kept_method" (has children, with sibling) + .byte 2 + .byte 3 # DW_AT_low_pc: addrx index 3 + .long .Lremoved_method-.Ldebug_info_dwo_start+4 # DW_AT_sibling: ref4 -> removed_method + .asciz "kept_method" # DW_AT_name + + # child: base_type "char" + .byte 3 # Abbrev 3: DW_TAG_base_type + .asciz "char" # DW_AT_name + .byte 1 # DW_AT_byte_size + .byte 8 # DW_AT_encoding (DW_ATE_unsigned_char) + + .byte 0 # End Of Children Mark (kept_method) + + # Abbrev 4: DW_TAG_subprogram "removed_method" (no children) +.Lremoved_method: + .byte 4 + .byte 4 # DW_AT_low_pc: addrx index 4 + .asciz "removed_method" # DW_AT_name + + .byte 0 # End Of Children Mark (MyStruct) + + # Abbrev 5: DW_TAG_structure_type "Neighbor" (has children) + .byte 5 + .asciz "Neighbor" # DW_AT_name + + # Abbrev 4: DW_TAG_subprogram "neighbor_method" (no children) + .byte 4 + .byte 5 # DW_AT_low_pc: addrx index 5 + .asciz "neighbor_method" # DW_AT_name + + .byte 0 # End Of Children Mark (Neighbor) + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, has children, DW_AT_low_pc(addrx) + DW_AT_sibling(ref4) + DW_AT_name(string) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 1 # DW_AT_sibling + .byte 19 # DW_FORM_ref4 + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_base_type, no children, DW_AT_name(string) + DW_AT_byte_size(data1) + DW_AT_encoding(data1) + .byte 3 # Abbreviation Code + .byte 36 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 11 # DW_AT_byte_size + .byte 11 # DW_FORM_data1 + .byte 62 # DW_AT_encoding + .byte 11 # DW_FORM_data1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_name(string) + .byte 4 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 5: DW_TAG_structure_type, has children, DW_AT_name(string) + .byte 5 # Abbreviation Code + .byte 19 # DW_TAG_structure_type + .byte 1 # DW_CHILDREN_yes + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-sibling-exec.s b/tests/inputs/gc-sibling-exec.s new file mode 100644 index 0000000..94373b0 --- /dev/null +++ b/tests/inputs/gc-sibling-exec.s @@ -0,0 +1,58 @@ +# Fake executable for gc-sibling.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xdeadc0de, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Six 8-byte addresses: +# index 0: 0x1000 (live - live_func) +# index 1: 0xffffffffffffffff (tombstone - dead_func) +# index 2: 0x2000 (live - survivor) +# index 3: 0x3000 (live - kept_method) +# index 4: 0xffffffffffffffff (tombstone - removed_method) +# index 5: 0x4000 (live - neighbor_method) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeadc0de # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0x1000 # Index 0: live (live_func) + .quad 0xffffffffffffffff # Index 1: tombstone (dead_func) + .quad 0x2000 # Index 2: live (survivor) + .quad 0x3000 # Index 3: live (kept_method) + .quad 0xffffffffffffffff # Index 4: tombstone (removed_method) + .quad 0x4000 # Index 5: live (neighbor_method) +.Laddr_end: diff --git a/tests/inputs/gc-type-unit-no-compact-dwo.s b/tests/inputs/gc-type-unit-no-compact-dwo.s new file mode 100644 index 0000000..b13cfad --- /dev/null +++ b/tests/inputs/gc-type-unit-no-compact-dwo.s @@ -0,0 +1,222 @@ +# .dwo file for gc-type-unit-no-compact.test +# +# DWARF5 split-compile unit AND a split type unit in the same .debug_info.dwo. +# The presence of the type unit should prevent compaction of rnglists, +# loclists, and str_offsets offset tables, because the type unit shares +# those sections with the compilation unit. +# +# Compile unit (DW_UT_split_compile, DWO ID 0xee): +# compile_unit (children) +# subprogram "live_func" (rnglistx 0, live, has children) +# variable "v": DW_AT_location = loclistx 0 +# subprogram "dead_func" (rnglistx 1, tombstoned, has children) +# variable "w": DW_AT_location = loclistx 1 +# +# Type unit (DW_UT_split_type, type sig 0x1234): +# type_unit (children) +# base_type "int": DW_AT_name = strx1 4 -> "int" +# +# .debug_str_offsets.dwo has 5 entries (indices 0-4). +# .debug_rnglists.dwo has 2 range lists. +# .debug_loclists.dwo has 2 location lists. +# +# After GC, dead_func and its variable should be removed from .debug_info, +# but the rnglists/loclists/str_offsets offset tables must NOT be compacted. + + .section .debug_info.dwo,"e",@progbits + + # --- Compile unit --- + .long .Ldebug_info_cu_end-.Ldebug_info_cu_start # Length of Unit +.Ldebug_info_cu_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xee # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + .byte 0 # DW_AT_name: strx1 index 0 -> "test.c" + .byte 1 # DW_AT_comp_dir: strx1 index 1 -> "/home" + .long .Lrnglists_table_base-.Lrnglists_section_start # DW_AT_rnglists_base + .long .Lloclists_table_base-.Lloclists_section_start # DW_AT_loclists_base + + # Abbrev 2: DW_TAG_subprogram "live_func" (rnglistx 0, has children) + .byte 2 + .byte 0 # DW_AT_ranges: rnglistx index 0 + .byte 2 # DW_AT_name: strx1 index 2 -> "live_func" + + # Abbrev 5: DW_TAG_variable "v" (loclistx 0) + .byte 5 + .byte 0 # DW_AT_location: loclistx index 0 + + .byte 0 # End of live_func children + + # Abbrev 2: DW_TAG_subprogram "dead_func" (rnglistx 1, has children) + .byte 2 + .byte 1 # DW_AT_ranges: rnglistx index 1 + .byte 3 # DW_AT_name: strx1 index 3 -> "dead_func" + + # Abbrev 5: DW_TAG_variable "w" (loclistx 1) + .byte 5 + .byte 1 # DW_AT_location: loclistx index 1 + + .byte 0 # End of dead_func children + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_cu_end: + + # --- Type unit --- +.Ldebug_info_tu_header: + .long .Ldebug_info_tu_end-.Ldebug_info_tu_start # Length of Unit +.Ldebug_info_tu_start: + .short 5 # DWARF version number + .byte 6 # DW_UT_split_type + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x1234 # Type signature + .long .Ltu_type_die-.Ldebug_info_tu_header # Type DIE offset (from unit start) + + # Abbrev 3: DW_TAG_type_unit (has children) + .byte 3 + +.Ltu_type_die: + # Abbrev 4: DW_TAG_base_type "int" + .byte 4 + .byte 4 # DW_AT_name: strx1 index 4 -> "int" + + .byte 0 # End Of Children Mark (type_unit) +.Ldebug_info_tu_end: + + .section .debug_str.dwo,"eMS",@progbits,1 +.Lstr0: + .asciz "test.c" +.Lstr1: + .asciz "/home" +.Lstr2: + .asciz "live_func" +.Lstr3: + .asciz "dead_func" +.Lstr4: + .asciz "int" + + .section .debug_str_offsets.dwo,"e",@progbits + .long .Lstr_offsets_end-.Lstr_offsets_start # Unit length +.Lstr_offsets_start: + .short 5 # Version + .short 0 # Padding + .long .Lstr0-.debug_str.dwo # Index 0: "test.c" + .long .Lstr1-.debug_str.dwo # Index 1: "/home" + .long .Lstr2-.debug_str.dwo # Index 2: "live_func" + .long .Lstr3-.debug_str.dwo # Index 3: "dead_func" + .long .Lstr4-.debug_str.dwo # Index 4: "int" +.Lstr_offsets_end: + + .section .debug_rnglists.dwo,"e",@progbits +.Lrnglists_section_start: + .long .Lrnglists_end-.Lrnglists_start # Unit length +.Lrnglists_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size + .long 2 # Offset entry count +.Lrnglists_table_base: + .long .Lrangelist0-.Lrnglists_table_base # Offset of range list 0 + .long .Lrangelist1-.Lrnglists_table_base # Offset of range list 1 +.Lrangelist0: + # Range list 0 (live_func): DW_RLE_startx_length addrx 0 (live), length 0x20 + .byte 0x03 # DW_RLE_startx_length + .byte 0 # Start address index (ULEB128): addrx 0 + .byte 0x20 # Length (ULEB128): 32 bytes + .byte 0x00 # DW_RLE_end_of_list +.Lrangelist1: + # Range list 1 (dead_func): DW_RLE_startx_length addrx 1 (tombstoned), length 0x10 + .byte 0x03 # DW_RLE_startx_length + .byte 1 # Start address index (ULEB128): addrx 1 + .byte 0x10 # Length (ULEB128): 16 bytes + .byte 0x00 # DW_RLE_end_of_list +.Lrnglists_end: + + .section .debug_loclists.dwo,"e",@progbits +.Lloclists_section_start: + .long .Lloclists_end-.Lloclists_start # Unit length +.Lloclists_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size + .long 2 # Offset entry count +.Lloclists_table_base: + .long .Lloclist0-.Lloclists_table_base # Offset of location list 0 + .long .Lloclist1-.Lloclists_table_base # Offset of location list 1 +.Lloclist0: + # Location list 0 (v in live_func): DW_LLE_default_location with DW_OP_reg0 + .byte 0x05 # DW_LLE_default_location + .byte 1 # Expression length (ULEB128): 1 byte + .byte 0x50 # DW_OP_reg0 + .byte 0x00 # DW_LLE_end_of_list +.Lloclist1: + # Location list 1 (w in dead_func): DW_LLE_default_location with DW_OP_reg1 + .byte 0x05 # DW_LLE_default_location + .byte 1 # Expression length (ULEB128): 1 byte + .byte 0x51 # DW_OP_reg1 + .byte 0x00 # DW_LLE_end_of_list +.Lloclists_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, + # DW_AT_name(strx1) + DW_AT_comp_dir(strx1) + + # DW_AT_rnglists_base(sec_offset) + DW_AT_loclists_base(sec_offset) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 3 # DW_AT_name + .byte 0x25 # DW_FORM_strx1 + .byte 0x1b # DW_AT_comp_dir + .byte 0x25 # DW_FORM_strx1 + .byte 0x74 # DW_AT_rnglists_base + .byte 23 # DW_FORM_sec_offset + .byte 0x8c, 0x01 # DW_AT_loclists_base (ULEB128: 140) + .byte 23 # DW_FORM_sec_offset + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, has children, + # DW_AT_ranges(rnglistx) + DW_AT_name(strx1) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 0x55 # DW_AT_ranges + .byte 0x23 # DW_FORM_rnglistx + .byte 3 # DW_AT_name + .byte 0x25 # DW_FORM_strx1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_type_unit, has children (no attributes) + .byte 3 # Abbreviation Code + .byte 0x41 # DW_TAG_type_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_base_type, no children, + # DW_AT_name(strx1) + .byte 4 # Abbreviation Code + .byte 0x24 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 0x25 # DW_FORM_strx1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 5: DW_TAG_variable, no children, + # DW_AT_location(loclistx) + .byte 5 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 0x22 # DW_FORM_loclistx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-type-unit-no-compact-exec.s b/tests/inputs/gc-type-unit-no-compact-exec.s new file mode 100644 index 0000000..b2feb30 --- /dev/null +++ b/tests/inputs/gc-type-unit-no-compact-exec.s @@ -0,0 +1,50 @@ +# Fake executable for gc-type-unit-no-compact.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xee, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Two 8-byte addresses: +# index 0: 0x1000 (live, live_func) +# index 1: 0xffffffffffffffff (tombstone, dead_func) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xee # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0x1000 # Index 0: live address (live_func) + .quad 0xffffffffffffffff # Index 1: tombstone (dead_func) +.Laddr_end: diff --git a/tests/inputs/gc-type-unit-no-compact-v4-dwo.s b/tests/inputs/gc-type-unit-no-compact-v4-dwo.s new file mode 100644 index 0000000..64e22f2 --- /dev/null +++ b/tests/inputs/gc-type-unit-no-compact-v4-dwo.s @@ -0,0 +1,142 @@ +# .dwo file for gc-type-unit-no-compact-v4.test +# +# DWARF4 split-compile unit AND a type unit in a SEPARATE .debug_types.dwo +# section. The presence of the type unit should prevent compaction of +# .debug_str_offsets.dwo, because the type unit shares that section with the +# compilation unit. +# +# Compile unit in .debug_info.dwo (DW_AT_GNU_dwo_id = 0xee): +# compile_unit (children) +# subprogram "live_func" (GNU_addr_index 0, live) +# subprogram "dead_func" (GNU_addr_index 1, tombstoned) +# +# Type unit in .debug_types.dwo (type sig 0x1234): +# type_unit (children) +# base_type "int": DW_AT_name = GNU_str_index 4 -> "int" +# +# .debug_str_offsets.dwo has 5 entries (indices 0-4), NO header (DWARF4). +# +# After GC, dead_func should be removed from .debug_info, but the +# str_offsets table must NOT be compacted because the type unit in +# .debug_types.dwo also references entries in it. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + # DW_AT_name: GNU_str_index 0 -> "test.c" + .byte 0 + # DW_AT_comp_dir: GNU_str_index 1 -> "/home" + .byte 1 + # DW_AT_GNU_dwo_id: data8 = 0xee + .quad 0xee + + # Abbrev 2: DW_TAG_subprogram "live_func" (GNU_addr_index 0, live) + .byte 2 + .byte 0 # DW_AT_low_pc: GNU_addr_index index 0 + # DW_AT_name: GNU_str_index 2 -> "live_func" + .byte 2 + + # Abbrev 2: DW_TAG_subprogram "dead_func" (GNU_addr_index 1, tombstoned) + .byte 2 + .byte 1 # DW_AT_low_pc: GNU_addr_index index 1 + # DW_AT_name: GNU_str_index 3 -> "dead_func" + .byte 3 + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_types.dwo,"e",@progbits + + # --- Type unit --- + .long .Ldebug_types_tu_end-.Ldebug_types_tu_start # Length of Unit +.Ldebug_types_tu_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + .quad 0x1234 # Type signature + .long .Ltu_type_die-.Ldebug_types_tu_start # Type DIE offset + + # Abbrev 3: DW_TAG_type_unit (has children) + .byte 3 + +.Ltu_type_die: + # Abbrev 4: DW_TAG_base_type "int" + .byte 4 + # DW_AT_name: GNU_str_index 4 -> "int" + .byte 4 + + .byte 0 # End Of Children Mark (type_unit) +.Ldebug_types_tu_end: + + .section .debug_str.dwo,"eMS",@progbits,1 +.Lstr0: + .asciz "test.c" +.Lstr1: + .asciz "/home" +.Lstr2: + .asciz "live_func" +.Lstr3: + .asciz "dead_func" +.Lstr4: + .asciz "int" + + .section .debug_str_offsets.dwo,"e",@progbits + # DWARF4: no header, just raw offset entries (4 bytes each, DWARF32) + .long .Lstr0-.debug_str.dwo # Index 0: "test.c" + .long .Lstr1-.debug_str.dwo # Index 1: "/home" + .long .Lstr2-.debug_str.dwo # Index 2: "live_func" + .long .Lstr3-.debug_str.dwo # Index 3: "dead_func" + .long .Lstr4-.debug_str.dwo # Index 4: "int" + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, + # DW_AT_name(GNU_str_index) + DW_AT_comp_dir(GNU_str_index) + + # DW_AT_GNU_dwo_id(data8) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .uleb128 3 # DW_AT_name + .uleb128 0x1f02 # DW_FORM_GNU_str_index + .uleb128 0x1b # DW_AT_comp_dir + .uleb128 0x1f02 # DW_FORM_GNU_str_index + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, + # DW_AT_low_pc(GNU_addr_index) + DW_AT_name(GNU_str_index) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .uleb128 17 # DW_AT_low_pc + .uleb128 0x1f01 # DW_FORM_GNU_addr_index + .uleb128 3 # DW_AT_name + .uleb128 0x1f02 # DW_FORM_GNU_str_index + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_type_unit, has children (no attributes) + .byte 3 # Abbreviation Code + .byte 0x41 # DW_TAG_type_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_base_type, no children, + # DW_AT_name(GNU_str_index) + .byte 4 # Abbreviation Code + .byte 0x24 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .uleb128 3 # DW_AT_name + .uleb128 0x1f02 # DW_FORM_GNU_str_index + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-type-unit-no-compact-v4-exec.s b/tests/inputs/gc-type-unit-no-compact-v4-exec.s new file mode 100644 index 0000000..c25e8f5 --- /dev/null +++ b/tests/inputs/gc-type-unit-no-compact-v4-exec.s @@ -0,0 +1,45 @@ +# Fake executable for gc-type-unit-no-compact-v4.test +# +# DWARF4 skeleton CU (GNU extension) containing: +# .debug_info: DW_TAG_compile_unit with DW_AT_GNU_dwo_id = 0xee, +# DW_AT_GNU_addr_base, and DW_AT_GNU_dwo_name +# .debug_addr: Two 8-byte addresses (no header, DWARF4 style): +# index 0: 0x1000 (live, live_func) +# index 1: 0xffffffffffffffff (tombstone, dead_func) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 4 # DWARF version number + .long 0 # Offset Into Abbrev. Section + .byte 8 # Address Size + + # [0x0b] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .quad 0xee # DW_AT_GNU_dwo_id + .long .Laddr_table_base # DW_AT_GNU_addr_base + .asciz "DWO_PATH" # DW_AT_GNU_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .uleb128 0x2131 # DW_AT_GNU_dwo_id + .uleb128 7 # DW_FORM_data8 + .uleb128 0x2133 # DW_AT_GNU_addr_base + .uleb128 23 # DW_FORM_sec_offset + .uleb128 0x2130 # DW_AT_GNU_dwo_name + .uleb128 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF4: no header, just raw address entries. + # DW_AT_GNU_addr_base points directly to the start of entries. +.Laddr_table_base: + .quad 0x1000 # Index 0: live address (live_func) + .quad 0xffffffffffffffff # Index 1: tombstone (dead_func) diff --git a/tests/inputs/gc-typed-expr-dwo.s b/tests/inputs/gc-typed-expr-dwo.s new file mode 100644 index 0000000..0d76eee --- /dev/null +++ b/tests/inputs/gc-typed-expr-dwo.s @@ -0,0 +1,145 @@ +# .dwo file for gc-typed-expr.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram "dead_func": DW_AT_low_pc = addrx 0 (tombstoned -> dead), +# DW_AT_type = ref4 -> dead_type +# base_type "dead_type": byte_size=8, encoding=DW_ATE_signed (only reachable +# from dead_func, so removed by GC) +# subprogram "conv_func": DW_AT_low_pc = addrx 1 (live), has children +# variable "v1": DW_AT_location = exprloc { DW_OP_lit0, DW_OP_convert , DW_OP_stack_value } +# null +# base_type "conv_type": byte_size=4, encoding=DW_ATE_signed +# subprogram "ct_func": DW_AT_low_pc = addrx 2 (live), has children +# variable "v2": DW_AT_location = exprloc { DW_OP_const_type 4 <4 bytes> } +# null +# base_type "ct_type": byte_size=4, encoding=DW_ATE_float +# null +# +# DWO ID is 0x77. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x77 # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # Abbrev 2: DW_TAG_subprogram (dead - addrx 0 is tombstoned, DW_AT_type -> dead_type) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + .long .Ldead_type-.Ldebug_info_dwo_start+4 # DW_AT_type: ref4 to dead_type + + # Abbrev 3: DW_TAG_base_type (dead_type, only reachable from dead_func) +.Ldead_type: + .byte 3 + .asciz "dt" # DW_AT_name + .byte 8 # DW_AT_byte_size + .byte 5 # DW_AT_encoding: DW_ATE_signed + + # Abbrev 4: DW_TAG_subprogram (conv_func - addrx 1, live, has children) + .byte 4 + .byte 1 # DW_AT_low_pc: addrx index 1 + + # Abbrev 5: DW_TAG_variable (v1) + # DW_AT_location: exprloc { DW_OP_lit0, DW_OP_convert , DW_OP_stack_value } + .byte 5 + .byte 4 # exprloc length = 4 bytes + .byte 0x30 # DW_OP_lit0 + .byte 0xa8 # DW_OP_convert + .byte .Lconv_type-.Ldebug_info_dwo_start+4 # ULEB128 CU-relative offset of conv_type + .byte 0x9f # DW_OP_stack_value + + .byte 0 # End Of Children Mark (conv_func) + + # Abbrev 3: DW_TAG_base_type (conv_type, kept alive by DW_OP_convert) +.Lconv_type: + .byte 3 + .asciz "it" # DW_AT_name + .byte 4 # DW_AT_byte_size + .byte 5 # DW_AT_encoding: DW_ATE_signed + + # Abbrev 4: DW_TAG_subprogram (ct_func - addrx 2, live, has children) + .byte 4 + .byte 2 # DW_AT_low_pc: addrx index 2 + + # Abbrev 5: DW_TAG_variable (v2) + # DW_AT_location: exprloc { DW_OP_const_type 4 <4 bytes of data> } + .byte 5 + .byte 7 # exprloc length = 7 bytes + .byte 0xa4 # DW_OP_const_type + .byte .Lct_type-.Ldebug_info_dwo_start+4 # ULEB128 CU-relative offset of ct_type + .byte 4 # size = 4 bytes + .byte 0x01 # literal data byte 0 + .byte 0x00 # literal data byte 1 + .byte 0x00 # literal data byte 2 + .byte 0x00 # literal data byte 3 + + .byte 0 # End Of Children Mark (ct_func) + + # Abbrev 3: DW_TAG_base_type (ct_type, kept alive by DW_OP_const_type) +.Lct_type: + .byte 3 + .asciz "ft" # DW_AT_name + .byte 4 # DW_AT_byte_size + .byte 4 # DW_AT_encoding: DW_ATE_float + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + DW_AT_type(ref4) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 73 # DW_AT_type + .byte 19 # DW_FORM_ref4 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_base_type, no children, DW_AT_name(string) + DW_AT_byte_size(data1) + DW_AT_encoding(data1) + .byte 3 # Abbreviation Code + .byte 36 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 11 # DW_AT_byte_size + .byte 11 # DW_FORM_data1 + .byte 62 # DW_AT_encoding + .byte 11 # DW_FORM_data1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_subprogram, has children, DW_AT_low_pc(addrx) + .byte 4 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 1 # DW_CHILDREN_yes + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 5: DW_TAG_variable, no children, DW_AT_location(exprloc) + .byte 5 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 2 # DW_AT_location + .byte 24 # DW_FORM_exprloc + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-typed-expr-exec.s b/tests/inputs/gc-typed-expr-exec.s new file mode 100644 index 0000000..9028ffd --- /dev/null +++ b/tests/inputs/gc-typed-expr-exec.s @@ -0,0 +1,52 @@ +# Fake executable for gc-typed-expr.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0x77, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Three 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone for dead_func) +# index 1: 0x1000 (live for conv_func) +# index 2: 0x2000 (live for ct_func) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0x77 # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func) + .quad 0x1000 # Index 1: live address (conv_func) + .quad 0x2000 # Index 2: live address (ct_func) +.Laddr_end: diff --git a/tests/inputs/gc-unreferenced-dwo-dwo.s b/tests/inputs/gc-unreferenced-dwo-dwo.s new file mode 100644 index 0000000..dad57d5 --- /dev/null +++ b/tests/inputs/gc-unreferenced-dwo-dwo.s @@ -0,0 +1,71 @@ +# .dwo file for gc-unreferenced-dwo.test +# +# DWARF5 split-compile unit with DWO ID 0xbb, which does NOT match +# the executable (which references 0xaa). The .dwo has str_offsets. +# +# When passed via -i with --gc, the unit should still be included in +# the output with its str_offsets section intact, even though GC +# cannot be performed (no executable data for this dwo_id). + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xbb # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + .byte 0 # DW_AT_name: strx1 index 0 -> "other.c" + .byte 1 # DW_AT_comp_dir: strx1 index 1 -> "/src" + + # Abbrev 2: DW_TAG_subprogram (no children) + .byte 2 + .byte 2 # DW_AT_name: strx1 index 2 -> "some_func" + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_str.dwo,"eMS",@progbits,1 +.Lstr0: + .asciz "other.c" +.Lstr1: + .asciz "/src" +.Lstr2: + .asciz "some_func" + + .section .debug_str_offsets.dwo,"e",@progbits + .long .Lstr_offsets_end-.Lstr_offsets_start # Unit length +.Lstr_offsets_start: + .short 5 # Version + .short 0 # Padding + .long .Lstr0-.debug_str.dwo # Index 0: "other.c" + .long .Lstr1-.debug_str.dwo # Index 1: "/src" + .long .Lstr2-.debug_str.dwo # Index 2: "some_func" +.Lstr_offsets_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, + # DW_AT_name(strx1) + DW_AT_comp_dir(strx1) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 3 # DW_AT_name + .byte 0x25 # DW_FORM_strx1 + .byte 0x1b # DW_AT_comp_dir + .byte 0x25 # DW_FORM_strx1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_name(strx1) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 0x25 # DW_FORM_strx1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-unreferenced-dwo-exec.s b/tests/inputs/gc-unreferenced-dwo-exec.s new file mode 100644 index 0000000..a28eea6 --- /dev/null +++ b/tests/inputs/gc-unreferenced-dwo-exec.s @@ -0,0 +1,44 @@ +# Fake executable for gc-unreferenced-dwo.test +# +# Contains a skeleton CU with DWO ID 0xaa. This does NOT match the +# .dwo file's DWO ID (0xbb), so the .dwo's CU will hit the early +# return in maybe_gc (no executable data for that dwo_id). + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xaa # DWO ID + + # Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + .quad 0x1000 # Index 0: some live address +.Laddr_end: diff --git a/tests/inputs/gc-unreferenced-dwo-matching-dwo.s b/tests/inputs/gc-unreferenced-dwo-matching-dwo.s new file mode 100644 index 0000000..ef6db06 --- /dev/null +++ b/tests/inputs/gc-unreferenced-dwo-matching-dwo.s @@ -0,0 +1,40 @@ +# Minimal .dwo file with DWO ID 0xaa that matches the executable. +# Used alongside gc-unreferenced-dwo-dwo.s (0xbb) in the test. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xaa # DWO ID + + # Abbrev 1: DW_TAG_compile_unit (no children) + .byte 1 + .byte 0 # DW_AT_name: strx1 index 0 -> "main.c" +.Ldebug_info_dwo_end: + + .section .debug_str.dwo,"eMS",@progbits,1 +.Lstr0: + .asciz "main.c" + + .section .debug_str_offsets.dwo,"e",@progbits + .long .Lstr_offsets_end-.Lstr_offsets_start # Unit length +.Lstr_offsets_start: + .short 5 # Version + .short 0 # Padding + .long .Lstr0-.debug_str.dwo # Index 0: "main.c" +.Lstr_offsets_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children, DW_AT_name(strx1) + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 0x25 # DW_FORM_strx1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-variable-root-dwo.s b/tests/inputs/gc-variable-root-dwo.s new file mode 100644 index 0000000..038f1c7 --- /dev/null +++ b/tests/inputs/gc-variable-root-dwo.s @@ -0,0 +1,110 @@ +# .dwo file for gc-variable-root.test +# +# DWARF5 split-compile unit containing: +# compile_unit (children) +# subprogram: DW_AT_low_pc = addrx 0 (will be tombstoned -> dead) +# variable "live_var": DW_AT_location = exprloc { DW_OP_addrx 1 } (live), +# DW_AT_type = ref4 -> base_type +# variable "dead_var": DW_AT_location = exprloc { DW_OP_addrx 2 } (tombstoned -> dead) +# base_type "int": name, byte_size=4, encoding=DW_ATE_signed +# (kept alive by live_var's DW_AT_type) +# +# The DWO ID is 0xdeadbeef. + + .section .debug_info.dwo,"e",@progbits + .long .Ldebug_info_dwo_end-.Ldebug_info_dwo_start # Length of Unit +.Ldebug_info_dwo_start: + .short 5 # DWARF version number + .byte 5 # DW_UT_split_compile + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeadbeef # DWO ID + # DIE tree begins at offset 0x14 (20 bytes into unit) + + # [0x14] Abbrev 1: DW_TAG_compile_unit (has children) + .byte 1 + + # Abbrev 2: DW_TAG_subprogram (dead - addrx 0 is tombstoned) + .byte 2 + .byte 0 # DW_AT_low_pc: addrx index 0 + + # Abbrev 3: DW_TAG_variable "live_var" (live - addrx 1 is real) + .byte 3 + .asciz "live_var" # DW_AT_name + .byte 2 # DW_AT_location: exprloc length (2 bytes) + .byte 0xa1 # DW_OP_addrx + .byte 1 # ULEB128 address index 1 + .long .Lbase_type-.Ldebug_info_dwo_start+4 # DW_AT_type: ref4 to base_type + + # Abbrev 4: DW_TAG_variable "dead_var" (dead - addrx 2 is tombstoned) + .byte 4 + .asciz "dead_var" # DW_AT_name + .byte 2 # DW_AT_location: exprloc length (2 bytes) + .byte 0xa1 # DW_OP_addrx + .byte 2 # ULEB128 address index 2 + + # base_type "int" (referenced by live_var's DW_AT_type) +.Lbase_type: + .byte 5 # Abbrev 5: DW_TAG_base_type + .asciz "int" # DW_AT_name + .byte 4 # DW_AT_byte_size + .byte 5 # DW_AT_encoding: DW_ATE_signed + + .byte 0 # End Of Children Mark (compile_unit) +.Ldebug_info_dwo_end: + + .section .debug_abbrev.dwo,"e",@progbits + # Abbrev 1: DW_TAG_compile_unit, has children, no attrs + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 1 # DW_CHILDREN_yes + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 2: DW_TAG_subprogram, no children, DW_AT_low_pc(addrx) + .byte 2 # Abbreviation Code + .byte 46 # DW_TAG_subprogram + .byte 0 # DW_CHILDREN_no + .byte 17 # DW_AT_low_pc + .byte 27 # DW_FORM_addrx + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 3: DW_TAG_variable, no children, DW_AT_name(string) + DW_AT_location(exprloc) + DW_AT_type(ref4) + .byte 3 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 2 # DW_AT_location + .byte 24 # DW_FORM_exprloc + .byte 73 # DW_AT_type + .byte 19 # DW_FORM_ref4 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 4: DW_TAG_variable, no children, DW_AT_name(string) + DW_AT_location(exprloc) + .byte 4 # Abbreviation Code + .byte 52 # DW_TAG_variable + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 2 # DW_AT_location + .byte 24 # DW_FORM_exprloc + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + # Abbrev 5: DW_TAG_base_type, no children, DW_AT_name(string) + DW_AT_byte_size(data1) + DW_AT_encoding(data1) + .byte 5 # Abbreviation Code + .byte 36 # DW_TAG_base_type + .byte 0 # DW_CHILDREN_no + .byte 3 # DW_AT_name + .byte 8 # DW_FORM_string + .byte 11 # DW_AT_byte_size + .byte 11 # DW_FORM_data1 + .byte 62 # DW_AT_encoding + .byte 11 # DW_FORM_data1 + .byte 0 # EOM(1) + .byte 0 # EOM(2) + + .byte 0 # End of abbreviations diff --git a/tests/inputs/gc-variable-root-exec.s b/tests/inputs/gc-variable-root-exec.s new file mode 100644 index 0000000..86c31b3 --- /dev/null +++ b/tests/inputs/gc-variable-root-exec.s @@ -0,0 +1,52 @@ +# Fake executable for gc-variable-root.test +# +# Contains: +# .debug_info: DWARF5 skeleton CU (DW_UT_skeleton) with DWO ID 0xdeadbeef, +# DW_AT_addr_base, and DW_AT_dwo_name +# .debug_addr: Three 8-byte addresses: +# index 0: 0xffffffffffffffff (tombstone for dead_func) +# index 1: 0x4000 (live address for live_var) +# index 2: 0xffffffffffffffff (tombstone for dead_var) +# .debug_abbrev: abbreviation table for the skeleton CU + + .section .debug_info,"",@progbits + .long .Ldebug_info_end-.Ldebug_info_start # Length of Unit +.Ldebug_info_start: + .short 5 # DWARF version number + .byte 4 # DW_UT_skeleton + .byte 8 # Address Size + .long 0 # Offset Into Abbrev. Section + .quad 0xdeadbeef # DWO ID + + # [0x14] Abbrev 1: DW_TAG_compile_unit + .byte 1 + .long .Laddr_table_base # DW_AT_addr_base + .asciz "DWO_PATH" # DW_AT_dwo_name (substituted by sed) +.Ldebug_info_end: + + .section .debug_abbrev,"",@progbits + # Abbrev 1: DW_TAG_compile_unit, no children + .byte 1 # Abbreviation Code + .byte 17 # DW_TAG_compile_unit + .byte 0 # DW_CHILDREN_no + .byte 115 # DW_AT_addr_base + .byte 23 # DW_FORM_sec_offset + .byte 118 # DW_AT_dwo_name + .byte 8 # DW_FORM_string + .byte 0 # EOM(1) + .byte 0 # EOM(2) + .byte 0 # End of abbreviations + + .section .debug_addr,"",@progbits + # DWARF5 .debug_addr header + .long .Laddr_end-.Laddr_start # Length +.Laddr_start: + .short 5 # Version + .byte 8 # Address size + .byte 0 # Segment selector size +.Laddr_table_base: + # Address table entries (DW_AT_addr_base points here) + .quad 0xffffffffffffffff # Index 0: tombstone (dead_func) + .quad 0x4000 # Index 1: live address (live_var) + .quad 0xffffffffffffffff # Index 2: tombstone (dead_var) +.Laddr_end: diff --git a/tests/loclists.s b/tests/loclists.s index d4f85cd..4f2bbfe 100644 --- a/tests/loclists.s +++ b/tests/loclists.s @@ -13,12 +13,12 @@ # CHECK-NEXT: DW_LLE_offset_pair (0x0000000000000004, 0x0000000000000008): DW_OP_reg3 RBX # CHECK-DAG: .debug_cu_index contents: -# CHECK: Index Signature INFO ABBREV LOCLISTS -# CHECK: 1 {{.*}} [0x00000018, 0x0000002d) [0x00000000, 0x00000004) [0x00000000, 0x0000001d) +# CHECK: Index Signature INFO ABBREV LOCLISTS +# CHECK: 1 {{.*}} [0x0000000000000018, 0x000000000000002d) [0x00000000, 0x00000004) [0x00000000, 0x0000001d) # CHECK-DAG: .debug_tu_index contents: -# CHECK: Index Signature INFO ABBREV LOCLISTS -# CHECK: 2 {{.*}} [0x00000000, 0x00000018) [0x00000000, 0x00000004) [0x00000000, 0x0000001d) +# CHECK: Index Signature INFO ABBREV LOCLISTS +# CHECK: 2 {{.*}} [0x0000000000000000, 0x0000000000000018) [0x00000000, 0x00000004) [0x00000000, 0x0000001d) .section .debug_info.dwo,"e",@progbits .long .Ldebug_info_dwo_end0-.Ldebug_info_dwo_start0 # Length of Unit diff --git a/tests/merge.test b/tests/merge.test index 9068abd..b40fc05 100644 --- a/tests/merge.test +++ b/tests/merge.test @@ -10,21 +10,21 @@ CHECK-LABEL: Abbrev table for offset: CHECK: 0x0000[[BAOFF:.*]] CHECK: .debug_info.dwo contents: -CHECK: [[COFF:0x[0-9a-f]*]]: +CHECK: 0x[[#%.8x,COFF:]]: CHECK-LABEL: Compile Unit: length = {{.*}}, version = 0x0004, abbr_offset = -CHECK: 0x[[CAOFF]], addr_size = 0x08 (next unit at [[AOFF:.*]]) +CHECK: 0x[[CAOFF]], addr_size = 0x08 (next unit at 0x[[#%.8x,AOFF:]]) CHECK: DW_AT_GNU_dwo_id {{.*}} ([[DWOC:.*]]) -CHECK: [[AOFF]]: +CHECK: [[#AOFF]]: CHECK-LABEL: Compile Unit: length = {{.*}}, version = 0x0004, abbr_offset = -CHECK: 0x[[AAOFF]], addr_size = 0x08 (next unit at [[BOFF:.*]]) +CHECK: 0x[[AAOFF]], addr_size = 0x08 (next unit at 0x[[#%.8x,BOFF:]]) CHECK: DW_AT_GNU_dwo_id {{.*}} ([[DWOA:.*]]) -CHECK: [[BOFF]]: +CHECK: [[#BOFF]]: CHECK-LABEL: Compile Unit: length = {{.*}}, version = 0x0004, abbr_offset = -CHECK: 0x[[BAOFF]], addr_size = 0x08 (next unit at [[XOFF:.*]]) +CHECK: 0x[[BAOFF]], addr_size = 0x08 (next unit at 0x[[#%.8x,XOFF:]]) CHECK: DW_AT_GNU_dwo_id {{.*}} ([[DWOB:.*]]) CHECK-LABEL: .debug_cu_index -CHECK: Index Signature INFO ABBREV LINE STR_OFFSETS -CHECK-DAG: [[DWOC]] {{\[}}[[COFF]], [[AOFF]]) [0x0000[[CAOFF]], 0x0000[[AAOFF]]) [0x00000000, 0x00000011) [0x00000000, 0x00000018) -CHECK-DAG: [[DWOA]] {{\[}}[[AOFF]], [[BOFF]]) [0x0000[[AAOFF]], 0x0000[[BAOFF]]) [0x00000011, 0x00000022) [0x00000018, 0x00000028) -CHECK-DAG: [[DWOB]] {{\[}}[[BOFF]], [[XOFF]]) [0x0000[[BAOFF]], 0x000000c3) [0x00000022, 0x00000033) [0x00000028, 0x0000003c) +CHECK: Index Signature INFO ABBREV LINE STR_OFFSETS +CHECK-DAG: [[DWOC]] [0x00000000[[#COFF]], 0x00000000[[#AOFF]]) [0x0000[[CAOFF]], 0x0000[[AAOFF]]) [0x00000000, 0x00000011) [0x00000000, 0x00000018) +CHECK-DAG: [[DWOA]] [0x00000000[[#AOFF]], 0x00000000[[#BOFF]]) [0x0000[[AAOFF]], 0x0000[[BAOFF]]) [0x00000011, 0x00000022) [0x00000018, 0x00000028) +CHECK-DAG: [[DWOB]] [0x00000000[[#BOFF]], 0x00000000[[#XOFF]]) [0x0000[[BAOFF]], 0x000000c3) [0x00000022, 0x00000033) [0x00000028, 0x0000003c) diff --git a/tests/rnglists.s b/tests/rnglists.s index e5afef5..9e854ba 100644 --- a/tests/rnglists.s +++ b/tests/rnglists.s @@ -4,12 +4,12 @@ # RUN: llvm-dwarfdump -debug-rnglists -debug-cu-index -debug-tu-index %t.dwp | FileCheck %s # CHECK-DAG: .debug_cu_index contents: -# CHECK: Index Signature INFO ABBREV RNGLISTS -# CHECK: 1 {{.*}} [0x00000018, 0x0000002d) [0x00000000, 0x00000004) [0x00000000, 0x00000017) +# CHECK: Index Signature INFO ABBREV RNGLISTS +# CHECK: 1 {{.*}} [0x0000000000000018, 0x000000000000002d) [0x00000000, 0x00000004) [0x00000000, 0x00000017) # CHECK-DAG: .debug_tu_index contents: -# CHECK: Index Signature INFO ABBREV RNGLISTS -# CHECK: 2 {{.*}} [0x00000000, 0x00000018) [0x00000000, 0x00000004) [0x00000000, 0x00000017) +# CHECK: Index Signature INFO ABBREV RNGLISTS +# CHECK: 2 {{.*}} [0x0000000000000000, 0x0000000000000018) [0x00000000, 0x00000004) [0x00000000, 0x00000017) # CHECK-DAG: .debug_rnglists.dwo contents: # range list header: length = 0x00000013, format = DWARF32, version = 0x0005, addr_size = 0x08, seg_size = 0x00, offset_entry_count = 0x00000001 diff --git a/tests/simple.test b/tests/simple.test index d3f67ea..d925cbf 100644 --- a/tests/simple.test +++ b/tests/simple.test @@ -18,9 +18,9 @@ CHECK: DW_TAG_subprogram CHECK: DW_TAG_formal_parameter CHECK: .debug_info.dwo contents: -CHECK: [[AOFF:0x[0-9a-f]*]]: +CHECK: 0x[[#%.8x,AOFF:]]: CHECK-LABEL: Compile Unit: length = {{.*}}, version = 0x0004, abbr_offset = -CHECK: 0x[[AAOFF]], addr_size = 0x08 (next unit at [[BOFF:.*]]) +CHECK: 0x[[AAOFF]], addr_size = 0x08 (next unit at 0x[[#%.8x,BOFF:]]) CHECK: DW_TAG_compile_unit CHECK: DW_AT_name {{.*}} "a.cpp" CHECK: DW_AT_GNU_dwo_id {{.*}} ([[DWOA:.*]]) @@ -30,9 +30,9 @@ CHECK: DW_TAG_structure_type NOTYP: DW_AT_name {{.*}} "foo" TYPES: DW_AT_signature {{.*}} ([[FOOSIG:.*]]) -CHECK: [[BOFF]]: +CHECK: 0x[[#BOFF]]: CHECK-LABEL: Compile Unit: length = {{.*}}, version = 0x0004, abbr_offset = -CHECK: 0x[[BAOFF]], addr_size = 0x08 (next unit at [[XOFF:.*]]) +CHECK: 0x[[BAOFF]], addr_size = 0x08 (next unit at 0x[[#%.8x,XOFF:]]) CHECK: DW_AT_name {{.*}} "b.cpp" CHECK: DW_AT_GNU_dwo_id {{.*}} ([[DWOB:.*]]) CHECK: DW_TAG_structure_type @@ -44,32 +44,32 @@ CHECK: DW_TAG_formal_parameter NOTYP-NOT: .debug_types.dwo contents: TYPES-LABEL: .debug_types.dwo contents: -TYPES: [[FOOUOFF:0x[0-9a-f]*]]: +TYPES: 0x[[#%.8x,FOOUOFF:]]: TYPES-LABEL: Type Unit: length = 0x00000020, format = DWARF32, version = 0x0004, abbr_offset = -TYPES: 0x[[AAOFF]], addr_size = 0x08, name = 'foo', type_signature = [[FOOSIG]], type_offset = 0x[[FOOOFF:.*]] (next unit at [[BARUOFF:.*]]) +TYPES: 0x[[AAOFF]], addr_size = 0x08, name = 'foo', type_signature = [[FOOSIG]], type_offset = 0x[[FOOOFF:.*]] (next unit at 0x[[#%.8x,BARUOFF:]]) TYPES: DW_TAG_type_unit TYPES: [[FOOOFF]]: DW_TAG_structure_type TYPES: DW_AT_name {{.*}} "foo" -TYPES: [[BARUOFF]]: +TYPES: 0x[[#BARUOFF]]: TYPES-LABEL: Type Unit: length = 0x00000020, format = DWARF32, version = 0x0004, abbr_offset = -TYPES: 0x[[BAOFF]], addr_size = 0x08, name = 'bar', type_signature = [[BARSIG]], type_offset = 0x001e (next unit at [[XUOFF:.*]]) +TYPES: 0x[[BAOFF]], addr_size = 0x08, name = 'bar', type_signature = [[BARSIG]], type_offset = 0x001e (next unit at 0x[[#%.8x,XUOFF:]]) TYPES: DW_TAG_type_unit TYPES: 0x00000042: DW_TAG_structure_type TYPES: DW_AT_name {{.*}} "bar" CHECK-LABEL: .debug_cu_index contents: -CHECK: Index Signature INFO ABBREV LINE STR_OFFSETS -TYPES: 1 [[DWOA]] {{\[}}[[AOFF]], [[BOFF]]) [0x0000[[AAOFF]], 0x0000[[BAOFF]]) [0x00000000, 0x0000001a) [0x00000000, 0x00000010) -TYPES: 3 [[DWOB]] {{\[}}[[BOFF]], [[XOFF]]) [0x0000[[BAOFF]], 0x00000099) [0x0000001a, 0x00000034) [0x00000010, 0x00000024) -NOTYP: 3 [[DWOA]] {{\[}}[[AOFF]], [[BOFF]]) [0x0000[[AAOFF]], 0x0000[[BAOFF]]) [0x00000000, 0x00000011) [0x00000000, 0x00000010) -NOTYP: 4 [[DWOB]] {{\[}}[[BOFF]], [[XOFF]]) [0x0000[[BAOFF]], 0x00000075) [0x00000011, 0x00000022) [0x00000010, 0x00000024) +CHECK: Index Signature INFO ABBREV LINE STR_OFFSETS +TYPES: 1 [[DWOA]] [0x00000000[[#AOFF]], 0x00000000[[#BOFF]]) [0x0000[[AAOFF]], 0x0000[[BAOFF]]) [0x00000000, 0x0000001a) [0x00000000, 0x00000010) +TYPES: 3 [[DWOB]] [0x00000000[[#BOFF]], 0x00000000[[#XOFF]]) [0x0000[[BAOFF]], 0x00000099) [0x0000001a, 0x00000034) [0x00000010, 0x00000024) +NOTYP: 3 [[DWOA]] [0x00000000[[#AOFF]], 0x00000000[[#BOFF]]) [0x0000[[AAOFF]], 0x0000[[BAOFF]]) [0x00000000, 0x00000011) [0x00000000, 0x00000010) +NOTYP: 4 [[DWOB]] [0x00000000[[#BOFF]], 0x00000000[[#XOFF]]) [0x0000[[BAOFF]], 0x00000075) [0x00000011, 0x00000022) [0x00000010, 0x00000024) Ensure we do not create a debug_tu_index, even an empty or malformed one. NOTYPOBJ-NOT: .debug_tu_index -TYPES: Index Signature TYPES ABBREV LINE STR_OFFSETS -TYPES: 1 [[FOOSIG]] {{\[}}[[FOOUOFF]], [[BARUOFF]]) [0x0000[[AAOFF]], 0x0000[[BAOFF]]) [0x00000000, 0x0000001a) [0x00000000, 0x00000010) -TYPES: 4 [[BARSIG]] {{\[}}[[BARUOFF]], [[XUOFF]]) [0x0000[[BAOFF]], 0x00000099) [0x0000001a, 0x00000034) [0x00000010, 0x00000024) +TYPES: Index Signature TYPES ABBREV LINE STR_OFFSETS +TYPES: 1 [[FOOSIG]] [0x00000000[[#FOOUOFF]], 0x00000000[[#BARUOFF]]) [0x0000[[AAOFF]], 0x0000[[BAOFF]]) [0x00000000, 0x0000001a) [0x00000000, 0x00000010) +TYPES: 4 [[BARSIG]] [0x00000000[[#BARUOFF]], 0x00000000[[#XUOFF]]) [0x0000[[BAOFF]], 0x00000099) [0x0000001a, 0x00000034) [0x00000010, 0x00000024) CHECK-LABEL: .debug_str.dwo contents: CHECK: "clang version diff --git a/tests/tu-units-v5.s b/tests/tu-units-v5.s index b117fbf..3ed7b2f 100644 --- a/tests/tu-units-v5.s +++ b/tests/tu-units-v5.s @@ -8,9 +8,9 @@ # CHECK: 0x0000001b: Type Unit: length = 0x00000017, format = DWARF32, version = 0x0005, unit_type = DW_UT_split_type, abbr_offset = 0x0000, addr_size = 0x08, name = '', type_signature = [[TUID2:.*]], type_offset = 0x0019 (next unit at 0x00000036) # CHECK-DAG: .debug_tu_index contents: # CHECK: version = 5, units = 2, slots = 4 -# CHECK: Index Signature INFO ABBREV -# CHECK: 1 [[TUID1]] [0x00000000, 0x0000001b) [0x00000000, 0x00000010) -# CHECK: 4 [[TUID2]] [0x0000001b, 0x00000036) [0x00000000, 0x00000010) +# CHECK: Index Signature INFO ABBREV +# CHECK: 1 [[TUID1]] [0x0000000000000000, 0x000000000000001b) [0x00000000, 0x00000010) +# CHECK: 4 [[TUID2]] [0x000000000000001b, 0x0000000000000036) [0x00000000, 0x00000010) .section .debug_info.dwo,"e",@progbits .long .Ldebug_info_dwo_end0-.Ldebug_info_dwo_start0 # Length of Unit diff --git a/thorin-bin/Cargo.toml b/thorin-bin/Cargo.toml index 7bd64d8..9564084 100644 --- a/thorin-bin/Cargo.toml +++ b/thorin-bin/Cargo.toml @@ -12,7 +12,7 @@ version = "0.10.0" edition = "2021" [dependencies] -thorin-dwp = { version = "0.10.0", path = "../thorin" } +thorin-dwp = { version = "0.10.0", path = "../thorin", features = ["gc"] } anyhow = "1.0.102" clap = { version = "4.5.60", features = ["derive"] } diff --git a/thorin-bin/src/main.rs b/thorin-bin/src/main.rs index d17fdc9..acf1634 100644 --- a/thorin-bin/src/main.rs +++ b/thorin-bin/src/main.rs @@ -42,6 +42,9 @@ struct Cli { /// Specify path to write the dwarf package to [default: -] #[arg(short, long = "output")] output: Option, + /// Garbage collect + #[arg(long = "gc")] + gc: bool, } /// Implementation of `thorin::Session` using `typed_arena` and `memmap2`. @@ -150,19 +153,39 @@ fn main() -> Result<()> { return Ok(()); } + if args.gc { + for executable in &args.executables { + package + .preprocess_gc_executable(executable) + .with_context(|| Error::AddExecutable(executable.display().to_string()))?; + } + } + for input in &args.inputs { - package - .add_input_object(input) - .with_context(|| Error::AddInputObject(input.display().to_string()))?; + if args.gc { + package + .add_gc_input_object(input) + .with_context(|| Error::AddInputObject(input.display().to_string()))?; + } else { + package + .add_input_object(input) + .with_context(|| Error::AddInputObject(input.display().to_string()))?; + } } for executable in &args.executables { // Failing to read the referenced object might be expected if the path referenced by // the executable isn't found but the referenced DWARF object is later found as an // input - calling `finish` will return an error in this case. - package - .add_executable(executable, thorin::MissingReferencedObjectBehaviour::Skip) - .with_context(|| Error::AddExecutable(executable.display().to_string()))?; + if args.gc { + package + .add_gc_executable(executable, thorin::MissingReferencedObjectBehaviour::Skip) + .with_context(|| Error::AddExecutable(executable.display().to_string()))?; + } else { + package + .add_executable(executable, thorin::MissingReferencedObjectBehaviour::Skip) + .with_context(|| Error::AddExecutable(executable.display().to_string()))?; + } } let output = args.output.unwrap_or_else(|| { diff --git a/thorin/Cargo.toml b/thorin/Cargo.toml index 41a64a9..5af8af0 100644 --- a/thorin/Cargo.toml +++ b/thorin/Cargo.toml @@ -11,12 +11,17 @@ repository = "https://github.com/rust-lang/thorin" version = "0.10.0" edition = "2021" +[features] +default = [] +gc = [] + [dependencies] +itertools = "0.12" tracing = "0.1.44" -hashbrown = "0.16.0" +hashbrown = "0.17.0" [dependencies.gimli] -version = "0.33.0" +version = "0.34.0" default-features = false # `gimli/std` pulls in `fallible-iterator` which we don't use, but can't opt out of, because of # cargo#8832. diff --git a/thorin/src/error.rs b/thorin/src/error.rs index f4a31f0..48c1cf8 100644 --- a/thorin/src/error.rs +++ b/thorin/src/error.rs @@ -86,6 +86,7 @@ pub enum Error { /// Section not found in unit's row in index, i.e. a DWARF package contains a section but its /// index doesn't record contributions to it. SectionNotInRow, + ContributionOutOfBounds(crate::index::Contribution, usize), /// Compilation unit in input DWARF object has no content. EmptyUnit(u64), /// Found multiple `.debug_info.dwo` sections. @@ -102,6 +103,23 @@ pub enum Error { NoOutputObjectCreated, /// Input objects have different encodings. MixedInputEncodings, + /// A DW_LLE value was unrecognized. + UnsupportedLocListsEntry(u8), + /// `.debug_info.dwo` compilation unit is malformed or truncated during GC. + MalformedDebugInfo, + /// An abbreviation form encountered during GC byte-level rewriting is not supported. + UnsupportedForm(u16), + /// A section-absolute reference was found where not expected. + UnexpectedSectionAbsoluteReference, + /// GC input object or executable added without prior `preprocess_gc_executable` call. + GcNotInitialized, + /// A `.dwo` is referenced by multiple executables that supply DWARF4 `.debug_ranges`. + /// + /// DWARF4 range lists hold raw addresses that live in the linked executable. + /// When several executables reference the same `.dwo`, each has its own (independently + /// tombstoned) `.debug_ranges`, but the GC currently only supports consulting one + /// of them. Proceeding could prune a range list still live in a different executable. + GcSharedDwarf4Ranges(crate::package::DwoId), /// Catch-all for `std::io::Error`. Io(std::io::Error), @@ -144,6 +162,7 @@ impl StdError for Error { Error::UnitNotInIndex(_) => None, Error::RowNotInIndex(source, _) => Some(source.as_dyn_error()), Error::SectionNotInRow => None, + Error::ContributionOutOfBounds(..) => None, Error::EmptyUnit(_) => None, Error::MultipleDebugInfoSection => None, Error::MultipleDebugTypesSection => None, @@ -152,6 +171,12 @@ impl StdError for Error { Error::MissingReferencedUnit(_) => None, Error::NoOutputObjectCreated => None, Error::MixedInputEncodings => None, + Error::UnsupportedLocListsEntry(_) => None, + Error::MalformedDebugInfo => None, + Error::UnsupportedForm(_) => None, + Error::UnexpectedSectionAbsoluteReference => None, + Error::GcNotInitialized => None, + Error::GcSharedDwarf4Ranges(_) => None, Error::Io(transparent) => StdError::source(transparent.as_dyn_error()), Error::ObjectRead(transparent) => StdError::source(transparent.as_dyn_error()), Error::ObjectWrite(transparent) => StdError::source(transparent.as_dyn_error()), @@ -230,6 +255,14 @@ impl fmt::Display for Error { write!(f, "Row {0} found in index's hash table not present in index", row) } Error::SectionNotInRow => write!(f, "Section not found in unit's row in index"), + Error::ContributionOutOfBounds(contribution, section_len) => { + write!( + f, + "Index contribution at offset 0x{:08x} with size 0x{:x} extends beyond section \ + of length 0x{:x}", + contribution.offset.0, contribution.size, section_len + ) + } Error::EmptyUnit(unit) => { write!(f, "Unit 0x{:08x} in input DWARF object with no data", unit) } @@ -250,6 +283,26 @@ impl fmt::Display for Error { } Error::NoOutputObjectCreated => write!(f, "No output object was created from inputs"), Error::MixedInputEncodings => write!(f, "Input objects haved mixed encodings"), + Error::UnsupportedLocListsEntry(s) => { + write!(f, "Unsupported DW_LLE value: {}", s) + } + Error::MalformedDebugInfo => write!(f, "Malformed `.debug_info.dwo` compilation unit"), + Error::UnsupportedForm(form) => { + write!(f, "Unsupported DWARF form 0x{:02x} during GC rewrite", form) + } + Error::UnexpectedSectionAbsoluteReference => { + write!(f, "A section-absolute reference was found in a .dwo file") + } + Error::GcNotInitialized => { + write!(f, "GC was requested but no executables were preprocessed") + } + Error::GcSharedDwarf4Ranges(dwo_id) => { + write!( + f, + "DWARF4 .debug_ranges for {dwo_id:?} is supplied by multiple executables; \ + cannot safely GC" + ) + } Error::Io(e) => fmt::Display::fmt(e, f), Error::ObjectRead(e) => fmt::Display::fmt(e, f), Error::ObjectWrite(e) => fmt::Display::fmt(e, f), diff --git a/thorin/src/gc.rs b/thorin/src/gc.rs new file mode 100644 index 0000000..8da3008 --- /dev/null +++ b/thorin/src/gc.rs @@ -0,0 +1,1893 @@ +//! Garbage Collection pass for DWARF package files. +//! +//! The basic idea is that we identify certain DIEs in the .debug_info.dwo +//! section as roots, mark and sweep the graph of DIEs, and then rewrite +//! other supporting sections including .debug_loclists.dwo, +//! .debug_rnglists.dwo, and .debug_str_offsets.dwo to drop now unneeded +//! data. We also need to rewrite the debug locations data for correctness +//! as a location expression could in theory refer back to the +//! .debug_info.dwo section. +//! +//! We do not attempt to examine type units (and thus disable dropping data +//! from the supporting sections in their presence). We also disable rewriting +//! .debug_str_offsets if a .debug_macro section is present, since we do not +//! attempt to examine .debug_macro and it can reference strings. Finally, +//! in the DWARF 4 variant of split DWARF, we do not attempt to shrink the +//! supporting .debug_locs/.debug_ranges sections. + +// gimli's `DW_FORM_*` / `DW_OP_*` constants are not upper-case; matching them in patterns would +// otherwise trigger `non_upper_case_globals` throughout this module. +#![allow(non_upper_case_globals)] + +use gimli::write::{EndianVec, Writer}; +use gimli::{DebugAddrIndex, Reader, RunTimeEndian, Section}; +use hashbrown::HashMap; +use itertools::izip; +use std::collections::{BTreeMap, BTreeSet}; +use std::mem; +use tracing::{debug, trace, warn}; + +use crate::{ + error::{Error, Result}, + package::DwoId, + relocate::Relocate, +}; + +/// Result of garbage-collecting a `.debug_info.dwo` compilation unit. +pub(crate) struct GcResult { + /// Rewritten `.debug_info.dwo` bytes, or `None` if nothing was removed. + pub rewritten: Option>, + /// Map from old DIE offsets to new offsets after dead DIEs were removed + /// and their level in the DIE tree. + /// Present only when `rewritten` is `Some`. Used to patch CU-relative references + /// embedded in location list expressions. + pub offset_remap: Option>, + /// `rnglistx` index values referenced by surviving DIEs, or `None` if the section + /// cannot be safely pruned (e.g. a DIE used `DW_FORM_sec_offset` for `DW_AT_ranges`). + pub referenced_rnglists: Option>, + /// `loclistx` index values referenced by surviving DIEs, or `None` if the section + /// cannot be safely pruned (e.g. a DIE used `DW_FORM_sec_offset` for `DW_AT_location`). + pub referenced_loclists: Option>, + /// `strx` index values referenced by surviving DIEs, or `None` if the section + /// cannot be safely pruned (e.g. a `.debug_macro` section is present). + pub referenced_str_offsets: Option>, +} + +/// Returns `true` if the given address is a tombstone value. +pub(crate) fn is_tombstone(addr: u64, address_size: u8) -> bool { + let negative_one = match address_size { + 4 => 0xffff_ffff_u64, + 8 => 0xffff_ffff_ffff_ffff_u64, + _ => return false, + }; + // GNU ld uses 0 as a tombstone, lld/mold use -1. + addr == 0 || addr == negative_one +} + +/// Serialize a `gimli::RawRngListEntry` back into bytes. +/// +/// gimli provides a parser (`RangeLists::raw_ranges`) but no writer for `.debug_rnglists`, so we +/// re-encode each surviving entry here. `DW_RLE_end_of_list` is emitted by the caller. +fn encode_raw_rng_entry( + entry: &gimli::RawRngListEntry, + out: &mut EndianVec, + address_size: u8, +) -> Result<()> { + use gimli::constants::*; + match *entry { + gimli::RawRngListEntry::BaseAddressx { addr } => { + out.write_u8(DW_RLE_base_addressx.0)?; + out.write_uleb128(addr.0 as u64)?; + } + gimli::RawRngListEntry::StartxEndx { begin, end } => { + out.write_u8(DW_RLE_startx_endx.0)?; + out.write_uleb128(begin.0 as u64)?; + out.write_uleb128(end.0 as u64)?; + } + gimli::RawRngListEntry::StartxLength { begin, length } => { + out.write_u8(DW_RLE_startx_length.0)?; + out.write_uleb128(begin.0 as u64)?; + out.write_uleb128(length)?; + } + gimli::RawRngListEntry::OffsetPair { begin, end } + | gimli::RawRngListEntry::AddressOrOffsetPair { begin, end } => { + out.write_u8(DW_RLE_offset_pair.0)?; + out.write_uleb128(begin)?; + out.write_uleb128(end)?; + } + gimli::RawRngListEntry::BaseAddress { addr } => { + out.write_u8(DW_RLE_base_address.0)?; + out.write_udata(addr, address_size)?; + } + gimli::RawRngListEntry::StartEnd { begin, end } => { + out.write_u8(DW_RLE_start_end.0)?; + out.write_udata(begin, address_size)?; + out.write_udata(end, address_size)?; + } + gimli::RawRngListEntry::StartLength { begin, length } => { + out.write_u8(DW_RLE_start_length.0)?; + out.write_udata(begin, address_size)?; + out.write_uleb128(length)?; + } + } + + Ok(()) +} + +/// Reassemble a `.debug_rnglists.dwo` or `.debug_loclists.dwo` section from +/// pre-encoded list bodies. Writes the unit header, offset table, and list data. +fn reassemble_offset_table_section( + encoded_lists: &[Vec], + header: &gimli::ListsHeader, + endian: RunTimeEndian, +) -> Result> { + let encoding = header.encoding; + let word_size = encoding.format.word_size() as usize; + let new_entry_count = encoded_lists.len() as u32; + let new_offset_array_size = new_entry_count as usize * word_size; + + let mut new_offsets: Vec = Vec::with_capacity(encoded_lists.len()); + let mut running_offset: u64 = new_offset_array_size as u64; + for enc in encoded_lists { + new_offsets.push(running_offset); + running_offset += enc.len() as u64; + } + let total_entries_size = running_offset - new_offset_array_size as u64; + + let initial_length_size = encoding.format.initial_length_size() as u64; + let new_unit_length: u64 = header.size() as u64 - initial_length_size + + (new_entry_count as u64 * word_size as u64) + + total_entries_size; + + let mut out = EndianVec::new(endian); + + if encoding.format == gimli::Format::Dwarf64 { + out.write_u32(0xffff_ffff)?; + out.write_u64(new_unit_length)?; + } else { + out.write_u32( + new_unit_length.try_into().expect("unit length w/out header larger than u32"), + )?; + } + + out.write_u16(encoding.version)?; + out.write_u8(encoding.address_size)?; + out.write_u8(0)?; + out.write_u32(new_entry_count)?; + + for &off in &new_offsets { + if encoding.format == gimli::Format::Dwarf64 { + out.write_u64(off)?; + } else { + out.write_u32(off.try_into().expect("offset larger than u32"))?; + } + } + + for enc in encoded_lists { + out.write(enc)?; + } + + Ok(out.into_vec()) +} + +/// Rewrite a `.debug_rnglists.dwo` section: remove tombstoned entries and replace +/// unreferenced range lists with empty ones. +/// +/// Returns `None` if nothing changed (caller should use the original data). +/// Returns `Some(vec)` with the new section bytes if anything was modified. +pub(crate) fn rewrite_rnglists( + data: gimli::EndianSlice<'_, RunTimeEndian>, + referenced_indices: &BTreeSet, + is_addr_live: &IsAddrLive, + dwo_id: DwoId, +) -> Result>> +where + IsAddrLive: Fn(DebugAddrIndex) -> Result, +{ + if data.is_empty() { + return Ok(None); + } + let endian = data.endian(); + + // Parse the header. + let mut input = data; + let header = gimli::ListsHeader::parse(&mut input)?; + let encoding = header.encoding; + let offset_entry_count = header.offset_entry_count; + let address_size = encoding.address_size; + + let header_size = header.size() as usize; + + let debug_rnglists = gimli::DebugRngLists::from(data); + let debug_ranges = gimli::DebugRanges::from(gimli::EndianSlice::new(&[][..], endian)); + let range_lists = gimli::RangeLists::new(debug_ranges, debug_rnglists); + let base = gimli::DebugRngListsBase(header_size); + + // Parse each range list in turn. + struct ParsedList(Vec>); + let mut parsed_lists: Vec = Vec::with_capacity(offset_entry_count as usize); + let mut modified_count = 0; + + for list_idx in 0..offset_entry_count as usize { + if !referenced_indices.contains(&(list_idx as u64)) { + trace!(list_idx, "removing unreferenced range list"); + continue; + } + + let offset = range_lists.get_offset(encoding, base, gimli::DebugRngListsIndex(list_idx))?; + + let mut entries: Vec> = Vec::new(); + let mut base_tombstoned = false; + let mut any_removed = false; + + let mut raw_iter = range_lists.raw_ranges(offset, encoding)?; + + while let Some(entry) = raw_iter.next()? { + match entry { + gimli::RawRngListEntry::BaseAddressx { addr } => { + if !is_addr_live(addr)? { + base_tombstoned = true; + any_removed = true; + trace!(list_idx, "removing tombstoned base_addressx idx={}", addr.0); + } else { + base_tombstoned = false; + entries.push(entry); + } + } + gimli::RawRngListEntry::BaseAddress { addr } => { + if is_tombstone(addr, address_size) { + base_tombstoned = true; + any_removed = true; + trace!(list_idx, "removing tombstoned base_address addr={:#x}", addr); + } else { + base_tombstoned = false; + entries.push(entry); + } + } + gimli::RawRngListEntry::OffsetPair { .. } + | gimli::RawRngListEntry::AddressOrOffsetPair { .. } => { + if base_tombstoned { + any_removed = true; + trace!(list_idx, "removing offset_pair under tombstoned base"); + } else { + entries.push(entry); + } + } + gimli::RawRngListEntry::StartxEndx { begin, .. } => { + if !is_addr_live(begin)? { + any_removed = true; + trace!(list_idx, "removing tombstoned startx_endx"); + } else { + entries.push(entry); + } + } + gimli::RawRngListEntry::StartxLength { begin, .. } => { + if !is_addr_live(begin)? { + any_removed = true; + trace!(list_idx, "removing tombstoned startx_length"); + } else { + entries.push(entry); + } + } + gimli::RawRngListEntry::StartEnd { begin, .. } => { + if is_tombstone(begin, address_size) { + any_removed = true; + trace!(list_idx, "removing tombstoned start_end"); + } else { + entries.push(entry); + } + } + gimli::RawRngListEntry::StartLength { begin, .. } => { + if is_tombstone(begin, address_size) { + any_removed = true; + trace!(list_idx, "removing tombstoned start_length"); + } else { + entries.push(entry); + } + } + } + } + + if any_removed { + modified_count += 1; + } + + parsed_lists.push(ParsedList(entries)); + } + + let new_entry_count = parsed_lists.len() as u32; + let lists_removed = offset_entry_count - new_entry_count; + + // If no list was modified and no lists were dropped, return None (no change). + if modified_count == 0 && lists_removed == 0 { + return Ok(None); + } + + debug!( + ?dwo_id, + lists_removed, + modified_count, + total = offset_entry_count, + remaining = new_entry_count, + "rewrite_rnglists: pruned range lists" + ); + + // Reassemble surviving range lists. + let mut encoded_lists: Vec> = Vec::with_capacity(parsed_lists.len()); + for list in &parsed_lists { + let mut buf = EndianVec::new(endian); + for entry in &list.0 { + encode_raw_rng_entry(entry, &mut buf, address_size)?; + } + buf.write_u8(gimli::constants::DW_RLE_end_of_list.0)?; + encoded_lists.push(buf.into_vec()); + } + + Ok(Some(reassemble_offset_table_section(&encoded_lists, &header, endian)?)) +} + +/// Patch CU-relative references in DWARF expressions within a `.debug_loc.dwo` section. +/// We do not attempt to remove unused location lists. +/// +/// DWARF4 split DWARF encodes location lists in `.debug_loc.dwo` using DW_LLE entry types +/// (GNU extension). The section has no header — it is a flat stream of DW_LLE entries. +/// +/// Returns `None` if nothing changed (caller should use the original data). +/// Returns `Some(vec)` with the patched section bytes if any expression was patched. +pub(crate) fn patch_debug_loc( + data: gimli::EndianSlice<'_, RunTimeEndian>, + encoding: gimli::Encoding, + offset_remap: &BTreeMap, +) -> Result>> { + let raw = data.slice(); + if raw.is_empty() { + return Ok(None); + } + let endian = data.endian(); + + let mut patched = raw.to_vec(); + let any_patched = patch_loclist_data(raw, endian, encoding, offset_remap, &mut patched)?; + if any_patched { + Ok(Some(patched)) + } else { + Ok(None) + } +} + +/// Rewrite a `.debug_loclists.dwo` section: remove unreferenced location lists and/or +/// patch CU-relative references in DWARF expressions embedded in location list entries. +/// +/// `referenced_indices` controls pruning. Only lists whose index is in this set +/// are retained. If `referenced_indices` is None, all lists are assumed to be referenced. +/// +/// `offset_remap` controls expression patching: when `Some`, CU-relative references +/// (e.g. `DW_OP_call4`) are updated to reflect new DIE offsets after GC. +/// +/// Returns `None` if nothing changed (caller should use the original data). +/// Returns `Some(vec)` with the new section bytes if anything was pruned or patched. +pub(crate) fn rewrite_loclists( + data: gimli::EndianSlice<'_, RunTimeEndian>, + referenced_indices: Option<&BTreeSet>, + offset_remap: Option<&BTreeMap>, +) -> Result>> { + if data.is_empty() { + return Ok(None); + } + let endian = data.endian(); + + // Parse the header. + let mut input = data; + let header = gimli::ListsHeader::parse(&mut input)?; + let encoding = header.encoding; + let offset_entry_count = header.offset_entry_count; + let header_size = header.size() as usize; + let debug_loclists = gimli::DebugLocLists::from(data); + let debug_loc = gimli::DebugLoc::from(gimli::EndianSlice::new(&[][..], endian)); + let loc_lists = gimli::LocationLists::new(debug_loc, debug_loclists); + + let raw = data.slice(); + + // When offset_entry_count == 0 (which is permitted by the DWARF 5 spec, as long + // as DW_FORM_sec_offset is used), there is no offset table to iterate. + // We can't prune individual lists, but we still need to patch CU-relative + // references in expressions. + if offset_entry_count == 0 { + if let Some(remap) = offset_remap { + let entry_data = &raw[header_size..]; + let mut patched = raw.to_vec(); + let did_patch = patch_loclist_data( + entry_data, + endian, + encoding, + remap, + &mut patched[header_size..], + )?; + if did_patch { + return Ok(Some(patched)); + } + } + return Ok(None); + } + + let base = gimli::DebugLocListsBase(header_size); + + // Resolve all offsets and determine byte spans for each list. Lists in the + // offset table are not necessarily contiguous or ordered, so we sort the + // start offsets to derive each list's extent. + let mut abs_offsets: Vec = Vec::with_capacity(offset_entry_count as usize); + for list_idx in 0..offset_entry_count as usize { + let offset = loc_lists.get_offset(encoding, base, gimli::DebugLocListsIndex(list_idx))?; + abs_offsets.push(offset.0); + } + + // Build a sorted list of (abs_offset, list_idx) to determine end boundaries. + let mut sorted: Vec<(usize, u32)> = + abs_offsets.iter().enumerate().map(|(i, &off)| (off, i as u32)).collect(); + sorted.sort_unstable(); + + // Map from list_idx -> byte length of that list in the original section. + let mut list_lengths: Vec = vec![0; offset_entry_count as usize]; + for (i, &(start, idx)) in sorted.iter().enumerate() { + let end = if i + 1 < sorted.len() { sorted[i + 1].0 } else { raw.len() }; + list_lengths[idx as usize] = end - start; + } + + // Reassemble surviving loclists. + let mut encoded_lists: Vec> = + Vec::with_capacity(referenced_indices.map_or(offset_entry_count as usize, |s| s.len())); + let mut pruned_count: u32 = 0; + let mut any_patched = false; + + for list_idx in 0..offset_entry_count as usize { + let keep = referenced_indices.is_none_or(|set| set.contains(&(list_idx as u64))); + if keep { + let start = abs_offsets[list_idx]; + let len = list_lengths[list_idx]; + let list_bytes = &raw[start..start + len]; + + if let Some(remap) = offset_remap { + let mut patched = list_bytes.to_vec(); + let did_patch = + patch_loclist_data(list_bytes, endian, encoding, remap, &mut patched)?; + any_patched |= did_patch; + encoded_lists.push(patched); + } else { + encoded_lists.push(list_bytes.to_vec()); + } + } else { + pruned_count += 1; + trace!(list_idx, "removing unreferenced location list"); + } + } + + if pruned_count == 0 && !any_patched { + return Ok(None); + } + + if pruned_count > 0 { + debug!( + pruned_count, + total = offset_entry_count, + remaining = encoded_lists.len(), + "rewrite_loclists: pruned location lists" + ); + } + + Ok(Some(reassemble_offset_table_section(&encoded_lists, &header, endian)?)) +} + +/// Patch CU-relative references in DWARF expressions within location list entries. +/// +/// `raw` is the byte slice containing the loclist entries to scan (no header or offset +/// table — just the entry data). `patched` is a mutable copy where patched bytes are +/// written; it must have the same length as `raw`. +/// +/// Returns `true` if any expression was patched. +fn patch_loclist_data( + raw: &[u8], + endian: RunTimeEndian, + encoding: gimli::Encoding, + offset_remap: &BTreeMap, + patched: &mut [u8], +) -> Result { + use gimli::constants::*; + assert_eq!(raw.len(), patched.len()); + + let mut any_patched = false; + let mut reader = gimli::EndianSlice::new(raw, endian); + let address_size = encoding.address_size; + + while !reader.is_empty() { + let entry_type = reader.read_u8()?; + + let has_expr = match DwLle(entry_type) { + DW_LLE_end_of_list => false, + DW_LLE_base_addressx => { + reader.read_uleb128()?; + false + } + DW_LLE_startx_endx => { + reader.read_uleb128()?; + reader.read_uleb128()?; + true + } + DW_LLE_startx_length => { + reader.read_uleb128()?; + if encoding.version >= 5 { + reader.read_uleb128()?; + } else { + reader.read_u32()?; + } + true + } + DW_LLE_offset_pair => { + reader.read_uleb128()?; + reader.read_uleb128()?; + true + } + DW_LLE_default_location => true, + DW_LLE_base_address => { + reader.read_address(address_size)?; + false + } + DW_LLE_start_end => { + reader.read_address(address_size)?; + reader.read_address(address_size)?; + true + } + DW_LLE_start_length => { + reader.read_address(address_size)?; + reader.read_uleb128()?; + true + } + x => return Err(Error::UnsupportedLocListsEntry(x.0)), + }; + + // NB: Because `patched` is initialized by the caller with `raw`, + // we only have to act if we change something. + if has_expr { + let expr_len = if encoding.version >= 5 { + reader.read_uleb128()? as usize + } else { + reader.read_u16()? as usize + }; + let expr_slice = reader.split(expr_len)?; + + let mut out = EndianVec::new(endian); + emit_expression(expr_slice, offset_remap, encoding, &mut out)?; + + if out.slice() != expr_slice.slice() { + let expr_offset = raw.len() - reader.len() - expr_len; + patched[expr_offset..expr_offset + expr_len].copy_from_slice(out.slice()); + any_patched = true; + } + } + } + + Ok(any_patched) +} + +/// Liveness state of a DIE. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Liveness { + /// Removed from output. + Dead, + /// The DIE, all of its descendants, and everything it references survive. + Live, + /// This DIE must survive to maintain the tree structure (i.e. it is the ancestor + /// of a Live DIE). We keep everything its attributes reference to avoid + /// dangling, but it does *not* keep its descendants alive. + Retained, +} + +/// A parsed DIE record from the analysis pass. +/// +/// Byte offsets and tags are re-derived from the raw byte stream during the +/// rewrite pass, so they are not stored here — only the grap edges and liveness +/// state needed by the mark-and-sweep are retained. +struct DieRecord { + /// Index of the parent DIE record, if any. + parent: Option, + /// Children of the current DIE record, if any. + children: Vec, + /// Edges from the attributes of this DIE to other DIEs. Because these can + /// be forward looking we can't resolve them into DIE indexes at this stage. + edges: Vec, + /// Current liveness state. + liveness: Liveness, +} + +/// Write an unsigned LEB128 value padded to exactly `width` bytes. +/// +/// ULEB128 allows redundant high zero bytes; we exploit this to keep the encoded length stable +/// when patching offsets (which only ever shrink). See the comment on [`rewrite_unit`] for why +/// we use this byte-stable strategy rather than re-encoding at the natural (shorter) width. +fn write_uleb128_padded( + out: &mut EndianVec, + value: gimli::UnitOffset, + width: usize, +) -> Result<()> { + // Number of bytes the natural encoding requires. + let mut tmp = value.0; + let mut natural = 0usize; + loop { + natural += 1; + tmp >>= 7; + if tmp == 0 { + break; + } + } + assert!(natural <= width); + let mut v = value.0; + for i in 0..width { + let mut byte = (v & 0x7f) as u8; + v >>= 7; + // All but the last byte get the continuation bit. + if i + 1 < width { + byte |= 0x80; + } + out.write_u8(byte)?; + } + Ok(()) +} + +/// Number of bytes a ULEB128 value occupies starting at `pos`. +fn uleb128_len(mut data: gimli::EndianSlice<'_, RunTimeEndian>) -> Result { + let before = data.len(); + data.read_uleb128()?; + Ok(before - data.len()) +} + +/// A CU-relative DIE reference embedded in a DWARF expression, located at a byte offset within +/// the expression and encoded with the given operand layout (so it can be patched in place). +#[derive(Clone, Copy, Debug)] +struct ExprRef { + /// Byte offset of the operand within the expression. + pos: usize, + /// Old CU-relative offset value. + old: gimli::UnitOffset, + /// Encoding of the operand. + enc: ExprRefEnc, +} + +/// Operand encoding of a CU-relative reference embedded in an expression. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ExprRefEnc { + /// 2-byte fixed (`DW_OP_call2`). + U2, + /// 4-byte fixed (`DW_OP_call4`, `DW_OP_GNU_parameter_ref`). + U4, + /// ULEB128 (`DW_OP_const_type`, `DW_OP_regval_type`, etc.). + Uleb, +} + +/// Walk a DWARF expression, invoking `on_ref` for each *CU-relative* DIE reference operation and +/// recording its byte position and encoding. Section-absolute references (`DW_OP_call_ref`, +/// `DW_OP_implicit_pointer`) are reported via `on_sec_ref` with their (section-absolute) value. +/// +/// This advances through the expression using gimli's operation parser, then +/// for the handful of reference-bearing opcodes recomputes the operand's byte +/// position from the opcode's known operand layout so it can be patched in place. +/// +/// Recurses into `DW_OP_entry_value` / `DW_OP_GNU_entry_value` sub-expressions; `base` is the +/// byte offset of `expr` within the enclosing buffer so positions are absolute to that buffer. +fn walk_expression( + expression: gimli::Expression>, + base: usize, + encoding: gimli::Encoding, + on_ref: &mut F, + on_sec_ref: &mut G, +) -> Result<()> +where + F: FnMut(ExprRef), + G: FnMut(usize), +{ + let mut iter = expression.operations(encoding); + let mut start = 0; + while let Some(op) = iter.next()? { + let end = iter.offset_from(&expression); + match op { + gimli::Operation::Call { offset } => match offset { + gimli::DieReference::UnitRef(unit_off) => { + // In order to get the size correct, we have to look at the opcode + // which gimli doesn't directly expose. + let (enc, opnd_pos) = match gimli::DwOp(expression.0.slice()[start]) { + gimli::constants::DW_OP_call2 => (ExprRefEnc::U2, start + 1), + gimli::constants::DW_OP_call4 => (ExprRefEnc::U4, start + 1), + _ => { + // Unexpected; treat conservatively as malformed. + return Err(Error::MalformedDebugInfo); + } + }; + on_ref(ExprRef { pos: base + opnd_pos, old: unit_off, enc }); + } + gimli::DieReference::DebugInfoRef(off) => { + // DW_OP_call_ref: section-absolute. + on_sec_ref(off.0); + } + }, + gimli::Operation::ImplicitPointer { value, .. } => { + // DW_OP_implicit_pointer / GNU: section-absolute offset. + on_sec_ref(value.0); + } + gimli::Operation::ParameterRef { offset } => { + // DW_OP_GNU_parameter_ref: CU-relative, 4-byte. + on_ref(ExprRef { pos: base + start + 1, old: offset, enc: ExprRefEnc::U4 }); + } + gimli::Operation::TypedLiteral { base_type, .. } => { + // DW_OP_const_type: ULEB128 CU-relative offset (operand 1). + if base_type.0 != 0 { + on_ref(ExprRef { + pos: base + start + 1, + old: base_type, + enc: ExprRefEnc::Uleb, + }); + } + } + gimli::Operation::RegisterOffset { base_type, .. } => { + // DW_OP_regval_type: ULEB128 register, then ULEB128 base_type offset. + if base_type.0 != 0 { + let reg_len = uleb128_len(expression.0.range_from(start + 1..))?; + on_ref(ExprRef { + pos: base + start + 1 + reg_len, + old: base_type, + enc: ExprRefEnc::Uleb, + }); + } + } + gimli::Operation::Deref { base_type, .. } => { + // DW_OP_deref_type / DW_OP_xderef_type: size byte, then ULEB128 base_type offset. + // (Plain DW_OP_deref/xderef have base_type == 0.) + if base_type.0 != 0 { + on_ref(ExprRef { + pos: base + start + 2, + old: base_type, + enc: ExprRefEnc::Uleb, + }); + } + } + gimli::Operation::Convert { base_type } + | gimli::Operation::Reinterpret { base_type } => { + // DW_OP_convert / DW_OP_reinterpret: ULEB128 CU-relative offset (0 == generic). + if base_type.0 != 0 { + on_ref(ExprRef { + pos: base + start + 1, + old: base_type, + enc: ExprRefEnc::Uleb, + }); + } + } + gimli::Operation::EntryValue { expression: sub } => { + // DW_OP_entry_value / GNU: nested sub-expression. Its bytes are the tail of this + // operation; compute its base relative to the original buffer. + let sub_base = base + (end - sub.slice().len()); + walk_expression(gimli::Expression(sub), sub_base, encoding, on_ref, on_sec_ref)?; + } + gimli::Operation::VariableValue { offset } => { + // DW_OP_GNU_variable_value: section-absolute .debug_info offset. + on_sec_ref(offset.0); + } + _ => {} + } + start = end; + } + Ok(()) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RefFormKind { + CuRelative, + SectionAbsolute, +} + +/// Is this form one of the DWARF `reference` class forms that we treat as a edge +/// when marking? +/// +/// Returns the kind of edge (CU-relative vs section-absolute), or `None` for +/// non-reference forms and for forms we deliberately skip (`ref_sig8`, +/// `ref_sup4/8`). +fn ref_form_kind(form: gimli::DwForm) -> Option { + use gimli::constants::*; + match form { + DW_FORM_ref1 | DW_FORM_ref2 | DW_FORM_ref4 | DW_FORM_ref8 | DW_FORM_ref_udata => { + Some(RefFormKind::CuRelative) + } + DW_FORM_ref_addr => Some(RefFormKind::SectionAbsolute), + // Skipped: ref_sig8 (type unit), ref_sup4/8 (supplementary file). + _ => None, + } +} + +/// Check whether a DWARF4 range list (`.debug_ranges`) contains any non-tombstoned entry. +/// +/// DWARF4 `.debug_ranges` uses raw address pairs (parsed as `BaseAddress` or +/// `AddressOrOffsetPair` by gimli). The `sec_offset` from `DW_AT_ranges` indexes +/// directly into the `.debug_ranges` data held by `range_lists`. +/// +/// Returns `Ok(true)` if at least one entry resolves to a non-tombstoned address, +/// `Ok(false)` if all entries are tombstoned or the list was empty. +/// Conservatively returns `Ok(true)` if resolution fails. +fn ranges_has_live_entry( + range_lists: &gimli::RangeLists>>, + offset: gimli::RangeListsOffset, + encoding: gimli::Encoding, + dwo_id: DwoId, +) -> Result { + let address_size = encoding.address_size; + + let Ok(mut raw_iter) = range_lists.raw_ranges(offset, encoding) else { + debug!(?dwo_id, offset = %format_args!("{:#x}", offset.0), "Can't parse ranges at offset, conservatively assuming live."); + return Ok(true); + }; + + let mut base_tombstoned = false; + + while let Some(entry) = raw_iter.next()? { + match entry { + gimli::RawRngListEntry::BaseAddress { addr } => { + base_tombstoned = is_tombstone(addr, address_size); + } + gimli::RawRngListEntry::AddressOrOffsetPair { begin, .. } => { + if base_tombstoned { + continue; + } + if !is_tombstone(begin, address_size) { + return Ok(true); + } + } + _ => { + debug!( + ?dwo_id, + "Unexpected entry type in .debug_ranges, conservatively assuming live." + ); + return Ok(true); + } + } + } + + Ok(false) +} + +/// Check whether a DWARF5 range list (`.debug_rnglists`) contains any non-tombstoned entry. +/// +/// Returns `Ok(true)` if at least one entry resolves to a non-tombstoned address, +/// `Ok(false)` if all entries are tombstoned or the list was empty. +/// Conservatively returns `Ok(true)` if resolution fails. +fn rnglist_has_live_entry( + range_lists: &gimli::RangeLists>>, + base: gimli::DebugRngListsBase, + index: gimli::DebugRngListsIndex, + encoding: gimli::Encoding, + is_addr_live: &IsAddrLive, + dwo_id: DwoId, +) -> Result +where + IsAddrLive: Fn(DebugAddrIndex) -> Result, +{ + // Resolve the rnglistx index to an absolute section offset. + let Ok(offset) = range_lists.get_offset(encoding, base, index) else { + debug!(?dwo_id, base = %format_args!("{:#x}", base.0), index = %format_args!("{:#x}", index.0), "Can't get rnglist offset, conservatively assuming live."); + return Ok(true); + }; + + let Ok(mut raw_iter) = range_lists.raw_ranges(offset, encoding) else { + debug!(?dwo_id, offset = %format_args!("{:#x}", offset.0), "Can't parse rnglist at offset, conservatively assuming live."); + return Ok(true); + }; + + let mut base_tombstoned = false; + + while let Some(entry) = raw_iter.next()? { + match entry { + gimli::RawRngListEntry::BaseAddressx { addr } => { + base_tombstoned = !is_addr_live(addr)?; + } + gimli::RawRngListEntry::BaseAddress { addr } => { + base_tombstoned = is_tombstone(addr, encoding.address_size); + } + gimli::RawRngListEntry::OffsetPair { .. } + | gimli::RawRngListEntry::AddressOrOffsetPair { .. } => { + if !base_tombstoned { + return Ok(true); + } + } + gimli::RawRngListEntry::StartxEndx { begin, .. } + | gimli::RawRngListEntry::StartxLength { begin, .. } => { + if is_addr_live(begin)? { + return Ok(true); + } + } + gimli::RawRngListEntry::StartEnd { begin, .. } + | gimli::RawRngListEntry::StartLength { begin, .. } => { + if !is_tombstone(begin, encoding.address_size) { + return Ok(true); + } + } + } + } + + Ok(false) +} + +/// Determine whether a `DW_TAG_subprogram`/`DW_TAG_variable` (or any root) is a live root. +/// +/// `attrs` are the raw `(name, form, value)` records of the DIE. Returns `Ok(true)` if the DIE +/// should be treated as an unconditionally live root. Conservatively returns `true` when liveness +/// cannot be determined. +fn is_root( + tag: gimli::DwTag, + attrs: &[( + gimli::DwAt, + gimli::DwForm, + gimli::AttributeValue>, + )], + range_lists: &gimli::RangeLists>>, + ranges_base: gimli::DebugRngListsBase, + rnglists_base: gimli::DebugRngListsBase, + is_addr_live: &IsAddrLive, + dwo_id: DwoId, + encoding: gimli::Encoding, +) -> Result +where + IsAddrLive: Fn(DebugAddrIndex) -> Result, +{ + match tag { + gimli::DW_TAG_subprogram => { + // DW_AT_low_pc via addrx*: live if the resolved address is not a tombstone. + for (name, _, value) in attrs { + if *name == gimli::DW_AT_low_pc { + if let gimli::AttributeValue::DebugAddrIndex(idx) = value { + if is_addr_live(*idx)? { + return Ok(true); + } + } else if let gimli::AttributeValue::Addr(addr) = value { + // Direct address (unusual in .dwo, but be safe). + if !is_tombstone(*addr, encoding.address_size) { + return Ok(true); + } + } else { + // Unknown low_pc form: conservatively live. + return Ok(true); + } + } + } + // DW_AT_ranges: live if any range list entry has a non-tombstoned address. + for (name, _, value) in attrs { + if *name == gimli::DW_AT_ranges { + if let gimli::AttributeValue::DebugRngListsIndex(idx) = value { + return rnglist_has_live_entry( + range_lists, + rnglists_base, + *idx, + encoding, + is_addr_live, + dwo_id, + ); + } else if let gimli::AttributeValue::SecOffset(offset) = value { + // DWARF4 `.debug_ranges` sec_offsets are relative to the skeleton's + // `DW_AT_GNU_ranges_base` within the executable's `.debug_ranges`. + return ranges_has_live_entry( + range_lists, + gimli::RangeListsOffset(ranges_base.0 + *offset), + encoding, + dwo_id, + ); + } else { + // Unknown ranges form: conservatively live. + return Ok(true); + } + } + } + Ok(false) + } + gimli::DW_TAG_variable => { + for (name, _, value) in attrs { + if *name == gimli::DW_AT_location { + if let gimli::AttributeValue::Exprloc(expr) = value { + if location_expr_live(*expr, is_addr_live, encoding)? { + return Ok(true); + } + } + // Location list forms (loclistx, sec_offset, etc.) or other + // non-exprloc forms describe local variables whose location + // varies across PC ranges. They have no independent static + // address and cannot be roots. + } + } + Ok(false) + } + _ => Ok(false), + } +} + +/// Examine a variable's location expression for `DW_OP_addrx` / `DW_OP_GNU_addr_index`. +fn location_expr_live( + expression: gimli::Expression>, + is_addr_live: &IsAddrLive, + encoding: gimli::Encoding, +) -> Result +where + IsAddrLive: Fn(DebugAddrIndex) -> Result, +{ + let mut iter = expression.operations(encoding); + let mut saw_addrx = false; + let mut any_live = false; + while let Some(op) = iter.next()? { + if let gimli::Operation::AddressIndex { index } = op { + saw_addrx = true; + if is_addr_live(index)? { + any_live = true; + } + } + } + if saw_addrx { + Ok(any_live) + } else { + Ok(false) + } +} + +/// Scan a single fully-live DIE's attributes for tracing edges. +fn scan_live_die_attrs( + attrs: &[( + gimli::DwAt, + gimli::DwForm, + gimli::AttributeValue>, + )], + loc_lists: &gimli::LocationLists>, + loclists_base: gimli::DebugLocListsBase, + encoding: gimli::Encoding, +) -> Result> { + let mut out = Vec::new(); + for (name, form, value) in attrs { + // DW_AT_sibling links don't keep DIEs alive. + if *name == gimli::DW_AT_sibling { + continue; + } + + // Reference-form attributes. + // Handling DW_FORM_indirect is tricky. gimli resolves it + // transparently, so the value contains the resolved attribute + // (e.g. UnitRef for an inner ref4), but the form from the + // abbreviation table is still DW_FORM_indirect. Check both + // the declared form and the resolved value type. + match ref_form_kind(*form) { + Some(RefFormKind::CuRelative) => { + if let gimli::AttributeValue::UnitRef(unit_off) = value { + out.push(*unit_off); + } + } + None => { + // DW_FORM_indirect wrapping a reference form: gimli resolves the + // inner form and produces a UnitRef value even though the outer form + // is DW_FORM_indirect. + if *form == gimli::DW_FORM_indirect { + if let gimli::AttributeValue::UnitRef(unit_off) = value { + out.push(*unit_off); + } + } + } + Some(RefFormKind::SectionAbsolute) => { + // DW_FORM_ref_addr does not occur in .dwo files produced by any + // toolchain known to me. LLVM merges CUs to avoid cross-CU + // references in split DWARF output, and GCC does not support + // split DWARF with LTO at all. The only way I could find to produce + // it is `llc -split-dwarf-cross-cu-references`, an experimental + // flag not exposed through any compiler driver. Error out so that + // if a future toolchain does emit ref_addr we notice. + return Err(Error::UnexpectedSectionAbsoluteReference); + } + } + + // Expressions embedded in exprloc attributes. + if let gimli::AttributeValue::Exprloc(expr) = value { + let mut on_ref = |r: ExprRef| out.push(r.old); + // Section-absolute expression refs (DW_OP_call_ref, DW_OP_implicit_pointer, + // DW_OP_GNU_variable_value) should not occur in .dwo files; see comment on + // RefFormKind::SectionAbsolute above. + let mut saw_section_absolute_reference = false; + let mut on_sec = |_: usize| { + saw_section_absolute_reference = true; + }; + walk_expression(*expr, 0, encoding, &mut on_ref, &mut on_sec)?; + if saw_section_absolute_reference { + return Err(Error::UnexpectedSectionAbsoluteReference); + } + } + + // Location lists referenced by DW_AT_location via loclistx / sec_offset. + if *name == gimli::DW_AT_location { + let loclist_offset = match value { + gimli::AttributeValue::SecOffset(off) => Some(*off), + gimli::AttributeValue::LocationListsRef(off) => Some(off.0), + gimli::AttributeValue::DebugLocListsIndex(idx) => { + Some(loc_lists.get_offset(encoding, loclists_base, *idx)?.0) + } + _ => None, + }; + if let Some(off) = loclist_offset { + scan_loclist_for_refs(loc_lists, off, encoding, &mut out)?; + } + } + } + Ok(out) +} + +/// Parse a DWARF5 `.debug_loclists.dwo` location list starting at byte `start`, +/// scanning each location expression for CU-relative DIE references. +fn scan_loclist_for_refs( + loc_lists: &gimli::LocationLists>, + start: usize, + encoding: gimli::Encoding, + out: &mut Vec, +) -> Result<()> { + let mut raw_iter = loc_lists.raw_locations_dwo(gimli::LocationListsOffset(start), encoding)?; + while let Some(raw_entry) = raw_iter.next()? { + // Extract expression from variants that carry one. + let expr: Option>> = + match &raw_entry { + gimli::RawLocListEntry::StartxEndx { data, .. } + | gimli::RawLocListEntry::StartxLength { data, .. } + | gimli::RawLocListEntry::OffsetPair { data, .. } + | gimli::RawLocListEntry::DefaultLocation { data } + | gimli::RawLocListEntry::StartEnd { data, .. } + | gimli::RawLocListEntry::StartLength { data, .. } + | gimli::RawLocListEntry::AddressOrOffsetPair { data, .. } => Some(*data), + // BaseAddressx, BaseAddress - no expression. + _ => None, + }; + if let Some(expr) = expr { + let mut on_ref = |r: ExprRef| out.push(r.old); + // As discussed above, section-absolute expression refs should not + // occur in .dwo files. + let mut saw_section_absolute_reference = false; + let mut on_sec = |_: usize| { + saw_section_absolute_reference = true; + }; + walk_expression(expr, 0, encoding, &mut on_ref, &mut on_sec)?; + if saw_section_absolute_reference { + return Err(Error::UnexpectedSectionAbsoluteReference); + } + } + } + Ok(()) +} + +/// Garbage-collect dead DIEs from a `.debug_info.dwo` compilation unit. +/// +/// See the module level comment for an explanation of the approach. +pub(crate) fn gc_debug_info( + debug_info: gimli::DebugInfo>, + debug_abbrev: gimli::DebugAbbrev>, + loc_lists: gimli::LocationLists>, + range_lists: gimli::RangeLists>>, + ranges_base: gimli::DebugRngListsBase, + is_addr_live: IsAddrLive, + dwo_id: DwoId, + has_type_units: bool, +) -> Result +where + IsAddrLive: Fn(DebugAddrIndex) -> Result, +{ + debug!(?dwo_id, has_type_units, "gc_debug_info: starting GC pass"); + + // Step 1: Walk the DIEs, build the graph, and identify roots. + let Some(header) = debug_info.units().next().map_err(Error::ParseUnitHeader)? else { + return Ok(GcResult { + rewritten: None, + offset_remap: None, + referenced_rnglists: None, + referenced_loclists: None, + referenced_str_offsets: None, + }); + }; + let encoding = header.encoding(); + let abbreviations = + header.abbreviations(&debug_abbrev).map_err(Error::ParseUnitAbbreviations)?; + + // Walk every DIE, recording it and (for fully-live DIEs later) its edges. + // We use `entries_raw` for byte-precise offsets. + let mut entries = header.entries_raw(&abbreviations, None).map_err(Error::ParseUnit)?; + + let mut dies: Vec = Vec::new(); + // Map from CU-relative DIE offset -> index in `dies`. + let mut offset_to_index: HashMap = HashMap::new(); + // Roots discovered during the walk. + let mut worklist: Vec = Vec::new(); + // Per-DIE rnglistx indices (from DW_FORM_rnglistx attributes). + let mut die_rnglistx: Vec> = Vec::new(); + // Per-DIE loclistx indices (from DW_FORM_loclistx attributes). + let mut die_loclistx: Vec> = Vec::new(); + // Per-DIE strx indices (from DW_FORM_strx* attributes). + let mut die_strx: Vec> = Vec::new(); + + // Stack of currently-open parent DIE indices (those with `has_children`). + // Each null entry in the byte stream terminates exactly one such children + // list, so closes one parent. + let mut parent_stack: Vec = Vec::new(); + + let mut can_prune_rnglists = !has_type_units; + let mut can_prune_loclists = !has_type_units; + let can_prune_str_offsets = !has_type_units; + + // The rnglists base used to resolve DW_FORM_rnglistx indices. Initialized + // to the default (header-size-only), but overridden by DW_AT_rnglists_base + // on the CU DIE. + let mut rnglists_base = gimli::DebugRngListsBase::default_for_encoding_and_file( + encoding, + gimli::DwarfFileType::Dwo, + ); + // The loclists base used to resolve DW_FORM_loclistx indices. Initialized + // to the default (header-size-only), but overridden by DW_AT_loclists_base + // on the CU DIE. + let mut loclists_base = gimli::DebugLocListsBase::default_for_encoding_and_file( + encoding, + gimli::DwarfFileType::Dwo, + ); + + while !entries.is_empty() { + let offset = entries.next_offset(); + let Some(abbrev) = entries.read_abbreviation().map_err(Error::ParseUnit)? else { + parent_stack.pop(); + continue; + }; + + let tag = abbrev.tag(); + let has_children = abbrev.has_children(); + + let mut attrs: Vec<(gimli::DwAt, gimli::DwForm, gimli::AttributeValue<_>)> = + Vec::with_capacity(abbrev.attributes().len()); + for spec in abbrev.attributes() { + let attr = entries.read_attribute(*spec).map_err(Error::ParseUnit)?; + attrs.push((spec.name(), spec.form(), attr.raw_value())); + } + + // Extract DW_AT_rnglists_base/DW_AT_loclists_base from the CU DIE + // so we can correctly resolve rnglistx/loclistx indices in child DIEs. + // The raw_value() for DW_FORM_sec_offset is AttributeValue::SecOffset; + // the normalization to DebugRngListsBase/DebugLocListsBase happens + // only in the value() accessor. + if tag == gimli::DW_TAG_compile_unit { + for (name, _, value) in &attrs { + if *name == gimli::DW_AT_rnglists_base { + if let gimli::AttributeValue::SecOffset(offset) = value { + rnglists_base = gimli::DebugRngListsBase(*offset); + } + } + if *name == gimli::DW_AT_loclists_base { + if let gimli::AttributeValue::SecOffset(offset) = value { + loclists_base = gimli::DebugLocListsBase(*offset); + } + } + } + } + + let index = dies.len(); + offset_to_index.insert(offset, index); + + // Identify roots. + let mut liveness = Liveness::Dead; + if is_root( + tag, + &attrs, + &range_lists, + ranges_base, + rnglists_base, + &is_addr_live, + dwo_id, + encoding, + )? { + liveness = Liveness::Live; + worklist.push(index); + } + + // Capture section references for post-GC pruning. + let mut rnglistx_vals: Vec = Vec::new(); + let mut loclistx_vals: Vec = Vec::new(); + let mut strx_vals: Vec = Vec::new(); + for (name, form, value) in &attrs { + if *form == gimli::DW_FORM_sec_offset { + if *name == gimli::DW_AT_ranges { + warn!( + ?dwo_id, + "DW_AT_ranges uses DW_FORM_sec_offset; disabling rnglists pruning" + ); + can_prune_rnglists = false; + } else if *name == gimli::DW_AT_location { + warn!( + ?dwo_id, + "DW_AT_location uses DW_FORM_sec_offset; disabling loclists pruning" + ); + can_prune_loclists = false; + } + } + if let gimli::AttributeValue::DebugRngListsIndex(idx) = value { + rnglistx_vals.push(idx.0 as u64); + } + if let gimli::AttributeValue::DebugLocListsIndex(idx) = value { + loclistx_vals.push(idx.0 as u64); + } + if let gimli::AttributeValue::DebugStrOffsetsIndex(idx) = value { + strx_vals.push(idx.0 as u64); + } + } + + // Collect outgoing edges (used only when this DIE is fully live). + let edges = scan_live_die_attrs(&attrs, &loc_lists, loclists_base, encoding)?; + + let parent = parent_stack.last().copied(); + dies.push(DieRecord { parent, children: vec![], edges, liveness }); + if let Some(parent) = parent { + dies[parent].children.push(index); + } + die_rnglistx.push(rnglistx_vals); + die_loclistx.push(loclistx_vals); + die_strx.push(strx_vals); + + if has_children { + parent_stack.push(index); + } + } + + if dies.is_empty() { + return Ok(GcResult { + rewritten: None, + offset_remap: None, + referenced_rnglists: None, + referenced_loclists: None, + referenced_str_offsets: None, + }); + } + + // The CU root must always survive even if every child DIE is dead. The + // skeleton CU in the linked executable references this CU by DwoId; + // dropping it from the .dwp would leave a dangling reference. That can't be + // fixed without modifying the executable, which we don't do. + // + // But note that we don't seed it as a *live* root because that would + // propagate downward to everything in the CU, entirely defeating the + // purpose of the GC. + dies[0].liveness = Liveness::Retained; + + // Step 2: Mark-and-sweep to fixed point: + // 1. Drain worklist (Live propagation down + edge tracing). + // 2. Upward retention for ancestors of Live DIEs. + // 3. Trace reference edges from Retained DIEs → newly Live targets. + // Repeat until stable. Step 3 is necessary because a Retained DIE (kept only for tree + // structure) may carry a reference edge (e.g. DW_FORM_ref4) whose target is otherwise Dead; + // that target's bytes must survive so the retained reference doesn't dangle after rewrite. + loop { + while let Some(i) = worklist.pop() { + // Downward: all descendants become fully live. + + // XXXkhuey arguably we should stop our descent at "things that + // could have become roots but didn't", so e.g. a live subprogram + // would keep alive a DW_TAG_variable for a local variable inside + // itself but not a DW_TAG_subprogram for a nested subprogram that + // became dead code. This brings along the entire descendant tree. + + // NB: Safe to mem::take because this is the only code that examines + // `children` and we will never add a DIE to the worklist twice. + let children = mem::take(&mut dies[i].children); + for c in children.into_iter() { + if dies[c].liveness != Liveness::Live { + dies[c].liveness = Liveness::Live; + worklist.push(c); + } + } + // Trace outgoing edges. + // NB: Safe to mem::take because the code below that examines `edges` + // only ever operates on Retained DIEs, and we will never add a DIE + // to the worklist twice, and we will never "demote" a Live DIE to + // Retained/Dead. + let edges = mem::take(&mut dies[i].edges); + for target in edges.into_iter() { + if let Some(&ti) = offset_to_index.get(&target) { + if dies[ti].liveness != Liveness::Live { + dies[ti].liveness = Liveness::Live; + worklist.push(ti); + } + } + } + } + + // Upward: mark ancestors of every live DIE as (at least) structurally retained. + for i in 0..dies.len() { + if dies[i].liveness != Liveness::Live { + continue; + } + + let mut p = dies[i].parent; + while let Some(pi) = p { + if dies[pi].liveness == Liveness::Dead { + dies[pi].liveness = Liveness::Retained; + p = dies[pi].parent; + } else { + // Already live or retained: ancestors above are handled too. + break; + } + } + } + + // Trace edges from Retained DIEs; any non-Live target becomes Live and + // re-enters the loop. + for i in 0..dies.len() { + if dies[i].liveness != Liveness::Retained { + continue; + } + + // Safe to mem::take because once a Retained DIE's edge targets are + // promoted, rescanning the DIE would find all edge targets + // already Live and produce no new work. + let edges = mem::take(&mut dies[i].edges); + for target in edges.into_iter() { + if let Some(&ti) = offset_to_index.get(&target) { + if dies[ti].liveness != Liveness::Live { + dies[ti].liveness = Liveness::Live; + worklist.push(ti); + } + } + } + } + + if worklist.is_empty() { + break; + } + } + + // Step 3: Collect section references from surviving DIEs for post-GC pruning. + let mut referenced_rnglists = BTreeSet::new(); + let mut referenced_loclists = BTreeSet::new(); + let mut referenced_str_offsets = BTreeSet::new(); + let total_dies = dies.len(); + let mut dead_dies = 0; + assert_eq!(total_dies, die_rnglistx.len()); + assert_eq!(total_dies, die_loclistx.len()); + assert_eq!(total_dies, die_strx.len()); + for (die, rnglistx, loclistx, strx) in + izip!(&dies, die_rnglistx.into_iter(), die_loclistx.into_iter(), die_strx.into_iter()) + { + if die.liveness == Liveness::Dead { + dead_dies += 1; + continue; + } + referenced_rnglists.extend(rnglistx); + referenced_loclists.extend(loclistx); + referenced_str_offsets.extend(strx); + } + + let referenced_rnglists = if can_prune_rnglists { Some(referenced_rnglists) } else { None }; + let referenced_loclists = if can_prune_loclists { Some(referenced_loclists) } else { None }; + let referenced_str_offsets = + if can_prune_str_offsets { Some(referenced_str_offsets) } else { None }; + // If every DIE survived, nothing to do. + if dead_dies == 0 { + return Ok(GcResult { + rewritten: None, + offset_remap: None, + referenced_rnglists, + referenced_loclists, + referenced_str_offsets, + }); + } + + debug!(?dwo_id, total_dies, dead_dies, "gc_debug_info: pruning dead DIEs"); + + // Build index remapping for compacting offset tables. Each referenced index + // maps to its position in the sorted set (its new compact index). + let rnglist_remap: Option> = referenced_rnglists + .as_ref() + .map(|set| set.iter().enumerate().map(|(new, &old)| (old, new as u64)).collect()); + let loclist_remap: Option> = referenced_loclists + .as_ref() + .map(|set| set.iter().enumerate().map(|(new, &old)| (old, new as u64)).collect()); + let strx_remap: Option> = referenced_str_offsets + .as_ref() + .map(|set| set.iter().enumerate().map(|(new, &old)| (old, new as u64)).collect()); + + // Step 4: rewrite at byte level + let (rewritten, new_offset) = rewrite_unit( + debug_info, + &header, + &abbreviations, + &dies, + &offset_to_index, + rnglist_remap.as_ref(), + loclist_remap.as_ref(), + strx_remap.as_ref(), + )?; + Ok(GcResult { + rewritten: Some(rewritten), + offset_remap: Some(new_offset), + referenced_rnglists, + referenced_loclists, + referenced_str_offsets, + }) +} + +/// Rewrite the unit's bytes, dropping dead DIEs and patching surviving CU-relative references. +/// +/// Each surviving DIE is emitted at exactly its original byte size: attribute bytes are copied +/// verbatim and only the values of CU-relative reference attributes are updated in place. +/// +/// The key insight that makes this ok is that offsets only ever shrink, they cannot expand. +/// Updating values for fixed-width reference forms is trivial. For variable-width reference +/// forms we take advantage of the fact that ULEB128 makes it straightforward to "pad" a +/// lower value to the size of a higher value. +/// +/// We're not producing the absolutely minimal byte sizes here but what we are leaving +/// on the table is immaterial compared to our gains. +fn rewrite_unit( + debug_info: gimli::DebugInfo>, + header: &gimli::UnitHeader>, + abbreviations: &gimli::Abbreviations, + dies: &[DieRecord], + offset_to_index: &HashMap, + rnglist_remap: Option<&HashMap>, + loclist_remap: Option<&HashMap>, + strx_remap: Option<&HashMap>, +) -> Result<(EndianVec, BTreeMap)> { + let endian = debug_info.reader().endian(); + let encoding = header.encoding(); + let header_size = header.size_of_header(); + + // Step 1: compute new offsets for surviving DIEs. + // CU-relative references (DW_FORM_ref*) can point forward to DIEs not yet emitted, so we + // can't patch references in a single pass. Instead, walk the DIE stream to compute + // every surviving DIE's new offset, then later emit the final bytes with all references + // patched. This works because reference patches never change byte width (offsets only shrink, + // and ULEB128 patches are padded to the original width), so each surviving DIE occupies + // exactly its original number of bytes in both passes. + let mut new_offset = BTreeMap::new(); + { + let mut scratch = EndianVec::new(endian); + emit_dies( + header, + abbreviations, + dies, + offset_to_index, + None, + rnglist_remap, + loclist_remap, + strx_remap, + &mut new_offset, + &mut scratch, + )?; + } + + // Step 2: emit with patches using the final `new_offset` map. + let mut body = EndianVec::new(endian); + { + let mut discard = BTreeMap::new(); + emit_dies( + header, + abbreviations, + dies, + offset_to_index, + Some(&new_offset), + rnglist_remap, + loclist_remap, + strx_remap, + &mut discard, + &mut body, + )?; + } + + // Assemble the output: header + body. + let mut out = EndianVec::new(endian); + out.write(&debug_info.reader().slice()[..header_size])?; + out.write(body.slice())?; + + // Patch unit_length in the header. unit_length excludes the initial length field itself. + let is_dwarf64 = encoding.format == gimli::Format::Dwarf64; + let new_unit_length = (out.slice().len() - if is_dwarf64 { 12 } else { 4 }) as u64; + if is_dwarf64 { + out.write_u64_at(4, new_unit_length)?; + } else { + out.write_u32_at( + 0, + new_unit_length.try_into().expect("unit length w/out header larger than u32"), + )?; + } + + Ok((out, new_offset)) +} + +/// Walk the raw DIE byte stream, emitting surviving DIEs to `out` and skipping dead ones. +/// +/// `dies` is the pre-order list from the analysis pass; this walk visits DIEs +/// in the same order, so `die_idx` advances in lockstep. For each surviving DIE, +/// its new offset (relative to the unit start) is recorded in `new_offset` +/// (keyed by old offset). When `patch` is `Some`, CU-relative references are +/// rewritten to their new offsets; when `None` (offset-computation pass), bytes +/// are emitted unpatched (their length is what matters). +fn emit_dies( + header: &gimli::UnitHeader>, + abbreviations: &gimli::Abbreviations, + dies: &[DieRecord], + offset_to_index: &HashMap, + patch: Option<&BTreeMap>, + rnglist_remap: Option<&HashMap>, + loclist_remap: Option<&HashMap>, + strx_remap: Option<&HashMap>, + new_offset: &mut BTreeMap, + out: &mut EndianVec, +) -> Result<()> { + let mut entries_raw = header.entries_raw(abbreviations, None).map_err(Error::ParseUnit)?; + + // Stack tracks whether each open parent with children is being emitted, + // so we know whether to write null terminators when their children list ends. + let mut emit_stack: Vec = Vec::new(); + + while !entries_raw.is_empty() { + let die_start = entries_raw.next_offset(); + let new_off = gimli::UnitOffset(header.root_offset().0 + out.slice().len()); + let Some(abbrev) = + entries_raw.read_abbreviation().map_err(Error::ParseUnitAbbreviations)? + else { + if emit_stack.pop() == Some(true) { + new_offset.insert(die_start, (new_off, emit_stack.len() + 1)); + out.write_u8(0)?; + } + continue; + }; + + let die_index = *offset_to_index.get(&die_start).ok_or(Error::MalformedDebugInfo)?; + let emit = dies[die_index].liveness != Liveness::Dead; + + if emit { + new_offset.insert(die_start, (new_off, emit_stack.len())); + + let after_code = entries_raw.next_offset(); + out.write(&header.range(die_start..after_code)?)?; + + for spec in abbrev.attributes() { + let attr_start = entries_raw.next_offset(); + entries_raw.read_attribute(*spec)?; + let attr_end = entries_raw.next_offset(); + if attr_end > attr_start { + emit_attribute( + header, + header.range(attr_start..attr_end)?, + spec, + emit_stack.len(), + patch, + rnglist_remap, + loclist_remap, + strx_remap, + out, + )?; + } + } + } else { + entries_raw.skip_attributes(abbrev.attributes())?; + } + + if abbrev.has_children() { + emit_stack.push(emit); + } + } + + Ok(()) +} + +/// Emit a single attribute value (the bytes in `data`) to `out`, patching CU-relative references +/// when `patch` is `Some`. +fn emit_attribute( + header: &gimli::UnitHeader>, + mut data: gimli::EndianSlice<'_, RunTimeEndian>, + spec: &gimli::AttributeSpecification, + depth: usize, + patch: Option<&BTreeMap>, + rnglist_remap: Option<&HashMap>, + loclist_remap: Option<&HashMap>, + strx_remap: Option<&HashMap>, + out: &mut EndianVec, +) -> Result<()> { + use gimli::constants::*; + + let name = spec.name(); + let form = spec.form(); + + // Resolve DW_FORM_indirect to its inline form first, copying the form ULEB128 bytes. + if form == DW_FORM_indirect { + let orig = data; + let inner = data.read_uleb128()?; + // Copy the form selector bytes verbatim. + out.write(&orig.range_to(..data.offset_from(orig)))?; + let inner_form = gimli::DwForm(inner as u16); + let inner_spec = gimli::AttributeSpecification::new(name, inner_form, None); + return emit_attribute( + header, + data, + &inner_spec, + depth, + patch, + rnglist_remap, + loclist_remap, + strx_remap, + out, + ); + } + + // When not patching (offset-computation pass), just copy verbatim. + let Some(patch) = patch else { + out.write(&data)?; + return Ok(()); + }; + + // CU-relative reference forms: patch to new offset (same byte width). + if matches!(form, DW_FORM_ref1 | DW_FORM_ref2 | DW_FORM_ref4 | DW_FORM_ref8 | DW_FORM_ref_udata) + { + let old = read_ref_value(data, form)?; + let new = match patch.get(&old) { + Some(n) => n.0, + None => { + // Target was dead. For DW_AT_sibling this can happen. + // Otherwise this should be impossible. + if name == DW_AT_sibling { + patch + .range(old..) + .skip_while(|&(_, v)| v.1 > depth) + .next() + .ok_or(Error::MalformedDebugInfo)? + .1 + .0 + } else { + return Err(Error::MalformedDebugInfo); + } + } + }; + write_ref_value(out, form, old, new)?; + return Ok(()); + } + + // exprloc: copy length prefix, then patch embedded CU-relative references in the expression. + if form == DW_FORM_exprloc { + let orig = data; + let expr_len = data.read_uleb128()? as usize; + // Copy the length prefix verbatim. + out.write(&orig.range_to(..data.offset_from(orig)))?; + // Then patch the expression. + emit_expression(data.range_to(..expr_len), patch, header.encoding(), out)?; + return Ok(()); + } + + // rnglistx/loclistx/strx: remap to compacted offset table index. + let remap = match form { + DW_FORM_rnglistx => rnglist_remap, + DW_FORM_loclistx => loclist_remap, + DW_FORM_strx | DW_FORM_GNU_str_index => strx_remap, + _ => None, + }; + if let Some(remap) = remap { + let old_width = data.len(); + let old_idx = data.read_uleb128()?; + let &new_idx = remap.get(&old_idx).ok_or(Error::MalformedDebugInfo)?; + write_uleb128_padded(out, gimli::UnitOffset(new_idx as usize), old_width)?; + return Ok(()); + } + + // Fixed-width strx forms: remap to compacted str_offsets table index. + let fixed_width: u8 = match form { + DW_FORM_strx1 => 1, + DW_FORM_strx2 => 2, + DW_FORM_strx3 => 3, + DW_FORM_strx4 => 4, + _ => 0, + }; + if fixed_width > 0 { + if let Some(strx_remap) = strx_remap { + let old_idx = data.read_uint(fixed_width as usize)?; + let &new_idx = strx_remap.get(&old_idx).ok_or(Error::MalformedDebugInfo)?; + out.write_udata(new_idx, fixed_width)?; + return Ok(()); + } + } + + // Everything else: copy verbatim. + out.write(&data)?; + Ok(()) +} + +/// Read a CU-relative reference value of the given reference form at `pos`. +fn read_ref_value( + mut data: gimli::EndianSlice<'_, RunTimeEndian>, + form: gimli::DwForm, +) -> Result { + use gimli::constants::*; + Ok(gimli::UnitOffset(match form { + DW_FORM_ref1 => data.read_u8()? as usize, + DW_FORM_ref2 => data.read_u16()? as usize, + DW_FORM_ref4 => data.read_u32()? as usize, + DW_FORM_ref8 => data.read_u64()? as usize, + DW_FORM_ref_udata => data.read_uleb128()? as usize, + _ => return Err(Error::UnsupportedForm(form.0)), + })) +} + +/// Write a CU-relative reference value of the given reference form to `out`, keeping the original +/// byte width. `old` is used to determine the original ULEB128 width for padding. +fn write_ref_value( + out: &mut EndianVec, + form: gimli::DwForm, + old: gimli::UnitOffset, + new: gimli::UnitOffset, +) -> Result<()> { + use gimli::constants::*; + match form { + DW_FORM_ref1 => out.write_u8(new.0 as u8)?, + DW_FORM_ref2 => out.write_u16(new.0 as u16)?, + DW_FORM_ref4 => out.write_u32(new.0 as u32)?, + DW_FORM_ref8 => out.write_u64(new.0 as u64)?, + DW_FORM_ref_udata => { + // Pad to the original number of bytes (offsets only shrink). + let mut v = old.0 as u64; + let mut width = 0; + loop { + width += 1; + v >>= 7; + if v == 0 { + break; + } + } + write_uleb128_padded(out, new, width)?; + } + _ => return Err(Error::UnsupportedForm(form.0)), + } + Ok(()) +} + +/// Emit a DWARF expression (already length-prefixed by the caller), patching any embedded +/// CU-relative DIE references to their new offsets. +fn emit_expression( + expr: gimli::EndianSlice<'_, RunTimeEndian>, + patch: &BTreeMap, + encoding: gimli::Encoding, + out: &mut EndianVec, +) -> Result<()> { + let mut refs: Vec = Vec::new(); + { + let mut on_ref = |r: ExprRef| refs.push(r); + let mut saw_section_absolute_reference = false; + let mut on_sec = |_: usize| { + saw_section_absolute_reference = true; + }; + walk_expression(gimli::Expression(expr), 0, encoding, &mut on_ref, &mut on_sec)?; + if saw_section_absolute_reference { + return Err(Error::UnexpectedSectionAbsoluteReference); + } + } + + if refs.is_empty() { + out.write(&expr)?; + return Ok(()); + } + + // Patch references in increasing position order; for ULEB128 patches we keep the original + // width so byte positions of later references are unchanged. + refs.sort_by_key(|r| r.pos); + let mut copied = 0usize; + for r in &refs { + // Copy bytes up to the reference operand. + out.write(&expr[copied..r.pos])?; + let new = patch.get(&r.old).ok_or(Error::MalformedDebugInfo)?.0; + match r.enc { + ExprRefEnc::U2 => { + out.write_u16(new.0 as u16)?; + copied = r.pos + 2; + } + ExprRefEnc::U4 => { + out.write_u32(new.0 as u32)?; + copied = r.pos + 4; + } + ExprRefEnc::Uleb => { + let old_width = uleb128_len(expr.range_from(r.pos..))?; + write_uleb128_padded(out, new, old_width)?; + copied = r.pos + old_width; + } + } + } + out.write(&expr[copied..])?; + Ok(()) +} diff --git a/thorin/src/index.rs b/thorin/src/index.rs index 8a65483..8159782 100644 --- a/thorin/src/index.rs +++ b/thorin/src/index.rs @@ -48,8 +48,8 @@ fn bucket(elements: &[B]) -> Vec { } /// New-type'd offset into a section of a compilation/type unit's contribution. -#[derive(Copy, Clone, Eq, Hash, PartialEq)] -pub(crate) struct ContributionOffset(pub(crate) u64); +#[derive(Copy, Clone, Default, Eq, Hash, PartialEq)] +pub struct ContributionOffset(pub(crate) u64); impl fmt::Debug for ContributionOffset { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -61,14 +61,40 @@ impl fmt::Debug for ContributionOffset { type ContributionSize = u64; /// Contribution to a section - offset and size. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] -pub(crate) struct Contribution { +#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct Contribution { /// Offset of this contribution into its containing section. pub(crate) offset: ContributionOffset, /// Size of this contribution in its containing section. pub(crate) size: ContributionSize, } +impl Contribution { + /// Byte range of this contribution within its containing section, suitable for slicing. + pub(crate) fn range(&self) -> std::ops::Range { + let start = self.offset.0 as usize; + start..start + self.size as usize + } +} + +impl From<(u32, u32)> for Contribution { + fn from(v: (u32, u32)) -> Contribution { + Contribution { offset: ContributionOffset(v.0 as _), size: v.1 as _ } + } +} + +impl From<(u64, u64)> for Contribution { + fn from(v: (u64, u64)) -> Contribution { + Contribution { offset: ContributionOffset(v.0), size: v.1 } + } +} + +impl From<(usize, usize)> for Contribution { + fn from(v: (usize, usize)) -> Contribution { + Contribution { offset: ContributionOffset(v.0 as _), size: v.1 as _ } + } +} + /// Populated columns in the `.debug_cu_index` or `.debug_tu_index` section. #[derive(Copy, Clone, Debug, Default)] pub(crate) struct IndexColumns { diff --git a/thorin/src/lib.rs b/thorin/src/lib.rs index 3e6f650..f2355d0 100644 --- a/thorin/src/lib.rs +++ b/thorin/src/lib.rs @@ -5,9 +5,13 @@ use std::{ collections::HashSet, fmt, path::{Path, PathBuf}, + rc::Rc, }; +#[cfg(feature = "gc")] +use gimli::UnitType; use gimli::{EndianSlice, Reader}; +use hashbrown::HashMap; use object::{write::Object as WritableObject, FileKind, Object, ObjectSection}; use tracing::{debug, trace}; @@ -15,18 +19,25 @@ use crate::{ error::Result, ext::EndianityExt, index::Bucketable, - package::{dwo_identifier_of_unit, DwarfObject, InProgressDwarfPackage}, + package::{dwo_identifier_of_unit, DwarfObject, InProgressDwarfPackage, SessionHolder}, relocate::{add_relocations, Relocate, RelocationMap}, }; mod error; mod ext; +#[cfg(feature = "gc")] +#[path = "gc.rs"] +pub(crate) mod gc; +#[cfg(not(feature = "gc"))] +#[path = "nogc.rs"] +pub(crate) mod gc; mod index; mod package; mod relocate; mod strings; pub use crate::error::Error; +pub use crate::package::DwoId; /// `Session` is expected to be implemented by users of `thorin`, allowing users of `thorin` to /// decide how to manage data, rather than `thorin` having arenas internally. @@ -56,6 +67,84 @@ pub trait Session { fn read_input<'session>(&'session self, path: &Path) -> std::io::Result<&'session [u8]>; } +#[cfg_attr(not(feature = "gc"), allow(dead_code))] +struct ExecutableData<'session>( + Rc>>>, +); + +#[cfg_attr(not(feature = "gc"), allow(dead_code))] +struct DwoData { + addr_size: u8, + addr_base: gimli::DebugAddrBase, + ranges_base: gimli::DebugRngListsBase, +} + +#[cfg_attr(not(feature = "gc"), allow(dead_code))] +#[derive(Default)] +struct GarbageCollectionData<'session> { + executable_data: HashMap>, + dwo_data: HashMap>, +} + +#[cfg_attr(not(feature = "gc"), allow(dead_code))] +impl<'session> GarbageCollectionData<'session> { + fn put_data_for_executable(&mut self, path: &Path, data: ExecutableData<'session>) { + self.executable_data.insert(path.to_path_buf(), data); + } + fn put_data_for_dwo(&mut self, executable_path: &'_ Path, dwo_id: DwoId, data: DwoData) { + self.dwo_data.entry(dwo_id).or_default().push((executable_path.to_path_buf(), data)); + } + fn get_data_for_executable(&self, path: &'_ Path) -> Option<&ExecutableData<'session>> { + self.executable_data.get(path) + } + fn get_data_for_dwo( + &self, + dwo_id: DwoId, + ) -> Option, &DwoData)>> { + let entries = self.dwo_data.get(&dwo_id)?; + let r = entries + .iter() + .filter_map(|(path, dwo_data)| { + self.executable_data.get(path).map(|exec| (exec, dwo_data)) + }) + .collect::>(); + if r.is_empty() { + return None; + } + Some(r) + } +} + +fn dwarf_from_executable<'session>( + sess: &'session impl Session, + path: &'_ Path, +) -> Result>>> { + let data = sess.read_input(path).map_err(Error::ReadInput)?; + let obj = object::File::parse(data).map_err(Error::ParseObjectFile)?; + + let exec_endian = obj.endianness().as_runtime_endian(); + + let mut load_section = |id: gimli::SectionId| -> Result<_> { + let mut relocations = RelocationMap::default(); + let data = match obj.section_by_name(&id.name()) { + Some(ref section) => { + add_relocations(&mut relocations, &obj, section)?; + section.compressed_data()?.decompress()? + } + // Use a non-zero capacity so that `ReaderOffsetId`s are unique. + None => Cow::Owned(Vec::with_capacity(1)), + }; + + let data_ref = sess.alloc_owned_cow(data); + let reader = EndianSlice::new(data_ref, exec_endian); + let section = reader; + let relocations = sess.alloc_relocation(relocations); + Ok(Relocate { relocations, section, reader }) + }; + + gimli::Dwarf::load(&mut load_section) +} + /// Should missing DWARF objects referenced by executables be skipped or result in an error? /// /// Referenced objects that are still missing when the DWARF package is finished will result in @@ -84,6 +173,8 @@ impl MissingReferencedObjectBehaviour { /// `finish`. pub struct DwarfPackage<'output, 'session: 'output, Sess: Session> { sess: &'session Sess, + #[cfg(feature = "gc")] + gc_data: Option>, maybe_in_progress: Option>, targets: HashSet, } @@ -106,7 +197,13 @@ where { /// Create a new `DwarfPackage` with the provided `Session` implementation. pub fn new(sess: &'session Sess) -> Self { - Self { sess, maybe_in_progress: None, targets: HashSet::new() } + Self { + sess, + #[cfg(feature = "gc")] + gc_data: None, + maybe_in_progress: None, + targets: HashSet::new(), + } } /// Add an input object to the in-progress package. @@ -133,50 +230,30 @@ where }; let sess = self.sess; - self.maybe_in_progress - .as_mut() - .expect("`process_input_object` is broken") - .add_input_object(sess, obj, encoding) + self.maybe_in_progress.as_mut().expect("`process_input_object` is broken").add_input_object( + SessionHolder::SimpleSession(sess), + obj, + encoding, + ) } - /// Add input objects referenced by executable to the DWARF package. - #[tracing::instrument(level = "trace")] - pub fn add_executable( + /// Calls F with the path of each dwo in the executable. + fn iterate_executable_dwo( &mut self, - path: &Path, - missing_behaviour: MissingReferencedObjectBehaviour, - ) -> Result<()> { - let data = self.sess.read_input(path).map_err(Error::ReadInput)?; - let obj = object::File::parse(data).map_err(Error::ParseObjectFile)?; - - let mut load_section = |id: gimli::SectionId| -> Result<_> { - let mut relocations = RelocationMap::default(); - let data = match obj.section_by_name(&id.name()) { - Some(ref section) => { - add_relocations(&mut relocations, &obj, section)?; - section.compressed_data()?.decompress()? - } - // Use a non-zero capacity so that `ReaderOffsetId`s are unique. - None => Cow::Owned(Vec::with_capacity(1)), - }; - - let data_ref = self.sess.alloc_owned_cow(data); - let reader = EndianSlice::new(data_ref, obj.endianness().as_runtime_endian()); - let section = reader; - let relocations = self.sess.alloc_relocation(relocations); - Ok(Relocate { relocations, section, reader }) - }; - - let dwarf = gimli::Dwarf::load(&mut load_section)?; - + dwarf: &gimli::Dwarf>>, + mut f: F, + ) -> Result<()> + where + F: FnMut(&mut Self, &Path) -> Result<()>, + { let mut iter = dwarf.units(); while let Some(header) = iter.next().map_err(Error::ParseUnitHeader)? { - let unit = dwarf.unit(header).map_err(Error::ParseUnit)?; + let unit = dwarf.unit(header.clone()).map_err(Error::ParseUnit)?; let target = match dwo_identifier_of_unit(&dwarf.debug_abbrev, &unit.header)? { Some(target) => target, None => { - debug!("no target"); + debug!("no target {:?}", header.offset()); continue; } }; @@ -225,21 +302,31 @@ where self.targets.insert(target); } - match self.add_input_object(&path) { - Ok(()) => (), - Err(Error::ReadInput(..)) if missing_behaviour.skip_missing() => (), - Err(e) => return Err(e), - } + f(self, &path)?; } Ok(()) } - /// Add an input object to the DWARF package. - /// - /// Input object must be an archive or an elf object. + /// Add input objects referenced by executable to the DWARF package. #[tracing::instrument(level = "trace")] - pub fn add_input_object(&mut self, path: &Path) -> Result<()> { + pub fn add_executable( + &mut self, + path: &Path, + missing_behaviour: MissingReferencedObjectBehaviour, + ) -> Result<()> { + let dwarf = dwarf_from_executable(self.sess, path)?; + self.iterate_executable_dwo(&dwarf, |this, path| match this.add_input_object(path) { + Ok(()) => Ok(()), + Err(Error::ReadInput(..)) if missing_behaviour.skip_missing() => Ok(()), + Err(e) => Err(e), + }) + } + + fn iterate_object(&mut self, path: &'_ Path, mut f: F) -> Result<()> + where + F: FnMut(&mut Self, &object::File) -> Result<()>, + { let data = self.sess.read_input(&path).map_err(Error::ReadInput)?; let kind = FileKind::parse(data).map_err(Error::ParseFileKind)?; @@ -264,7 +351,7 @@ where match kind { FileKind::Elf32 | FileKind::Elf64 => { let obj = object::File::parse(data).map_err(Error::ParseObjectFile)?; - self.process_input_object(&obj)?; + f(self, &obj)?; } _ => { trace!("skipping non-elf archive member"); @@ -276,12 +363,20 @@ where } FileKind::Elf32 | FileKind::Elf64 => { let obj = object::File::parse(data).map_err(Error::ParseObjectFile)?; - self.process_input_object(&obj) + f(self, &obj) } _ => Err(Error::InvalidInputKind), } } + /// Add an input object to the DWARF package. + /// + /// Input object must be an archive or an elf object. + #[tracing::instrument(level = "trace")] + pub fn add_input_object(&mut self, path: &Path) -> Result<()> { + self.iterate_object(path, |this, obj| this.process_input_object(obj)) + } + /// Returns the `object::write::Object` containing the created DWARF package. /// /// Returns an `Error::MissingReferencedUnit` if DWARF objects referenced by executables were @@ -309,4 +404,165 @@ where None => Err(Error::NoOutputObjectCreated), } } + + #[cfg(feature = "gc")] + #[tracing::instrument(level = "trace")] + pub fn preprocess_gc_executable(&mut self, path: &Path) -> Result<()> { + let dwarf = Rc::new(dwarf_from_executable(self.sess, path)?); + let gc_data = self.gc_data.get_or_insert_default(); + gc_data.put_data_for_executable(path, ExecutableData(dwarf.clone())); + + let mut iter = dwarf.units(); + while let Some(header) = iter.next().map_err(Error::ParseUnitHeader)? { + let unit = dwarf.unit(header.clone()).map_err(Error::ParseUnit)?; + + let target = match dwo_identifier_of_unit(&dwarf.debug_abbrev, &unit.header)? { + Some(DwarfObject::Compilation(dwo_id)) => dwo_id, + Some(_) => continue, + None => { + debug!("skipping unit without DWO ID in executable skeleton"); + continue; + } + }; + + match header.type_() { + UnitType::Skeleton(_) => { + // DWARF5: addr_base is in DW_AT_addr_base. + let mut cursor = unit.header.entries(&unit.abbreviations); + cursor.next_dfs()?; + if let Some(root) = cursor.current() { + if let Some(gimli::AttributeValue::DebugAddrBase(addr_base)) = + root.attr_value(gimli::DW_AT_addr_base) + { + let addr_size = header.address_size(); + trace!( + ?target, + addr_size, + ?addr_base, + "found dwo data for DWARF5 skeleton CU" + ); + gc_data.put_data_for_dwo( + path, + target, + DwoData { + addr_size, + addr_base, + // Always 0 for DWARF 5. + ranges_base: gimli::DebugRngListsBase(0), + }, + ); + } + } + } + UnitType::Compilation => { + // DWARF4 GNU extension: skeleton units have DW_AT_GNU_dwo_id, + // DW_AT_GNU_addr_base, and DW_AT_GNU_ranges_base. If no addr + // or ranges base is present, default to 0. + let mut cursor = unit.header.entries(&unit.abbreviations); + cursor.next_dfs()?; + if let Some(root) = cursor.current() { + // Check for DW_AT_GNU_dwo_id to identify skeleton units. + if let Some(gimli::AttributeValue::DwoId(_)) = + root.attr_value(gimli::constants::DW_AT_GNU_dwo_id) + { + // DW_AT_GNU_addr_base defaults to 0 if absent. + let addr_base = root + .attr_value(gimli::constants::DW_AT_GNU_addr_base) + .and_then(|v| { + let gimli::AttributeValue::DebugAddrBase(base) = v else { + return None; + }; + Some(base) + }) + .unwrap_or(gimli::DebugAddrBase(0)); + // DW_AT_GNU_ranges_base defaults to 0 if absent. + let ranges_base = root + .attr_value(gimli::constants::DW_AT_GNU_ranges_base) + .and_then(|v| match v { + gimli::AttributeValue::DebugRngListsBase(base) => Some(base), + _ => None, + }) + .unwrap_or(gimli::DebugRngListsBase(0)); + let addr_size = header.address_size(); + trace!( + ?target, + ?addr_base, + ?ranges_base, + addr_size, + "found dwo data for DWARF4 GNU skeleton CU" + ); + gc_data.put_data_for_dwo( + path, + target, + DwoData { addr_size, addr_base, ranges_base }, + ); + } + } + } + _ => {} + } + } + + Ok(()) + } + + /// Add an input object to the in-progress package with GC. + #[cfg(feature = "gc")] + #[tracing::instrument(level = "trace", skip(obj))] + fn process_gc_input_object<'input>(&mut self, obj: &'input object::File<'input>) -> Result<()> { + if self.maybe_in_progress.is_none() { + self.maybe_in_progress = + Some(InProgressDwarfPackage::new(obj.architecture(), obj.endianness())); + } + + let encoding = if let Some(section) = obj.section_by_name(".debug_info.dwo") { + let data = section.compressed_data()?.decompress()?; + let data_ref = self.sess.alloc_owned_cow(data); + let debug_info = gimli::DebugInfo::new(data_ref, obj.endianness().as_runtime_endian()); + debug_info + .units() + .next() + .map_err(Error::ParseUnitHeader)? + .map(|root_header| root_header.encoding()) + .ok_or(Error::NoCompilationUnits)? + } else { + debug!("no `.debug_info.dwo` in input dwarf object"); + return Ok(()); + }; + + let sess = self.sess; + let gc_data = self.gc_data.as_ref().ok_or(Error::GcNotInitialized)?; + self.maybe_in_progress.as_mut().expect("`process_input_object` is broken").add_input_object( + SessionHolder::new_gc(sess, gc_data), + obj, + encoding, + ) + } + + /// Add an input object to the DWARF package with GC. + /// + /// Input object must be an archive or an elf object. + #[cfg(feature = "gc")] + #[tracing::instrument(level = "trace")] + pub fn add_gc_input_object(&mut self, path: &Path) -> Result<()> { + self.iterate_object(path, |this, obj| this.process_gc_input_object(obj)) + } + + #[cfg(feature = "gc")] + #[tracing::instrument(level = "trace")] + pub fn add_gc_executable( + &mut self, + path: &Path, + missing_behaviour: MissingReferencedObjectBehaviour, + ) -> Result<()> { + let dwarf = self.gc_data.as_ref().ok_or(Error::GcNotInitialized)?.get_data_for_executable(path) + .expect("All executables passed to add_gc_executable() must have preprocess_gc_executable() called first.") + .0 + .clone(); + self.iterate_executable_dwo(&dwarf, |this, path| match this.add_gc_input_object(path) { + Ok(()) => Ok(()), + Err(Error::ReadInput(..)) if missing_behaviour.skip_missing() => Ok(()), + Err(e) => Err(e), + }) + } } diff --git a/thorin/src/nogc.rs b/thorin/src/nogc.rs new file mode 100644 index 0000000..a411585 --- /dev/null +++ b/thorin/src/nogc.rs @@ -0,0 +1,77 @@ +//! Stub module used when the `gc` feature is disabled. +//! +//! Exposes the same crate-internal surface as `gc.rs` so that `package.rs` +//! type-checks without any `#[cfg]` threading in the pipeline. All functions +//! are no-ops that return `None`/`Ok(None)`. + +#![allow(unused_variables)] + +use std::collections::{BTreeMap, BTreeSet}; + +use gimli::{DebugAddrIndex, RunTimeEndian}; + +use crate::{error::Result, package::DwoId, relocate::Relocate}; + +/// Stub result of "garbage-collecting" a `.debug_info.dwo` compilation unit. +/// All fields are always `None` when the `gc` feature is disabled. +pub(crate) struct GcResult { + pub rewritten: Option>, + pub offset_remap: Option>, + pub referenced_rnglists: Option>, + pub referenced_loclists: Option>, + pub referenced_str_offsets: Option>, +} + +pub(crate) fn is_tombstone(addr: u64, address_size: u8) -> bool { + false +} + +pub(crate) fn gc_debug_info( + _debug_info: gimli::DebugInfo>, + _debug_abbrev: gimli::DebugAbbrev>, + _loc_lists: gimli::LocationLists>, + _range_lists: gimli::RangeLists>>, + _ranges_base: gimli::DebugRngListsBase, + _is_addr_live: IsAddrLive, + _dwo_id: DwoId, + _has_type_units: bool, +) -> Result +where + IsAddrLive: Fn(DebugAddrIndex) -> Result, +{ + Ok(GcResult { + rewritten: None, + offset_remap: None, + referenced_rnglists: None, + referenced_loclists: None, + referenced_str_offsets: None, + }) +} + +pub(crate) fn rewrite_rnglists( + _data: gimli::EndianSlice<'_, RunTimeEndian>, + _referenced_indices: &BTreeSet, + _is_addr_live: &IsAddrLive, + _dwo_id: DwoId, +) -> Result>> +where + IsAddrLive: Fn(DebugAddrIndex) -> Result, +{ + Ok(None) +} + +pub(crate) fn patch_debug_loc( + _data: gimli::EndianSlice<'_, RunTimeEndian>, + _encoding: gimli::Encoding, + _offset_remap: &BTreeMap, +) -> Result>> { + Ok(None) +} + +pub(crate) fn rewrite_loclists( + _data: gimli::EndianSlice<'_, RunTimeEndian>, + _referenced_indices: Option<&BTreeSet>, + _offset_remap: Option<&BTreeMap>, +) -> Result>> { + Ok(None) +} diff --git a/thorin/src/package.rs b/thorin/src/package.rs index 1ee1ff8..74eb4d4 100644 --- a/thorin/src/package.rs +++ b/thorin/src/package.rs @@ -1,6 +1,10 @@ -use std::{collections::HashSet, fmt}; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + fmt, +}; -use gimli::{Encoding, RunTimeEndian, UnitHeader, UnitIndex, UnitType}; +use gimli::{Encoding, Reader, RunTimeEndian, Section, UnitHeader, UnitIndex, UnitType}; use object::{ write::{Object as WritableObject, SectionId}, BinaryFormat, Object, ObjectSection, SectionKind, @@ -10,16 +14,17 @@ use tracing::debug; use crate::{ error::{Error, Result}, ext::{CompressedDataRangeExt, EndianityExt, IndexSectionExt, PackageFormatExt}, + gc::is_tombstone, index::{write_index, Bucketable, Contribution, ContributionOffset, IndexEntry}, - relocate::RelocationMap, + relocate::{Relocate, RelocationMap}, strings::PackageStringTable, - Session, + GarbageCollectionData, Session, }; /// New-type'd index (constructed from `gimli::DwoId`) with a custom `Debug` implementation to /// print in hexadecimal. #[derive(Copy, Clone, Eq, Hash, PartialEq)] -pub(crate) struct DwoId(pub(crate) u64); +pub struct DwoId(pub(crate) u64); impl Bucketable for DwoId { fn index(&self) -> u64 { @@ -105,7 +110,7 @@ pub(crate) fn dwo_identifier_of_unit( // Compilation units with GNU Extension UnitType::Compilation => { let abbreviations = - header.abbreviations(&debug_abbrev).map_err(Error::ParseUnitAbbreviations)?; + header.abbreviations(debug_abbrev).map_err(Error::ParseUnitAbbreviations)?; let mut cursor = header.entries(&abbreviations); cursor.next_dfs()?; let root = cursor.current().ok_or(Error::NoDie)?; @@ -114,11 +119,10 @@ pub(crate) fn dwo_identifier_of_unit( _ => return Err(Error::TopLevelDieNotUnit), } for attr in root.attrs() { - match (attr.name(), attr.value()) { - (gimli::constants::DW_AT_GNU_dwo_id, gimli::AttributeValue::DwoId(dwo_id)) => { - return Ok(Some(DwarfObject::Compilation(dwo_id.into()))) - } - _ => (), + if let (gimli::constants::DW_AT_GNU_dwo_id, gimli::AttributeValue::DwoId(dwo_id)) = + (attr.name(), attr.value()) + { + return Ok(Some(DwarfObject::Compilation(dwo_id.into()))); } } @@ -231,20 +235,12 @@ where match (index, contribution) { // dwp input with section (Some(index), Some(contribution)) => { - let idx = identifier.index(); - let row_id = index.find(idx).ok_or(Error::UnitNotInIndex(idx))?; - let section = index - .sections(row_id) - .map_err(|e| Error::RowNotInIndex(e, row_id))? - .find(|index_section| index_section.section == target_section_id) + let section = find_index_section(index, identifier, target_section_id)? .ok_or(Error::SectionNotInRow)?; let adjusted_offset: u64 = contribution.offset.0 + *adjustment; *adjustment += section.size as u64; - Ok(Some(Contribution { - offset: ContributionOffset(adjusted_offset), - size: section.size as u64, - })) + Ok(Some(Contribution::from((adjusted_offset, section.size as u64)))) } // dwp input without section (Some(_) | None, None) => Ok(contribution), @@ -254,6 +250,65 @@ where } } +/// Look up a unit's entry for the specified section in a `UnitIndex`. +fn find_index_section( + index: &UnitIndex, + id: DwarfObject, + section_id: gimli::IndexSectionId, +) -> Result> { + let idx = id.index(); + let row_id = index.find(idx).ok_or(Error::UnitNotInIndex(idx))?; + let section = index + .sections(row_id) + .map_err(|e| Error::RowNotInIndex(e, row_id))? + .find(|index_section| index_section.section == section_id); + Ok(section) +} + +/// Resolve a unit's [`Contribution`] within a shared input section. +/// +/// For `.dwp` inputs the range comes from the unit index. For `.dwo` inputs +/// (no index) the unit's contribution is the whole section, so a `Contribution` +/// covering the entire section is returned. This mirrors the per-unit slicing +/// done by `create_contribution_adjustor`, but is used by GC to rewrite each +/// unit's subrange of the section independently. +fn unit_section_range( + index: Option<&UnitIndex>, + id: DwarfObject, + section_id: gimli::IndexSectionId, + whole_len: usize, +) -> Result { + let Some(index) = index else { + return Ok(Contribution::from((0, whole_len))); + }; + + let section = find_index_section(index, id, section_id)?; + let contribution = section.map_or(Contribution::default(), |s| { + Contribution::from((s.offset as usize, s.size as usize)) + }); + // The offset and size come straight from the (possibly malformed) input index, so + // validate them against the real section length before they are used to slice it. + if contribution.offset.0.checked_add(contribution.size).is_none_or(|end| end > whole_len as u64) + { + return Err(Error::ContributionOutOfBounds(contribution, whole_len)); + } + Ok(contribution) +} + +/// Per-CU data accumulated during the first pass and carried forward +/// to output the shared sections. +/// +/// `entry` is built incrementally: the non-shared fields are set during the +/// first pass, and the four shared fields (`debug_loc`, `debug_loclists`, +/// `debug_rnglists`, `debug_str_offsets`) are either filled in directly by +/// `emit_gc_shared_sections` (GC no-type-units path) or computed from +/// adjustors in the second pass (all other paths). +struct PendingEntry { + entry: IndexEntry, + /// GC result for this unit, used to rewrite shared sections. + gc_result: Option, +} + /// Wrapper around `object::write::Object` that keeps track of the section indexes relevant to /// DWARF packaging. struct DwarfPackageObject<'file> { @@ -306,10 +361,7 @@ macro_rules! generate_append_for { // FIXME: correct alignment let offset = self.obj.append_section_data(id, data, 1); debug!(?offset, ?data); - Some(Contribution { - offset: ContributionOffset(offset), - size: data.len().try_into().expect("data size larger than u64"), - }) + Some(Contribution::from((offset, data.len().try_into().expect("data size larger than u64")))) } )+ }; @@ -363,6 +415,727 @@ impl<'file> DwarfPackageObject<'file> { } } +#[cfg_attr(not(feature = "gc"), allow(dead_code))] +pub(crate) struct GcSessionData<'input, 'gc, 'session, Sess> +where + Sess: Session, +{ + session: &'session Sess, + gc_data: &'gc GarbageCollectionData<'session>, + debug_loc: Cow<'input, [u8]>, + debug_loclists: Cow<'input, [u8]>, + debug_rnglists: Cow<'input, [u8]>, + debug_str_offsets: gimli::DebugStrOffsets>, + debug_str: gimli::DebugStr>, +} + +#[cfg_attr(not(feature = "gc"), allow(dead_code))] +pub(crate) enum SessionHolder<'input, 'gc, 'session, Sess> +where + Sess: Session, +{ + SimpleSession(&'session Sess), + GcSession(GcSessionData<'input, 'gc, 'session, Sess>), +} + +impl<'input, 'gc, 'session, Sess> SessionHolder<'input, 'gc, 'session, Sess> +where + Sess: Session, +{ + fn session(&'_ self) -> &'session Sess { + match self { + SessionHolder::SimpleSession(session) + | SessionHolder::GcSession(GcSessionData { session, .. }) => session, + } + } + + fn save_debug_loc_for_gc(&mut self, debug_loc: Cow<'input, [u8]>) -> Option> { + let SessionHolder::GcSession(ref mut data) = self else { + return Some(debug_loc); + }; + + data.debug_loc = debug_loc; + None + } + + fn save_debug_loclists_for_gc( + &mut self, + debug_loclists: Cow<'input, [u8]>, + ) -> Option> { + let SessionHolder::GcSession(ref mut data) = self else { + return Some(debug_loclists); + }; + + data.debug_loclists = debug_loclists; + None + } + + fn save_debug_rnglists_for_gc( + &mut self, + debug_rnglists: Cow<'input, [u8]>, + ) -> Option> { + let SessionHolder::GcSession(ref mut data) = self else { + return Some(debug_rnglists); + }; + + data.debug_rnglists = debug_rnglists; + None + } + + fn save_debug_str_offsets_for_gc( + &mut self, + debug_str_offsets: gimli::DebugStrOffsets>, + ) -> bool { + let SessionHolder::GcSession(ref mut data) = self else { + return false; + }; + + data.debug_str_offsets = debug_str_offsets; + true + } + + fn save_debug_str_for_gc( + &mut self, + debug_str: gimli::DebugStr>, + ) -> bool { + let SessionHolder::GcSession(ref mut data) = self else { + return false; + }; + + data.debug_str = debug_str; + true + } +} + +impl<'input, 'gc, 'session: 'input, S: Session> + SessionHolder<'input, 'gc, 'session, S> +{ + fn maybe_gc( + data: &GcSessionData<'input, 'gc, 'session, S>, + cu_index: Option<&UnitIndex>, + id: DwarfObject, + debug_info: &'input [u8], + debug_abbrev: &gimli::DebugAbbrev>, + endian: RunTimeEndian, + has_type_units: bool, + ) -> Result> { + let DwarfObject::Compilation(dwo_id) = id else { + return Ok(None); + }; + let Some(exec_entries) = data.gc_data.get_data_for_dwo(dwo_id) else { + return Ok(None); + }; + debug_assert!(!exec_entries.is_empty()); + + // The DWARF4 `.debug_ranges` contains addresses separately from the + // `.debug_addr` section. Rather than implement the `.debug_addr` style + // merging we do with is_addr_live below for cases with more than one + // executable with `.debug_ranges`, just error out to avoid the + // complexity of handling an extreme edge case. + if exec_entries + .iter() + .filter(|(exec, _)| !exec.0.ranges.debug_ranges().reader().is_empty()) + .count() + > 1 + { + return Err(crate::error::Error::GcSharedDwarf4Ranges(dwo_id)); + } + + let is_addr_live = |index: gimli::DebugAddrIndex| -> crate::error::Result { + for (executable_data, dwo_data) in &exec_entries { + let addr = executable_data + .0 + .debug_addr + .get_address(dwo_data.addr_size, dwo_data.addr_base, index) + .map_err(crate::error::Error::from)?; + if !is_tombstone(addr, dwo_data.addr_size) { + return Ok(true); + } + } + Ok(false) + }; + + // For `.dwp` inputs the shared sections are concatenations of per-CU + // contributions, and the CU header's offsets (e.g. abbrev) are relative + // to this CU's contribution. Slice each section to this CU's subrange + // so the DIE walk resolves abbreviations, location lists, + // and range lists correctly (for `.dwo` inputs each subrange is the + // whole section and this is a noop). + let abbrev_bytes = debug_abbrev.reader().slice(); + let abbrev_contribution = unit_section_range( + cu_index, + id, + gimli::IndexSectionId::DebugAbbrev, + abbrev_bytes.len(), + )?; + let debug_abbrev = + gimli::DebugAbbrev::new(&abbrev_bytes[abbrev_contribution.range()], endian); + + let loc_contribution = unit_section_range( + cu_index, + id, + gimli::IndexSectionId::DebugLoc, + data.debug_loc.len(), + )?; + let loclists_contribution = unit_section_range( + cu_index, + id, + gimli::IndexSectionId::DebugLocLists, + data.debug_loclists.len(), + )?; + let rnglists_contribution = unit_section_range( + cu_index, + id, + gimli::IndexSectionId::DebugRngLists, + data.debug_rnglists.len(), + )?; + + // `.debug_rnglists` data in the .dwo is self-contained; only the + // executable's `.debug_ranges` is needed to construct `RangeLists`. + // At most one executable has non-empty `.debug_ranges` here (enforced + // above), so use that one. If none do, just take the first one since + // it's unused. + let ranges_idx = exec_entries + .iter() + .position(|(exec, _)| !exec.0.ranges.debug_ranges().reader().is_empty()) + .unwrap_or(0); + let ranges_executable_data = &exec_entries[ranges_idx].0; + let ranges_base = exec_entries[ranges_idx].1.ranges_base; + + let gc_result = crate::gc::gc_debug_info( + gimli::DebugInfo::new(debug_info, endian), + debug_abbrev, + gimli::LocationLists::new( + gimli::DebugLoc::from(gimli::EndianSlice::new( + &data.debug_loc[loc_contribution.range()], + endian, + )), + gimli::DebugLocLists::from(gimli::EndianSlice::new( + &data.debug_loclists[loclists_contribution.range()], + endian, + )), + ), + gimli::RangeLists::new(ranges_executable_data.0.ranges.debug_ranges().clone(), { + let relocations = data.session.alloc_relocation(RelocationMap::default()); + let section = gimli::EndianSlice::new( + &data.debug_rnglists[rnglists_contribution.range()], + endian, + ); + let reader = section; + gimli::DebugRngLists::from(Relocate { relocations, section, reader }) + }), + ranges_base, + is_addr_live, + dwo_id, + has_type_units, + )?; + + Ok(Some(gc_result)) + } + + /// Run garbage collection for a single unit. The shared per-input-object + /// sections will be emitted once after all units have been GC'd by + /// `emit_gc_shared_sections`. Returns the unit's [`crate::gc::GcResult`], + /// if any. + fn run_unit_gc( + &self, + cu_index: Option<&UnitIndex>, + id: DwarfObject, + debug_info: &'input [u8], + debug_abbrev: &gimli::DebugAbbrev>, + endian: RunTimeEndian, + has_type_units: bool, + ) -> Result> { + let SessionHolder::GcSession(ref data) = self else { + return Ok(None); + }; + Self::maybe_gc(data, cu_index, id, debug_info, debug_abbrev, endian, has_type_units) + } + + /// Emit the per-input-object shared sections (`.debug_rnglists`, + /// `.debug_loc`, `.debug_loclists`, `.debug_str_offsets`) once, + /// using the GC results of *all* units. + /// + /// These sections are shared between the units of an input object (in + /// particular, a type unit shares them with its compilation unit, and a + /// `.dwp` shares them across CUs), so they cannot be emitted inside the + /// per-unit loop. + /// Each unit's contribution to a shared section is rewritten independently + /// using that unit's GC result and its own subrange of the section so + /// CU-relative references and referenced-index sets from different units + /// never bleed together. + /// + /// There are two regimes, selected by `has_type_units`: + /// + /// - **No type units**: list pruning is possible, so the rewritten bytes + /// may shrink. Each CU's subrange is rewritten, the chunks are + /// concatenated, and per-unit contributions are recomputed from the + /// actual chunk sizes. The returned map supplies these contributions + /// directly (the caller must not run the input-index-derived adjustors + /// for these sections). This method returns `Ok(true)` indicating that + /// it did the work. + /// - **Type units present**: pruning is disabled, so only size-preserving + /// expression patching happens. Each CU's subrange is patched in place, + /// the byte layout is unchanged, and the base contributions are stored in + /// `debug_*`. The caller runs the normal adjustors and this method returns + /// `Ok(false)`. + /// + /// For a non-GC session this is a no-op returning `Ok(false)` (the shared + /// sections were already appended in the section loop). + fn emit_gc_shared_sections( + &self, + cu_index: Option<&UnitIndex>, + pending: &mut [PendingEntry], + encoding: Encoding, + endian: RunTimeEndian, + has_type_units: bool, + has_debug_macro: bool, + obj: &mut DwarfPackageObject<'_>, + string_table: &mut PackageStringTable, + debug_loc: &mut Option, + debug_loclists: &mut Option, + debug_rnglists: &mut Option, + debug_str_offsets: &mut Option, + ) -> Result { + let SessionHolder::GcSession(ref data) = self else { + return Ok(false); + }; + + if has_type_units { + // Pruning is disabled. Patch CU-relative references in place, preserving the + // exact byte layout so input-index-derived contributions remain valid. + let str_offsets_whole = data.debug_str_offsets.reader().slice(); + let mut unit_has_loc: Vec = Vec::with_capacity(pending.len()); + let mut unit_has_loclists: Vec = Vec::with_capacity(pending.len()); + let mut unit_has_str_off: Vec = Vec::with_capacity(pending.len()); + match cu_index { + None => { + for _ in pending.iter() { + unit_has_loc.push(Contribution::from((0, data.debug_loc.len()))); + unit_has_loclists.push(Contribution::from((0, data.debug_loclists.len()))); + unit_has_str_off.push(Contribution::from((0, str_offsets_whole.len()))); + } + } + Some(index) => { + for unit in pending.iter() { + let id = unit.entry.id; + let idx = id.index(); + let row_id = index.find(idx).ok_or(Error::UnitNotInIndex(idx))?; + let mut loc = Contribution::default(); + let mut loclists = Contribution::default(); + let mut str_off = Contribution::default(); + for section in + index.sections(row_id).map_err(|e| Error::RowNotInIndex(e, row_id))? + { + let r = Contribution::from((section.offset, section.size)); + match section.section { + gimli::IndexSectionId::DebugLoc => loc = r, + gimli::IndexSectionId::DebugLocLists => loclists = r, + gimli::IndexSectionId::DebugStrOffsets => str_off = r, + _ => {} + } + } + unit_has_loc.push(loc); + unit_has_loclists.push(loclists); + unit_has_str_off.push(str_off); + } + } + } + + if !data.debug_loc.is_empty() { + let original = &data.debug_loc; + let mut buf: Option> = None; + for (idx, unit) in pending.iter().enumerate() { + let DwarfObject::Compilation(_) = unit.entry.id else { continue }; + let Some(gc) = &unit.gc_result else { continue }; + let Some(remap) = gc.rewritten.as_ref().and(gc.offset_remap.as_ref()) else { + continue; + }; + let contribution = unit_has_loc[idx]; + if contribution.size == 0 { + continue; + } + let bytes = buf.get_or_insert_with(|| original.to_vec()); + if let Some(patched) = crate::gc::patch_debug_loc( + gimli::EndianSlice::new(&bytes[contribution.range()], endian), + encoding, + remap, + )? { + bytes[contribution.range()].copy_from_slice(&patched); + } + } + *debug_loc = obj.append_to_debug_loc(buf.as_deref().unwrap_or(original)); + } + + if !data.debug_loclists.is_empty() { + let original = &data.debug_loclists; + let mut buf: Option> = None; + for (idx, unit) in pending.iter().enumerate() { + let DwarfObject::Compilation(_) = unit.entry.id else { continue }; + let Some(gc) = &unit.gc_result else { continue }; + let Some(remap) = gc.rewritten.as_ref().and(gc.offset_remap.as_ref()) else { + continue; + }; + let contribution = unit_has_loclists[idx]; + if contribution.size == 0 { + continue; + } + let bytes = buf.get_or_insert_with(|| original.to_vec()); + if let Some(patched) = crate::gc::rewrite_loclists( + gimli::EndianSlice::new(&bytes[contribution.range()], endian), + None, + Some(remap), + )? { + bytes[contribution.range()].copy_from_slice(&patched); + } + } + *debug_loclists = obj.append_to_debug_loclists(buf.as_deref().unwrap_or(original)); + } + + // Range lists carry no DIE references and are not pruned when type units are present. + if !data.debug_rnglists.is_empty() { + *debug_rnglists = obj.append_to_debug_rnglists(&data.debug_rnglists); + } + + // String offsets are never pruned here (no type-unit-safe compaction), but each CU's + // contribution must still be remapped into the merged string table. Remap per-CU and + // write back in place, so multi-CU DWARF 5 inputs (one header per contribution) are + // handled correctly while the exact byte layout (and thus the adjustors) is preserved. + // Type units share their compilation unit's contribution, so only compilation units + // are iterated, remapping each distinct contribution exactly once. + if !str_offsets_whole.is_empty() { + let mut bytes = str_offsets_whole.to_vec(); + for (idx, unit) in pending.iter().enumerate() { + let DwarfObject::Compilation(_) = unit.entry.id else { continue }; + let contribution = unit_has_str_off[idx]; + if contribution.size == 0 { + continue; + } + let sub_str_offsets = gimli::DebugStrOffsets::from(gimli::EndianSlice::new( + &bytes[contribution.range()], + endian, + )); + let remapped = string_table.remap_str_offsets_section( + data.debug_str, + sub_str_offsets, + endian, + encoding, + None, + )?; + let remapped = remapped.slice(); + assert_eq!( + remapped.len(), + contribution.size as usize, + "unpruned str_offsets remap must preserve contribution size" + ); + bytes[contribution.range()].copy_from_slice(remapped); + } + *debug_str_offsets = obj.append_to_debug_str_offsets(&bytes); + } + + return Ok(false); + } + + // No type units: list pruning is possible, so rewrite each CU's subrange and write the + // recomputed per-unit contributions directly into each pending entry. + + // Records each unit's `(start, len)` chunk within the freshly appended section directly + // into the corresponding `pending` entry. `ranges` entries are `(pending_index, start, len)`. + fn record( + pending: &mut [PendingEntry], + base: Option, + ranges: &[(usize, usize, usize)], + field: impl Fn(&mut IndexEntry) -> &mut Option, + ) { + let Some(base) = base else { return }; + for &(idx, start, len) in ranges { + if len > 0 { + *field(&mut pending[idx].entry) = Some(Contribution { + offset: ContributionOffset(base.offset.0 + start as u64), + size: len as u64, + }); + } + } + } + + // Assembles output bytes from per-unit rewritten chunks (or the original section bytes), + // appends to the output object, and records contributions into pending entries. + fn assemble_and_record( + pending: &mut [PendingEntry], + whole: &[u8], + unit_ranges: &[Contribution], + modified: Option)>>, + mut append: FA, + field: FF, + ) where + FA: FnMut(&[u8]) -> Option, + FF: Fn(&mut IndexEntry) -> &mut Option, + { + let (base, ranges) = if let Some(modified) = modified { + let modified: HashMap> = modified.into_iter().collect(); + let mut bytes = Vec::new(); + let mut ranges: Vec<(usize, usize, usize)> = Vec::new(); + for (idx, _) in pending.iter().enumerate() { + let chunk: &[u8] = if let Some(v) = modified.get(&idx) { + v + } else { + &whole[unit_ranges[idx].range()] + }; + let start = bytes.len(); + bytes.extend_from_slice(chunk); + ranges.push((idx, start, chunk.len())); + } + (append(&bytes), ranges) + } else { + let ranges = unit_ranges + .iter() + .enumerate() + .map(|(idx, c)| (idx, c.offset.0 as usize, c.size as usize)) + .collect(); + (append(whole), ranges) + }; + record(pending, base, &ranges, field); + } + + // Pre-compute each unit's (offset, size) for all four shared sections in a single + // index lookup per unit: one index.find + one sections scan covers all four sections, + // rather than four separate unit_section_range calls (each doing their own find + scan). + let str_offsets_whole = data.debug_str_offsets.reader().slice(); + let mut unit_rnglists: Vec = Vec::with_capacity(pending.len()); + let mut unit_loc: Vec = Vec::with_capacity(pending.len()); + let mut unit_loclists: Vec = Vec::with_capacity(pending.len()); + let mut unit_str_offsets: Vec = Vec::with_capacity(pending.len()); + for unit in pending.iter() { + let (rng, loc, loclists, str_off) = match cu_index { + None => ( + Contribution::from((0, data.debug_rnglists.len())), + Contribution::from((0, data.debug_loc.len())), + Contribution::from((0, data.debug_loclists.len())), + Contribution::from((0, str_offsets_whole.len())), + ), + Some(index) => { + let id = unit.entry.id; + let idx = id.index(); + let row_id = index.find(idx).ok_or(Error::UnitNotInIndex(idx))?; + let mut rng = Contribution::default(); + let mut loc = Contribution::default(); + let mut loclists = Contribution::default(); + let mut str_off = Contribution::default(); + for section in + index.sections(row_id).map_err(|e| Error::RowNotInIndex(e, row_id))? + { + let r = Contribution::from((section.offset, section.size)); + match section.section { + gimli::IndexSectionId::DebugRngLists => rng = r, + gimli::IndexSectionId::DebugLoc => loc = r, + gimli::IndexSectionId::DebugLocLists => loclists = r, + gimli::IndexSectionId::DebugStrOffsets => str_off = r, + _ => {} + } + } + (rng, loc, loclists, str_off) + } + }; + unit_rnglists.push(rng); + unit_loc.push(loc); + unit_loclists.push(loclists); + unit_str_offsets.push(str_off); + } + + if !data.debug_rnglists.is_empty() { + let whole = &data.debug_rnglists; + // Collect only the units whose rnglists were actually rewritten. In the common case + // (no rewriting) this stays None and no extra allocation is needed. + let mut modified: Option)>> = None; + for (idx, unit) in pending.iter().enumerate() { + let contribution = unit_rnglists[idx]; + if contribution.size == 0 { + continue; + } + let Some(gc) = &unit.gc_result else { continue }; + let Some(referenced) = gc.rewritten.as_ref().and(gc.referenced_rnglists.as_ref()) + else { + continue; + }; + let DwarfObject::Compilation(dwo_id) = unit.entry.id else { + unreachable!("rnglists referenced set is only set for compilation units") + }; + let Some(exec_entries) = data.gc_data.get_data_for_dwo(dwo_id) else { + continue; + }; + let is_addr_live = + |index: gimli::DebugAddrIndex| -> crate::error::Result { + for (executable_data, dwo_data) in &exec_entries { + let addr = executable_data + .0 + .debug_addr + .get_address(dwo_data.addr_size, dwo_data.addr_base, index) + .map_err(crate::error::Error::from)?; + if !is_tombstone(addr, dwo_data.addr_size) { + return Ok(true); + } + } + Ok(false) + }; + if let Some(v) = crate::gc::rewrite_rnglists( + gimli::EndianSlice::new(&whole[contribution.range()], endian), + referenced, + &is_addr_live, + dwo_id, + )? { + modified.get_or_insert_with(Vec::new).push((idx, v)); + } + } + assemble_and_record( + pending, + whole, + &unit_rnglists, + modified, + |b| obj.append_to_debug_rnglists(b), + |e| &mut e.debug_rnglists, + ); + } + + if !data.debug_loc.is_empty() { + let whole = &data.debug_loc; + let mut modified: Option)>> = None; + for (idx, unit) in pending.iter().enumerate() { + let contribution = unit_loc[idx]; + if contribution.size == 0 { + continue; + } + let Some(gc) = &unit.gc_result else { continue }; + let Some(remap) = gc.rewritten.as_ref().and(gc.offset_remap.as_ref()) else { + continue; + }; + if let Some(v) = crate::gc::patch_debug_loc( + gimli::EndianSlice::new(&whole[contribution.range()], endian), + encoding, + remap, + )? { + modified.get_or_insert_with(Vec::new).push((idx, v)); + } + } + assemble_and_record( + pending, + whole, + &unit_loc, + modified, + |b| obj.append_to_debug_loc(b), + |e| &mut e.debug_loc, + ); + } + + if !data.debug_loclists.is_empty() { + let whole = &data.debug_loclists; + let mut modified: Option)>> = None; + for (idx, unit) in pending.iter().enumerate() { + let contribution = unit_loclists[idx]; + if contribution.size == 0 { + continue; + } + let Some(gc) = &unit.gc_result else { continue }; + let remap = gc.rewritten.as_ref().and(gc.offset_remap.as_ref()); + if remap.is_some() { + if let Some(v) = crate::gc::rewrite_loclists( + gimli::EndianSlice::new(&whole[contribution.range()], endian), + gc.rewritten.as_ref().and(gc.referenced_loclists.as_ref()), + remap, + )? { + modified.get_or_insert_with(Vec::new).push((idx, v)); + } + } + } + assemble_and_record( + pending, + whole, + &unit_loclists, + modified, + |b| obj.append_to_debug_loclists(b), + |e| &mut e.debug_loclists, + ); + } + + if !str_offsets_whole.is_empty() { + // Count how many pending units share each contribution range. When cu_index=None + // every unit gets (0, whole_len), so all CUs share one range. In that case the + // per-CU strx filter cannot be applied safely: the filter indices are 0-based + // within each CU's own sub-table, but the slice starts at offset 0 of the + // full section (i.e. the first CU's territory). Use filter=None for any range + // that is shared, preserving all entries. + let mut range_count: HashMap = HashMap::new(); + for &contribution in &unit_str_offsets { + if contribution.size > 0 { + *range_count.entry(contribution).or_insert(0) += 1; + } + } + + let mut bytes = Vec::new(); + let mut ranges = Vec::new(); + // Cache contribution → (start, len) in `bytes` so units sharing a range + // reuse the same remapped bytes rather than re-remapping (which would be + // wrong for per-CU filters and wasteful even for filter=None). + let mut range_cache: HashMap = HashMap::new(); + for (idx, unit) in pending.iter().enumerate() { + let contribution = unit_str_offsets[idx]; + if contribution.size == 0 { + continue; + } + if let Some(&(start, len)) = range_cache.get(&contribution) { + ranges.push((idx, start, len)); + continue; + } + // A `.debug_macro` section indexes strings by `strx`, so the offset table cannot + // be compacted even when DIEs are removed. Likewise, when this contribution range + // is shared by multiple units (cu_index=None, multi-CU .dwo), per-CU filtering + // cannot be applied correctly. + let filter = if has_debug_macro + || range_count.get(&contribution).copied().unwrap_or(0) > 1 + { + None + } else { + unit.gc_result.as_ref().and_then(|gc| { + gc.rewritten.as_ref().and(gc.referenced_str_offsets.as_ref()) + }) + }; + let sub_str_offsets = gimli::DebugStrOffsets::from(gimli::EndianSlice::new( + &str_offsets_whole[contribution.range()], + endian, + )); + let remapped = string_table.remap_str_offsets_section( + data.debug_str, + sub_str_offsets, + endian, + encoding, + filter, + )?; + let start = bytes.len(); + bytes.extend_from_slice(remapped.slice()); + let len = bytes.len() - start; + range_cache.insert(contribution, (start, len)); + ranges.push((idx, start, len)); + } + let base = obj.append_to_debug_str_offsets(&bytes); + record(pending, base, &ranges, |e| &mut e.debug_str_offsets); + } + + Ok(true) + } + + #[cfg_attr(not(feature = "gc"), allow(dead_code))] + pub(crate) fn new_gc(sess: &'session S, gc_data: &'gc GarbageCollectionData<'session>) -> Self { + SessionHolder::GcSession(GcSessionData { + session: sess, + gc_data, + debug_loc: Default::default(), + debug_loclists: Default::default(), + debug_rnglists: Default::default(), + debug_str_offsets: Default::default(), + debug_str: Default::default(), + }) + } +} + /// In-progress DWARF package being produced. pub(crate) struct InProgressDwarfPackage<'file> { /// Endianness of the DWARF package being created. @@ -417,22 +1190,22 @@ impl<'file> InProgressDwarfPackage<'file> { /// /// Copies relevant debug sections, compilation/type units and strings from the `input` DWARF /// object into this DWARF package. - #[tracing::instrument(level = "trace", skip(sess, input,))] - pub(crate) fn add_input_object<'input, 'session: 'input>( + #[tracing::instrument(level = "trace", skip(sess, input))] + pub(crate) fn add_input_object<'input, 'gc, 'session: 'input>( &mut self, - sess: &'session impl Session, + mut sess: SessionHolder<'input, 'gc, 'session, impl Session>, input: &object::File<'input>, encoding: Encoding, ) -> Result<()> { // Load index sections (if they exist). let cu_index = maybe_load_index_section::<_, gimli::DebugCuIndex<_>, _, _>( - sess, + sess.session(), encoding, self.endian, input, )?; let tu_index = maybe_load_index_section::<_, gimli::DebugTuIndex<_>, _, _>( - sess, + sess.session(), encoding, self.endian, input, @@ -459,6 +1232,7 @@ impl<'file> InProgressDwarfPackage<'file> { // Iterate over sections rather than using `section_by_name` because sections can be // repeated. + let mut has_type_units = false; for section in input.sections() { match section.name() { Ok(".debug_abbrev.dwo" | ".zdebug_abbrev.dwo") => { @@ -471,11 +1245,15 @@ impl<'file> InProgressDwarfPackage<'file> { } Ok(".debug_loc.dwo" | ".zdebug_loc.dwo") => { let data = section.compressed_data()?.decompress()?; - update!(debug_loc += self.obj.append_to_debug_loc(&data)); + if let Some(data) = sess.save_debug_loc_for_gc(data) { + update!(debug_loc += self.obj.append_to_debug_loc(&data)); + } } Ok(".debug_loclists.dwo" | ".zdebug_loclists.dwo") => { let data = section.compressed_data()?.decompress()?; - update!(debug_loclists += self.obj.append_to_debug_loclists(&data)); + if let Some(data) = sess.save_debug_loclists_for_gc(data) { + update!(debug_loclists += self.obj.append_to_debug_loclists(&data)); + } } Ok(".debug_macinfo.dwo" | ".zdebug_macinfo.dwo") => { let data = section.compressed_data()?.decompress()?; @@ -487,41 +1265,44 @@ impl<'file> InProgressDwarfPackage<'file> { } Ok(".debug_rnglists.dwo" | ".zdebug_rnglists.dwo") => { let data = section.compressed_data()?.decompress()?; - update!(debug_rnglists += self.obj.append_to_debug_rnglists(&data)); + if let Some(data) = sess.save_debug_rnglists_for_gc(data) { + update!(debug_rnglists += self.obj.append_to_debug_rnglists(&data)); + } } Ok(".debug_str_offsets.dwo" | ".zdebug_str_offsets.dwo") => { - let (debug_str_offsets_section, debug_str_offsets_section_len) = { - let data = section.compressed_data()?.decompress()?; - let len = data.len() as u64; - let data_ref = sess.alloc_owned_cow(data); - ( - gimli::DebugStrOffsets::from(gimli::EndianSlice::new( - data_ref, - self.endian, - )), - len, - ) - }; + let data = section.compressed_data()?.decompress()?; + let data_ref = sess.session().alloc_owned_cow(data); + let debug_str_offsets_section = gimli::DebugStrOffsets::from( + gimli::EndianSlice::new(data_ref, self.endian), + ); let debug_str_section = - if let Some(section) = input.section_by_name(".debug_str.dwo") { - let data = section.compressed_data()?.decompress()?; - let data_ref = sess.alloc_owned_cow(data); - gimli::DebugStr::new(data_ref, self.endian) + if let Some(str_section) = input.section_by_name(".debug_str.dwo") { + let str_data = str_section.compressed_data()?.decompress()?; + let str_data_ref = sess.session().alloc_owned_cow(str_data); + gimli::DebugStr::new(str_data_ref, self.endian) } else { return Err(Error::MissingRequiredSection(".debug_str.dwo")); }; - - let data = self.string_table.remap_str_offsets_section( - debug_str_section, - debug_str_offsets_section, - debug_str_offsets_section_len, - self.endian, - encoding, - )?; - update!( - debug_str_offsets += self.obj.append_to_debug_str_offsets(data.slice()) - ); + let saved_offsets = + sess.save_debug_str_offsets_for_gc(debug_str_offsets_section); + let saved_str = sess.save_debug_str_for_gc(debug_str_section); + if !saved_offsets && !saved_str { + let remapped = self.string_table.remap_str_offsets_section( + debug_str_section, + debug_str_offsets_section, + self.endian, + encoding, + None, + )?; + update!( + debug_str_offsets += + self.obj.append_to_debug_str_offsets(remapped.slice()) + ); + } + } + Ok(".debug_types.dwo" | ".zdebug_types.dwo") => { + has_type_units = true; } _ => (), } @@ -532,7 +1313,7 @@ impl<'file> InProgressDwarfPackage<'file> { let debug_abbrev_section = if let Some(section) = input.section_by_name(".debug_abbrev.dwo") { let data = section.compressed_data()?.decompress()?; - let data_ref = sess.alloc_owned_cow(data); + let data_ref = sess.session().alloc_owned_cow(data); gimli::DebugAbbrev::new(data_ref, self.endian) } else { return Err(Error::MissingRequiredSection(".debug_abbrev.dwo")); @@ -554,6 +1335,11 @@ impl<'file> InProgressDwarfPackage<'file> { let mut seen_debug_info = false; let mut seen_debug_types = false; + // Pass 1: GC each unit and collect its non-shared contributions. The shared sections are + // per-input-object (a type unit shares them with its compilation unit), so they are + // emitted once, after this loop, using every unit's GC result. + let mut pending: Vec = Vec::new(); + for section in input.sections() { let data; let mut iter = match section.name() { @@ -567,9 +1353,20 @@ impl<'file> InProgressDwarfPackage<'file> { Ok(".debug_info.dwo" | ".zdebug_info.dwo") => { data = section.compressed_data()?.decompress()?; seen_debug_info = true; - UnitHeaderIterator::DebugInfo( - gimli::DebugInfo::new(&data, self.endian).units(), - ) + let debug_info = gimli::DebugInfo::new(&data, self.endian); + let mut prescan = debug_info.units(); + while let Some(h) = + prescan.next().map_err(Error::ParseUnitHeader)? + { + if matches!( + h.type_(), + UnitType::SplitType { .. } | UnitType::Type { .. } + ) { + has_type_units = true; + break; + } + } + UnitHeaderIterator::DebugInfo(debug_info.units()) } Ok(".debug_types.dwo" | ".zdebug_types.dwo") // Report an error if a input DWARF package has multiple `.debug_types` @@ -617,51 +1414,62 @@ impl<'file> InProgressDwarfPackage<'file> { let data = section .compressed_data_range( - sess, + sess.session(), header.offset().0.try_into().expect("offset larger than u64"), size, ) .map_err(Error::DecompressData)? .ok_or(Error::EmptyUnit(id.index()))?; + let gc_result = sess.run_unit_gc( + cu_index.as_ref(), + id, + data, + &debug_abbrev_section, + self.endian, + has_type_units, + )?; + + // Use the rewritten `.debug_info` bytes if GC removed any DIEs. + let debug_info_data = match gc_result.as_ref().and_then(|gc| gc.rewritten.as_ref()) + { + Some(rewritten) => rewritten.slice(), + None => data, + }; + let (debug_info, debug_types) = match (&iter, id) { (UnitHeaderIterator::DebugTypes(_), DwarfObject::Type(_)) => { - (None, self.obj.append_to_debug_types(data)) + (None, self.obj.append_to_debug_types(debug_info_data)) } (_, DwarfObject::Compilation(_) | DwarfObject::Type(_)) => { - (self.obj.append_to_debug_info(data), None) + (self.obj.append_to_debug_info(debug_info_data), None) } }; + // Non-shared sections are adjusted here, in unit order. The shared sections + // (loc, loclists, rnglists, str_offsets) are handled after this loop. let debug_abbrev = abbrev_adjustor(id, debug_abbrev)?; let debug_line = line_adjustor(id, debug_line)?; - let debug_loc = loc_adjustor(id, debug_loc)?; - let debug_loclists = loclists_adjustor(id, debug_loclists)?; - let debug_rnglists = rnglists_adjustor(id, debug_rnglists)?; - let debug_str_offsets = str_offsets_adjustor(id, debug_str_offsets)?; let debug_macinfo = macinfo_adjustor(id, debug_macinfo)?; let debug_macro = macro_adjustor(id, debug_macro)?; - let entry = IndexEntry { - encoding, - id, - debug_info, - debug_types, - debug_abbrev, - debug_line, - debug_loc, - debug_loclists, - debug_rnglists, - debug_str_offsets, - debug_macinfo, - debug_macro, - }; - debug!(?entry); - - match id { - DwarfObject::Compilation(_) => self.cu_index_entries.push(entry), - DwarfObject::Type(_) => self.tu_index_entries.push(entry), - } + pending.push(PendingEntry { + entry: IndexEntry { + encoding, + id, + debug_info, + debug_types, + debug_abbrev, + debug_line, + debug_loc: None, + debug_loclists: None, + debug_rnglists: None, + debug_str_offsets: None, + debug_macinfo, + debug_macro, + }, + gc_result, + }); self.contained_units.insert(id); } } @@ -671,6 +1479,43 @@ impl<'file> InProgressDwarfPackage<'file> { return Err(Error::MissingRequiredSection(".debug_info.dwo")); } + // Emit the shared sections once. When list pruning is possible (no type units), this + // writes recomputed per-unit contributions directly into each `pending` entry and returns + // `true`; otherwise the shared fields are left as `None` and adjustors fill them below. + let shared_populated = sess.emit_gc_shared_sections( + cu_index.as_ref(), + &mut pending, + encoding, + self.endian, + has_type_units, + debug_macro.is_some(), + &mut self.obj, + &mut self.string_table, + &mut debug_loc, + &mut debug_loclists, + &mut debug_rnglists, + &mut debug_str_offsets, + )?; + + // Pass 2: build each unit's index entry. The non-shared fields were filled in pass 1. + // Fill the four shared fields either from the pending entry (GC path) or from adjustors. + for unit in &mut pending { + let id = unit.entry.id; + if !shared_populated { + unit.entry.debug_loc = loc_adjustor(id, debug_loc)?; + unit.entry.debug_loclists = loclists_adjustor(id, debug_loclists)?; + unit.entry.debug_rnglists = rnglists_adjustor(id, debug_rnglists)?; + unit.entry.debug_str_offsets = str_offsets_adjustor(id, debug_str_offsets)?; + } + let entry = unit.entry; + debug!(?entry); + + match id { + DwarfObject::Compilation(_) => self.cu_index_entries.push(entry), + DwarfObject::Type(_) => self.tu_index_entries.push(entry), + } + } + Ok(()) } diff --git a/thorin/src/strings.rs b/thorin/src/strings.rs index 4fa2dfb..7e910d9 100644 --- a/thorin/src/strings.rs +++ b/thorin/src/strings.rs @@ -4,6 +4,7 @@ use gimli::{ Section, }; use hashbrown::HashMap; +use itertools::Either; use tracing::debug; use crate::{ @@ -56,15 +57,18 @@ impl PackageStringTable { } /// Adds strings from input `.debug_str_offsets` and `.debug_str` into the string table, returns - /// data for a equivalent `.debug_str_offsets` section with offsets pointing into the new + /// data for an equivalent `.debug_str_offsets` section with offsets pointing into the new /// `.debug_str` section. + /// + /// When `filter` is `Some`, only entries whose index is in the set are included, producing a + /// compacted offset table. The caller must have already remapped strx values in `.debug_info`. pub(crate) fn remap_str_offsets_section( &mut self, debug_str: gimli::DebugStr>, debug_str_offsets: gimli::DebugStrOffsets>, - section_size: u64, endian: E, encoding: Encoding, + filter: Option<&std::collections::BTreeSet>, ) -> Result> { let entry_size = match encoding.format { Format::Dwarf32 => 4, @@ -80,22 +84,22 @@ impl PackageStringTable { let base: gimli::DebugStrOffsetsBase = DebugStrOffsetsBase::default_for_encoding_and_file(encoding, DwarfFileType::Dwo); + let num_elements = (debug_str_offsets.reader().len() - base.0) as u64 / entry_size; + let output_count = + filter.map_or(num_elements, |set| set.range(..num_elements).count() as u64); + if encoding.is_std_dwarf_package_format() { + // Unit length = version (2) + padding (2) + entries. + let payload = 4 + output_count * entry_size; match encoding.format { Format::Dwarf32 => { - // Unit length (4 bytes): size of the offsets section without this - // header (8 bytes total). data.write_u32( - (section_size - 8) - .try_into() - .expect("section size w/out header larger than u32"), + payload.try_into().expect("section size w/out header larger than u32"), )?; } Format::Dwarf64 => { - // Unit length (4 bytes then 8 bytes): size of the offsets section without - // this header (16 bytes total). data.write_u32(u32::MAX)?; - data.write_u64(section_size - 16)?; + data.write_u64(payload)?; } }; // Version (2 bytes): DWARF 5 @@ -105,11 +109,10 @@ impl PackageStringTable { } debug!(?base); - let base_offset: u64 = base.0.try_into().expect("base offset larger than u64"); - let num_elements = (section_size - base_offset) / entry_size; - debug!(?section_size, ?base_offset, ?num_elements); - - for i in 0..num_elements { + let indices = filter.map_or(Either::Right(0..num_elements), |set| { + Either::Left(set.range(..num_elements).copied()) + }); + for i in indices { let dwo_index = DebugStrOffsetsIndex(i as usize); let dwo_offset = debug_str_offsets .get_str_offset(encoding.format, base, dwo_index)