From 0c439cce601d5812209df1fc1188afe90100fca6 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 20:09:10 +0800 Subject: [PATCH 1/3] wasm: repair invalid DWARF scope ranges after transforms Preserve nonzero tombstones, reject lost or reversed low/high pairs, normalize range lists, and repair parent scope ranges from surviving children. Ambiguous sibling scopes are made unavailable instead of being assigned incorrect code ranges. --- src/wasm/wasm-debug.cpp | 432 ++++++++++++++++-- test/passes/class_with_dwarf_noprint.bin.txt | 21 +- .../dwarfdump_roundtrip_dwarfdump.bin.txt | 2 +- test/passes/fannkuch3_manyopts_dwarf.bin.txt | 15 +- test/passes/fib2_dwarf.bin.txt | 2 +- test/passes/fib2_emptylocspan_dwarf.bin.txt | 2 +- test/passes/ignore_missing_func_dwarf.bin.txt | 15 +- test/passes/inlined_to_start_dwarf.bin.txt | 8 +- test/passes/reverse_dwarf_abbrevs.bin.txt | 4 +- 9 files changed, 441 insertions(+), 60 deletions(-) diff --git a/src/wasm/wasm-debug.cpp b/src/wasm/wasm-debug.cpp index c9d3e533311..53d70018839 100644 --- a/src/wasm/wasm-debug.cpp +++ b/src/wasm/wasm-debug.cpp @@ -823,6 +823,8 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, // may be an offset relative to the low. First, process everything but // the high pcs, so we see the low pcs first. BinaryLocation oldLowPC = 0, newLowPC = 0; + llvm::DWARFYAML::FormValue* lowPCValue = nullptr; + bool unresolvedZeroLowPC = false; iterContextAndYAML( abbrevDecl->attributes(), yamlEntry.Values, @@ -832,10 +834,16 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, if (attr == llvm::dwarf::DW_AT_low_pc) { // This is an address. BinaryLocation oldValue = yamlValue.Value, newValue = 0; - if (tag == llvm::dwarf::DW_TAG_GNU_call_site || - tag == llvm::dwarf::DW_TAG_inlined_subroutine || - tag == llvm::dwarf::DW_TAG_lexical_block || - tag == llvm::dwarf::DW_TAG_label) { + lowPCValue = &yamlValue; + // Preserve LLVM's nonzero tombstone addresses. Mapping them as if + // they were instruction offsets turns dead DIEs into live ranges at + // address zero. + if (oldValue == BinaryLocation(-1) || oldValue == BinaryLocation(-2)) { + newValue = oldValue; + } else if (tag == llvm::dwarf::DW_TAG_GNU_call_site || + tag == llvm::dwarf::DW_TAG_inlined_subroutine || + tag == llvm::dwarf::DW_TAG_lexical_block || + tag == llvm::dwarf::DW_TAG_label) { newValue = locationUpdater.getNewStart(oldValue); } else if (tag == llvm::dwarf::DW_TAG_compile_unit) { newValue = locationUpdater.getNewFuncStart(oldValue); @@ -849,6 +857,9 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, Fatal() << "unknown tag with low_pc " << llvm::dwarf::TagString(tag).str(); } + unresolvedZeroLowPC = tag != llvm::dwarf::DW_TAG_compile_unit && + newValue == 0 && oldValue != BinaryLocation(-1) && + oldValue != BinaryLocation(-2); oldLowPC = oldValue; newLowPC = newValue; yamlValue.Value = newValue; @@ -879,10 +890,12 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, if (isRelative) { oldValue += oldLowPC; } - if (tag == llvm::dwarf::DW_TAG_GNU_call_site || - tag == llvm::dwarf::DW_TAG_inlined_subroutine || - tag == llvm::dwarf::DW_TAG_lexical_block || - tag == llvm::dwarf::DW_TAG_label) { + if (newLowPC == BinaryLocation(-1) || newLowPC == BinaryLocation(-2)) { + newValue = newLowPC; + } else if (tag == llvm::dwarf::DW_TAG_GNU_call_site || + tag == llvm::dwarf::DW_TAG_inlined_subroutine || + tag == llvm::dwarf::DW_TAG_lexical_block || + tag == llvm::dwarf::DW_TAG_label) { newValue = locationUpdater.getNewExprEnd(oldValue); } else if (tag == llvm::dwarf::DW_TAG_compile_unit || tag == llvm::dwarf::DW_TAG_subprogram) { @@ -891,11 +904,30 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, Fatal() << "unknown tag with low_pc " << llvm::dwarf::TagString(tag).str(); } + // If one endpoint disappeared or moved before the other, the exact + // scope can no longer be represented by this low/high pair. Tombstone + // the DIE instead of emitting a wrapping or unrelated address range. + if (newLowPC != BinaryLocation(-1) && newLowPC != BinaryLocation(-2) && + tag != llvm::dwarf::DW_TAG_compile_unit && + (newValue == 0 || newValue < newLowPC)) { + newLowPC = BinaryLocation(-1); + newValue = isRelative ? newLowPC : BinaryLocation(-1); + assert(lowPCValue); + lowPCValue->Value = newLowPC; + } else if (newValue > newLowPC) { + // A valid high_pc disambiguates address zero as a real start rather + // than an old-style tombstone. + unresolvedZeroLowPC = false; + } if (isRelative) { newValue -= newLowPC; } yamlValue.Value = newValue; }); + if (unresolvedZeroLowPC) { + assert(lowPCValue); + lowPCValue->Value = BinaryLocation(-1); + } } static void updateCompileUnits(const BinaryenDWARFInfo& info, @@ -938,28 +970,26 @@ static void updateCompileUnits(const BinaryenDWARFInfo& info, static void updateRanges(llvm::DWARFYAML::Data& yaml, const LocationUpdater& locationUpdater) { - // In each range section, try to update the start and end. If we no longer - // have something to map them to, we must skip that part. - size_t skip = 0; - for (size_t i = 0; i < yaml.Ranges.size(); i++) { - auto& range = yaml.Ranges[i]; + // In each range section, update the start and end. If either endpoint no + // longer has a mapping, emit an empty range that a debugger can safely + // ignore. Do not use (0, 0), since that is the list terminator. + for (auto& range : yaml.Ranges) { BinaryLocation oldStart = range.Start, oldEnd = range.End, newStart = 0, newEnd = 0; - // If this is an end marker (0, 0), or an invalid range (0, x) or (x, 0) - // then just emit it as it is - either to mark the end, or to mark an - // invalid entry. - if (isTombstone(oldStart) || isTombstone(oldEnd)) { + if ((oldStart == 0 && oldEnd == 0) || oldStart == BinaryLocation(-1)) { newStart = oldStart; newEnd = oldEnd; + } else if (oldStart == BinaryLocation(-2) || isTombstone(oldEnd)) { + newStart = 1; + newEnd = 1; } else { - // This was a valid entry; update it. - newStart = locationUpdater.getNewStart(oldStart); + // Zero is a valid offset from the current range-list base. It is only a + // tombstone when paired with a zero end as handled above. + newStart = oldStart == 0 ? 0 : locationUpdater.getNewStart(oldStart); newEnd = locationUpdater.getNewEnd(oldEnd); - if (isTombstone(newStart) || isTombstone(newEnd)) { - // This part of the range no longer has a mapping, so we must skip it. - // Don't use (0, 0) as that would be an end marker; emit something - // invalid for the debugger to ignore. - newStart = 0; + if ((oldStart != 0 && isTombstone(newStart)) || isTombstone(newEnd) || + newEnd <= newStart) { + newStart = 1; newEnd = 1; } // TODO even if range start and end markers have been preserved, @@ -967,12 +997,358 @@ static void updateRanges(llvm::DWARFYAML::Data& yaml, // longer contiguous. We should check that, and possibly split/merge // the range. Or, we may need to have tracking in the IR for this. } - auto& writtenRange = yaml.Ranges[i - skip]; - writtenRange.Start = newStart; - writtenRange.End = newEnd; + range.Start = newStart; + range.End = newEnd; + } +} + +// A pass may reorder instructions such that independently mapped range +// endpoints no longer describe a valid DWARF scope tree. In that case it is +// safer to make the affected scope unavailable than to assign variables to +// code that did not originally belong to the scope. +struct DIEAddressInfo { + llvm::DWARFYAML::Entry* yamlEntry = nullptr; + llvm::DWARFYAML::FormValue* rangesValue = nullptr; + const llvm::DWARFAbbreviationDeclaration* abbrevDecl = nullptr; + llvm::dwarf::Tag tag = llvm::dwarf::DW_TAG_null; + uint32_t depth = 0; + size_t parent = size_t(-1); + std::vector> ranges; + bool hasRangeDescription = false; + bool rangeListDirty = false; + bool malformed = false; + bool unavailable = false; +}; + +static void readDIEAddressRanges(DIEAddressInfo& info, + llvm::DWARFYAML::Data& yaml, + BinaryLocation compileUnitBase) { + std::optional lowPC; + std::optional highPC; + std::optional rangesOffset; + bool highPCIsRelative = false; + + iterContextAndYAML( + info.abbrevDecl->attributes(), + info.yamlEntry->Values, + [&](const llvm::DWARFAbbreviationDeclaration::AttributeSpec& attrSpec, + llvm::DWARFYAML::FormValue& yamlValue) { + if (attrSpec.Attr == llvm::dwarf::DW_AT_low_pc) { + lowPC = BinaryLocation(yamlValue.Value); + } else if (attrSpec.Attr == llvm::dwarf::DW_AT_high_pc) { + highPC = BinaryLocation(yamlValue.Value); + highPCIsRelative = attrSpec.Form == llvm::dwarf::DW_FORM_data4; + } else if (attrSpec.Attr == llvm::dwarf::DW_AT_ranges) { + rangesOffset = BinaryLocation(yamlValue.Value); + info.rangesValue = &yamlValue; + } + }); + + if (rangesOffset) { + info.hasRangeDescription = true; + constexpr size_t RangeEntrySize = 2 * AddressSize; + if (*rangesOffset % RangeEntrySize != 0 || + *rangesOffset / RangeEntrySize >= yaml.Ranges.size()) { + info.malformed = true; + return; + } + auto base = uint64_t(compileUnitBase); + bool terminated = false; + for (size_t i = *rangesOffset / RangeEntrySize; i < yaml.Ranges.size(); + ++i) { + auto start = BinaryLocation(yaml.Ranges[i].Start); + auto end = BinaryLocation(yaml.Ranges[i].End); + if (start == 0 && end == 0) { + terminated = true; + break; + } + if (start == BinaryLocation(-1)) { + base = end; + continue; + } + // A zero start is a valid offset from the current base. Only (0, 0), + // handled above, terminates the list. + if (start == BinaryLocation(-2) || isTombstone(end)) { + continue; + } + auto absoluteStart = base + start; + auto absoluteEnd = base + end; + if (absoluteStart > absoluteEnd) { + info.malformed = true; + } else if (absoluteStart < absoluteEnd) { + info.ranges.emplace_back(absoluteStart, absoluteEnd); + } + } + if (!terminated) { + info.malformed = true; + } + } else if (lowPC && highPC) { + info.hasRangeDescription = true; + if (!isTombstone(*lowPC)) { + uint64_t start = *lowPC; + uint64_t end = highPCIsRelative ? start + *highPC : *highPC; + if (start > end) { + info.malformed = true; + } else if (start < end) { + info.ranges.emplace_back(start, end); + } + } + } + + auto original = info.ranges; + std::sort(info.ranges.begin(), info.ranges.end()); + size_t written = 0; + for (auto range : info.ranges) { + if (written && range.first <= info.ranges[written - 1].second) { + info.ranges[written - 1].second = + std::max(info.ranges[written - 1].second, range.second); + } else { + info.ranges[written++] = range; + } + } + info.ranges.resize(written); + info.rangeListDirty |= info.rangesValue && info.ranges != original; +} + +static bool containsRanges(const DIEAddressInfo& parent, + const DIEAddressInfo& child) { + size_t parentIndex = 0; + for (auto childRange : child.ranges) { + while (parentIndex < parent.ranges.size() && + parent.ranges[parentIndex].second <= childRange.first) { + ++parentIndex; + } + if (parentIndex == parent.ranges.size() || + parent.ranges[parentIndex].first > childRange.first || + parent.ranges[parentIndex].second < childRange.second) { + return false; + } + } + return true; +} + +static bool rangesOverlap(const DIEAddressInfo& left, + const DIEAddressInfo& right) { + size_t leftIndex = 0, rightIndex = 0; + while (leftIndex < left.ranges.size() && rightIndex < right.ranges.size()) { + auto leftRange = left.ranges[leftIndex]; + auto rightRange = right.ranges[rightIndex]; + if (leftRange.first < rightRange.second && + rightRange.first < leftRange.second) { + return true; + } + if (leftRange.second <= rightRange.first) { + ++leftIndex; + } else { + ++rightIndex; + } + } + return false; +} + +static void addRanges(DIEAddressInfo& destination, + const DIEAddressInfo& source) { + if (source.ranges.empty()) { + return; + } + auto original = destination.ranges; + destination.ranges.insert( + destination.ranges.end(), source.ranges.begin(), source.ranges.end()); + std::sort(destination.ranges.begin(), destination.ranges.end()); + size_t written = 0; + for (auto range : destination.ranges) { + if (written && range.first <= destination.ranges[written - 1].second) { + destination.ranges[written - 1].second = + std::max(destination.ranges[written - 1].second, range.second); + } else { + destination.ranges[written++] = range; + } + } + destination.ranges.resize(written); + destination.rangeListDirty |= destination.ranges != original; +} + +static void writeRangeList(DIEAddressInfo& info, llvm::DWARFYAML::Data& yaml) { + assert(info.rangesValue); + constexpr size_t RangeEntrySize = 2 * AddressSize; + info.rangesValue->Value = yaml.Ranges.size() * RangeEntrySize; + // Use an explicit zero base so the new entries remain absolute and can be + // updated again without recovering an implicit compile-unit base. + yaml.Ranges.push_back(llvm::DWARFYAML::Range{BinaryLocation(-1), 0, 0}); + for (auto [start, end] : info.ranges) { + yaml.Ranges.push_back(llvm::DWARFYAML::Range{start, end, 0}); + } + yaml.Ranges.push_back(llvm::DWARFYAML::Range{0, 0, 0}); +} + +static void markUnavailable(std::vector& infos, size_t root) { + auto depth = infos[root].depth; + for (size_t i = root; + i < infos.size() && (i == root || infos[i].depth > depth); + ++i) { + infos[i].unavailable = true; } } +static void +writeUnavailableDIE(DIEAddressInfo& info, + llvm::DWARFYAML::Data& yaml, + std::optional& emptyRangeListOffset) { + iterContextAndYAML( + info.abbrevDecl->attributes(), + info.yamlEntry->Values, + [&](const llvm::DWARFAbbreviationDeclaration::AttributeSpec& attrSpec, + llvm::DWARFYAML::FormValue& yamlValue) { + if (attrSpec.Attr == llvm::dwarf::DW_AT_low_pc) { + yamlValue.Value = BinaryLocation(-1); + } else if (attrSpec.Attr == llvm::dwarf::DW_AT_high_pc) { + yamlValue.Value = + attrSpec.Form == llvm::dwarf::DW_FORM_data4 ? 0 : BinaryLocation(-1); + } else if (attrSpec.Attr == llvm::dwarf::DW_AT_ranges) { + constexpr size_t RangeEntrySize = 2 * AddressSize; + if (!emptyRangeListOffset) { + emptyRangeListOffset = yaml.Ranges.size() * RangeEntrySize; + yaml.Ranges.push_back(llvm::DWARFYAML::Range{0, 0, 0}); + } + yamlValue.Value = *emptyRangeListOffset; + } + }); +} + +static void repairDIEAddressRanges(const BinaryenDWARFInfo& dwarfInfo, + llvm::DWARFYAML::Data& yaml, + const LocationUpdater& locationUpdater) { + size_t compileUnitIndex = 0; + std::optional emptyRangeListOffset; + iterContextAndYAML( + dwarfInfo.context->compile_units(), + yaml.CompileUnits, + [&](const std::unique_ptr& CU, + llvm::DWARFYAML::Unit& yamlUnit) { + std::vector infos(yamlUnit.Entries.size()); + std::vector ancestors; + auto yamlEntry = yamlUnit.Entries.begin(); + size_t index = 0; + BinaryLocation compileUnitBase = 0; + if (auto iter = locationUpdater.compileUnitBases.find(compileUnitIndex); + iter != locationUpdater.compileUnitBases.end()) { + compileUnitBase = iter->second.second; + } + for (const auto& DIE : CU->dies()) { + assert(yamlEntry != yamlUnit.Entries.end()); + auto abbrevDecl = DIE.getAbbreviationDeclarationPtr(); + if (abbrevDecl) { + auto& info = infos[index]; + info.yamlEntry = &*yamlEntry; + info.abbrevDecl = abbrevDecl; + info.tag = DIE.getTag(); + info.depth = DIE.getDepth(); + while (ancestors.size() > info.depth) { + ancestors.pop_back(); + } + if (info.depth > 0 && ancestors.size() == info.depth) { + info.parent = ancestors.back(); + } + if (ancestors.size() == info.depth) { + ancestors.push_back(index); + } else { + assert(ancestors.size() > info.depth); + ancestors[info.depth] = index; + ancestors.resize(info.depth + 1); + } + readDIEAddressRanges(info, yaml, compileUnitBase); + } + ++yamlEntry; + ++index; + } + assert(yamlEntry == yamlUnit.Entries.end()); + + for (size_t i = 0; i < infos.size(); ++i) { + if (!infos[i].abbrevDecl) { + continue; + } + bool hasRangedDescendant = false; + for (size_t child = i + 1; + child < infos.size() && infos[child].depth > infos[i].depth; + ++child) { + if (!infos[child].ranges.empty()) { + hasRangedDescendant = true; + break; + } + } + if (infos[i].malformed || + (!infos[i].rangesValue && infos[i].hasRangeDescription && + infos[i].ranges.empty() && hasRangedDescendant)) { + markUnavailable(infos, i); + } + } + + std::vector> children(infos.size()); + for (size_t i = 0; i < infos.size(); ++i) { + if (infos[i].abbrevDecl && !infos[i].unavailable && + infos[i].parent != size_t(-1) && !infos[i].ranges.empty()) { + children[infos[i].parent].push_back(i); + } + } + for (auto& siblings : children) { + for (size_t i = 0; i < siblings.size(); ++i) { + for (size_t j = i + 1; j < siblings.size(); ++j) { + if (!infos[siblings[i]].unavailable && + !infos[siblings[j]].unavailable && + rangesOverlap(infos[siblings[i]], infos[siblings[j]])) { + markUnavailable(infos, siblings[i]); + markUnavailable(infos, siblings[j]); + } + } + } + } + + // A range-list parent can represent the exact union of its surviving + // children's mapped ranges. Rebuild that union bottom-up rather than + // discarding a valid child merely because optimization moved it outside + // the old parent endpoints. + for (size_t i = infos.size(); i-- > 0;) { + auto& child = infos[i]; + if (!child.abbrevDecl || child.unavailable || child.ranges.empty() || + child.parent == size_t(-1)) { + continue; + } + auto& parent = infos[child.parent]; + if (!parent.unavailable && parent.rangesValue) { + addRanges(parent, child); + } + } + + for (size_t i = 0; i < infos.size(); ++i) { + auto& child = infos[i]; + if (!child.abbrevDecl || child.unavailable || + child.parent == size_t(-1)) { + continue; + } + auto& parent = infos[child.parent]; + if (parent.unavailable) { + markUnavailable(infos, i); + continue; + } + bool subprogramPair = child.tag == llvm::dwarf::DW_TAG_subprogram && + parent.tag == llvm::dwarf::DW_TAG_subprogram; + if (!subprogramPair && !child.ranges.empty() && + !parent.ranges.empty() && !containsRanges(parent, child)) { + markUnavailable(infos, i); + } + } + + for (auto& info : infos) { + if (info.abbrevDecl && info.unavailable) { + writeUnavailableDIE(info, yaml, emptyRangeListOffset); + } else if (info.abbrevDecl && info.rangeListDirty) { + writeRangeList(info, yaml); + } + } + ++compileUnitIndex; + }); +} + // A location that is ignoreable, i.e., not a special value like 0 or -1 (which // would indicate an end or a base in .debug_loc). static const BinaryLocation IGNOREABLE_LOCATION = 1; @@ -1099,6 +1475,8 @@ void writeDWARFSections(Module& wasm, const BinaryLocations& newLocations) { updateRanges(data, locationUpdater); + repairDIEAddressRanges(info, data, locationUpdater); + updateLoc(data, locationUpdater); // Convert to binary sections. diff --git a/test/passes/class_with_dwarf_noprint.bin.txt b/test/passes/class_with_dwarf_noprint.bin.txt index 50963b311aa..03ea6b5d79d 100644 --- a/test/passes/class_with_dwarf_noprint.bin.txt +++ b/test/passes/class_with_dwarf_noprint.bin.txt @@ -3,7 +3,7 @@ DWARF debug info Contains section .debug_info (429 bytes) Contains section .debug_loc (199 bytes) -Contains section .debug_ranges (24 bytes) +Contains section .debug_ranges (32 bytes) Contains section .debug_abbrev (337 bytes) Contains section .debug_line (438 bytes) Contains section .debug_str (364 bytes) @@ -335,8 +335,8 @@ Abbrev table for offset: 0x00000000 DW_AT_type [DW_FORM_ref4] (cu + 0x01a5 => {0x000001a5} "int") 0x0000011f: DW_TAG_lexical_block [22] * - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000030) - DW_AT_high_pc [DW_FORM_data4] (0xffffffd0) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) + DW_AT_high_pc [DW_FORM_data4] (0x00000000) 0x00000128: DW_TAG_variable [21] DW_AT_location [DW_FORM_sec_offset] (0x00000046: @@ -348,8 +348,8 @@ Abbrev table for offset: 0x00000000 DW_AT_type [DW_FORM_ref4] (cu + 0x01a5 => {0x000001a5} "int") 0x00000137: DW_TAG_lexical_block [22] * - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000030) - DW_AT_high_pc [DW_FORM_data4] (0x0000005f) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) + DW_AT_high_pc [DW_FORM_data4] (0x00000000) 0x00000140: DW_TAG_variable [21] DW_AT_location [DW_FORM_sec_offset] (0x0000009b: @@ -369,9 +369,7 @@ Abbrev table for offset: 0x00000000 0x0000015e: DW_TAG_inlined_subroutine [23] * DW_AT_abstract_origin [DW_FORM_ref4] (cu + 0x00c5 => {0x000000c5} "_ZN6Random3getEf") - DW_AT_ranges [DW_FORM_sec_offset] (0x00000000 - [0x00000006, 0x00000007) - [0x0000006b, 0x00000074)) + DW_AT_ranges [DW_FORM_sec_offset] (0x00000018) DW_AT_call_file [DW_FORM_data1] ("/tmp/emscripten_test_wasm3_2u9tontv/src.cpp") DW_AT_call_line [DW_FORM_data1] (28) DW_AT_call_column [DW_FORM_data1] (0x15) @@ -388,8 +386,8 @@ Abbrev table for offset: 0x00000000 0x00000179: DW_TAG_inlined_subroutine [26] * DW_AT_abstract_origin [DW_FORM_ref4] (cu + 0x00c5 => {0x000000c5} "_ZN6Random3getEf") - DW_AT_low_pc [DW_FORM_addr] (0x000000000000004a) - DW_AT_high_pc [DW_FORM_data4] (0x00000019) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) + DW_AT_high_pc [DW_FORM_data4] (0x00000000) DW_AT_call_file [DW_FORM_data1] ("/tmp/emscripten_test_wasm3_2u9tontv/src.cpp") DW_AT_call_line [DW_FORM_data1] (29) DW_AT_call_column [DW_FORM_data1] (0x15) @@ -703,6 +701,7 @@ file_names[ 3]: 0x00000169: "x1" .debug_ranges contents: -00000000 00000000 00000001 +00000000 00000001 00000001 00000000 00000065 0000006e 00000000 +00000018 diff --git a/test/passes/dwarfdump_roundtrip_dwarfdump.bin.txt b/test/passes/dwarfdump_roundtrip_dwarfdump.bin.txt index 8f88bbbffac..4246ba8bd00 100644 --- a/test/passes/dwarfdump_roundtrip_dwarfdump.bin.txt +++ b/test/passes/dwarfdump_roundtrip_dwarfdump.bin.txt @@ -141,7 +141,7 @@ Abbrev table for offset: 0x00000000 DW_AT_high_pc [DW_FORM_data4] (0x00000000) 0x00000026: DW_TAG_subprogram [2] - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) DW_AT_high_pc [DW_FORM_data4] (0x00000000) DW_AT_linkage_name [DW_FORM_strp] ( .debug_str[0x000000c8] = "_Z3foov") DW_AT_name [DW_FORM_strp] ( .debug_str[0x000000d0] = "foo") diff --git a/test/passes/fannkuch3_manyopts_dwarf.bin.txt b/test/passes/fannkuch3_manyopts_dwarf.bin.txt index 98d1c7bb8e9..5e7468d35ed 100644 --- a/test/passes/fannkuch3_manyopts_dwarf.bin.txt +++ b/test/passes/fannkuch3_manyopts_dwarf.bin.txt @@ -2301,7 +2301,7 @@ DWARF debug info Contains section .debug_info (851 bytes) Contains section .debug_loc (1073 bytes) -Contains section .debug_ranges (88 bytes) +Contains section .debug_ranges (96 bytes) Contains section .debug_abbrev (333 bytes) Contains section .debug_line (2642 bytes) Contains section .debug_str (434 bytes) @@ -2850,8 +2850,8 @@ Abbrev table for offset: 0x00000000 0x00000278: DW_TAG_inlined_subroutine [24] * DW_AT_abstract_origin [DW_FORM_ref4] (cu + 0x01a8 => {0x000001a8} "_ZL8fannkuchi") - DW_AT_low_pc [DW_FORM_addr] (0x00000000000003c2) - DW_AT_high_pc [DW_FORM_data4] (0xfffffc3e) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) + DW_AT_high_pc [DW_FORM_data4] (0x00000000) DW_AT_call_file [DW_FORM_data1] ("/usr/local/google/home/azakai/Dev/emscripten/tests/fannkuch.cpp") DW_AT_call_line [DW_FORM_data1] (159) DW_AT_call_column [DW_FORM_data1] (0x29) @@ -2923,12 +2923,10 @@ Abbrev table for offset: 0x00000000 0x000002da: DW_TAG_label [28] DW_AT_abstract_origin [DW_FORM_ref4] (cu + 0x0226 => {0x00000226} "cleanup") - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000604) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) 0x000002e3: DW_TAG_lexical_block [14] * - DW_AT_ranges [DW_FORM_sec_offset] (0x00000028 - [0x000004cd, 0x00000512) - [0x00000585, 0x000005d0)) + DW_AT_ranges [DW_FORM_sec_offset] (0x00000058) 0x000002e8: DW_TAG_variable [26] DW_AT_location [DW_FORM_sec_offset] (0x000003bc: @@ -4671,6 +4669,7 @@ file_names[ 4]: 00000040 00000006 00000381 00000040 00000383 00000662 00000040 +00000058 (module (type $0 (func (param i32) (result i32))) (type $1 (func (param i32 i32) (result i32))) @@ -6847,7 +6846,7 @@ file_names[ 4]: ) ;; custom section ".debug_info", size 851 ;; custom section ".debug_loc", size 1073 - ;; custom section ".debug_ranges", size 88 + ;; custom section ".debug_ranges", size 96 ;; custom section ".debug_abbrev", size 333 ;; custom section ".debug_line", size 2642 ;; custom section ".debug_str", size 434 diff --git a/test/passes/fib2_dwarf.bin.txt b/test/passes/fib2_dwarf.bin.txt index c83a51ccd24..3159e2f6a76 100644 --- a/test/passes/fib2_dwarf.bin.txt +++ b/test/passes/fib2_dwarf.bin.txt @@ -453,7 +453,7 @@ Abbrev table for offset: 0x00000000 DW_AT_external [DW_FORM_flag_present] (true) 0x0000009a: DW_TAG_GNU_call_site [7] - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) 0x0000009f: NULL diff --git a/test/passes/fib2_emptylocspan_dwarf.bin.txt b/test/passes/fib2_emptylocspan_dwarf.bin.txt index 4ab6d4cccde..d8d4df81cf3 100644 --- a/test/passes/fib2_emptylocspan_dwarf.bin.txt +++ b/test/passes/fib2_emptylocspan_dwarf.bin.txt @@ -453,7 +453,7 @@ Abbrev table for offset: 0x00000000 DW_AT_external [DW_FORM_flag_present] (true) 0x0000009a: DW_TAG_GNU_call_site [7] - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) 0x0000009f: NULL diff --git a/test/passes/ignore_missing_func_dwarf.bin.txt b/test/passes/ignore_missing_func_dwarf.bin.txt index e46c16fa9ac..46437d1f8b6 100644 --- a/test/passes/ignore_missing_func_dwarf.bin.txt +++ b/test/passes/ignore_missing_func_dwarf.bin.txt @@ -549,7 +549,7 @@ DWARF debug info ================ Contains section .debug_info (175 bytes) -Contains section .debug_ranges (32 bytes) +Contains section .debug_ranges (64 bytes) Contains section .debug_abbrev (117 bytes) Contains section .debug_line (169 bytes) Contains section .debug_str (235 bytes) @@ -621,8 +621,9 @@ Abbrev table for offset: 0x00000000 DW_AT_stmt_list [DW_FORM_sec_offset] (0x00000000) DW_AT_comp_dir [DW_FORM_strp] ( .debug_str[0x0000009b] = "/home/alon/Dev/emscripten") DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) - DW_AT_ranges [DW_FORM_sec_offset] (0x00000000 - [0x00000005, 0x0000006d)) + DW_AT_ranges [DW_FORM_sec_offset] (0x00000020 + [0x00000005, 0x0000006d) + [0x0000006e, 0x000000d3)) 0x00000026: DW_TAG_variable [2] DW_AT_name [DW_FORM_strp] ( .debug_str[0x000000b5] = "quine") @@ -668,7 +669,7 @@ Abbrev table for offset: 0x00000000 0x00000074: NULL 0x00000075: DW_TAG_subprogram [6] * - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) DW_AT_high_pc [DW_FORM_data4] (0x00000000) DW_AT_linkage_name [DW_FORM_strp] ( .debug_str[0x000000d2] = "_Z6unusedi") DW_AT_name [DW_FORM_strp] ( .debug_str[0x000000dd] = "unused") @@ -821,6 +822,10 @@ file_names[ 1]: 00000000 00000010 0000006e 000000d3 00000010 +00000020 ffffffff 00000000 +00000020 00000005 0000006d +00000020 0000006e 000000d3 +00000020 (module (type $0 (func)) (type $1 (func (param i32) (result i32))) @@ -1074,7 +1079,7 @@ file_names[ 1]: ) ) ;; custom section ".debug_info", size 175 - ;; custom section ".debug_ranges", size 32 + ;; custom section ".debug_ranges", size 64 ;; custom section ".debug_abbrev", size 117 ;; custom section ".debug_line", size 169 ;; custom section ".debug_str", size 235 diff --git a/test/passes/inlined_to_start_dwarf.bin.txt b/test/passes/inlined_to_start_dwarf.bin.txt index 830abfa7cdb..887475102db 100644 --- a/test/passes/inlined_to_start_dwarf.bin.txt +++ b/test/passes/inlined_to_start_dwarf.bin.txt @@ -115,12 +115,12 @@ Abbrev table for offset: 0x00000000 DW_AT_comp_dir [DW_FORM_strp] ( .debug_str[0x0000006f] = "/home/azakai/Dev/emscripten") DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) DW_AT_ranges [DW_FORM_sec_offset] (0x00000000 - [0x00000000, 0x00000007) + [0x00000001, 0x00000001) [0x00000005, 0x00000012) [0x00000013, 0x0000001b)) 0x00000026: DW_TAG_subprogram [2] * - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) DW_AT_high_pc [DW_FORM_data4] (0x00000000) DW_AT_frame_base [DW_FORM_exprloc] (DW_OP_WASM_location 0x3 +0, 00 00 9f) DW_AT_GNU_all_call_sites [DW_FORM_flag_present] (true) @@ -260,7 +260,7 @@ Abbrev table for offset: 0x00000000 DW_AT_type [DW_FORM_ref4] (cu + 0x0071 => {0x00000071} "int") 0x00000129: DW_TAG_GNU_call_site [13] - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) 0x0000012e: NULL @@ -404,7 +404,7 @@ file_names[ 1]: 0x000000d4: "char" .debug_ranges contents: -00000000 00000000 00000007 +00000000 00000001 00000001 00000000 00000005 00000012 00000000 00000013 0000001b 00000000 diff --git a/test/passes/reverse_dwarf_abbrevs.bin.txt b/test/passes/reverse_dwarf_abbrevs.bin.txt index ef9169a95c3..7222875b19e 100644 --- a/test/passes/reverse_dwarf_abbrevs.bin.txt +++ b/test/passes/reverse_dwarf_abbrevs.bin.txt @@ -50,7 +50,7 @@ Abbrev table for offset: 0x00000000 DW_AT_high_pc [DW_FORM_data4] (0x00000000) 0x00000026: DW_TAG_subprogram [3] * - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) DW_AT_high_pc [DW_FORM_data4] (0x00000000) DW_AT_frame_base [DW_FORM_exprloc] (DW_OP_WASM_location 0x3 +0, 00 00 9f) DW_AT_GNU_all_call_sites [DW_FORM_flag_present] (true) @@ -61,7 +61,7 @@ Abbrev table for offset: 0x00000000 DW_AT_external [DW_FORM_flag_present] (true) 0x00000041: DW_TAG_GNU_call_site [2] - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) 0x00000046: NULL From 1c489ebd12aba4f97282ebbb986f7d9a296b1505 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 9 Aug 2026 19:53:28 +0800 Subject: [PATCH 2/3] wasm: clarify and test DWARF range repair --- src/wasm/dwarf-ranges.h | 119 +++++++++++++++ src/wasm/wasm-debug.cpp | 296 ++++++++++++++---------------------- test/gtest/CMakeLists.txt | 1 + test/gtest/dwarf-ranges.cpp | 40 +++++ test/unit/test_dwarf.py | 33 ++++ 5 files changed, 310 insertions(+), 179 deletions(-) create mode 100644 src/wasm/dwarf-ranges.h create mode 100644 test/gtest/dwarf-ranges.cpp diff --git a/src/wasm/dwarf-ranges.h b/src/wasm/dwarf-ranges.h new file mode 100644 index 00000000000..6182c701693 --- /dev/null +++ b/src/wasm/dwarf-ranges.h @@ -0,0 +1,119 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef wasm_dwarf_ranges_h +#define wasm_dwarf_ranges_h + +#include +#include +#include +#include +#include +#include +#include + +namespace wasm::Debug { + +using DwarfRange = std::pair; + +// A set of nonempty, half-open DWARF address ranges. Call normalize before +// querying containment or overlap; adding one set to another normalizes the +// result automatically. +class DwarfRanges { + std::vector ranges; + + void normalizeInPlace() { + std::sort(ranges.begin(), ranges.end()); + size_t written = 0; + for (auto range : ranges) { + if (written && range.first <= ranges[written - 1].second) { + ranges[written - 1].second = + std::max(ranges[written - 1].second, range.second); + } else { + ranges[written++] = range; + } + } + ranges.resize(written); + } + +public: + DwarfRanges() = default; + DwarfRanges(std::initializer_list ranges) : ranges(ranges) {} + + bool empty() const { return ranges.empty(); } + + const std::vector& get() const { return ranges; } + + void add(uint64_t start, uint64_t end) { + assert(start < end); + ranges.emplace_back(start, end); + } + + // Returns whether normalization changed the range representation. + bool normalize() { + auto original = ranges; + normalizeInPlace(); + return ranges != original; + } + + // Adds another set and returns whether the resulting union changed this set. + bool add(const DwarfRanges& other) { + if (other.empty()) { + return false; + } + auto original = ranges; + ranges.insert(ranges.end(), other.ranges.begin(), other.ranges.end()); + normalizeInPlace(); + return ranges != original; + } + + bool contains(const DwarfRanges& other) const { + size_t index = 0; + for (auto otherRange : other.ranges) { + while (index < ranges.size() && + ranges[index].second <= otherRange.first) { + ++index; + } + if (index == ranges.size() || ranges[index].first > otherRange.first || + ranges[index].second < otherRange.second) { + return false; + } + } + return true; + } + + bool overlaps(const DwarfRanges& other) const { + size_t left = 0, right = 0; + while (left < ranges.size() && right < other.ranges.size()) { + auto leftRange = ranges[left]; + auto rightRange = other.ranges[right]; + if (leftRange.first < rightRange.second && + rightRange.first < leftRange.second) { + return true; + } + if (leftRange.second <= rightRange.first) { + ++left; + } else { + ++right; + } + } + return false; + } +}; + +} // namespace wasm::Debug + +#endif // wasm_dwarf_ranges_h diff --git a/src/wasm/wasm-debug.cpp b/src/wasm/wasm-debug.cpp index 53d70018839..bbd1342bfdc 100644 --- a/src/wasm/wasm-debug.cpp +++ b/src/wasm/wasm-debug.cpp @@ -15,6 +15,7 @@ */ #include "wasm-debug.h" +#include "dwarf-ranges.h" #include "wasm.h" #ifdef BUILD_LLVM_DWARF @@ -60,7 +61,17 @@ bool hasDWARFSections(const Module& wasm) { #ifdef BUILD_LLVM_DWARF // In wasm32 the address size is 32 bits. -static const size_t AddressSize = 4; +static constexpr size_t AddressSize = 4; +static constexpr size_t RangeEntrySize = 2 * AddressSize; + +// DWARF v6 reserves the all-ones address for a non-existent entity. LLVM also +// recognizes max-minus-one in legacy range and location data, where all-ones +// is already the base-address-selection marker. Zero is a historical linker +// tombstone, but is context-dependent because it can also be a valid address. +static constexpr BinaryLocation DwarfTombstone = BinaryLocation(-1); +static constexpr BinaryLocation LegacyRangeTombstone = BinaryLocation(-2); +static constexpr BinaryLocation EmptyRangeAddress = 1; +static constexpr size_t NoParent = size_t(-1); struct BinaryenDWARFInfo { llvm::StringMap> sections; @@ -670,17 +681,14 @@ struct LocationUpdater { } }; -// A tombstone value is a value that is placed where something used to exist, -// but no longer does, like a reference to a function that was DCE'd out during -// linking. In theory the value can be any invalid location, and tools will -// basically ignore it. -// Earlier LLVM used to use 0 there, and newer versions use -1 or -2 depending -// on the DWARF section. For now, support them all, but TODO stop supporting 0, -// as there are apparently some possible corner cases where 0 is a valid value. -static bool isTombstone(uint32_t x) { - return x == 0 || x == uint32_t(-1) || x == uint32_t(-2); +static bool isNonzeroTombstone(BinaryLocation location) { + return location == DwarfTombstone || location == LegacyRangeTombstone; } +// Keep accepting the historical zero tombstone. Callers whose encoding permits +// a real address zero must use isNonzeroTombstone instead. +static bool isTombstone(uint32_t x) { return x == 0 || isNonzeroTombstone(x); } + // Update debug lines, and update the locationUpdater with debug line offset // changes so we can update offsets into the debug line section. static void updateDebugLines(llvm::DWARFYAML::Data& data, @@ -835,10 +843,9 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, // This is an address. BinaryLocation oldValue = yamlValue.Value, newValue = 0; lowPCValue = &yamlValue; - // Preserve LLVM's nonzero tombstone addresses. Mapping them as if - // they were instruction offsets turns dead DIEs into live ranges at - // address zero. - if (oldValue == BinaryLocation(-1) || oldValue == BinaryLocation(-2)) { + // Preserve reserved dead-address sentinels. Mapping them as instruction + // offsets would turn dead DIEs into live ranges at address zero. + if (isNonzeroTombstone(oldValue)) { newValue = oldValue; } else if (tag == llvm::dwarf::DW_TAG_GNU_call_site || tag == llvm::dwarf::DW_TAG_inlined_subroutine || @@ -858,8 +865,7 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, << llvm::dwarf::TagString(tag).str(); } unresolvedZeroLowPC = tag != llvm::dwarf::DW_TAG_compile_unit && - newValue == 0 && oldValue != BinaryLocation(-1) && - oldValue != BinaryLocation(-2); + newValue == 0 && !isNonzeroTombstone(oldValue); oldLowPC = oldValue; newLowPC = newValue; yamlValue.Value = newValue; @@ -890,7 +896,7 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, if (isRelative) { oldValue += oldLowPC; } - if (newLowPC == BinaryLocation(-1) || newLowPC == BinaryLocation(-2)) { + if (isNonzeroTombstone(newLowPC)) { newValue = newLowPC; } else if (tag == llvm::dwarf::DW_TAG_GNU_call_site || tag == llvm::dwarf::DW_TAG_inlined_subroutine || @@ -907,11 +913,11 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, // If one endpoint disappeared or moved before the other, the exact // scope can no longer be represented by this low/high pair. Tombstone // the DIE instead of emitting a wrapping or unrelated address range. - if (newLowPC != BinaryLocation(-1) && newLowPC != BinaryLocation(-2) && + if (!isNonzeroTombstone(newLowPC) && tag != llvm::dwarf::DW_TAG_compile_unit && (newValue == 0 || newValue < newLowPC)) { - newLowPC = BinaryLocation(-1); - newValue = isRelative ? newLowPC : BinaryLocation(-1); + newLowPC = DwarfTombstone; + newValue = isRelative ? newLowPC : DwarfTombstone; assert(lowPCValue); lowPCValue->Value = newLowPC; } else if (newValue > newLowPC) { @@ -926,7 +932,7 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, }); if (unresolvedZeroLowPC) { assert(lowPCValue); - lowPCValue->Value = BinaryLocation(-1); + lowPCValue->Value = DwarfTombstone; } } @@ -976,12 +982,12 @@ static void updateRanges(llvm::DWARFYAML::Data& yaml, for (auto& range : yaml.Ranges) { BinaryLocation oldStart = range.Start, oldEnd = range.End, newStart = 0, newEnd = 0; - if ((oldStart == 0 && oldEnd == 0) || oldStart == BinaryLocation(-1)) { + if ((oldStart == 0 && oldEnd == 0) || oldStart == DwarfTombstone) { newStart = oldStart; newEnd = oldEnd; - } else if (oldStart == BinaryLocation(-2) || isTombstone(oldEnd)) { - newStart = 1; - newEnd = 1; + } else if (oldStart == LegacyRangeTombstone || isTombstone(oldEnd)) { + newStart = EmptyRangeAddress; + newEnd = EmptyRangeAddress; } else { // Zero is a valid offset from the current range-list base. It is only a // tombstone when paired with a zero end as handled above. @@ -989,8 +995,8 @@ static void updateRanges(llvm::DWARFYAML::Data& yaml, newEnd = locationUpdater.getNewEnd(oldEnd); if ((oldStart != 0 && isTombstone(newStart)) || isTombstone(newEnd) || newEnd <= newStart) { - newStart = 1; - newEnd = 1; + newStart = EmptyRangeAddress; + newEnd = EmptyRangeAddress; } // TODO even if range start and end markers have been preserved, // instructions in the middle may have moved around, making the range no @@ -1012,8 +1018,8 @@ struct DIEAddressInfo { const llvm::DWARFAbbreviationDeclaration* abbrevDecl = nullptr; llvm::dwarf::Tag tag = llvm::dwarf::DW_TAG_null; uint32_t depth = 0; - size_t parent = size_t(-1); - std::vector> ranges; + size_t parent = NoParent; + DwarfRanges ranges; bool hasRangeDescription = false; bool rangeListDirty = false; bool malformed = false; @@ -1046,7 +1052,6 @@ static void readDIEAddressRanges(DIEAddressInfo& info, if (rangesOffset) { info.hasRangeDescription = true; - constexpr size_t RangeEntrySize = 2 * AddressSize; if (*rangesOffset % RangeEntrySize != 0 || *rangesOffset / RangeEntrySize >= yaml.Ranges.size()) { info.malformed = true; @@ -1062,13 +1067,13 @@ static void readDIEAddressRanges(DIEAddressInfo& info, terminated = true; break; } - if (start == BinaryLocation(-1)) { + if (start == DwarfTombstone) { base = end; continue; } // A zero start is a valid offset from the current base. Only (0, 0), // handled above, terminates the list. - if (start == BinaryLocation(-2) || isTombstone(end)) { + if (start == LegacyRangeTombstone || isTombstone(end)) { continue; } auto absoluteStart = base + start; @@ -1076,7 +1081,7 @@ static void readDIEAddressRanges(DIEAddressInfo& info, if (absoluteStart > absoluteEnd) { info.malformed = true; } else if (absoluteStart < absoluteEnd) { - info.ranges.emplace_back(absoluteStart, absoluteEnd); + info.ranges.add(absoluteStart, absoluteEnd); } } if (!terminated) { @@ -1084,109 +1089,48 @@ static void readDIEAddressRanges(DIEAddressInfo& info, } } else if (lowPC && highPC) { info.hasRangeDescription = true; - if (!isTombstone(*lowPC)) { + // Unlike a range-list terminator, zero is a valid low_pc when paired with + // a nonzero high_pc. Only the reserved nonzero sentinels are unambiguously + // unavailable in this encoding. + if (!isNonzeroTombstone(*lowPC)) { uint64_t start = *lowPC; uint64_t end = highPCIsRelative ? start + *highPC : *highPC; if (start > end) { info.malformed = true; } else if (start < end) { - info.ranges.emplace_back(start, end); + info.ranges.add(start, end); } } } - auto original = info.ranges; - std::sort(info.ranges.begin(), info.ranges.end()); - size_t written = 0; - for (auto range : info.ranges) { - if (written && range.first <= info.ranges[written - 1].second) { - info.ranges[written - 1].second = - std::max(info.ranges[written - 1].second, range.second); - } else { - info.ranges[written++] = range; - } - } - info.ranges.resize(written); - info.rangeListDirty |= info.rangesValue && info.ranges != original; -} - -static bool containsRanges(const DIEAddressInfo& parent, - const DIEAddressInfo& child) { - size_t parentIndex = 0; - for (auto childRange : child.ranges) { - while (parentIndex < parent.ranges.size() && - parent.ranges[parentIndex].second <= childRange.first) { - ++parentIndex; - } - if (parentIndex == parent.ranges.size() || - parent.ranges[parentIndex].first > childRange.first || - parent.ranges[parentIndex].second < childRange.second) { - return false; - } - } - return true; -} - -static bool rangesOverlap(const DIEAddressInfo& left, - const DIEAddressInfo& right) { - size_t leftIndex = 0, rightIndex = 0; - while (leftIndex < left.ranges.size() && rightIndex < right.ranges.size()) { - auto leftRange = left.ranges[leftIndex]; - auto rightRange = right.ranges[rightIndex]; - if (leftRange.first < rightRange.second && - rightRange.first < leftRange.second) { - return true; - } - if (leftRange.second <= rightRange.first) { - ++leftIndex; - } else { - ++rightIndex; - } - } - return false; -} - -static void addRanges(DIEAddressInfo& destination, - const DIEAddressInfo& source) { - if (source.ranges.empty()) { - return; - } - auto original = destination.ranges; - destination.ranges.insert( - destination.ranges.end(), source.ranges.begin(), source.ranges.end()); - std::sort(destination.ranges.begin(), destination.ranges.end()); - size_t written = 0; - for (auto range : destination.ranges) { - if (written && range.first <= destination.ranges[written - 1].second) { - destination.ranges[written - 1].second = - std::max(destination.ranges[written - 1].second, range.second); - } else { - destination.ranges[written++] = range; - } - } - destination.ranges.resize(written); - destination.rangeListDirty |= destination.ranges != original; + info.rangeListDirty |= info.rangesValue && info.ranges.normalize(); } static void writeRangeList(DIEAddressInfo& info, llvm::DWARFYAML::Data& yaml) { assert(info.rangesValue); - constexpr size_t RangeEntrySize = 2 * AddressSize; info.rangesValue->Value = yaml.Ranges.size() * RangeEntrySize; // Use an explicit zero base so the new entries remain absolute and can be // updated again without recovering an implicit compile-unit base. - yaml.Ranges.push_back(llvm::DWARFYAML::Range{BinaryLocation(-1), 0, 0}); - for (auto [start, end] : info.ranges) { + yaml.Ranges.push_back(llvm::DWARFYAML::Range{DwarfTombstone, 0, 0}); + for (auto [start, end] : info.ranges.get()) { yaml.Ranges.push_back(llvm::DWARFYAML::Range{start, end, 0}); } yaml.Ranges.push_back(llvm::DWARFYAML::Range{0, 0, 0}); } -static void markUnavailable(std::vector& infos, size_t root) { - auto depth = infos[root].depth; - for (size_t i = root; - i < infos.size() && (i == root || infos[i].depth > depth); - ++i) { - infos[i].unavailable = true; +static void markUnavailable(std::vector& infos, + const std::vector>& children, + size_t root) { + std::vector pending{root}; + // Follow the explicit tree rather than scanning by depth. Null DIE + // terminators do not have abbreviation data and would stop a depth scan + // before all descendants were reached. + while (!pending.empty()) { + auto index = pending.back(); + pending.pop_back(); + infos[index].unavailable = true; + pending.insert( + pending.end(), children[index].begin(), children[index].end()); } } @@ -1200,12 +1144,11 @@ writeUnavailableDIE(DIEAddressInfo& info, [&](const llvm::DWARFAbbreviationDeclaration::AttributeSpec& attrSpec, llvm::DWARFYAML::FormValue& yamlValue) { if (attrSpec.Attr == llvm::dwarf::DW_AT_low_pc) { - yamlValue.Value = BinaryLocation(-1); + yamlValue.Value = DwarfTombstone; } else if (attrSpec.Attr == llvm::dwarf::DW_AT_high_pc) { yamlValue.Value = - attrSpec.Form == llvm::dwarf::DW_FORM_data4 ? 0 : BinaryLocation(-1); + attrSpec.Form == llvm::dwarf::DW_FORM_data4 ? 0 : DwarfTombstone; } else if (attrSpec.Attr == llvm::dwarf::DW_AT_ranges) { - constexpr size_t RangeEntrySize = 2 * AddressSize; if (!emptyRangeListOffset) { emptyRangeListOffset = yaml.Ranges.size() * RangeEntrySize; yaml.Ranges.push_back(llvm::DWARFYAML::Range{0, 0, 0}); @@ -1263,78 +1206,77 @@ static void repairDIEAddressRanges(const BinaryenDWARFInfo& dwarfInfo, } assert(yamlEntry == yamlUnit.Entries.end()); + std::vector> children(infos.size()); + for (size_t i = 0; i < infos.size(); ++i) { + if (infos[i].abbrevDecl && infos[i].parent != NoParent) { + children[infos[i].parent].push_back(i); + } + } + + // Compute this once bottom-up instead of rescanning every subtree. + std::vector hasRangedDescendant(infos.size()); + for (size_t i = infos.size(); i-- > 0;) { + if (infos[i].parent != NoParent && + (!infos[i].ranges.empty() || hasRangedDescendant[i])) { + hasRangedDescendant[infos[i].parent] = true; + } + } for (size_t i = 0; i < infos.size(); ++i) { if (!infos[i].abbrevDecl) { continue; } - bool hasRangedDescendant = false; - for (size_t child = i + 1; - child < infos.size() && infos[child].depth > infos[i].depth; - ++child) { - if (!infos[child].ranges.empty()) { - hasRangedDescendant = true; - break; - } - } if (infos[i].malformed || (!infos[i].rangesValue && infos[i].hasRangeDescription && - infos[i].ranges.empty() && hasRangedDescendant)) { - markUnavailable(infos, i); + infos[i].ranges.empty() && hasRangedDescendant[i])) { + markUnavailable(infos, children, i); } } - std::vector> children(infos.size()); - for (size_t i = 0; i < infos.size(); ++i) { - if (infos[i].abbrevDecl && !infos[i].unavailable && - infos[i].parent != size_t(-1) && !infos[i].ranges.empty()) { - children[infos[i].parent].push_back(i); + // Process each sibling group after its children have reached their final + // ranges. This ensures that extending a range-list child cannot create an + // overlap that was checked only against its old endpoints. + for (size_t parentIndex = infos.size(); parentIndex-- > 0;) { + auto& parent = infos[parentIndex]; + if (!parent.abbrevDecl || parent.unavailable) { + continue; } - } - for (auto& siblings : children) { + auto& siblings = children[parentIndex]; for (size_t i = 0; i < siblings.size(); ++i) { for (size_t j = i + 1; j < siblings.size(); ++j) { if (!infos[siblings[i]].unavailable && !infos[siblings[j]].unavailable && - rangesOverlap(infos[siblings[i]], infos[siblings[j]])) { - markUnavailable(infos, siblings[i]); - markUnavailable(infos, siblings[j]); + !infos[siblings[i]].ranges.empty() && + !infos[siblings[j]].ranges.empty() && + infos[siblings[i]].ranges.overlaps(infos[siblings[j]].ranges)) { + markUnavailable(infos, children, siblings[i]); + markUnavailable(infos, children, siblings[j]); } } } - } - // A range-list parent can represent the exact union of its surviving - // children's mapped ranges. Rebuild that union bottom-up rather than - // discarding a valid child merely because optimization moved it outside - // the old parent endpoints. - for (size_t i = infos.size(); i-- > 0;) { - auto& child = infos[i]; - if (!child.abbrevDecl || child.unavailable || child.ranges.empty() || - child.parent == size_t(-1)) { - continue; - } - auto& parent = infos[child.parent]; - if (!parent.unavailable && parent.rangesValue) { - addRanges(parent, child); + // A range-list parent can represent the union of its existing ranges + // and its surviving children. Extend it before it is checked as a + // child at the next level of the tree. + if (parent.rangesValue) { + for (auto childIndex : siblings) { + auto& child = infos[childIndex]; + if (!child.unavailable) { + parent.rangeListDirty |= parent.ranges.add(child.ranges); + } + } } - } - for (size_t i = 0; i < infos.size(); ++i) { - auto& child = infos[i]; - if (!child.abbrevDecl || child.unavailable || - child.parent == size_t(-1)) { - continue; - } - auto& parent = infos[child.parent]; - if (parent.unavailable) { - markUnavailable(infos, i); - continue; - } - bool subprogramPair = child.tag == llvm::dwarf::DW_TAG_subprogram && - parent.tag == llvm::dwarf::DW_TAG_subprogram; - if (!subprogramPair && !child.ranges.empty() && - !parent.ranges.empty() && !containsRanges(parent, child)) { - markUnavailable(infos, i); + for (auto childIndex : siblings) { + auto& child = infos[childIndex]; + if (child.unavailable) { + continue; + } + bool subprogramPair = child.tag == llvm::dwarf::DW_TAG_subprogram && + parent.tag == llvm::dwarf::DW_TAG_subprogram; + if (!subprogramPair && !child.ranges.empty() && + !parent.ranges.empty() && !parent.ranges.contains(child.ranges)) { + markUnavailable(infos, children, childIndex); + } } } @@ -1349,12 +1291,8 @@ static void repairDIEAddressRanges(const BinaryenDWARFInfo& dwarfInfo, }); } -// A location that is ignoreable, i.e., not a special value like 0 or -1 (which -// would indicate an end or a base in .debug_loc). -static const BinaryLocation IGNOREABLE_LOCATION = 1; - static bool isNewBaseLoc(const llvm::DWARFYAML::Loc& loc) { - return loc.Start == BinaryLocation(-1); + return loc.Start == DwarfTombstone; } static bool isEndMarkerLoc(const llvm::DWARFYAML::Loc& loc) { @@ -1404,7 +1342,7 @@ static void updateLoc(llvm::DWARFYAML::Data& yaml, locationUpdater.getNewStart(futureLoc.Start + oldBase); // If we found a valid mapping, this is a relevant value for us. If the // optimizer removed it, it's a 0, and we can ignore it here - we will - // emit IGNOREABLE_LOCATION for it later anyhow. + // emit EmptyRangeAddress for it later anyhow. if (updatedStart != 0) { smallest = std::min(smallest, updatedStart); } @@ -1412,7 +1350,7 @@ static void updateLoc(llvm::DWARFYAML::Data& yaml, // If we found no valid values that will be relativized here, just use 0 // as the new (never-to-be-used) base, which is less confusing (otherwise // the value looks like it means something). - if (smallest == BinaryLocation(-1)) { + if (smallest == DwarfTombstone) { smallest = 0; } newBase = newEnd = smallest; @@ -1428,7 +1366,7 @@ static void updateLoc(llvm::DWARFYAML::Data& yaml, if (newStart == 0 || newEnd == 0 || newStart > newEnd) { // This part of the loc no longer has a mapping, or after the mapping // it is no longer a proper span, so we must ignore it. - newStart = newEnd = IGNOREABLE_LOCATION; + newStart = newEnd = EmptyRangeAddress; } else { // We picked a new base that ensures it is smaller than the values we // will relativize to it. @@ -1443,7 +1381,7 @@ static void updateLoc(llvm::DWARFYAML::Data& yaml, // This can happen if the very first span in a compile unit is an // empty span, in which case relative to the base of the compile unit // we would have (0, 0). - newStart = newEnd = IGNOREABLE_LOCATION; + newStart = newEnd = EmptyRangeAddress; } } // The loc start and end markers have been preserved. However, TODO diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 18c9464dadf..50167a10ac0 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -14,6 +14,7 @@ set(unittest_SOURCES delta_debugging.cpp dfa_minimization.cpp disjoint_sets.cpp + dwarf-ranges.cpp graph.cpp int128.cpp leaves.cpp diff --git a/test/gtest/dwarf-ranges.cpp b/test/gtest/dwarf-ranges.cpp new file mode 100644 index 00000000000..0451b13a082 --- /dev/null +++ b/test/gtest/dwarf-ranges.cpp @@ -0,0 +1,40 @@ +#include "wasm/dwarf-ranges.h" +#include "gtest/gtest.h" + +using namespace wasm::Debug; + +TEST(DwarfRangesTest, Normalize) { + DwarfRanges ranges{{8, 10}, {2, 4}, {4, 6}, {3, 5}}; + + EXPECT_TRUE(ranges.normalize()); + EXPECT_EQ(ranges.get(), (std::vector{{2, 6}, {8, 10}})); + EXPECT_FALSE(ranges.normalize()); +} + +TEST(DwarfRangesTest, Add) { + DwarfRanges ranges{{2, 4}, {8, 10}}; + DwarfRanges added{{4, 8}, {12, 14}}; + + EXPECT_TRUE(ranges.add(added)); + EXPECT_EQ(ranges.get(), (std::vector{{2, 10}, {12, 14}})); + EXPECT_FALSE(ranges.add({{4, 8}})); + EXPECT_FALSE(ranges.add({})); +} + +TEST(DwarfRangesTest, Contains) { + DwarfRanges ranges{{0, 4}, {8, 12}}; + + EXPECT_TRUE(ranges.contains({{1, 3}, {9, 12}})); + EXPECT_TRUE(ranges.contains({})); + EXPECT_FALSE(ranges.contains({{3, 9}})); + EXPECT_FALSE(ranges.contains({{12, 13}})); +} + +TEST(DwarfRangesTest, Overlaps) { + DwarfRanges ranges{{0, 4}, {8, 12}}; + + EXPECT_TRUE(ranges.overlaps({{3, 5}})); + EXPECT_TRUE(ranges.overlaps({{10, 14}})); + EXPECT_FALSE(ranges.overlaps({{4, 8}})); + EXPECT_FALSE(ranges.overlaps({{12, 14}})); +} diff --git a/test/unit/test_dwarf.py b/test/unit/test_dwarf.py index 69367650000..b750bf4e1b5 100644 --- a/test/unit/test_dwarf.py +++ b/test/unit/test_dwarf.py @@ -1,5 +1,6 @@ import os import subprocess +import tempfile from scripts.test import shared @@ -7,6 +8,38 @@ class DWARFTest(utils.BinaryenTestCase): + def test_tombstone_roundtrip(self): + def custom_section(name, contents): + name = name.encode() + payload = bytes([len(name)]) + name + contents + self.assertLess(len(payload), 128) + return bytes([0, len(payload)]) + payload + + # A minimal DWARF v4 unit whose compile unit and subprogram both use + # the all-ones dead-address sentinel for DW_AT_low_pc. + sections = { + '.debug_abbrev': '011101030e110112060000022e0011011206030e000000', + '.debug_info': ('22000000040000000000040100000000ffffffff' + '0300000002ffffffff030000000f00000000'), + '.debug_str': '746573742d636c616e672e63707000666f6f00', + } + wasm = bytes.fromhex('0061736d01000000') + for name, contents in sections.items(): + wasm += custom_section(name, bytes.fromhex(contents)) + + with tempfile.TemporaryDirectory() as temp_dir: + input_file = os.path.join(temp_dir, 'input.wasm') + output_file = os.path.join(temp_dir, 'output.wasm') + with open(input_file, 'wb') as f: + f.write(wasm) + shared.run_process(shared.WASM_OPT + + [input_file, '--roundtrip', '-g', + '-o', output_file]) + dump = shared.run_process(shared.WASM_OPT + + [output_file, '--dwarfdump'], + capture_output=True).stdout + self.assertEqual(dump.count('0x00000000ffffffff'), 2) + def test_no_crash(self): # run dwarf processing on some interesting large files, too big to be # worth putting in passes where the text output would be massive. We From eb002d1b3f71bc241d9dd6623830aa2d17e69517 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 9 Aug 2026 20:05:20 +0800 Subject: [PATCH 3/3] wasm: narrow DWARF range constant cleanup --- src/wasm/wasm-debug.cpp | 42 ++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/wasm/wasm-debug.cpp b/src/wasm/wasm-debug.cpp index bbd1342bfdc..c58e17b5fa1 100644 --- a/src/wasm/wasm-debug.cpp +++ b/src/wasm/wasm-debug.cpp @@ -61,15 +61,15 @@ bool hasDWARFSections(const Module& wasm) { #ifdef BUILD_LLVM_DWARF // In wasm32 the address size is 32 bits. -static constexpr size_t AddressSize = 4; +static const size_t AddressSize = 4; static constexpr size_t RangeEntrySize = 2 * AddressSize; // DWARF v6 reserves the all-ones address for a non-existent entity. LLVM also // recognizes max-minus-one in legacy range and location data, where all-ones // is already the base-address-selection marker. Zero is a historical linker // tombstone, but is context-dependent because it can also be a valid address. -static constexpr BinaryLocation DwarfTombstone = BinaryLocation(-1); -static constexpr BinaryLocation LegacyRangeTombstone = BinaryLocation(-2); +static constexpr BinaryLocation AllOnesAddress = BinaryLocation(-1); +static constexpr BinaryLocation LegacyTombstoneAddress = BinaryLocation(-2); static constexpr BinaryLocation EmptyRangeAddress = 1; static constexpr size_t NoParent = size_t(-1); @@ -682,7 +682,7 @@ struct LocationUpdater { }; static bool isNonzeroTombstone(BinaryLocation location) { - return location == DwarfTombstone || location == LegacyRangeTombstone; + return location == AllOnesAddress || location == LegacyTombstoneAddress; } // Keep accepting the historical zero tombstone. Callers whose encoding permits @@ -916,8 +916,8 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, if (!isNonzeroTombstone(newLowPC) && tag != llvm::dwarf::DW_TAG_compile_unit && (newValue == 0 || newValue < newLowPC)) { - newLowPC = DwarfTombstone; - newValue = isRelative ? newLowPC : DwarfTombstone; + newLowPC = AllOnesAddress; + newValue = isRelative ? newLowPC : AllOnesAddress; assert(lowPCValue); lowPCValue->Value = newLowPC; } else if (newValue > newLowPC) { @@ -932,7 +932,7 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, }); if (unresolvedZeroLowPC) { assert(lowPCValue); - lowPCValue->Value = DwarfTombstone; + lowPCValue->Value = AllOnesAddress; } } @@ -982,10 +982,10 @@ static void updateRanges(llvm::DWARFYAML::Data& yaml, for (auto& range : yaml.Ranges) { BinaryLocation oldStart = range.Start, oldEnd = range.End, newStart = 0, newEnd = 0; - if ((oldStart == 0 && oldEnd == 0) || oldStart == DwarfTombstone) { + if ((oldStart == 0 && oldEnd == 0) || oldStart == AllOnesAddress) { newStart = oldStart; newEnd = oldEnd; - } else if (oldStart == LegacyRangeTombstone || isTombstone(oldEnd)) { + } else if (oldStart == LegacyTombstoneAddress || isTombstone(oldEnd)) { newStart = EmptyRangeAddress; newEnd = EmptyRangeAddress; } else { @@ -1067,13 +1067,13 @@ static void readDIEAddressRanges(DIEAddressInfo& info, terminated = true; break; } - if (start == DwarfTombstone) { + if (start == AllOnesAddress) { base = end; continue; } // A zero start is a valid offset from the current base. Only (0, 0), // handled above, terminates the list. - if (start == LegacyRangeTombstone || isTombstone(end)) { + if (start == LegacyTombstoneAddress || isTombstone(end)) { continue; } auto absoluteStart = base + start; @@ -1111,7 +1111,7 @@ static void writeRangeList(DIEAddressInfo& info, llvm::DWARFYAML::Data& yaml) { info.rangesValue->Value = yaml.Ranges.size() * RangeEntrySize; // Use an explicit zero base so the new entries remain absolute and can be // updated again without recovering an implicit compile-unit base. - yaml.Ranges.push_back(llvm::DWARFYAML::Range{DwarfTombstone, 0, 0}); + yaml.Ranges.push_back(llvm::DWARFYAML::Range{AllOnesAddress, 0, 0}); for (auto [start, end] : info.ranges.get()) { yaml.Ranges.push_back(llvm::DWARFYAML::Range{start, end, 0}); } @@ -1144,10 +1144,10 @@ writeUnavailableDIE(DIEAddressInfo& info, [&](const llvm::DWARFAbbreviationDeclaration::AttributeSpec& attrSpec, llvm::DWARFYAML::FormValue& yamlValue) { if (attrSpec.Attr == llvm::dwarf::DW_AT_low_pc) { - yamlValue.Value = DwarfTombstone; + yamlValue.Value = AllOnesAddress; } else if (attrSpec.Attr == llvm::dwarf::DW_AT_high_pc) { yamlValue.Value = - attrSpec.Form == llvm::dwarf::DW_FORM_data4 ? 0 : DwarfTombstone; + attrSpec.Form == llvm::dwarf::DW_FORM_data4 ? 0 : AllOnesAddress; } else if (attrSpec.Attr == llvm::dwarf::DW_AT_ranges) { if (!emptyRangeListOffset) { emptyRangeListOffset = yaml.Ranges.size() * RangeEntrySize; @@ -1291,8 +1291,12 @@ static void repairDIEAddressRanges(const BinaryenDWARFInfo& dwarfInfo, }); } +// A location that is ignoreable, i.e., not a special value like 0 or -1 (which +// would indicate an end or a base in .debug_loc). +static const BinaryLocation IGNOREABLE_LOCATION = 1; + static bool isNewBaseLoc(const llvm::DWARFYAML::Loc& loc) { - return loc.Start == DwarfTombstone; + return loc.Start == BinaryLocation(-1); } static bool isEndMarkerLoc(const llvm::DWARFYAML::Loc& loc) { @@ -1342,7 +1346,7 @@ static void updateLoc(llvm::DWARFYAML::Data& yaml, locationUpdater.getNewStart(futureLoc.Start + oldBase); // If we found a valid mapping, this is a relevant value for us. If the // optimizer removed it, it's a 0, and we can ignore it here - we will - // emit EmptyRangeAddress for it later anyhow. + // emit IGNOREABLE_LOCATION for it later anyhow. if (updatedStart != 0) { smallest = std::min(smallest, updatedStart); } @@ -1350,7 +1354,7 @@ static void updateLoc(llvm::DWARFYAML::Data& yaml, // If we found no valid values that will be relativized here, just use 0 // as the new (never-to-be-used) base, which is less confusing (otherwise // the value looks like it means something). - if (smallest == DwarfTombstone) { + if (smallest == BinaryLocation(-1)) { smallest = 0; } newBase = newEnd = smallest; @@ -1366,7 +1370,7 @@ static void updateLoc(llvm::DWARFYAML::Data& yaml, if (newStart == 0 || newEnd == 0 || newStart > newEnd) { // This part of the loc no longer has a mapping, or after the mapping // it is no longer a proper span, so we must ignore it. - newStart = newEnd = EmptyRangeAddress; + newStart = newEnd = IGNOREABLE_LOCATION; } else { // We picked a new base that ensures it is smaller than the values we // will relativize to it. @@ -1381,7 +1385,7 @@ static void updateLoc(llvm::DWARFYAML::Data& yaml, // This can happen if the very first span in a compile unit is an // empty span, in which case relative to the base of the compile unit // we would have (0, 0). - newStart = newEnd = EmptyRangeAddress; + newStart = newEnd = IGNOREABLE_LOCATION; } } // The loc start and end markers have been preserved. However, TODO