From 65f2810494a1fef3c3203e8e545359eb3b37479e Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 20:09:10 +0800 Subject: [PATCH 1/7] 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 | 19 +- 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, 443 insertions(+), 62 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 b6fd3128e21..6765dde14ff 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 (2300 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] (0x00000000000003b8) - DW_AT_high_pc [DW_FORM_data4] (0xfffffc48) + 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] (0x000000000000052a) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) 0x000002e3: DW_TAG_lexical_block [14] * - DW_AT_ranges [DW_FORM_sec_offset] (0x00000028 - [0x000004af, 0x000004f4) - [0x00000000, 0x00000001)) + DW_AT_ranges [DW_FORM_sec_offset] (0x00000058) 0x000002e8: DW_TAG_variable [26] DW_AT_location [DW_FORM_sec_offset] (0x000003bc: @@ -2963,7 +2961,7 @@ Abbrev table for offset: 0x00000000 DW_AT_low_pc [DW_FORM_addr] (0x0000000000000472) 0x00000316: DW_TAG_GNU_call_site [15] - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) + DW_AT_low_pc [DW_FORM_addr] (0x00000000ffffffff) 0x0000031b: DW_TAG_GNU_call_site [16] DW_AT_abstract_origin [DW_FORM_ref4] (cu + 0x019a => {0x0000019a} "free") @@ -4458,11 +4456,12 @@ file_names[ 4]: 00000000 0000035a 00000363 00000000 00000028 000004af 000004f4 -00000028 00000000 00000001 +00000028 00000001 00000001 00000028 00000040 00000006 00000377 00000040 00000379 00000588 00000040 +00000058 (module (type $0 (func (param i32) (result i32))) (type $1 (func (param i32 i32) (result i32))) @@ -6338,7 +6337,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 2300 ;; 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 625b443d51847d941955079cac98ea072057a6df Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 9 Aug 2026 19:53:28 +0800 Subject: [PATCH 2/7] 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 b0a73d21d1e..5040aa82879 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -15,6 +15,7 @@ set(unittest_SOURCES delta_debugging.cpp dfa_minimization.cpp disjoint_sets.cpp + dwarf-ranges.cpp effects.cpp graph.cpp int128.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 a16642fb72dd5b4c05ad1181283db96d14b559a7 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 9 Aug 2026 20:05:20 +0800 Subject: [PATCH 3/7] 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 From e3947897037a950ce4af5308a1a56f1aff15f35e Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 25 Sep 2026 09:55:41 +0800 Subject: [PATCH 4/7] wasm: address DWARF range review and lost starts --- src/wasm/dwarf-ranges.h | 7 ++-- src/wasm/wasm-debug.cpp | 49 ++++++++++++++---------- test/passes/epilogue_begin_dwarf.bin.txt | 4 +- test/unit/test_dwarf.py | 28 ++++++++++++++ 4 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/wasm/dwarf-ranges.h b/src/wasm/dwarf-ranges.h index 6182c701693..6088602bd8f 100644 --- a/src/wasm/dwarf-ranges.h +++ b/src/wasm/dwarf-ranges.h @@ -29,9 +29,10 @@ 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. +// A set of nonempty, half-open DWARF address ranges. Normalization sorts the +// ranges and merges overlapping or adjacent intervals, leaving disjoint ranges +// in address order. Call normalize before querying containment or overlap; +// adding one set to another normalizes the result automatically. class DwarfRanges { std::vector ranges; diff --git a/src/wasm/wasm-debug.cpp b/src/wasm/wasm-debug.cpp index c58e17b5fa1..ac3088f6517 100644 --- a/src/wasm/wasm-debug.cpp +++ b/src/wasm/wasm-debug.cpp @@ -60,7 +60,8 @@ bool hasDWARFSections(const Module& wasm) { #ifdef BUILD_LLVM_DWARF -// In wasm32 the address size is 32 bits. +// DWARF address fields in the wasm32 input are 4 bytes wide. A legacy +// .debug_ranges entry stores a start and an end address of that width. static const size_t AddressSize = 4; static constexpr size_t RangeEntrySize = 2 * AddressSize; @@ -69,8 +70,11 @@ static constexpr size_t RangeEntrySize = 2 * AddressSize; // 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 AllOnesAddress = BinaryLocation(-1); +// LLVM's legacy dead-address encoding where all-ones selects a range base. static constexpr BinaryLocation LegacyTombstoneAddress = BinaryLocation(-2); +// A nonzero empty range avoids the (0, 0) range-list terminator. static constexpr BinaryLocation EmptyRangeAddress = 1; +// Root DIEs have no parent in the per-compilation-unit index. static constexpr size_t NoParent = size_t(-1); struct BinaryenDWARFInfo { @@ -832,7 +836,6 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, // 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, @@ -864,8 +867,12 @@ 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 && !isNonzeroTombstone(oldValue); + // The location mapper returns zero when a function or expression no + // longer has a mapped start. Unlike a CU base, that is not a usable + // start for a non-CU DIE even if its end still has a mapping. + if (tag != llvm::dwarf::DW_TAG_compile_unit && newValue == 0) { + newValue = AllOnesAddress; + } oldLowPC = oldValue; newLowPC = newValue; yamlValue.Value = newValue; @@ -920,20 +927,12 @@ static void updateDIE(const llvm::DWARFDebugInfoEntry& DIE, newValue = isRelative ? newLowPC : AllOnesAddress; 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 = AllOnesAddress; - } } static void updateCompileUnits(const BinaryenDWARFInfo& info, @@ -1078,7 +1077,11 @@ static void readDIEAddressRanges(DIEAddressInfo& info, } auto absoluteStart = base + start; auto absoluteEnd = base + end; - if (absoluteStart > absoluteEnd) { + // Keep the addition wide so an offset that escapes wasm32's address + // space cannot wrap back into an apparently valid range. + if (absoluteStart > std::numeric_limits::max() || + absoluteEnd > std::numeric_limits::max() || + absoluteStart > absoluteEnd) { info.malformed = true; } else if (absoluteStart < absoluteEnd) { info.ranges.add(absoluteStart, absoluteEnd); @@ -1095,7 +1098,10 @@ static void readDIEAddressRanges(DIEAddressInfo& info, if (!isNonzeroTombstone(*lowPC)) { uint64_t start = *lowPC; uint64_t end = highPCIsRelative ? start + *highPC : *highPC; - if (start > end) { + // A relative high_pc can overflow the 32-bit wasm32 address space; + // doing the addition in uint64_t makes that detectable rather than + // wrapping to a plausible lower address. + if (end > std::numeric_limits::max() || start > end) { info.malformed = true; } else if (start < end) { info.ranges.add(start, end); @@ -1192,13 +1198,13 @@ static void repairDIEAddressRanges(const BinaryenDWARFInfo& dwarfInfo, 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); + // In preorder, a valid DIE cannot descend more than one level at + // once. Keep malformed depths unavailable rather than indexing past + // the ancestor stack in non-asserting builds. + if (ancestors.size() < info.depth) { + info.malformed = true; } + ancestors.push_back(index); readDIEAddressRanges(info, yaml, compileUnitBase); } ++yamlEntry; @@ -1248,6 +1254,9 @@ static void repairDIEAddressRanges(const BinaryenDWARFInfo& dwarfInfo, !infos[siblings[i]].ranges.empty() && !infos[siblings[j]].ranges.empty() && infos[siblings[i]].ranges.overlaps(infos[siblings[j]].ranges)) { + // Remove conflicting scopes immediately. Later pairs need only + // compare surviving siblings; a third scope may remain usable + // if it overlapped only one that has already been removed. markUnavailable(infos, children, siblings[i]); markUnavailable(infos, children, siblings[j]); } diff --git a/test/passes/epilogue_begin_dwarf.bin.txt b/test/passes/epilogue_begin_dwarf.bin.txt index 624398fd3db..464dee9166e 100644 --- a/test/passes/epilogue_begin_dwarf.bin.txt +++ b/test/passes/epilogue_begin_dwarf.bin.txt @@ -142,8 +142,8 @@ Abbrev table for offset: 0x00000000 DW_AT_high_pc [DW_FORM_data4] (0x00000007) 0x00000026: DW_TAG_subprogram [2] - DW_AT_low_pc [DW_FORM_addr] (0x0000000000000000) - DW_AT_high_pc [DW_FORM_data4] (0x00000007) + 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") DW_AT_decl_file [DW_FORM_data1] ("/usr/local/google/home/azakai/Dev/emscripten/a.cpp") diff --git a/test/unit/test_dwarf.py b/test/unit/test_dwarf.py index b750bf4e1b5..4c53a91487d 100644 --- a/test/unit/test_dwarf.py +++ b/test/unit/test_dwarf.py @@ -40,6 +40,34 @@ def custom_section(name, contents): capture_output=True).stdout self.assertEqual(dump.count('0x00000000ffffffff'), 2) + def test_missing_start_with_mapped_end(self): + # The unused subprogram starts at zero (no location mapping). Point its + # relative high_pc at the end of the preceding live subprogram. The + # surviving end must not make the missing start look like a live scope. + path = os.path.join(shared.options.binaryen_test, 'passes', + 'ignore_missing_func_dwarf.wasm') + with open(path, 'rb') as f: + wasm = f.read() + old_pair = bytes.fromhex('000000005a000000') + self.assertEqual(wasm.count(old_pair), 1) + wasm = wasm.replace(old_pair, bytes.fromhex('000000005f000000')) + + 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 + unused = next(part for part in dump.split('DW_TAG_subprogram') + if '"unused"' in part) + self.assertIn('DW_AT_low_pc [DW_FORM_addr]\t' + '(0x00000000ffffffff)', unused) + 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 34ba0d626e13bff32cfdd652c5fe1d4118d1d7e4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 25 Sep 2026 10:12:29 +0800 Subject: [PATCH 5/7] wasm: avoid unsupported Memory64 range-list repair --- src/wasm/wasm-debug.cpp | 37 +++++++++++++++++-- .../input/dwarf/memory64_ranges.wasm.gz.b64 | 12 ++++++ test/unit/test_dwarf.py | 26 +++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 test/unit/input/dwarf/memory64_ranges.wasm.gz.b64 diff --git a/src/wasm/wasm-debug.cpp b/src/wasm/wasm-debug.cpp index ac3088f6517..6f0523b0d1c 100644 --- a/src/wasm/wasm-debug.cpp +++ b/src/wasm/wasm-debug.cpp @@ -974,13 +974,30 @@ static void updateCompileUnits(const BinaryenDWARFInfo& info, } static void updateRanges(llvm::DWARFYAML::Data& yaml, - const LocationUpdater& locationUpdater) { + const LocationUpdater& locationUpdater, + bool canRepairRanges) { // 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 (!canRepairRanges) { + // Preserve the previous updater's behavior for non-wasm32 CUs. The + // vendored emitter cannot write address-sized range entries for them. + if (isTombstone(oldStart) || isTombstone(oldEnd)) { + continue; + } + newStart = locationUpdater.getNewStart(oldStart); + newEnd = locationUpdater.getNewEnd(oldEnd); + if (isTombstone(newStart) || isTombstone(newEnd)) { + newStart = 0; + newEnd = 1; + } + range.Start = newStart; + range.End = newEnd; + continue; + } if ((oldStart == 0 && oldEnd == 0) || oldStart == AllOnesAddress) { newStart = oldStart; newEnd = oldEnd; @@ -1424,9 +1441,21 @@ void writeDWARFSections(Module& wasm, const BinaryLocations& newLocations) { bool is64 = wasm.memories.size() > 0 ? wasm.memories[0]->is64() : false; updateCompileUnits(info, data, locationUpdater, is64); - updateRanges(data, locationUpdater); - - repairDIEAddressRanges(info, data, locationUpdater); + // The vendored DWARFYAML emitter still writes .debug_ranges entries as + // pairs of 4-byte addresses. Rebuilding lists for 8-byte or mixed-width + // CUs would write invalid DW_AT_ranges offsets. Keep the earlier updater + // behavior until that emitter supports address-sized entries. + bool canRepairRanges = true; + for (const auto& CU : data.CompileUnits) { + if (CU.AddrSize != AddressSize) { + canRepairRanges = false; + break; + } + } + updateRanges(data, locationUpdater, canRepairRanges); + if (canRepairRanges) { + repairDIEAddressRanges(info, data, locationUpdater); + } updateLoc(data, locationUpdater); diff --git a/test/unit/input/dwarf/memory64_ranges.wasm.gz.b64 b/test/unit/input/dwarf/memory64_ranges.wasm.gz.b64 new file mode 100644 index 00000000000..c2ed56d433a --- /dev/null +++ b/test/unit/input/dwarf/memory64_ranges.wasm.gz.b64 @@ -0,0 +1,12 @@ +H4sIAAAAAAAAA31TzU7bQBCeteOQ2MUpSVugqCIHUA+WHSVCiFKUQnuB3jlVKrWdjbWwXlu7ayAn +HqHH3pprDzxCX4kn6KFd/6VApY7tHc3Mzt83HvBFjAAANdEXdINudF0DMHTUQEtdvRnjOOEzDdBU +3YgAmZtaF/rqOco/7ePG+RE6t8xSZRxLC74i05vgIIvOaBL+rkiFLGil4t2KN+BOgzk8otrr+SN9 +u+JNuAMYbs1r2fxvNMDLVUF+EHB8iVbQtt0zdPvp6oa9gk5XATRPKbvNw7Vv79d1e996a71eP+m9 +WwfQDdDWStVJT2XYAW31r2hsgW6PLcvK0/xEVpWHsGlyW9STUwsdqvMVfF5U+ZC0GpwclKU7Pe98 +Ps4ngn6oU2+oZuf7C7lw1xYiaDVK5j33g9yuPXIv5MZudb0UwchlQwUd1yhxn0VY1EU9q3idZfMf +eCu6qMcuJJ/BNQxOBeZiQMk5wQORZDzEgyhJaSYGAWE+n2Hmpnzvze6Oq4ZC8BUQJoFBBFM4TmKs +RnXVD6kqpn+pApGE9Ucjb+jtgauS1VBTwvCnApXNAhL0y17OWfFH5++BkBPCxmWN7QXWyNg2b42X +nrF9YIz6GlI+ylo32VNWaYy8Ur/RYH6MwTIJSzPpXeULs5SvBFIbMW6nPJlkoSpRa+XVZn6Ekf5h +OIQnyhJiIfDEDWbIfthUs2rmO+pIn0dYnk2xLzOORduxgoxeuOXqOZ17gpuk0nkR+pS6hE0Ix6F0 +E4UOTVjktMo7uzuOGWdUkkufZtjpxJn0A4rdiCaBT4XTYwmT3E9TwiJ3mspEwe50OJ5ijlmIXTlL +sXBagkTMxdfyDw+2KgUfBAAA diff --git a/test/unit/test_dwarf.py b/test/unit/test_dwarf.py index 4c53a91487d..4380257f794 100644 --- a/test/unit/test_dwarf.py +++ b/test/unit/test_dwarf.py @@ -1,3 +1,5 @@ +import base64 +import gzip import os import subprocess import tempfile @@ -68,12 +70,36 @@ def test_missing_start_with_mapped_end(self): self.assertIn('DW_AT_low_pc [DW_FORM_addr]\t' '(0x00000000ffffffff)', unused) + def test_memory64_range_list_fallback(self): + # This fixture is a two-function wasm64 module with a DWARF v4 CU and + # .debug_ranges, compiled using clang --target=wasm64-unknown-unknown + # -O1 -g. The vendored emitter still writes 4-byte range entries, so + # range-list repair must not append an invalid 8-byte-CU offset. + path = self.input_path('dwarf/memory64_ranges.wasm.gz.b64') + with open(path, 'rb') as f: + wasm = gzip.decompress(base64.b64decode(f.read())) + 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.assertIn('DW_AT_ranges [DW_FORM_sec_offset]\t(0x00000000', + dump) + 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 # just check that no assertion are hit. path = self.input_path('dwarf') for name in os.listdir(path): + if not name.endswith('.wasm'): + continue args = [os.path.join(path, name)] + \ ['-g', '--dwarfdump', '--roundtrip', '--dwarfdump'] shared.run_process(shared.WASM_OPT + args, capture_output=True) From 2682cdb5266a16e5869c64134a69029862610805 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 29 Sep 2026 09:21:32 +0800 Subject: [PATCH 6/7] test: store Memory64 DWARF fixture as wasm --- test/unit/input/dwarf/memory64_ranges.wasm | Bin 0 -> 1055 bytes .../unit/input/dwarf/memory64_ranges.wasm.gz.b64 | 12 ------------ test/unit/test_dwarf.py | 9 +-------- 3 files changed, 1 insertion(+), 20 deletions(-) create mode 100644 test/unit/input/dwarf/memory64_ranges.wasm delete mode 100644 test/unit/input/dwarf/memory64_ranges.wasm.gz.b64 diff --git a/test/unit/input/dwarf/memory64_ranges.wasm b/test/unit/input/dwarf/memory64_ranges.wasm new file mode 100644 index 0000000000000000000000000000000000000000..0e1e6a01f24af63ebd3b84d45644c16d880f7ca4 GIT binary patch literal 1055 zcmZuw%~IPi5MKEwBz8$k(iSMwX$~+wcrDXD24kaio%R5%T`<;vL3GoEWvqA>d|5`K)uWJ3jbp+>toNQn86JU&T=gB0VNj% z4{~0J7!MMj(IOv2goR>;nV*UMmwUUuIuwGhfiMXS5}3jXPq8|x>wZiXnf{e3L^$h@ zM!_)H2Or?FmYNcV-zzQ_XkrMV@9kA>kVg7F%;khIEvFi-rPj#aaqjG=fxUBRkNO5u zIVxH=*;7$zkAl*_Ww9{D;5vk5g$%Csg;~*80;wA@Ze@Z`ida!Jnwo79CK#D-heLRv zA~DW$Jn^F$YX8|(qiW_0>c~(eSe{RDC<}q}mghzVz4Xhg=XHJqKF_50Bn~O{37Zgs zW9f6H4&^NInq|V?uIHo$mE Date: Wed, 30 Sep 2026 09:04:58 +0800 Subject: [PATCH 7/7] test: cover DWARF zero-offset ranges and sibling conflicts --- test/unit/test_dwarf.py | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/unit/test_dwarf.py b/test/unit/test_dwarf.py index 1d5c81c1718..7e0535054be 100644 --- a/test/unit/test_dwarf.py +++ b/test/unit/test_dwarf.py @@ -8,6 +8,73 @@ class DWARFTest(utils.BinaryenTestCase): + def test_overlapping_inline_siblings(self): + # Make the outer lexical block valid and move one inlined call onto + # its sibling's range. Both siblings must become unavailable: keeping + # either one would assign its variables to the other's instructions. + path = os.path.join(shared.options.binaryen_test, 'passes', + 'class_with_dwarf_noprint.wasm') + with open(path, 'rb') as f: + wasm = f.read() + replacements = ( + ('26000000fcffffff', '260000005f000000'), + ('4000000019000000', '6100000009000000'), + ) + for old, new in replacements: + old_bytes = bytes.fromhex(old) + self.assertEqual(wasm.count(old_bytes), 1) + wasm = wasm.replace(old_bytes, bytes.fromhex(new)) + + 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 + first = dump.split('0x0000015e:', 1)[1].split('0x00000179:', 1)[0] + second = dump.split('0x00000179:', 1)[1].split('0x00000189:', 1)[0] + ranges_line = next(line for line in first.splitlines() + if 'DW_AT_ranges' in line) + self.assertTrue(ranges_line.endswith(')')) + empty_offset = ranges_line.rsplit('(0x', 1)[1].split(')', 1)[0] + ranges = dump.split('.debug_ranges contents:\n', 1)[1] + self.assertIn(f'{empty_offset} ', ranges) + self.assertIn('DW_AT_low_pc [DW_FORM_addr]\t' + '(0x00000000ffffffff)', second) + + def test_zero_start_range_offset(self): + # A zero start with a nonzero end is a valid offset from the current + # .debug_ranges base, not a tombstone or end-of-list marker. Replace + # the fixture's unmapped (0, 1) entry with a contiguous mapped range. + path = os.path.join(shared.options.binaryen_test, 'passes', + 'class_with_dwarf_noprint.wasm') + with open(path, 'rb') as f: + wasm = f.read() + old_ranges = bytes.fromhex('00000000010000005b00000064000000') + new_ranges = bytes.fromhex('000000005b0000005b00000064000000') + self.assertEqual(wasm.count(old_ranges), 1) + wasm = wasm.replace(old_ranges, new_ranges) + + 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 + ranges = dump.split('.debug_ranges contents:\n', 1)[1] + self.assertRegex( + ranges, r'(?m)^00000000 00000000 (?!00000000)[0-9a-f]{8}$') + def test_tombstone_roundtrip(self): def custom_section(name, contents): name = name.encode()