Skip to content

Commit 48c7945

Browse files
committed
small reorganization to trampoline unwind generation
1 parent 0f6b805 commit 48c7945

3 files changed

Lines changed: 394 additions & 291 deletions

File tree

Lib/test/test_perf_profiler.py

Lines changed: 61 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import glob
21
import struct
32
import unittest
43
import string
@@ -15,7 +14,6 @@
1514
)
1615
from test.support import import_helper
1716
from test.support.os_helper import temp_dir
18-
from test import test_tools
1917

2018

2119
if not support.has_subprocess_support:
@@ -754,78 +752,86 @@ def _run_and_get_jitdump(self, code):
754752
# The child prints its pid so we open exactly its own jitdump file
755753
# rather than whatever another test worker left in /tmp.
756754
code = "import os, sys\nsys.stdout.write(str(os.getpid()))\n" + code
757-
rc, out, err = assert_python_ok("-Xperf_jit", "-c", code, PYTHON_JIT="0")
758-
path = f"/tmp/jit-{int(out)}.dump"
759-
if not os.path.exists(path):
755+
_, out, _ = assert_python_ok("-Xperf_jit", "-c", code, PYTHON_JIT="0")
756+
path = pathlib.Path(f"/tmp/jit-{int(out)}.dump")
757+
try:
758+
data = path.read_bytes()
759+
except FileNotFoundError:
760760
# perf_map_jit_init() gives up silently when it cannot create
761761
# the file (for example an unwritable /tmp).
762762
self.skipTest("jitdump file was not created")
763-
self.addCleanup(os.unlink, path)
764-
with open(path, "rb") as f:
765-
data = f.read()
763+
self.addCleanup(path.unlink)
766764
if not data:
767765
# The file is created before the executable mapping of the
768766
# jitdump; if that mapping fails (for example a noexec /tmp) the
769767
# backend gives up silently and never writes the header.
770768
self.skipTest("jitdump could not be initialized")
771769
return data
772770

771+
def _check_code_load(self, data, pos, size):
772+
"""Validate a code load record and return its name and code size."""
773+
code_size = struct.unpack_from(
774+
f"{JITDUMP_ENDIAN}Q", data, pos + CODE_LOAD_CODE_SIZE_OFFSET)[0]
775+
name_start = pos + CODE_LOAD_NAME_OFFSET
776+
name_end = data.find(b"\x00", name_start, pos + size)
777+
self.assertGreater(name_end, 0)
778+
name = data[name_start:name_end].decode("utf-8", errors="replace")
779+
# The machine code follows the name inside the load record.
780+
self.assertLessEqual(name_end + 1 + code_size, pos + size, name)
781+
return name, code_size
782+
783+
def _check_unwind_info(self, data, pos, size, name, code_size):
784+
"""Check the FDE and perf header against their code load record."""
785+
unwind_data_size, eh_frame_hdr_size = struct.unpack_from(
786+
f"{JITDUMP_ENDIAN}QQ", data, pos + UNWIND_DATA_SIZE_OFFSET)
787+
self.assertEqual(eh_frame_hdr_size, EH_FRAME_HDR_SIZE)
788+
self.assertLessEqual(UNWIND_EH_FRAME_OFFSET + unwind_data_size, size)
789+
eh_frame_size = unwind_data_size - eh_frame_hdr_size
790+
self.assertGreater(eh_frame_size, 0)
791+
start = pos + UNWIND_EH_FRAME_OFFSET
792+
eh_frame = data[start:start + eh_frame_size]
793+
794+
cie_length, cie_id = struct.unpack_from(f"{JITDUMP_ENDIAN}II", eh_frame, 0)
795+
self.assertEqual(cie_id, 0, "first entry must be a CIE")
796+
self.assertEqual(eh_frame[8], 1, "CIE version must be 1")
797+
self.assertEqual(eh_frame[9:12], b"zR\x00")
798+
encoding = _fde_pointer_encoding(eh_frame)
799+
if encoding == DW_EH_PE_PCREL_SDATA4:
800+
fields = "iI"
801+
elif encoding == DW_EH_PE_PCREL_ABSPTR:
802+
fields = "qQ"
803+
else:
804+
self.fail(f"unexpected FDE pointer encoding {encoding:#x}")
805+
# jit_unwind.c patches initial_location and address_range for
806+
# perf's DSO layout, where the .eh_frame follows the code at
807+
# code_size rounded up to 8 bytes.
808+
pc_offset = 4 + cie_length + 8
809+
initial_location, address_range = struct.unpack_from(
810+
f"{JITDUMP_ENDIAN}{fields}", eh_frame, pc_offset)
811+
self.assertEqual(address_range, code_size, name)
812+
rounded_code_size = (code_size + 7) & ~7
813+
self.assertEqual(initial_location, -(rounded_code_size + pc_offset), name)
814+
# perf's eh_frame_hdr must point back at the code with the same
815+
# rounding as the FDE.
816+
hdr_from = struct.unpack_from(
817+
f"{JITDUMP_ENDIAN}i", data,
818+
start + eh_frame_size + EH_FRAME_HDR_FROM_OFFSET)[0]
819+
self.assertEqual(hdr_from, -(rounded_code_size + eh_frame_size), name)
820+
773821
def _check_unwinding_records(self, data):
774-
"""Check every unwinding record against the code load record it
775-
describes; return {name: code_size} for the regions seen."""
822+
"""Return {name: code_size} after checking each unwind/load pair."""
776823
records = list(_jitdump_records(data))
777824
regions = {}
778825
for index, (event, pos, size) in enumerate(records):
779826
if event != PERF_UNWINDING_INFO:
780827
continue
781-
# The unwinding info record is immediately followed by the
782-
# code load record it describes.
828+
# Unwinding info immediately precedes the code it describes.
783829
self.assertLess(index + 1, len(records))
784830
load_event, load_pos, load_size = records[index + 1]
785831
self.assertEqual(load_event, PERF_LOAD)
786-
code_size = struct.unpack_from(
787-
f"{JITDUMP_ENDIAN}Q", data, load_pos + CODE_LOAD_CODE_SIZE_OFFSET)[0]
788-
name_start = load_pos + CODE_LOAD_NAME_OFFSET
789-
name_end = data.find(b"\x00", name_start, load_pos + load_size)
790-
self.assertGreater(name_end, 0)
791-
name = data[name_start:name_end].decode("utf-8", errors="replace")
792-
# The machine code follows the name inside the load record.
793-
self.assertLessEqual(name_end + 1 + code_size, load_pos + load_size, name)
794-
unwind_data_size, eh_frame_hdr_size = struct.unpack_from(
795-
f"{JITDUMP_ENDIAN}QQ", data, pos + UNWIND_DATA_SIZE_OFFSET)
796-
self.assertEqual(eh_frame_hdr_size, EH_FRAME_HDR_SIZE)
797-
self.assertLessEqual(UNWIND_EH_FRAME_OFFSET + unwind_data_size, size)
798-
eh_frame_size = unwind_data_size - eh_frame_hdr_size
799-
self.assertGreater(eh_frame_size, 0)
800-
start = pos + UNWIND_EH_FRAME_OFFSET
801-
eh_frame = data[start:start + eh_frame_size]
802-
803-
cie_length, cie_id = struct.unpack_from(f"{JITDUMP_ENDIAN}II", eh_frame, 0)
804-
self.assertEqual(cie_id, 0, "first entry must be a CIE")
805-
self.assertEqual(eh_frame[8], 1, "CIE version must be 1")
806-
self.assertEqual(eh_frame[9:12], b"zR\x00")
807-
encoding = _fde_pointer_encoding(eh_frame)
808-
if encoding == DW_EH_PE_PCREL_SDATA4:
809-
fields = "iI"
810-
elif encoding == DW_EH_PE_PCREL_ABSPTR:
811-
fields = "qQ"
812-
else:
813-
self.fail(f"unexpected FDE pointer encoding {encoding:#x}")
814-
# jit_unwind.c patches initial_location and address_range for
815-
# perf's DSO layout, where the .eh_frame follows the code at
816-
# code_size rounded up to 8 bytes.
817-
pc_offset = 4 + cie_length + 8
818-
initial_location, address_range = struct.unpack_from(
819-
f"{JITDUMP_ENDIAN}{fields}", eh_frame, pc_offset)
820-
self.assertEqual(address_range, code_size, name)
821-
rounded_code_size = (code_size + 7) & ~7
822-
self.assertEqual(initial_location, -(rounded_code_size + pc_offset), name)
823-
# perf's eh_frame_hdr must point back at the code with the same
824-
# rounding as the FDE.
825-
hdr_from = struct.unpack_from(
826-
f"{JITDUMP_ENDIAN}i", data,
827-
start + eh_frame_size + EH_FRAME_HDR_FROM_OFFSET)[0]
828-
self.assertEqual(hdr_from, -(rounded_code_size + eh_frame_size), name)
832+
name, code_size = self._check_code_load(data, load_pos, load_size)
833+
with self.subTest(region=name):
834+
self._check_unwind_info(data, pos, size, name, code_size)
829835
regions[name] = code_size
830836
self.assertTrue(regions, "no CodeUnwindingInfoEvent found")
831837
return regions
@@ -839,164 +845,6 @@ def test_jitdump_unwinding_info(self):
839845
self.assertTrue(any("my_test_func" in name for name in regions))
840846

841847

842-
try:
843-
with test_tools.imports_under_tool("jit"):
844-
import _trampoline_ehframe
845-
except ImportError:
846-
# Installed Python without the Tools directory.
847-
_trampoline_ehframe = None
848-
849-
850-
def _fake_cie(*, version=1, augmentation=b"zR", ra_column=16,
851-
encoding=DW_EH_PE_PCREL_SDATA4, cie_id=0):
852-
"""A CIE like the assembler's: code align 1, data align -8, one
853-
DW_CFA_def_cfa instruction, padded with DW_CFA_nop to 8 bytes."""
854-
body = bytes([version]) + augmentation + b"\x00"
855-
body += bytes([1, 0x78, ra_column, 1, encoding])
856-
body += bytes([0x0C, 7, 8]) # DW_CFA_def_cfa: r7 (rsp) ofs 8
857-
body += b"\x00" * (-(8 + len(body)) % 8)
858-
return struct.pack("<II", 4 + len(body), cie_id) + body
859-
860-
861-
def _fake_fde(cie_total, *, field_size=4, address_range=8,
862-
instructions=b"\x41\x0e\x10\x86\x02"):
863-
"""An FDE right after a CIE of cie_total bytes, padded to 8 bytes."""
864-
body = struct.pack("<I", cie_total + 4) # CIE pointer, relative to itself
865-
# initial_location as an assembler would leave it, the parser zeroes it.
866-
body += (-40).to_bytes(field_size, "little", signed=True)
867-
body += address_range.to_bytes(field_size, "little")
868-
body += b"\x00" # augmentation data length
869-
body += instructions
870-
body += b"\x00" * (-(4 + len(body)) % 8)
871-
return struct.pack("<I", len(body)) + body
872-
873-
874-
@unittest.skipIf(_trampoline_ehframe is None,
875-
"Tools/jit/_trampoline_ehframe.py not found")
876-
class TestTrampolineEhframeScript(unittest.TestCase):
877-
"""Tests for Tools/jit/_trampoline_ehframe.py."""
878-
879-
ehframe = _trampoline_ehframe
880-
881-
def parse(self, data, text_size=8):
882-
return self.ehframe.parse_ehframe(bytes(data), "<", text_size)
883-
884-
def test_parse(self):
885-
"""Both FDE pointer encodings: ELF sdata4 and Darwin absptr."""
886-
cases = [(DW_EH_PE_PCREL_SDATA4, 4, 16, 8), (DW_EH_PE_PCREL_ABSPTR, 8, 30, 20)]
887-
for encoding, field_size, ra_column, text_size in cases:
888-
with self.subTest(encoding=hex(encoding)):
889-
cie = _fake_cie(encoding=encoding, ra_column=ra_column)
890-
fde = _fake_fde(len(cie), field_size=field_size,
891-
address_range=text_size)
892-
result = self.parse(cie + fde, text_size)
893-
self.assertEqual(result.field_size, field_size)
894-
self.assertEqual(result.fde_pc_offset, len(cie) + 8)
895-
self.assertEqual(result.fde_range_offset, len(cie) + 8 + field_size)
896-
# Both patchable fields zeroed, everything else untouched.
897-
expected = bytearray(cie + fde)
898-
expected[len(cie) + 8:len(cie) + 8 + 2 * field_size] = bytes(2 * field_size)
899-
self.assertEqual(result.data, bytes(expected))
900-
901-
def test_parse_rejects_malformed(self):
902-
cie = _fake_cie()
903-
fde = _fake_fde(len(cie))
904-
cases = [
905-
("version", _fake_cie(version=3) + fde, 8),
906-
("augmentation", _fake_cie(augmentation=b"zPLR") + fde, 8),
907-
("encoding", _fake_cie(encoding=0x1A) + fde, 8),
908-
("exactly one FDE", cie + fde + fde, 8),
909-
("address_range", cie + fde, 12),
910-
("no FDE", cie, 8),
911-
]
912-
for message, data, text_size in cases:
913-
with self.subTest(message):
914-
with self.assertRaisesRegex(ValueError, message):
915-
self.parse(data, text_size)
916-
917-
def _build_trampoline_objects(self):
918-
"""The object(s) the Makefile fed to the generator."""
919-
builddir = sysconfig.get_config_var("abs_builddir") or "."
920-
universal2 = os.path.join(builddir, "Python", "asm_trampoline_universal2.o")
921-
if os.path.exists(universal2):
922-
return [universal2]
923-
return sorted(
924-
path for path in glob.glob(
925-
os.path.join(builddir, "Python", "asm_trampoline_*.o"))
926-
if "apple-darwin" not in os.path.basename(path))
927-
928-
def test_macho_thin_and_fat(self):
929-
"""Mach-O objects and fat containers are parsed with no external tools."""
930-
E = self.ehframe
931-
932-
def macho(cputype, text, eh_frame):
933-
# A minimal MH_OBJECT: one __TEXT segment with __text and
934-
# __eh_frame sections, section data right after the load command.
935-
segment_size = 72 + 2 * 80
936-
text_offset = 32 + segment_size
937-
eh_offset = text_offset + len(text)
938-
sections = b""
939-
for name, size, offset in (("__text", len(text), text_offset),
940-
("__eh_frame", len(eh_frame), eh_offset)):
941-
sections += struct.pack("<16s16sQQIIIIIIII", name.encode(),
942-
b"__TEXT", 0, size, offset,
943-
0, 0, 0, 0, 0, 0, 0)
944-
segment = struct.pack("<II16sQQQQIIII", E._LC_SEGMENT_64,
945-
segment_size, b"__TEXT", 0,
946-
len(text) + len(eh_frame), text_offset,
947-
len(text) + len(eh_frame), 7, 5, 2, 0)
948-
header = struct.pack("<IIIIIIII", E._MH_MAGIC_64, cputype, 0,
949-
1, 1, segment_size, 0, 0)
950-
return header + segment + sections + text + eh_frame
951-
952-
x86 = macho(E._CPU_TYPE_X86_64, b"\x55\xc3", b"x86 eh_frame")
953-
arm = macho(E._CPU_TYPE_ARM64, b"\xc0\x03\x5f\xd6", b"arm64 eh_frame")
954-
# The fat header and its fat_arch entries are big-endian.
955-
blobs = [(E._CPU_TYPE_X86_64, x86), (E._CPU_TYPE_ARM64, arm)]
956-
offset = 8 + 20 * len(blobs)
957-
entries = b""
958-
body = b""
959-
for cputype, blob in blobs:
960-
entries += struct.pack(">IIIII", cputype, 0, offset + len(body),
961-
len(blob), 0)
962-
body += blob
963-
fat = struct.pack(">II", E._FAT_MAGIC, len(blobs)) + entries + body
964-
965-
with temp_dir() as tmp:
966-
thin_path = os.path.join(tmp, "thin.o")
967-
fat_path = os.path.join(tmp, "fat.o")
968-
with open(thin_path, "wb") as f:
969-
f.write(arm)
970-
with open(fat_path, "wb") as f:
971-
f.write(fat)
972-
(thin,) = E.load_object(thin_path)
973-
fat_slices = E.load_object(fat_path)
974-
975-
self.assertEqual(thin.arch_macro, "__aarch64__")
976-
self.assertEqual(thin.sections[".text"], b"\xc0\x03\x5f\xd6")
977-
self.assertEqual(thin.sections[".eh_frame"], b"arm64 eh_frame")
978-
self.assertEqual([s.arch_macro for s in fat_slices],
979-
["__x86_64__", "__aarch64__"])
980-
self.assertEqual(fat_slices[0].sections[".eh_frame"], b"x86 eh_frame")
981-
self.assertEqual(fat_slices[1].sections[".text"], b"\xc0\x03\x5f\xd6")
982-
983-
def test_generated_header_is_current(self):
984-
"""The header in the build directory matches a fresh generation."""
985-
objects = self._build_trampoline_objects()
986-
builddir = sysconfig.get_config_var("abs_builddir") or "."
987-
header = os.path.join(builddir, "trampoline_ehframe.h")
988-
if not objects or not os.path.exists(header):
989-
self.skipTest("trampoline object or generated header not found")
990-
with open(header) as f:
991-
current = f.read()
992-
with temp_dir() as tmp:
993-
fresh_path = os.path.join(tmp, "trampoline_ehframe.h")
994-
self.ehframe.generate(objects, fresh_path)
995-
with open(fresh_path) as f:
996-
fresh = f.read()
997-
self.assertEqual(current, fresh)
998-
999-
1000848
class TestTrampolineEhframeHeader(unittest.TestCase):
1001849
"""Structural checks on the generated trampoline_ehframe.h data."""
1002850

0 commit comments

Comments
 (0)