| import os |
| import subprocess |
| import tempfile |
| |
| from scripts.test import shared |
| |
| from . import utils |
| |
| |
| 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:', 1)[1] |
| self.assertIn(f'{empty_offset} <End of list>', 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:', 1)[1] |
| self.assertRegex( |
| '\n'.join(ranges.splitlines()), |
| r'(?m)^00000000 00000000 (?!00000000)[0-9a-f]{8}$') |
| |
| 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_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_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. |
| input_file = self.input_path('dwarf/memory64_ranges.wasm') |
| with tempfile.TemporaryDirectory() as temp_dir: |
| output_file = os.path.join(temp_dir, 'output.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) |
| |
| def test_dwarf_incompatibility(self): |
| warning = 'not fully compatible with DWARF' |
| path = self.input_path(os.path.join('dwarf', 'cubescript.wasm')) |
| args = [path, '-g'] |
| # flatten warns |
| err = shared.run_process(shared.WASM_OPT + args + ['--flatten'], stderr=subprocess.PIPE).stderr |
| self.assertIn(warning, err) |
| # safe passes do not |
| err = shared.run_process(shared.WASM_OPT + args + ['--metrics'], stderr=subprocess.PIPE).stderr |
| self.assertNotIn(warning, err) |
| |
| def test_strip_dwarf_and_opts(self): |
| # some optimizations are disabled when DWARF is present (as they would |
| # destroy it). we scan the wasm to see if there is any DWARF when |
| # making the decision whether to run them. this test checks that we also |
| # check if --strip* is being run, which would remove the DWARF anyhow |
| path = self.input_path(os.path.join('dwarf', 'cubescript.wasm')) |
| # strip the DWARF, then run all the opts to check as much as possible |
| args = [path, '--strip-dwarf', '-Oz'] |
| # run it normally, without -g. in this case no DWARF will be preserved |
| # in a trivial way |
| shared.run_process(shared.WASM_OPT + args + ['-o', 'a.wasm']) |
| # run it with -g. in this case we need to be clever as described above, |
| # and see --strip-dwarf removes the need for DWARF |
| shared.run_process(shared.WASM_OPT + args + ['-o', 'b.wasm', '-g']) |
| # run again on the last output without -g, as we don't want the names |
| # section to skew the results |
| shared.run_process(shared.WASM_OPT + ['b.wasm', '-o', 'c.wasm']) |
| # compare the sizes. there might be a tiny difference in size to to |
| # minor roundtrip changes, so ignore up to a tiny % |
| a_size = os.path.getsize('a.wasm') |
| c_size = os.path.getsize('c.wasm') |
| self.assertLess((100 * abs(a_size - c_size)) / c_size, 1) |