|
| 1 | +import glob |
| 2 | +import struct |
1 | 3 | import unittest |
2 | 4 | import string |
3 | 5 | import subprocess |
|
11 | 13 | assert_python_failure, |
12 | 14 | assert_python_ok, |
13 | 15 | ) |
| 16 | +from test.support import import_helper |
14 | 17 | from test.support.os_helper import temp_dir |
| 18 | +from test import test_tools |
15 | 19 |
|
16 | 20 |
|
17 | 21 | if not support.has_subprocess_support: |
@@ -685,5 +689,324 @@ def tearDown(self) -> None: |
685 | 689 | file.unlink() |
686 | 690 |
|
687 | 691 |
|
| 692 | +JITDUMP_MAGIC = 0x4A695444 # "JiTD" |
| 693 | +JITDUMP_VERSION = 1 |
| 694 | +PERF_LOAD = 0 |
| 695 | +PERF_UNWINDING_INFO = 4 |
| 696 | +JITDUMP_ENDIAN = "<" if sys.byteorder == "little" else ">" |
| 697 | +# Every jitdump record starts with event(u32), size(u32), timestamp(u64). |
| 698 | +JITDUMP_RECORD_HEADER_SIZE = 16 |
| 699 | +# CodeLoadEvent: record header, pid(u32), tid(u32), vma(u64), code_addr(u64), |
| 700 | +# code_size(u64), code_id(u64), then the NUL-terminated name. |
| 701 | +CODE_LOAD_CODE_SIZE_OFFSET = JITDUMP_RECORD_HEADER_SIZE + 4 + 4 + 8 + 8 |
| 702 | +CODE_LOAD_NAME_OFFSET = CODE_LOAD_CODE_SIZE_OFFSET + 8 + 8 |
| 703 | +# CodeUnwindingInfoEvent: record header, unwind_data_size(u64), |
| 704 | +# eh_frame_hdr_size(u64), mapped_size(u64), then the .eh_frame bytes followed |
| 705 | +# by perf's 20-byte eh_frame_hdr (EhFrameHeader in perf_jit_trampoline.c), |
| 706 | +# whose "from" field is the signed distance back to the code. |
| 707 | +UNWIND_DATA_SIZE_OFFSET = JITDUMP_RECORD_HEADER_SIZE |
| 708 | +UNWIND_EH_FRAME_OFFSET = JITDUMP_RECORD_HEADER_SIZE + 3 * 8 |
| 709 | +EH_FRAME_HDR_SIZE = 20 |
| 710 | +EH_FRAME_HDR_FROM_OFFSET = 12 |
| 711 | +# DWARF FDE pointer encodings: DW_EH_PE_pcrel | DW_EH_PE_sdata4 (ELF |
| 712 | +# assemblers) and DW_EH_PE_pcrel | DW_EH_PE_absptr (Darwin assemblers). |
| 713 | +DW_EH_PE_PCREL_SDATA4 = 0x1B |
| 714 | +DW_EH_PE_PCREL_ABSPTR = 0x10 |
| 715 | + |
| 716 | + |
| 717 | +def _jitdump_records(data): |
| 718 | + """Yield (event, offset, size) for each record of a jitdump file.""" |
| 719 | + header_size = struct.unpack_from(f"{JITDUMP_ENDIAN}I", data, 8)[0] |
| 720 | + pos = header_size |
| 721 | + while pos < len(data): |
| 722 | + if pos + JITDUMP_RECORD_HEADER_SIZE > len(data): |
| 723 | + raise ValueError(f"truncated record header at offset {pos}") |
| 724 | + event, size = struct.unpack_from(f"{JITDUMP_ENDIAN}II", data, pos) |
| 725 | + if size < JITDUMP_RECORD_HEADER_SIZE or pos + size > len(data): |
| 726 | + raise ValueError(f"record at offset {pos} has a bad size {size}") |
| 727 | + yield event, pos, size |
| 728 | + pos += size |
| 729 | + |
| 730 | + |
| 731 | +def _fde_pointer_encoding(eh_frame): |
| 732 | + """Return the FDE pointer encoding byte of a version 1 "zR" CIE.""" |
| 733 | + cie_length = struct.unpack_from(f"{JITDUMP_ENDIAN}I", eh_frame, 0)[0] |
| 734 | + cie_total = 4 + cie_length |
| 735 | + pos = 12 # past length, CIE_id, version and "zR\0" |
| 736 | + for _ in range(2): # code and data alignment factors (LEB128) |
| 737 | + while eh_frame[pos] & 0x80: |
| 738 | + pos += 1 |
| 739 | + pos += 1 |
| 740 | + pos += 1 # return address column |
| 741 | + while eh_frame[pos] & 0x80: # augmentation data length (LEB128) |
| 742 | + pos += 1 |
| 743 | + pos += 1 |
| 744 | + if pos >= cie_total: |
| 745 | + raise ValueError("truncated CIE augmentation data") |
| 746 | + return eh_frame[pos] |
| 747 | + |
| 748 | + |
| 749 | +class TestJitdumpFileFormat(unittest.TestCase): |
| 750 | + """Validate the jitdump written by -Xperf_jit without requiring perf.""" |
| 751 | + |
| 752 | + def _run_and_get_jitdump(self, code): |
| 753 | + # The child prints its pid so we open exactly its own jitdump file |
| 754 | + # rather than whatever another test worker left in /tmp. |
| 755 | + code = "import os, sys\nsys.stdout.write(str(os.getpid()))\n" + code |
| 756 | + rc, out, err = assert_python_ok("-Xperf_jit", "-c", code, PYTHON_JIT="0") |
| 757 | + path = f"/tmp/jit-{int(out)}.dump" |
| 758 | + if not os.path.exists(path): |
| 759 | + # perf_map_jit_init() gives up silently when it cannot create |
| 760 | + # the file (for example an unwritable /tmp). |
| 761 | + self.skipTest("jitdump file was not created") |
| 762 | + self.addCleanup(os.unlink, path) |
| 763 | + with open(path, "rb") as f: |
| 764 | + data = f.read() |
| 765 | + if not data: |
| 766 | + # The file is created before the executable mapping of the |
| 767 | + # jitdump; if that mapping fails (for example a noexec /tmp) the |
| 768 | + # backend gives up silently and never writes the header. |
| 769 | + self.skipTest("jitdump could not be initialized") |
| 770 | + return data |
| 771 | + |
| 772 | + def _check_unwinding_records(self, data): |
| 773 | + """Check every unwinding record against the code load record it |
| 774 | + describes; return {name: code_size} for the regions seen.""" |
| 775 | + records = list(_jitdump_records(data)) |
| 776 | + regions = {} |
| 777 | + for index, (event, pos, size) in enumerate(records): |
| 778 | + if event != PERF_UNWINDING_INFO: |
| 779 | + continue |
| 780 | + # The unwinding info record is immediately followed by the |
| 781 | + # code load record it describes. |
| 782 | + self.assertLess(index + 1, len(records)) |
| 783 | + load_event, load_pos, load_size = records[index + 1] |
| 784 | + self.assertEqual(load_event, PERF_LOAD) |
| 785 | + code_size = struct.unpack_from( |
| 786 | + f"{JITDUMP_ENDIAN}Q", data, load_pos + CODE_LOAD_CODE_SIZE_OFFSET)[0] |
| 787 | + name_start = load_pos + CODE_LOAD_NAME_OFFSET |
| 788 | + name_end = data.find(b"\x00", name_start, load_pos + load_size) |
| 789 | + self.assertGreater(name_end, 0) |
| 790 | + name = data[name_start:name_end].decode("utf-8", errors="replace") |
| 791 | + # The machine code follows the name inside the load record. |
| 792 | + self.assertLessEqual(name_end + 1 + code_size, load_pos + load_size, name) |
| 793 | + unwind_data_size, eh_frame_hdr_size = struct.unpack_from( |
| 794 | + f"{JITDUMP_ENDIAN}QQ", data, pos + UNWIND_DATA_SIZE_OFFSET) |
| 795 | + self.assertEqual(eh_frame_hdr_size, EH_FRAME_HDR_SIZE) |
| 796 | + self.assertLessEqual(UNWIND_EH_FRAME_OFFSET + unwind_data_size, size) |
| 797 | + eh_frame_size = unwind_data_size - eh_frame_hdr_size |
| 798 | + self.assertGreater(eh_frame_size, 0) |
| 799 | + start = pos + UNWIND_EH_FRAME_OFFSET |
| 800 | + eh_frame = data[start:start + eh_frame_size] |
| 801 | + |
| 802 | + cie_length, cie_id = struct.unpack_from(f"{JITDUMP_ENDIAN}II", eh_frame, 0) |
| 803 | + self.assertEqual(cie_id, 0, "first entry must be a CIE") |
| 804 | + self.assertEqual(eh_frame[8], 1, "CIE version must be 1") |
| 805 | + self.assertEqual(eh_frame[9:12], b"zR\x00") |
| 806 | + encoding = _fde_pointer_encoding(eh_frame) |
| 807 | + if encoding == DW_EH_PE_PCREL_SDATA4: |
| 808 | + fields = "iI" |
| 809 | + elif encoding == DW_EH_PE_PCREL_ABSPTR: |
| 810 | + fields = "qQ" |
| 811 | + else: |
| 812 | + self.fail(f"unexpected FDE pointer encoding {encoding:#x}") |
| 813 | + # jit_unwind.c patches initial_location and address_range for |
| 814 | + # perf's DSO layout, where the .eh_frame follows the code at |
| 815 | + # code_size rounded up to 8 bytes. |
| 816 | + pc_offset = 4 + cie_length + 8 |
| 817 | + initial_location, address_range = struct.unpack_from( |
| 818 | + f"{JITDUMP_ENDIAN}{fields}", eh_frame, pc_offset) |
| 819 | + self.assertEqual(address_range, code_size, name) |
| 820 | + rounded_code_size = (code_size + 7) & ~7 |
| 821 | + self.assertEqual(initial_location, -(rounded_code_size + pc_offset), name) |
| 822 | + # perf's eh_frame_hdr must point back at the code with the same |
| 823 | + # rounding as the FDE. |
| 824 | + hdr_from = struct.unpack_from( |
| 825 | + f"{JITDUMP_ENDIAN}i", data, |
| 826 | + start + eh_frame_size + EH_FRAME_HDR_FROM_OFFSET)[0] |
| 827 | + self.assertEqual(hdr_from, -(rounded_code_size + eh_frame_size), name) |
| 828 | + regions[name] = code_size |
| 829 | + self.assertTrue(regions, "no CodeUnwindingInfoEvent found") |
| 830 | + return regions |
| 831 | + |
| 832 | + def test_jitdump_unwinding_info(self): |
| 833 | + """Each region's .eh_frame is patched for that region's size.""" |
| 834 | + data = self._run_and_get_jitdump("def my_test_func(): pass\nmy_test_func()") |
| 835 | + magic, version = struct.unpack_from(f"{JITDUMP_ENDIAN}II", data, 0) |
| 836 | + self.assertEqual((magic, version), (JITDUMP_MAGIC, JITDUMP_VERSION)) |
| 837 | + regions = self._check_unwinding_records(data) |
| 838 | + self.assertTrue(any("my_test_func" in name for name in regions)) |
| 839 | + |
| 840 | + |
| 841 | +try: |
| 842 | + with test_tools.imports_under_tool("jit"): |
| 843 | + import _trampoline_ehframe |
| 844 | +except ImportError: |
| 845 | + # Installed Python without the Tools directory. |
| 846 | + _trampoline_ehframe = None |
| 847 | + |
| 848 | + |
| 849 | +def _fake_cie(*, version=1, augmentation=b"zR", ra_column=16, |
| 850 | + encoding=DW_EH_PE_PCREL_SDATA4, cie_id=0): |
| 851 | + """A CIE like the assembler's: code align 1, data align -8, one |
| 852 | + DW_CFA_def_cfa instruction, padded with DW_CFA_nop to 8 bytes.""" |
| 853 | + body = bytes([version]) + augmentation + b"\x00" |
| 854 | + body += bytes([1, 0x78, ra_column, 1, encoding]) |
| 855 | + body += bytes([0x0C, 7, 8]) # DW_CFA_def_cfa: r7 (rsp) ofs 8 |
| 856 | + body += b"\x00" * (-(8 + len(body)) % 8) |
| 857 | + return struct.pack("<II", 4 + len(body), cie_id) + body |
| 858 | + |
| 859 | + |
| 860 | +def _fake_fde(cie_total, *, field_size=4, address_range=8, |
| 861 | + instructions=b"\x41\x0e\x10\x86\x02"): |
| 862 | + """An FDE right after a CIE of cie_total bytes, padded to 8 bytes.""" |
| 863 | + body = struct.pack("<I", cie_total + 4) # CIE pointer, relative to itself |
| 864 | + # initial_location as an assembler would leave it, the parser zeroes it. |
| 865 | + body += (-40).to_bytes(field_size, "little", signed=True) |
| 866 | + body += address_range.to_bytes(field_size, "little") |
| 867 | + body += b"\x00" # augmentation data length |
| 868 | + body += instructions |
| 869 | + body += b"\x00" * (-(4 + len(body)) % 8) |
| 870 | + return struct.pack("<I", len(body)) + body |
| 871 | + |
| 872 | + |
| 873 | +@unittest.skipIf(_trampoline_ehframe is None, |
| 874 | + "Tools/jit/_trampoline_ehframe.py not found") |
| 875 | +class TestTrampolineEhframeScript(unittest.TestCase): |
| 876 | + """Tests for Tools/jit/_trampoline_ehframe.py.""" |
| 877 | + |
| 878 | + ehframe = _trampoline_ehframe |
| 879 | + |
| 880 | + def parse(self, data, text_size=8): |
| 881 | + return self.ehframe.parse_ehframe(bytes(data), "<", text_size) |
| 882 | + |
| 883 | + def test_parse(self): |
| 884 | + """Both FDE pointer encodings: ELF sdata4 and Darwin absptr.""" |
| 885 | + cases = [(DW_EH_PE_PCREL_SDATA4, 4, 16, 8), (DW_EH_PE_PCREL_ABSPTR, 8, 30, 20)] |
| 886 | + for encoding, field_size, ra_column, text_size in cases: |
| 887 | + with self.subTest(encoding=hex(encoding)): |
| 888 | + cie = _fake_cie(encoding=encoding, ra_column=ra_column) |
| 889 | + fde = _fake_fde(len(cie), field_size=field_size, |
| 890 | + address_range=text_size) |
| 891 | + result = self.parse(cie + fde, text_size) |
| 892 | + self.assertEqual(result.field_size, field_size) |
| 893 | + self.assertEqual(result.fde_pc_offset, len(cie) + 8) |
| 894 | + self.assertEqual(result.fde_range_offset, len(cie) + 8 + field_size) |
| 895 | + # Both patchable fields zeroed, everything else untouched. |
| 896 | + expected = bytearray(cie + fde) |
| 897 | + expected[len(cie) + 8:len(cie) + 8 + 2 * field_size] = bytes(2 * field_size) |
| 898 | + self.assertEqual(result.data, bytes(expected)) |
| 899 | + |
| 900 | + def test_parse_rejects_malformed(self): |
| 901 | + cie = _fake_cie() |
| 902 | + fde = _fake_fde(len(cie)) |
| 903 | + cases = [ |
| 904 | + ("version", _fake_cie(version=3) + fde, 8), |
| 905 | + ("augmentation", _fake_cie(augmentation=b"zPLR") + fde, 8), |
| 906 | + ("encoding", _fake_cie(encoding=0x1A) + fde, 8), |
| 907 | + ("exactly one FDE", cie + fde + fde, 8), |
| 908 | + ("address_range", cie + fde, 12), |
| 909 | + ("no FDE", cie, 8), |
| 910 | + ] |
| 911 | + for message, data, text_size in cases: |
| 912 | + with self.subTest(message): |
| 913 | + with self.assertRaisesRegex(ValueError, message): |
| 914 | + self.parse(data, text_size) |
| 915 | + |
| 916 | + def _build_trampoline_objects(self): |
| 917 | + """The object(s) the Makefile fed to the generator.""" |
| 918 | + builddir = sysconfig.get_config_var("abs_builddir") or "." |
| 919 | + universal2 = os.path.join(builddir, "Python", "asm_trampoline_universal2.o") |
| 920 | + if os.path.exists(universal2): |
| 921 | + return [universal2] |
| 922 | + return sorted( |
| 923 | + path for path in glob.glob( |
| 924 | + os.path.join(builddir, "Python", "asm_trampoline_*.o")) |
| 925 | + if "apple-darwin" not in os.path.basename(path)) |
| 926 | + |
| 927 | + def test_macho_thin_and_fat(self): |
| 928 | + """Mach-O objects and fat containers are parsed with no external tools.""" |
| 929 | + E = self.ehframe |
| 930 | + |
| 931 | + def macho(cputype, text, eh_frame): |
| 932 | + # A minimal MH_OBJECT: one __TEXT segment with __text and |
| 933 | + # __eh_frame sections, section data right after the load command. |
| 934 | + segment_size = 72 + 2 * 80 |
| 935 | + text_offset = 32 + segment_size |
| 936 | + eh_offset = text_offset + len(text) |
| 937 | + sections = b"" |
| 938 | + for name, size, offset in (("__text", len(text), text_offset), |
| 939 | + ("__eh_frame", len(eh_frame), eh_offset)): |
| 940 | + sections += struct.pack("<16s16sQQIIIIIIII", name.encode(), |
| 941 | + b"__TEXT", 0, size, offset, |
| 942 | + 0, 0, 0, 0, 0, 0, 0) |
| 943 | + segment = struct.pack("<II16sQQQQIIII", E._LC_SEGMENT_64, |
| 944 | + segment_size, b"__TEXT", 0, |
| 945 | + len(text) + len(eh_frame), text_offset, |
| 946 | + len(text) + len(eh_frame), 7, 5, 2, 0) |
| 947 | + header = struct.pack("<IIIIIIII", E._MH_MAGIC_64, cputype, 0, |
| 948 | + 1, 1, segment_size, 0, 0) |
| 949 | + return header + segment + sections + text + eh_frame |
| 950 | + |
| 951 | + x86 = macho(E._CPU_TYPE_X86_64, b"\x55\xc3", b"x86 eh_frame") |
| 952 | + arm = macho(E._CPU_TYPE_ARM64, b"\xc0\x03\x5f\xd6", b"arm64 eh_frame") |
| 953 | + # The fat header and its fat_arch entries are big-endian. |
| 954 | + blobs = [(E._CPU_TYPE_X86_64, x86), (E._CPU_TYPE_ARM64, arm)] |
| 955 | + offset = 8 + 20 * len(blobs) |
| 956 | + entries = b"" |
| 957 | + body = b"" |
| 958 | + for cputype, blob in blobs: |
| 959 | + entries += struct.pack(">IIIII", cputype, 0, offset + len(body), |
| 960 | + len(blob), 0) |
| 961 | + body += blob |
| 962 | + fat = struct.pack(">II", E._FAT_MAGIC, len(blobs)) + entries + body |
| 963 | + |
| 964 | + with temp_dir() as tmp: |
| 965 | + thin_path = os.path.join(tmp, "thin.o") |
| 966 | + fat_path = os.path.join(tmp, "fat.o") |
| 967 | + with open(thin_path, "wb") as f: |
| 968 | + f.write(arm) |
| 969 | + with open(fat_path, "wb") as f: |
| 970 | + f.write(fat) |
| 971 | + (thin,) = E.load_object(thin_path) |
| 972 | + fat_slices = E.load_object(fat_path) |
| 973 | + |
| 974 | + self.assertEqual(thin.arch_macro, "__aarch64__") |
| 975 | + self.assertEqual(thin.sections[".text"], b"\xc0\x03\x5f\xd6") |
| 976 | + self.assertEqual(thin.sections[".eh_frame"], b"arm64 eh_frame") |
| 977 | + self.assertEqual([s.arch_macro for s in fat_slices], |
| 978 | + ["__x86_64__", "__aarch64__"]) |
| 979 | + self.assertEqual(fat_slices[0].sections[".eh_frame"], b"x86 eh_frame") |
| 980 | + self.assertEqual(fat_slices[1].sections[".text"], b"\xc0\x03\x5f\xd6") |
| 981 | + |
| 982 | + def test_generated_header_is_current(self): |
| 983 | + """The header in the build directory matches a fresh generation.""" |
| 984 | + objects = self._build_trampoline_objects() |
| 985 | + builddir = sysconfig.get_config_var("abs_builddir") or "." |
| 986 | + header = os.path.join(builddir, "trampoline_ehframe.h") |
| 987 | + if not objects or not os.path.exists(header): |
| 988 | + self.skipTest("trampoline object or generated header not found") |
| 989 | + with open(header) as f: |
| 990 | + current = f.read() |
| 991 | + with temp_dir() as tmp: |
| 992 | + fresh_path = os.path.join(tmp, "trampoline_ehframe.h") |
| 993 | + self.ehframe.generate(objects, fresh_path) |
| 994 | + with open(fresh_path) as f: |
| 995 | + fresh = f.read() |
| 996 | + self.assertEqual(current, fresh) |
| 997 | + |
| 998 | + |
| 999 | +class TestTrampolineEhframeHeader(unittest.TestCase): |
| 1000 | + """Structural checks on the generated trampoline_ehframe.h data.""" |
| 1001 | + |
| 1002 | + def test_generated_header_structure(self): |
| 1003 | + _testinternalcapi = import_helper.import_module("_testinternalcapi") |
| 1004 | + check = getattr(_testinternalcapi, "test_trampoline_ehframe", None) |
| 1005 | + if check is None: |
| 1006 | + self.skipTest("_testinternalcapi built without the perf trampoline") |
| 1007 | + # Raises AssertionError describing the first failed check. |
| 1008 | + check() |
| 1009 | + |
| 1010 | + |
688 | 1011 | if __name__ == "__main__": |
689 | 1012 | unittest.main() |
0 commit comments