| 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) |