[llvm] [BOLT] Fix shifted DWARF inline-scope ranges; track scope boundaries (PR #207291)

Rafael Auler via llvm-commits llvm-commits at lists.llvm.org
Mon Jul 13 16:46:59 PDT 2026


https://github.com/rafaelauler updated https://github.com/llvm/llvm-project/pull/207291

>From 879f762ed59c6f4efcada0441bf16cda7cd7417d Mon Sep 17 00:00:00 2001
From: Rafael Auler <rafaelauler at fb.com>
Date: Wed, 24 Jun 2026 12:00:28 -0700
Subject: [PATCH 1/3] [BOLT] Fix shifted DWARF inline-scope ranges; track scope
 boundaries

Summary:
BOLT updated DWARF lexical-scope ranges (DW_TAG_inlined_subroutine /
lexical_block low_pc/high_pc and DW_AT_ranges) via
translateInputToOutputRange(), which mapped a boundary using its input
offset relative to the start of the containing basic block:

  OutAddr = BB.getOutputAddressRange().first + (InputOffset - BB.getOffset())

This assumes intra-block byte offsets are preserved input->output. Any
pass that changes instruction sizes within a block ahead of a scope
boundary breaks that assumption. With --plt=all, each `call foo at PLT`
(5 bytes, e8+rel32) is rewritten to `call *foo at GOT(%rip)` (6 bytes,
ff 15+rel32); N such calls before a boundary shift its emitted low_pc/
high_pc N bytes too early, onto the preceding instruction. The range
stays within the parent so `llvm-dwarfdump --verify` does not catch it;
symbolizers then attribute samples on those instructions to the wrong
inlined frames.

Fix is to resolve scope-range boundaries through the precise per-instruction
input-to-output AddressMap (the same map BAT already uses) instead of
input-relative block offsets. Boundary instructions are arbitrary (not
just calls/branches), so they are normally absent from that map; to make
them resolvable, disassembly now keeps an offset for the boundary
instructions (only the boundaries, to keep the AddressMap small) and
those offsets are emitted as AddressMap entries whenever an address map
is required (requiresAddressMap(), which includes --update-debug-sections
via requiresPreciseAddressMap()), not only under BAT.

Boundary collection (BinaryContext::collectDebugScopeBoundaries) runs in
readDebugInfo() after preprocessDebugInfo(). It streams the DIEs of the
CUs that will be updated directly from the already-loaded DWARFContext
with DWARFDebugInfoEntry::extractFast, decoding one DIE at a time into a
single reusable entry, so no DIE forest is materialized and no second
DWARF context is created. For split DWARF the .dwo DIEs are already
extracted by preprocessDWODebugInfo(), so the cached array is reused
instead. Boundaries are stored per function in a sorted, deduplicated
vector (BinaryFunction::DebugScopeBoundaryOffsets), queried during
disassembly with a monotonic cursor, and freed once the function is
disassembled, so the feature adds no global state.

The behavior is on by default and can be disabled with
--accurate-debug-ranges=0, which falls back to the old block-relative
mapping.

Test on big binaries with split dwarf, -lite=0:
                                wall                Max RSS
      -accurate-debug-ranges=0  1070.1s             94.91
      -accurate-debug-ranges=1  1173.0s             100.07
      delta                     +102.9s             +5.16   (+4.6% CPU,
                                                             +5.4% RSS)

Test on big binaries with split dwarf, lite=1:
      -accurate-debug-ranges=0  433.9s              83.23
      -accurate-debug-ranges=1  434.7s              83.79   (+0.7% RSS)

    --time-rewrite shows the boundary-collection phase itself is cheap:
    "read debug ranges" is 5.8s of 4960s total user time on one binary
    (0.1%) and 18.8s of 6307s on another binary; the feature's
    cost is dominated by emitting/translating the extra AddressMap entries,
    not by collection.
---
 bolt/include/bolt/Core/BinaryContext.h       |  15 +-
 bolt/include/bolt/Core/BinaryFunction.h      |  58 +-
 bolt/lib/Core/BinaryContext.cpp              |  81 +++
 bolt/lib/Core/BinaryEmitter.cpp              |   6 +-
 bolt/lib/Core/BinaryFunction.cpp             |  54 +-
 bolt/lib/Rewrite/RewriteInstance.cpp         |  14 +
 bolt/test/X86/dwarf-inline-range-plt-shift.s | 614 +++++++++++++++++++
 7 files changed, 814 insertions(+), 28 deletions(-)
 create mode 100644 bolt/test/X86/dwarf-inline-range-plt-shift.s

diff --git a/bolt/include/bolt/Core/BinaryContext.h b/bolt/include/bolt/Core/BinaryContext.h
index 73ccb911039de..670832288ecbd 100644
--- a/bolt/include/bolt/Core/BinaryContext.h
+++ b/bolt/include/bolt/Core/BinaryContext.h
@@ -1446,6 +1446,11 @@ class BinaryContext {
   /// Populate some internal data structures with debug info.
   void preprocessDebugInfo();
 
+  /// Record DWARF lexical-scope range boundaries into the containing functions'
+  /// BinaryFunction::DebugScopeBoundaryOffsets. Relies on preprocessDebugInfo's
+  /// actions: populated ProcessedCUs and pre-extracted DIEs for split dwarf.
+  void collectDebugScopeBoundaries();
+
   /// Add a filename entry from SrcCUID to DestCUID.
   unsigned addDebugFilenameToUnit(const uint32_t DestCUID,
                                   const uint32_t SrcCUID, unsigned FileIndex);
@@ -1513,16 +1518,6 @@ class BinaryContext {
   /// count of profiled functions.
   uint64_t getHotThreshold() const;
 
-  /// Return true if instruction \p Inst requires an offset for further
-  /// processing (e.g. assigning a profile).
-  bool keepOffsetForInstruction(const MCInst &Inst) const {
-    if (MIB->isCall(Inst) || MIB->isBranch(Inst) || MIB->isReturn(Inst) ||
-        MIB->isPrefix(Inst) || MIB->isIndirectBranch(Inst)) {
-      return true;
-    }
-    return false;
-  }
-
   /// Return true if the function should be emitted to the output file.
   bool shouldEmit(const BinaryFunction &Function) const;
 
diff --git a/bolt/include/bolt/Core/BinaryFunction.h b/bolt/include/bolt/Core/BinaryFunction.h
index 84fbd5661fd0a..df42eecb98512 100644
--- a/bolt/include/bolt/Core/BinaryFunction.h
+++ b/bolt/include/bolt/Core/BinaryFunction.h
@@ -295,6 +295,18 @@ class BinaryFunction {
   /// Offsets of indirect branches with unknown destinations.
   std::set<uint64_t> UnknownIndirectBranchOffsets;
 
+  /// Offsets of instructions that begin or end a DWARF lexical scope
+  /// (inlined_subroutine / lexical_block low_pc/high_pc and DW_AT_ranges).
+  /// Populated (unsorted, with possible duplicates) before disassembly, sorted
+  /// and deduplicated at the start of disassembly, and cleared once disassembly
+  /// of this function is done.
+  std::vector<uint64_t> DebugScopeBoundaryOffsets;
+
+  /// Cursor into the sorted DebugScopeBoundaryOffsets. Disassembly queries
+  /// offsets in monotonically increasing order, so isDebugScopeBoundaryOffset()
+  /// advances this cursor instead of binary-searching from scratch.
+  size_t DebugScopeBoundaryCursor = 0;
+
   /// A set of local and global symbols corresponding to secondary entry points.
   /// Each additional function entry point has a corresponding entry in the map.
   /// The key is a local symbol corresponding to a basic block and the value
@@ -1348,6 +1360,42 @@ class BinaryFunction {
     return InternalRefDataRelocations;
   }
 
+  /// Append function-relative \p Offset as a DWARF lexical-scope boundary. The
+  /// list is sorted later (sortDebugScopeBoundaryOffsets) before it is queried.
+  void addDebugScopeBoundaryOffset(uint64_t Offset) {
+    DebugScopeBoundaryOffsets.push_back(Offset);
+  }
+
+  /// Sort and deduplicate the collected scope boundaries and reset the query
+  /// cursor. Call once, before the first isDebugScopeBoundaryOffset() query.
+  void sortDebugScopeBoundaryOffsets() {
+    llvm::sort(DebugScopeBoundaryOffsets);
+    DebugScopeBoundaryOffsets.erase(llvm::unique(DebugScopeBoundaryOffsets),
+                                    DebugScopeBoundaryOffsets.end());
+    DebugScopeBoundaryCursor = 0;
+  }
+
+  /// Return true if function-relative \p Offset begins/ends a DWARF scope.
+  /// Requires sortDebugScopeBoundaryOffsets() to have been called, and that
+  /// queries arrive with non-decreasing \p Offset. This is designed to be
+  /// used in lock-step with the disassembly loop, don't use it outside of it.
+  bool isDebugScopeBoundaryOffset(uint64_t Offset) {
+    const size_t Size = DebugScopeBoundaryOffsets.size();
+    while (DebugScopeBoundaryCursor < Size &&
+           DebugScopeBoundaryOffsets[DebugScopeBoundaryCursor] < Offset)
+      ++DebugScopeBoundaryCursor;
+    return DebugScopeBoundaryCursor < Size &&
+           DebugScopeBoundaryOffsets[DebugScopeBoundaryCursor] == Offset;
+  }
+
+  /// Return true if an "Offset" annotation should be kept for instruction
+  /// \p Inst located at function-relative \p Offset. Offsets are kept for
+  /// control-flow instructions (profile matching) and for instructions that
+  /// begin/end a DWARF lexical scope (needed to translate scope ranges
+  /// precisely; see DebugScopeBoundaryOffsets).
+  bool keepOffsetForInstruction(const MCInst &Inst,
+                                uint64_t Offset);
+
   /// Return the name of the section this function originated from.
   std::optional<StringRef> getOriginSectionName() const {
     if (!OriginSection)
@@ -2439,15 +2487,17 @@ class BinaryFunction {
   /// is corrupted. If it is unable to fix it, it returns false.
   bool finalizeCFIState();
 
-  /// Return true if this function needs an address-translation table after
-  /// its code emission.
-  bool requiresAddressTranslation() const;
-
   /// Return true if the linker needs to generate an address map for this
   /// function. Used for keeping track of the mapping from input to out
   /// addresses of basic blocks.
   bool requiresAddressMap() const;
 
+  /// Return true if this function needs an address-translation table after
+  /// its code emission, or to update any metadata accurately (debug info,
+  /// SDT probes). This is gated since it incurs extra cost for the linker
+  /// to keep track of more addresses.
+  bool requiresPreciseAddressMap() const;
+
   /// Adjust branch instructions to match the CFG.
   ///
   /// As it comes to internal branches, the CFG represents "the ultimate source
diff --git a/bolt/lib/Core/BinaryContext.cpp b/bolt/lib/Core/BinaryContext.cpp
index 130c523bcda54..9d07775508da6 100644
--- a/bolt/lib/Core/BinaryContext.cpp
+++ b/bolt/lib/Core/BinaryContext.cpp
@@ -49,6 +49,7 @@ using namespace llvm;
 namespace opts {
 
 extern cl::opt<bool> LargeCodeModel;
+extern cl::opt<bool> UpdateDebugSections;
 
 static cl::opt<bool>
     NoHugePages("no-huge-pages",
@@ -1962,6 +1963,86 @@ void BinaryContext::preprocessDebugInfo() {
   }
 }
 
+void BinaryContext::collectDebugScopeBoundaries() {
+  // Record DWARF lexical-scope boundaries (inlined_subroutine / lexical_block)
+  // into each containing BinaryFunction, so disassembly keeps an offset for the
+  // boundary instructions (see BinaryFunction::DebugScopeBoundaryOffsets) and
+  // translateInputToOutputRange() can map scope ranges precisely.
+
+  auto recordRange = [&](uint64_t LowPC, uint64_t HighPC) {
+    BinaryFunction *BF = getBinaryFunctionContainingAddress(LowPC);
+    if (!BF)
+      return;
+    BF->addDebugScopeBoundaryOffset(LowPC - BF->getAddress());
+    // Mark HighPC only if it lies strictly inside the same function.
+    if (HighPC > LowPC && getBinaryFunctionContainingAddress(HighPC) == BF)
+      BF->addDebugScopeBoundaryOffset(HighPC - BF->getAddress());
+  };
+
+  // Record the boundaries of a single scope DIE.
+  auto processScopeDie = [&](const DWARFDie &Die) {
+    const dwarf::Tag Tag = Die.getTag();
+    if (Tag != dwarf::DW_TAG_inlined_subroutine &&
+        Tag != dwarf::DW_TAG_lexical_block &&
+        Tag != dwarf::DW_TAG_try_block &&
+        Tag != dwarf::DW_TAG_catch_block)
+      return;
+    if (Expected<DWARFAddressRangesVector> Ranges = Die.getAddressRanges()) {
+      for (const DWARFAddressRange &R : *Ranges)
+        recordRange(R.LowPC, R.HighPC);
+    } else {
+      consumeError(Ranges.takeError());
+    }
+  };
+
+  for (const std::unique_ptr<DWARFUnit> &CUPtr : DwCtx->compile_units()) {
+    DWARFUnit *CU = CUPtr.get();
+    if (!ProcessedCUs.count(CU))
+      continue;
+
+    // Extract only the CU DIE (the .dwo's, for split DWARF). This is cheap (one
+    // DIE) and sets the unit's range/addr/str-offset bases that getAddressRanges
+    // needs, without materializing the full DIE array.
+    DWARFDie CUDie = CU->getNonSkeletonUnitDIE(/*ExtractUnitDIEOnly=*/true);
+    if (!CUDie)
+      continue;
+    DWARFUnit *DIEUnit = CUDie.getDwarfUnit();
+
+    // For split DWARF, preprocessDWODebugInfo already fully extracts the .dwo's
+    // DIE array.
+    if (DIEUnit->isDWOUnit()) {
+      for (const DWARFDebugInfoEntry &Entry : DIEUnit->dies())
+        processScopeDie(DWARFDie(DIEUnit, &Entry));
+      continue;
+    }
+    // Walk the unit's DIEs by streaming them one at a time. Track nesting depth
+    // with a counter: a DIE with children descends a level (++), a null entry
+    // (sibling-chain terminator) ascends (--), the unit-end offset limits the
+    // walk. This is done to avoid recording all DIEs in a vector like
+    // DWARFUnit's extractDIEsToVector() does, since that is more work than
+    // needed if we just want to lookup specific tags.
+    DWARFDataExtractor DebugInfoData = DIEUnit->getDebugInfoExtractor();
+    uint64_t DIEOffset = DIEUnit->getOffset() + DIEUnit->getHeaderSize();
+    const uint64_t NextCUOffset = DIEUnit->getNextUnitOffset();
+    DWARFDebugInfoEntry DIEEntry;
+    int32_t CurrentDepth = 1;
+    while (CurrentDepth > 0 && DIEOffset < NextCUOffset &&
+           DIEEntry.extractFast(*DIEUnit, &DIEOffset, DebugInfoData,
+                                NextCUOffset, 0)) {
+      const DWARFAbbreviationDeclaration *Abbrev =
+          DIEEntry.getAbbreviationDeclarationPtr();
+      if (!Abbrev) {
+        // End of the current sibling chain.
+        --CurrentDepth;
+        continue;
+      }
+      processScopeDie(DWARFDie(DIEUnit, &DIEEntry));
+      if (Abbrev->hasChildren())
+        ++CurrentDepth;
+    }
+  }
+}
+
 bool BinaryContext::shouldEmit(const BinaryFunction &Function) const {
   if (Function.isPseudo())
     return false;
diff --git a/bolt/lib/Core/BinaryEmitter.cpp b/bolt/lib/Core/BinaryEmitter.cpp
index a555c7185448e..9243b6701b918 100644
--- a/bolt/lib/Core/BinaryEmitter.cpp
+++ b/bolt/lib/Core/BinaryEmitter.cpp
@@ -481,8 +481,10 @@ void BinaryEmitter::emitFunctionBody(BinaryFunction &BF, FunctionFragment &FF,
         }
 
         // Prepare to tag this location with a label if we need to keep track of
-        // the location of calls/returns for BOLT address translation maps
-        if (BF.requiresAddressTranslation() && BC.MIB->getOffset(Instr)) {
+        // an instruction's output address to augment the IO address map (BAT,
+        // SDT/probe address translation, or --update-debug-sections DWARF range
+        // updates).
+        if (BF.requiresPreciseAddressMap() && BC.MIB->getOffset(Instr)) {
           const uint32_t Offset = *BC.MIB->getOffset(Instr);
           if (!InstrLabel)
             InstrLabel = BC.Ctx->createTempSymbol();
diff --git a/bolt/lib/Core/BinaryFunction.cpp b/bolt/lib/Core/BinaryFunction.cpp
index 200e286d8e80e..6c23bbd1ff8c8 100644
--- a/bolt/lib/Core/BinaryFunction.cpp
+++ b/bolt/lib/Core/BinaryFunction.cpp
@@ -1302,6 +1302,15 @@ BinaryFunction::getInstructionSequenceLength(uint64_t Offset,
   return Current - Offset;
 }
 
+bool BinaryFunction::keepOffsetForInstruction(const MCInst &Inst,
+                                              uint64_t Offset) {
+  MCPlusBuilder &MIB = *BC.MIB;
+  if (MIB.isCall(Inst) || MIB.isBranch(Inst) || MIB.isReturn(Inst) ||
+      MIB.isPrefix(Inst) || MIB.isIndirectBranch(Inst))
+    return true;
+  return isDebugScopeBoundaryOffset(Offset);
+}
+
 Error BinaryFunction::disassemble() {
   NamedRegionTimer T("disassemble", "Disassemble function", "buildfuncs",
                      "Build Binary Functions", opts::TimeBuild);
@@ -1327,6 +1336,10 @@ Error BinaryFunction::disassemble() {
   // corresponding %pcrel_hi.
   LabelsMapType InstructionLabels;
 
+  // Scope boundaries were appended unsorted during preprocessDebugInfo(); sort
+  // them so keepOffsetForInstruction() can query them below.
+  sortDebugScopeBoundaryOffsets();
+
   uint64_t Size = 0; // instruction size
   for (uint64_t Offset = 0; Offset < getSize(); Offset += Size) {
     MCInst Instruction;
@@ -1550,8 +1563,9 @@ Error BinaryFunction::disassemble() {
       }
     }
 
-    // Record offset of the instruction for profile matching.
-    if (BC.keepOffsetForInstruction(Instruction))
+    // Record offset of the instruction for profile matching, for control-flow
+    // instructions, and for instructions on a DWARF lexical-scope boundary.
+    if (keepOffsetForInstruction(Instruction, Offset))
       MIB->setOffset(Instruction, static_cast<uint32_t>(Offset));
 
     if (BC.isX86() && BC.MIB->isNoop(Instruction)) {
@@ -1564,6 +1578,9 @@ Error BinaryFunction::disassemble() {
     addInstruction(Offset, std::move(Instruction));
   }
 
+  // Scope-boundary markers are only consulted while assigning offsets above.
+  clearList(DebugScopeBoundaryOffsets);
+
   for (auto [Offset, Label] : InstructionLabels) {
     InstrMapType::iterator II = Instructions.find(Offset);
     assert(II != Instructions.end() && "reference to non-existing instruction");
@@ -2590,10 +2607,16 @@ void BinaryFunction::postProcessCFG() {
   // The final cleanup of intermediate structures.
   clearList(IgnoredBranches);
 
-  // Remove "Offset" annotations, unless we need an address-translation table
-  // later. This has no cost, since annotations are allocated by a bumpptr
-  // allocator and won't be released anyway until late in the pipeline.
-  if (!requiresAddressTranslation() && !opts::Instrument) {
+  // Remove "Offset" annotations, unless we need to build an instruction
+  // accurate IO address map later (used for BAT, SDT/pseudo-probe address
+  // translation, or --update-debug-sections, which relies on per-instruction
+  // offsets to translate DWARF scope ranges).
+  // This has no cost in memory for the BF object, since annotations are
+  // allocated by a bumpptr allocator and won't be released anyway until late
+  // in the pipeline, but the IO address map that is derived from these offsets
+  // does add non-trivial overhead and can get expensive -- that's why we don't
+  // add offsets to every single instruction.
+  if (!requiresPreciseAddressMap() && !opts::Instrument) {
     for (BinaryBasicBlock &BB : blocks())
       for (MCInst &Inst : BB)
         BC.MIB->clearOffset(Inst);
@@ -3261,16 +3284,16 @@ bool BinaryFunction::finalizeCFIState() {
   return true;
 }
 
-bool BinaryFunction::requiresAddressTranslation() const {
-  return opts::EnableBAT || hasSDTMarker() || hasPseudoProbe();
+bool BinaryFunction::requiresPreciseAddressMap() const {
+  return opts::UpdateDebugSections || opts::EnableBAT || hasSDTMarker() ||
+         hasPseudoProbe();
 }
 
 bool BinaryFunction::requiresAddressMap() const {
   if (isInjected())
     return false;
 
-  return opts::UpdateDebugSections || isMultiEntry() ||
-         requiresAddressTranslation();
+  return isMultiEntry() || requiresPreciseAddressMap();
 }
 
 uint64_t BinaryFunction::getInstructionCount() const {
@@ -4666,8 +4689,6 @@ uint64_t BinaryFunction::translateInputToOutputAddress(uint64_t Address) const {
       return *OutputAddress;
   }
 
-  // FIXME: #18950828 - we rely on relative offsets inside basic blocks to stay
-  //        intact. Instead we can use pseudo instructions and/or annotations.
   const uint64_t Offset = Address - getAddress();
   const BinaryBasicBlock *BB = getBasicBlockContainingOffset(Offset);
   if (!BB) {
@@ -4735,6 +4756,15 @@ BinaryFunction::translateInputToOutputRange(DebugAddressRange InRange) const {
     // to /p Offset. The output address should fall within the same basic
     // block boundaries.
     auto translateBlockOffset = [&](const uint64_t Offset) {
+      // Prefer the precise per-instruction input-to-output address map when
+      // available. The block-relative fallback below assumes intra-block byte
+      // offsets are preserved from input to output, which is not true after a
+      // size-changing pass (e.g. PLT optimization growing call at PLT -> call
+      // *@GOT) runs ahead of the boundary, and silently shifts the range.
+      if (BC.hasIOAddressMap())
+        if (std::optional<uint64_t> OutAddr = BC.getIOAddressMap().lookup(
+                getAddress() + BB.getOffset() + Offset))
+          return *OutAddr;
       const uint64_t OutAddress = BB.getOutputAddressRange().first + Offset;
       return std::min(OutAddress, BB.getOutputAddressRange().second);
     };
diff --git a/bolt/lib/Rewrite/RewriteInstance.cpp b/bolt/lib/Rewrite/RewriteInstance.cpp
index 9e00d48e03853..49ea2ec198efe 100644
--- a/bolt/lib/Rewrite/RewriteInstance.cpp
+++ b/bolt/lib/Rewrite/RewriteInstance.cpp
@@ -265,6 +265,14 @@ static cl::opt<std::string> DWPPathName("dwp",
                                         cl::Hidden, cl::init(""),
                                         cl::cat(BoltCategory));
 
+static cl::opt<bool> AccurateDebugRanges(
+    "accurate-debug-ranges",
+    cl::desc("with --update-debug-sections, track DWARF lexical-scope "
+             "boundaries so scope ranges are translated precisely (instead of "
+             "via input-relative block offsets). Disable to trade range "
+             "accuracy for lower memory/time."),
+    cl::init(true), cl::Hidden, cl::cat(BoltCategory));
+
 static cl::opt<bool>
 UseGnuStack("use-gnu-stack",
   cl::desc("use GNU_STACK program header for new segment (workaround for "
@@ -3586,6 +3594,12 @@ void RewriteInstance::readDebugInfo() {
     return;
 
   BC->preprocessDebugInfo();
+
+  if (opts::AccurateDebugRanges) {
+    NamedRegionTimer T("readDebugRanges", "read debug ranges", TimerGroupName,
+                       TimerGroupDesc, opts::TimeRewrite);
+    BC->collectDebugScopeBoundaries();
+  }
 }
 
 void RewriteInstance::preprocessProfileData() {
diff --git a/bolt/test/X86/dwarf-inline-range-plt-shift.s b/bolt/test/X86/dwarf-inline-range-plt-shift.s
new file mode 100644
index 0000000000000..12ef0ddaca0da
--- /dev/null
+++ b/bolt/test/X86/dwarf-inline-range-plt-shift.s
@@ -0,0 +1,614 @@
+## Regression test for inlined_subroutine address-range corruption.
+##
+## BOLT updates DWARF lexical-scope ranges (DW_TAG_inlined_subroutine /
+## lexical_block low_pc/high_pc and DW_AT_ranges) via
+## BinaryFunction::translateInputToOutputRange(), which maps a boundary using
+## its input offset relative to the start of the containing basic block:
+##
+##   OutAddr = BB.getOutputAddressRange().first + (InputOffset - BB.getOffset())
+##
+## This assumes intra-block byte offsets are preserved input->output. Any pass
+## that changes instruction sizes within a block with scope boundary breaks that
+## assumption. Here --plt=all rewrites three `call ext at PLT` (5 bytes, e8+rel32)
+## into `call *ext at GOT(%rip)` (6 bytes, ff 15+rel32), growing the block by +3B
+## before the inlined copy of inl() begins. As a result the inlined_subroutine's
+## low_pc is emitted 3 bytes too early -- landing inside the preceding converted
+## call instruction instead of on inl()'s first instruction (the `leaq`).
+##
+## The range stays within the parent, so `llvm-dwarfdump --verify` does not catch
+## it; we check the exact low_pc against the disassembled instruction address.
+##
+## Source (compiled with -O2 -g -gdwarf-4):
+##   extern long exta(long), extb(long), extc(long);   // resolved from a DSO -> PLT
+##   static inline __attribute__((always_inline)) long inl(long x){ return x*3+7; }
+##   __attribute__((noinline)) long foo(long a){
+##     long t = exta(a); t = extb(t); t = extc(t); return inl(t);
+##   }
+##   int main(int argc, char **argv){ return (int)foo(argc); }
+##
+## The fix is enabled through --accurate-debug-ranges and should track all dwarf
+## inline scopes, translating them accordingly.
+
+# REQUIRES: system-linux
+
+## Build a DSO so the exta/extb/extc calls go through the PLT.
+# RUN: echo 'long exta(long x){return x+1;} long extb(long x){return x+2;} long extc(long x){return x+3;}' \
+# RUN:   | %clang %cflags -fpic -shared -xc - -o %t.so
+# RUN: %clang %cflags -gdwarf-4 %s %t.so -o %t.exe -Wl,-q -no-pie
+# RUN: llvm-bolt %t.exe -o %t.bolt --plt=all --update-debug-sections
+## Concatenate disassembly then debug-info so FileCheck can capture the
+## instruction address and compare it against the inline scope's low_pc.
+# RUN: llvm-objdump -d --no-show-raw-insn %t.bolt > %t.out
+# RUN: llvm-dwarfdump --debug-info %t.bolt >> %t.out
+# RUN: FileCheck %s < %t.out
+
+## inl() is inlined into foo() right after the three (now GOT-indirect) calls.
+## Its inlined_subroutine must start exactly at inl()'s first instruction.
+# CHECK-LABEL: <foo>:
+# CHECK: [[INL:[0-9a-f]+]]:{{.*}}leaq	(%rax,%rax,2), %rax
+# CHECK: DW_TAG_inlined_subroutine
+# CHECK: DW_AT_abstract_origin {{.*}}"inl"
+# CHECK-NEXT: DW_AT_low_pc {{.*}}0x{{0*}}[[INL]])
+
+	.att_syntax
+	.file	"t.c"
+	.text
+	.globl	foo                             # -- Begin function foo
+	.p2align	4, 0x90
+	.type	foo, at function
+foo:                                    # @foo
+.Lfunc_begin0:
+	.file	1 "." "t.c"
+	.loc	1 9 0                           # t.c:9:0
+	.cfi_startproc
+# %bb.0:                                # %entry
+	#DEBUG_VALUE: foo:a <- $rdi
+	pushq	%rax
+	.cfi_def_cfa_offset 16
+.Ltmp0:
+	.loc	1 10 12 prologue_end            # t.c:10:12
+	callq	exta at PLT
+.Ltmp1:
+	#DEBUG_VALUE: foo:a <- [DW_OP_LLVM_entry_value 1] $rdi
+	#DEBUG_VALUE: foo:t <- $rax
+	.loc	1 11 7                          # t.c:11:7
+	movq	%rax, %rdi
+	callq	extb at PLT
+.Ltmp2:
+	#DEBUG_VALUE: foo:t <- $rax
+	.loc	1 12 7                          # t.c:12:7
+	movq	%rax, %rdi
+	callq	extc at PLT
+.Ltmp3:
+	#DEBUG_VALUE: foo:t <- $rax
+	#DEBUG_VALUE: inl:x <- $rax
+	.loc	1 6 16                          # t.c:6:16 @[ t.c:13:10 ]
+	leaq	(%rax,%rax,2), %rax
+.Ltmp4:
+	addq	$7, %rax
+.Ltmp5:
+	.loc	1 13 3 epilogue_begin           # t.c:13:3
+	popq	%rcx
+	.cfi_def_cfa_offset 8
+	retq
+.Ltmp6:
+.Lfunc_end0:
+	.size	foo, .Lfunc_end0-foo
+	.cfi_endproc
+                                        # -- End function
+	.globl	main                            # -- Begin function main
+	.p2align	4, 0x90
+	.type	main, at function
+main:                                   # @main
+.Lfunc_begin1:
+	.cfi_startproc
+# %bb.0:                                # %entry
+	#DEBUG_VALUE: main:argc <- $edi
+	#DEBUG_VALUE: main:argv <- $rsi
+	.loc	1 16 51 prologue_end            # t.c:16:51
+	movslq	%edi, %rdi
+.Ltmp7:
+	#DEBUG_VALUE: main:argc <- [DW_OP_LLVM_entry_value 1] $edi
+	.loc	1 16 47 is_stmt 0               # t.c:16:47
+	jmp	foo                             # TAILCALL
+.Ltmp8:
+.Lfunc_end1:
+	.size	main, .Lfunc_end1-main
+	.cfi_endproc
+                                        # -- End function
+	.section	.debug_loc,"", at progbits
+.Ldebug_loc0:
+	.quad	.Lfunc_begin0-.Lfunc_begin0
+	.quad	.Ltmp1-.Lfunc_begin0
+	.short	1                               # Loc expr size
+	.byte	85                              # DW_OP_reg5
+	.quad	.Ltmp1-.Lfunc_begin0
+	.quad	.Lfunc_end0-.Lfunc_begin0
+	.short	4                               # Loc expr size
+	.byte	243                             # DW_OP_GNU_entry_value
+	.byte	1                               # 1
+	.byte	85                              # DW_OP_reg5
+	.byte	159                             # DW_OP_stack_value
+	.quad	0
+	.quad	0
+.Ldebug_loc1:
+	.quad	.Ltmp1-.Lfunc_begin0
+	.quad	.Ltmp4-.Lfunc_begin0
+	.short	1                               # Loc expr size
+	.byte	80                              # DW_OP_reg0
+	.quad	0
+	.quad	0
+.Ldebug_loc2:
+	.quad	.Ltmp3-.Lfunc_begin0
+	.quad	.Ltmp4-.Lfunc_begin0
+	.short	1                               # Loc expr size
+	.byte	80                              # DW_OP_reg0
+	.quad	0
+	.quad	0
+.Ldebug_loc3:
+	.quad	.Lfunc_begin1-.Lfunc_begin0
+	.quad	.Ltmp7-.Lfunc_begin0
+	.short	1                               # Loc expr size
+	.byte	85                              # super-register DW_OP_reg5
+	.quad	.Ltmp7-.Lfunc_begin0
+	.quad	.Lfunc_end1-.Lfunc_begin0
+	.short	4                               # Loc expr size
+	.byte	243                             # DW_OP_GNU_entry_value
+	.byte	1                               # 1
+	.byte	85                              # super-register DW_OP_reg5
+	.byte	159                             # DW_OP_stack_value
+	.quad	0
+	.quad	0
+	.section	.debug_abbrev,"", at progbits
+	.byte	1                               # Abbreviation Code
+	.byte	17                              # DW_TAG_compile_unit
+	.byte	1                               # DW_CHILDREN_yes
+	.byte	37                              # DW_AT_producer
+	.byte	14                              # DW_FORM_strp
+	.byte	19                              # DW_AT_language
+	.byte	5                               # DW_FORM_data2
+	.byte	3                               # DW_AT_name
+	.byte	14                              # DW_FORM_strp
+	.byte	16                              # DW_AT_stmt_list
+	.byte	23                              # DW_FORM_sec_offset
+	.byte	27                              # DW_AT_comp_dir
+	.byte	14                              # DW_FORM_strp
+	.byte	17                              # DW_AT_low_pc
+	.byte	1                               # DW_FORM_addr
+	.byte	18                              # DW_AT_high_pc
+	.byte	6                               # DW_FORM_data4
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	2                               # Abbreviation Code
+	.byte	36                              # DW_TAG_base_type
+	.byte	0                               # DW_CHILDREN_no
+	.byte	3                               # DW_AT_name
+	.byte	14                              # DW_FORM_strp
+	.byte	62                              # DW_AT_encoding
+	.byte	11                              # DW_FORM_data1
+	.byte	11                              # DW_AT_byte_size
+	.byte	11                              # DW_FORM_data1
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	3                               # Abbreviation Code
+	.byte	46                              # DW_TAG_subprogram
+	.byte	1                               # DW_CHILDREN_yes
+	.byte	3                               # DW_AT_name
+	.byte	14                              # DW_FORM_strp
+	.byte	58                              # DW_AT_decl_file
+	.byte	11                              # DW_FORM_data1
+	.byte	59                              # DW_AT_decl_line
+	.byte	11                              # DW_FORM_data1
+	.byte	39                              # DW_AT_prototyped
+	.byte	25                              # DW_FORM_flag_present
+	.byte	73                              # DW_AT_type
+	.byte	19                              # DW_FORM_ref4
+	.byte	32                              # DW_AT_inline
+	.byte	11                              # DW_FORM_data1
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	4                               # Abbreviation Code
+	.byte	5                               # DW_TAG_formal_parameter
+	.byte	0                               # DW_CHILDREN_no
+	.byte	3                               # DW_AT_name
+	.byte	14                              # DW_FORM_strp
+	.byte	58                              # DW_AT_decl_file
+	.byte	11                              # DW_FORM_data1
+	.byte	59                              # DW_AT_decl_line
+	.byte	11                              # DW_FORM_data1
+	.byte	73                              # DW_AT_type
+	.byte	19                              # DW_FORM_ref4
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	5                               # Abbreviation Code
+	.byte	46                              # DW_TAG_subprogram
+	.byte	1                               # DW_CHILDREN_yes
+	.byte	17                              # DW_AT_low_pc
+	.byte	1                               # DW_FORM_addr
+	.byte	18                              # DW_AT_high_pc
+	.byte	6                               # DW_FORM_data4
+	.byte	64                              # DW_AT_frame_base
+	.byte	24                              # DW_FORM_exprloc
+	.ascii	"\227B"                         # DW_AT_GNU_all_call_sites
+	.byte	25                              # DW_FORM_flag_present
+	.byte	3                               # DW_AT_name
+	.byte	14                              # DW_FORM_strp
+	.byte	58                              # DW_AT_decl_file
+	.byte	11                              # DW_FORM_data1
+	.byte	59                              # DW_AT_decl_line
+	.byte	11                              # DW_FORM_data1
+	.byte	39                              # DW_AT_prototyped
+	.byte	25                              # DW_FORM_flag_present
+	.byte	73                              # DW_AT_type
+	.byte	19                              # DW_FORM_ref4
+	.byte	63                              # DW_AT_external
+	.byte	25                              # DW_FORM_flag_present
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	6                               # Abbreviation Code
+	.byte	5                               # DW_TAG_formal_parameter
+	.byte	0                               # DW_CHILDREN_no
+	.byte	2                               # DW_AT_location
+	.byte	23                              # DW_FORM_sec_offset
+	.byte	3                               # DW_AT_name
+	.byte	14                              # DW_FORM_strp
+	.byte	58                              # DW_AT_decl_file
+	.byte	11                              # DW_FORM_data1
+	.byte	59                              # DW_AT_decl_line
+	.byte	11                              # DW_FORM_data1
+	.byte	73                              # DW_AT_type
+	.byte	19                              # DW_FORM_ref4
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	7                               # Abbreviation Code
+	.byte	52                              # DW_TAG_variable
+	.byte	0                               # DW_CHILDREN_no
+	.byte	2                               # DW_AT_location
+	.byte	23                              # DW_FORM_sec_offset
+	.byte	3                               # DW_AT_name
+	.byte	14                              # DW_FORM_strp
+	.byte	58                              # DW_AT_decl_file
+	.byte	11                              # DW_FORM_data1
+	.byte	59                              # DW_AT_decl_line
+	.byte	11                              # DW_FORM_data1
+	.byte	73                              # DW_AT_type
+	.byte	19                              # DW_FORM_ref4
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	8                               # Abbreviation Code
+	.byte	29                              # DW_TAG_inlined_subroutine
+	.byte	1                               # DW_CHILDREN_yes
+	.byte	49                              # DW_AT_abstract_origin
+	.byte	19                              # DW_FORM_ref4
+	.byte	17                              # DW_AT_low_pc
+	.byte	1                               # DW_FORM_addr
+	.byte	18                              # DW_AT_high_pc
+	.byte	6                               # DW_FORM_data4
+	.byte	88                              # DW_AT_call_file
+	.byte	11                              # DW_FORM_data1
+	.byte	89                              # DW_AT_call_line
+	.byte	11                              # DW_FORM_data1
+	.byte	87                              # DW_AT_call_column
+	.byte	11                              # DW_FORM_data1
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	9                               # Abbreviation Code
+	.byte	5                               # DW_TAG_formal_parameter
+	.byte	0                               # DW_CHILDREN_no
+	.byte	2                               # DW_AT_location
+	.byte	23                              # DW_FORM_sec_offset
+	.byte	49                              # DW_AT_abstract_origin
+	.byte	19                              # DW_FORM_ref4
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	10                              # Abbreviation Code
+	.ascii	"\211\202\001"                  # DW_TAG_GNU_call_site
+	.byte	1                               # DW_CHILDREN_yes
+	.byte	49                              # DW_AT_abstract_origin
+	.byte	19                              # DW_FORM_ref4
+	.byte	17                              # DW_AT_low_pc
+	.byte	1                               # DW_FORM_addr
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	11                              # Abbreviation Code
+	.ascii	"\212\202\001"                  # DW_TAG_GNU_call_site_parameter
+	.byte	0                               # DW_CHILDREN_no
+	.byte	2                               # DW_AT_location
+	.byte	24                              # DW_FORM_exprloc
+	.ascii	"\221B"                         # DW_AT_GNU_call_site_value
+	.byte	24                              # DW_FORM_exprloc
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	12                              # Abbreviation Code
+	.ascii	"\211\202\001"                  # DW_TAG_GNU_call_site
+	.byte	0                               # DW_CHILDREN_no
+	.byte	49                              # DW_AT_abstract_origin
+	.byte	19                              # DW_FORM_ref4
+	.byte	17                              # DW_AT_low_pc
+	.byte	1                               # DW_FORM_addr
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	13                              # Abbreviation Code
+	.byte	46                              # DW_TAG_subprogram
+	.byte	1                               # DW_CHILDREN_yes
+	.byte	3                               # DW_AT_name
+	.byte	14                              # DW_FORM_strp
+	.byte	58                              # DW_AT_decl_file
+	.byte	11                              # DW_FORM_data1
+	.byte	59                              # DW_AT_decl_line
+	.byte	11                              # DW_FORM_data1
+	.byte	39                              # DW_AT_prototyped
+	.byte	25                              # DW_FORM_flag_present
+	.byte	73                              # DW_AT_type
+	.byte	19                              # DW_FORM_ref4
+	.byte	60                              # DW_AT_declaration
+	.byte	25                              # DW_FORM_flag_present
+	.byte	63                              # DW_AT_external
+	.byte	25                              # DW_FORM_flag_present
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	14                              # Abbreviation Code
+	.byte	5                               # DW_TAG_formal_parameter
+	.byte	0                               # DW_CHILDREN_no
+	.byte	73                              # DW_AT_type
+	.byte	19                              # DW_FORM_ref4
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	15                              # Abbreviation Code
+	.byte	5                               # DW_TAG_formal_parameter
+	.byte	0                               # DW_CHILDREN_no
+	.byte	2                               # DW_AT_location
+	.byte	24                              # DW_FORM_exprloc
+	.byte	3                               # DW_AT_name
+	.byte	14                              # DW_FORM_strp
+	.byte	58                              # DW_AT_decl_file
+	.byte	11                              # DW_FORM_data1
+	.byte	59                              # DW_AT_decl_line
+	.byte	11                              # DW_FORM_data1
+	.byte	73                              # DW_AT_type
+	.byte	19                              # DW_FORM_ref4
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	16                              # Abbreviation Code
+	.ascii	"\211\202\001"                  # DW_TAG_GNU_call_site
+	.byte	1                               # DW_CHILDREN_yes
+	.byte	49                              # DW_AT_abstract_origin
+	.byte	19                              # DW_FORM_ref4
+	.ascii	"\225B"                         # DW_AT_GNU_tail_call
+	.byte	25                              # DW_FORM_flag_present
+	.byte	17                              # DW_AT_low_pc
+	.byte	1                               # DW_FORM_addr
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	17                              # Abbreviation Code
+	.byte	15                              # DW_TAG_pointer_type
+	.byte	0                               # DW_CHILDREN_no
+	.byte	73                              # DW_AT_type
+	.byte	19                              # DW_FORM_ref4
+	.byte	0                               # EOM(1)
+	.byte	0                               # EOM(2)
+	.byte	0                               # EOM(3)
+	.section	.debug_info,"", at progbits
+.Lcu_begin0:
+	.long	.Ldebug_info_end0-.Ldebug_info_start0 # Length of Unit
+.Ldebug_info_start0:
+	.short	4                               # DWARF version number
+	.long	.debug_abbrev                   # Offset Into Abbrev. Section
+	.byte	8                               # Address Size (in bytes)
+	.byte	1                               # Abbrev [1] 0xb:0x16c DW_TAG_compile_unit
+	.long	.Linfo_string0                  # DW_AT_producer
+	.short	29                              # DW_AT_language
+	.long	.Linfo_string1                  # DW_AT_name
+	.long	.Lline_table_start0             # DW_AT_stmt_list
+	.long	.Linfo_string2                  # DW_AT_comp_dir
+	.quad	.Lfunc_begin0                   # DW_AT_low_pc
+	.long	.Lfunc_end1-.Lfunc_begin0       # DW_AT_high_pc
+	.byte	2                               # Abbrev [2] 0x2a:0x7 DW_TAG_base_type
+	.long	.Linfo_string3                  # DW_AT_name
+	.byte	5                               # DW_AT_encoding
+	.byte	4                               # DW_AT_byte_size
+	.byte	3                               # Abbrev [3] 0x31:0x18 DW_TAG_subprogram
+	.long	.Linfo_string4                  # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	5                               # DW_AT_decl_line
+                                        # DW_AT_prototyped
+	.long	73                              # DW_AT_type
+	.byte	1                               # DW_AT_inline
+	.byte	4                               # Abbrev [4] 0x3d:0xb DW_TAG_formal_parameter
+	.long	.Linfo_string6                  # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	5                               # DW_AT_decl_line
+	.long	73                              # DW_AT_type
+	.byte	0                               # End Of Children Mark
+	.byte	2                               # Abbrev [2] 0x49:0x7 DW_TAG_base_type
+	.long	.Linfo_string5                  # DW_AT_name
+	.byte	5                               # DW_AT_encoding
+	.byte	8                               # DW_AT_byte_size
+	.byte	5                               # Abbrev [5] 0x50:0x85 DW_TAG_subprogram
+	.quad	.Lfunc_begin0                   # DW_AT_low_pc
+	.long	.Lfunc_end0-.Lfunc_begin0       # DW_AT_high_pc
+	.byte	1                               # DW_AT_frame_base
+	.byte	87
+                                        # DW_AT_GNU_all_call_sites
+	.long	.Linfo_string10                 # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	9                               # DW_AT_decl_line
+                                        # DW_AT_prototyped
+	.long	73                              # DW_AT_type
+                                        # DW_AT_external
+	.byte	6                               # Abbrev [6] 0x69:0xf DW_TAG_formal_parameter
+	.long	.Ldebug_loc0                    # DW_AT_location
+	.long	.Linfo_string12                 # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	9                               # DW_AT_decl_line
+	.long	73                              # DW_AT_type
+	.byte	7                               # Abbrev [7] 0x78:0xf DW_TAG_variable
+	.long	.Ldebug_loc1                    # DW_AT_location
+	.long	.Linfo_string13                 # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	10                              # DW_AT_decl_line
+	.long	73                              # DW_AT_type
+	.byte	8                               # Abbrev [8] 0x87:0x1e DW_TAG_inlined_subroutine
+	.long	49                              # DW_AT_abstract_origin
+	.quad	.Ltmp3                          # DW_AT_low_pc
+	.long	.Ltmp5-.Ltmp3                   # DW_AT_high_pc
+	.byte	1                               # DW_AT_call_file
+	.byte	13                              # DW_AT_call_line
+	.byte	10                              # DW_AT_call_column
+	.byte	9                               # Abbrev [9] 0x9b:0x9 DW_TAG_formal_parameter
+	.long	.Ldebug_loc2                    # DW_AT_location
+	.long	61                              # DW_AT_abstract_origin
+	.byte	0                               # End Of Children Mark
+	.byte	10                              # Abbrev [10] 0xa5:0x15 DW_TAG_GNU_call_site
+	.long	213                             # DW_AT_abstract_origin
+	.quad	.Ltmp1                          # DW_AT_low_pc
+	.byte	11                              # Abbrev [11] 0xb2:0x7 DW_TAG_GNU_call_site_parameter
+	.byte	1                               # DW_AT_location
+	.byte	85
+	.byte	3                               # DW_AT_GNU_call_site_value
+	.byte	243
+	.byte	1
+	.byte	85
+	.byte	0                               # End Of Children Mark
+	.byte	12                              # Abbrev [12] 0xba:0xd DW_TAG_GNU_call_site
+	.long	230                             # DW_AT_abstract_origin
+	.quad	.Ltmp2                          # DW_AT_low_pc
+	.byte	12                              # Abbrev [12] 0xc7:0xd DW_TAG_GNU_call_site
+	.long	247                             # DW_AT_abstract_origin
+	.quad	.Ltmp3                          # DW_AT_low_pc
+	.byte	0                               # End Of Children Mark
+	.byte	13                              # Abbrev [13] 0xd5:0x11 DW_TAG_subprogram
+	.long	.Linfo_string7                  # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	1                               # DW_AT_decl_line
+                                        # DW_AT_prototyped
+	.long	73                              # DW_AT_type
+                                        # DW_AT_declaration
+                                        # DW_AT_external
+	.byte	14                              # Abbrev [14] 0xe0:0x5 DW_TAG_formal_parameter
+	.long	73                              # DW_AT_type
+	.byte	0                               # End Of Children Mark
+	.byte	13                              # Abbrev [13] 0xe6:0x11 DW_TAG_subprogram
+	.long	.Linfo_string8                  # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	2                               # DW_AT_decl_line
+                                        # DW_AT_prototyped
+	.long	73                              # DW_AT_type
+                                        # DW_AT_declaration
+                                        # DW_AT_external
+	.byte	14                              # Abbrev [14] 0xf1:0x5 DW_TAG_formal_parameter
+	.long	73                              # DW_AT_type
+	.byte	0                               # End Of Children Mark
+	.byte	13                              # Abbrev [13] 0xf7:0x11 DW_TAG_subprogram
+	.long	.Linfo_string9                  # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	3                               # DW_AT_decl_line
+                                        # DW_AT_prototyped
+	.long	73                              # DW_AT_type
+                                        # DW_AT_declaration
+                                        # DW_AT_external
+	.byte	14                              # Abbrev [14] 0x102:0x5 DW_TAG_formal_parameter
+	.long	73                              # DW_AT_type
+	.byte	0                               # End Of Children Mark
+	.byte	5                               # Abbrev [5] 0x108:0x5d DW_TAG_subprogram
+	.quad	.Lfunc_begin1                   # DW_AT_low_pc
+	.long	.Lfunc_end1-.Lfunc_begin1       # DW_AT_high_pc
+	.byte	1                               # DW_AT_frame_base
+	.byte	87
+                                        # DW_AT_GNU_all_call_sites
+	.long	.Linfo_string11                 # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	16                              # DW_AT_decl_line
+                                        # DW_AT_prototyped
+	.long	42                              # DW_AT_type
+                                        # DW_AT_external
+	.byte	6                               # Abbrev [6] 0x121:0xf DW_TAG_formal_parameter
+	.long	.Ldebug_loc3                    # DW_AT_location
+	.long	.Linfo_string14                 # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	16                              # DW_AT_decl_line
+	.long	42                              # DW_AT_type
+	.byte	15                              # Abbrev [15] 0x130:0xd DW_TAG_formal_parameter
+	.byte	1                               # DW_AT_location
+	.byte	84
+	.long	.Linfo_string15                 # DW_AT_name
+	.byte	1                               # DW_AT_decl_file
+	.byte	16                              # DW_AT_decl_line
+	.long	357                             # DW_AT_type
+	.byte	16                              # Abbrev [16] 0x13d:0x27 DW_TAG_GNU_call_site
+	.long	80                              # DW_AT_abstract_origin
+                                        # DW_AT_GNU_tail_call
+	.quad	.Ltmp8                          # DW_AT_low_pc
+	.byte	11                              # Abbrev [11] 0x14a:0x19 DW_TAG_GNU_call_site_parameter
+	.byte	1                               # DW_AT_location
+	.byte	85
+	.byte	21                              # DW_AT_GNU_call_site_value
+	.byte	243
+	.byte	1
+	.byte	85
+	.byte	16
+	.ascii	"\377\377\377\377\017"
+	.byte	26
+	.byte	18
+	.byte	16
+	.byte	31
+	.byte	37
+	.byte	48
+	.byte	32
+	.byte	30
+	.byte	16
+	.byte	32
+	.byte	36
+	.byte	33
+	.byte	0                               # End Of Children Mark
+	.byte	0                               # End Of Children Mark
+	.byte	17                              # Abbrev [17] 0x165:0x5 DW_TAG_pointer_type
+	.long	362                             # DW_AT_type
+	.byte	17                              # Abbrev [17] 0x16a:0x5 DW_TAG_pointer_type
+	.long	367                             # DW_AT_type
+	.byte	2                               # Abbrev [2] 0x16f:0x7 DW_TAG_base_type
+	.long	.Linfo_string16                 # DW_AT_name
+	.byte	6                               # DW_AT_encoding
+	.byte	1                               # DW_AT_byte_size
+	.byte	0                               # End Of Children Mark
+.Ldebug_info_end0:
+	.section	.debug_str,"MS", at progbits,1
+.Linfo_string0:
+	.asciz	".........clang version 23........................................................................................................................" # string offset=0 ;
+.Linfo_string1:
+	.asciz	"t.c"                           # string offset=146 ; t.c
+.Linfo_string2:
+	.asciz	"."                 # string offset=150 ; .
+.Linfo_string3:
+	.asciz	"int"                           # string offset=164 ; int
+.Linfo_string4:
+	.asciz	"inl"                           # string offset=168 ; inl
+.Linfo_string5:
+	.asciz	"long"                          # string offset=172 ; long
+.Linfo_string6:
+	.asciz	"x"                             # string offset=177 ; x
+.Linfo_string7:
+	.asciz	"exta"                          # string offset=179 ; exta
+.Linfo_string8:
+	.asciz	"extb"                          # string offset=184 ; extb
+.Linfo_string9:
+	.asciz	"extc"                          # string offset=189 ; extc
+.Linfo_string10:
+	.asciz	"foo"                           # string offset=194 ; foo
+.Linfo_string11:
+	.asciz	"main"                          # string offset=198 ; main
+.Linfo_string12:
+	.asciz	"a"                             # string offset=203 ; a
+.Linfo_string13:
+	.asciz	"t"                             # string offset=205 ; t
+.Linfo_string14:
+	.asciz	"argc"                          # string offset=207 ; argc
+.Linfo_string15:
+	.asciz	"argv"                          # string offset=212 ; argv
+.Linfo_string16:
+	.asciz	"char"                          # string offset=217 ; char
+	.ident	"clang version 23"
+	.section	".note.GNU-stack","", at progbits
+	.addrsig
+	.section	.debug_line,"", at progbits
+.Lline_table_start0:

>From 170ad9e8b52fb89d1d3de0ad261cc3cbff1eaeb2 Mon Sep 17 00:00:00 2001
From: Rafael Auler <rafaelauler at fb.com>
Date: Mon, 6 Jul 2026 15:13:01 -0700
Subject: [PATCH 2/3] Formatting

---
 bolt/include/bolt/Core/BinaryFunction.h | 3 +--
 bolt/lib/Core/BinaryContext.cpp         | 7 +++----
 2 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/bolt/include/bolt/Core/BinaryFunction.h b/bolt/include/bolt/Core/BinaryFunction.h
index df42eecb98512..d1c71479cc7d6 100644
--- a/bolt/include/bolt/Core/BinaryFunction.h
+++ b/bolt/include/bolt/Core/BinaryFunction.h
@@ -1393,8 +1393,7 @@ class BinaryFunction {
   /// control-flow instructions (profile matching) and for instructions that
   /// begin/end a DWARF lexical scope (needed to translate scope ranges
   /// precisely; see DebugScopeBoundaryOffsets).
-  bool keepOffsetForInstruction(const MCInst &Inst,
-                                uint64_t Offset);
+  bool keepOffsetForInstruction(const MCInst &Inst, uint64_t Offset);
 
   /// Return the name of the section this function originated from.
   std::optional<StringRef> getOriginSectionName() const {
diff --git a/bolt/lib/Core/BinaryContext.cpp b/bolt/lib/Core/BinaryContext.cpp
index 9d07775508da6..733dfe6fa0535 100644
--- a/bolt/lib/Core/BinaryContext.cpp
+++ b/bolt/lib/Core/BinaryContext.cpp
@@ -1983,8 +1983,7 @@ void BinaryContext::collectDebugScopeBoundaries() {
   auto processScopeDie = [&](const DWARFDie &Die) {
     const dwarf::Tag Tag = Die.getTag();
     if (Tag != dwarf::DW_TAG_inlined_subroutine &&
-        Tag != dwarf::DW_TAG_lexical_block &&
-        Tag != dwarf::DW_TAG_try_block &&
+        Tag != dwarf::DW_TAG_lexical_block && Tag != dwarf::DW_TAG_try_block &&
         Tag != dwarf::DW_TAG_catch_block)
       return;
     if (Expected<DWARFAddressRangesVector> Ranges = Die.getAddressRanges()) {
@@ -2001,8 +2000,8 @@ void BinaryContext::collectDebugScopeBoundaries() {
       continue;
 
     // Extract only the CU DIE (the .dwo's, for split DWARF). This is cheap (one
-    // DIE) and sets the unit's range/addr/str-offset bases that getAddressRanges
-    // needs, without materializing the full DIE array.
+    // DIE) and sets the unit's range/addr/str-offset bases that
+    // getAddressRanges needs, without materializing the full DIE array.
     DWARFDie CUDie = CU->getNonSkeletonUnitDIE(/*ExtractUnitDIEOnly=*/true);
     if (!CUDie)
       continue;

>From 60ea4b3f69eb3bb087bcf358748283c2514ac6d0 Mon Sep 17 00:00:00 2001
From: Rafael Auler <rafaelauler at fb.com>
Date: Mon, 13 Jul 2026 15:08:00 -0700
Subject: [PATCH 3/3] Change DebugScopeBoundaryOffsets to be stored in
 SparseBitVector instead of sorted vector

---
 bolt/include/bolt/Core/BinaryFunction.h | 43 ++++++-------------------
 bolt/lib/Core/BinaryContext.cpp         |  6 ++--
 bolt/lib/Core/BinaryFunction.cpp        | 10 ++----
 3 files changed, 17 insertions(+), 42 deletions(-)

diff --git a/bolt/include/bolt/Core/BinaryFunction.h b/bolt/include/bolt/Core/BinaryFunction.h
index d1c71479cc7d6..029a514d2b4b5 100644
--- a/bolt/include/bolt/Core/BinaryFunction.h
+++ b/bolt/include/bolt/Core/BinaryFunction.h
@@ -39,6 +39,7 @@
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/SetVector.h"
 #include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/SparseBitVector.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/iterator.h"
 #include "llvm/ADT/iterator_range.h"
@@ -297,15 +298,9 @@ class BinaryFunction {
 
   /// Offsets of instructions that begin or end a DWARF lexical scope
   /// (inlined_subroutine / lexical_block low_pc/high_pc and DW_AT_ranges).
-  /// Populated (unsorted, with possible duplicates) before disassembly, sorted
-  /// and deduplicated at the start of disassembly, and cleared once disassembly
-  /// of this function is done.
-  std::vector<uint64_t> DebugScopeBoundaryOffsets;
-
-  /// Cursor into the sorted DebugScopeBoundaryOffsets. Disassembly queries
-  /// offsets in monotonically increasing order, so isDebugScopeBoundaryOffset()
-  /// advances this cursor instead of binary-searching from scratch.
-  size_t DebugScopeBoundaryCursor = 0;
+  /// Populated before disassembly and cleared once disassembly of this
+  /// function is done.
+  SparseBitVector<> DebugScopeBoundaryOffsets;
 
   /// A set of local and global symbols corresponding to secondary entry points.
   /// Each additional function entry point has a corresponding entry in the map.
@@ -1360,32 +1355,14 @@ class BinaryFunction {
     return InternalRefDataRelocations;
   }
 
-  /// Append function-relative \p Offset as a DWARF lexical-scope boundary. The
-  /// list is sorted later (sortDebugScopeBoundaryOffsets) before it is queried.
-  void addDebugScopeBoundaryOffset(uint64_t Offset) {
-    DebugScopeBoundaryOffsets.push_back(Offset);
-  }
-
-  /// Sort and deduplicate the collected scope boundaries and reset the query
-  /// cursor. Call once, before the first isDebugScopeBoundaryOffset() query.
-  void sortDebugScopeBoundaryOffsets() {
-    llvm::sort(DebugScopeBoundaryOffsets);
-    DebugScopeBoundaryOffsets.erase(llvm::unique(DebugScopeBoundaryOffsets),
-                                    DebugScopeBoundaryOffsets.end());
-    DebugScopeBoundaryCursor = 0;
+  /// Add function-relative \p Offset as a DWARF lexical-scope boundary.
+  void addDebugScopeBoundaryOffset(uint32_t Offset) {
+    DebugScopeBoundaryOffsets.set(Offset);
   }
 
   /// Return true if function-relative \p Offset begins/ends a DWARF scope.
-  /// Requires sortDebugScopeBoundaryOffsets() to have been called, and that
-  /// queries arrive with non-decreasing \p Offset. This is designed to be
-  /// used in lock-step with the disassembly loop, don't use it outside of it.
-  bool isDebugScopeBoundaryOffset(uint64_t Offset) {
-    const size_t Size = DebugScopeBoundaryOffsets.size();
-    while (DebugScopeBoundaryCursor < Size &&
-           DebugScopeBoundaryOffsets[DebugScopeBoundaryCursor] < Offset)
-      ++DebugScopeBoundaryCursor;
-    return DebugScopeBoundaryCursor < Size &&
-           DebugScopeBoundaryOffsets[DebugScopeBoundaryCursor] == Offset;
+  bool isDebugScopeBoundaryOffset(uint32_t Offset) {
+    return DebugScopeBoundaryOffsets.test(Offset);
   }
 
   /// Return true if an "Offset" annotation should be kept for instruction
@@ -1393,7 +1370,7 @@ class BinaryFunction {
   /// control-flow instructions (profile matching) and for instructions that
   /// begin/end a DWARF lexical scope (needed to translate scope ranges
   /// precisely; see DebugScopeBoundaryOffsets).
-  bool keepOffsetForInstruction(const MCInst &Inst, uint64_t Offset);
+  bool keepOffsetForInstruction(const MCInst &Inst, uint32_t Offset);
 
   /// Return the name of the section this function originated from.
   std::optional<StringRef> getOriginSectionName() const {
diff --git a/bolt/lib/Core/BinaryContext.cpp b/bolt/lib/Core/BinaryContext.cpp
index 733dfe6fa0535..a243b383d5ccc 100644
--- a/bolt/lib/Core/BinaryContext.cpp
+++ b/bolt/lib/Core/BinaryContext.cpp
@@ -1973,10 +1973,12 @@ void BinaryContext::collectDebugScopeBoundaries() {
     BinaryFunction *BF = getBinaryFunctionContainingAddress(LowPC);
     if (!BF)
       return;
-    BF->addDebugScopeBoundaryOffset(LowPC - BF->getAddress());
+    BF->addDebugScopeBoundaryOffset(
+        static_cast<uint32_t>(LowPC - BF->getAddress()));
     // Mark HighPC only if it lies strictly inside the same function.
     if (HighPC > LowPC && getBinaryFunctionContainingAddress(HighPC) == BF)
-      BF->addDebugScopeBoundaryOffset(HighPC - BF->getAddress());
+      BF->addDebugScopeBoundaryOffset(
+          static_cast<uint32_t>(HighPC - BF->getAddress()));
   };
 
   // Record the boundaries of a single scope DIE.
diff --git a/bolt/lib/Core/BinaryFunction.cpp b/bolt/lib/Core/BinaryFunction.cpp
index 6c23bbd1ff8c8..63a5bfe96471f 100644
--- a/bolt/lib/Core/BinaryFunction.cpp
+++ b/bolt/lib/Core/BinaryFunction.cpp
@@ -1303,7 +1303,7 @@ BinaryFunction::getInstructionSequenceLength(uint64_t Offset,
 }
 
 bool BinaryFunction::keepOffsetForInstruction(const MCInst &Inst,
-                                              uint64_t Offset) {
+                                              uint32_t Offset) {
   MCPlusBuilder &MIB = *BC.MIB;
   if (MIB.isCall(Inst) || MIB.isBranch(Inst) || MIB.isReturn(Inst) ||
       MIB.isPrefix(Inst) || MIB.isIndirectBranch(Inst))
@@ -1336,10 +1336,6 @@ Error BinaryFunction::disassemble() {
   // corresponding %pcrel_hi.
   LabelsMapType InstructionLabels;
 
-  // Scope boundaries were appended unsorted during preprocessDebugInfo(); sort
-  // them so keepOffsetForInstruction() can query them below.
-  sortDebugScopeBoundaryOffsets();
-
   uint64_t Size = 0; // instruction size
   for (uint64_t Offset = 0; Offset < getSize(); Offset += Size) {
     MCInst Instruction;
@@ -1565,7 +1561,7 @@ Error BinaryFunction::disassemble() {
 
     // Record offset of the instruction for profile matching, for control-flow
     // instructions, and for instructions on a DWARF lexical-scope boundary.
-    if (keepOffsetForInstruction(Instruction, Offset))
+    if (keepOffsetForInstruction(Instruction, static_cast<uint32_t>(Offset)))
       MIB->setOffset(Instruction, static_cast<uint32_t>(Offset));
 
     if (BC.isX86() && BC.MIB->isNoop(Instruction)) {
@@ -1579,7 +1575,7 @@ Error BinaryFunction::disassemble() {
   }
 
   // Scope-boundary markers are only consulted while assigning offsets above.
-  clearList(DebugScopeBoundaryOffsets);
+  DebugScopeBoundaryOffsets.clear();
 
   for (auto [Offset, Label] : InstructionLabels) {
     InstrMapType::iterator II = Instructions.find(Offset);



More information about the llvm-commits mailing list