Skip to content

Commit 6d9f194

Browse files
committed
small review fixes
1 parent 0f3f47a commit 6d9f194

9 files changed

Lines changed: 557 additions & 175 deletions

File tree

Include/internal/pycore_jit_unwind.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ typedef struct {
6464
} _PyTrampolineEhFrame;
6565

6666
PyAPI_DATA(const _PyTrampolineEhFrame) _Py_trampoline_ehframe;
67+
68+
/* Copy eh's .eh_frame into buffer and fill in the FDE's initial_location
69+
* and address_range for code_size bytes of code that perf maps right
70+
* before the frame (see perf_jit_trampoline.c). Returns the number of
71+
* bytes written, or 0 when the data is absent (the bootstrap programs'
72+
* stub), inconsistent, larger than the buffer, or when code_size does not
73+
* fit the offsets. Export for '_testinternalcapi'. */
74+
PyAPI_FUNC(size_t) _PyJitUnwind_PatchTrampolineEhFrame(
75+
const _PyTrampolineEhFrame *eh, uint8_t *buffer, size_t buffer_size,
76+
size_t code_size);
6777
#endif
6878

6979
/* Return the size of the generated .eh_frame data for the given encoding. */

Lib/test/test_perf_profiler.py

Lines changed: 62 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -846,74 +846,21 @@ def test_jitdump_unwinding_info(self):
846846
# Installed Python without the Tools directory.
847847
_trampoline_ehframe = None
848848

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
849+
try:
850+
from test.test_tools.test_trampoline_ehframe import fake_cie, fake_fde
851+
except (ImportError, unittest.SkipTest):
852+
# Installed Python without the Tools directory.
853+
fake_cie = fake_fde = None
872854

873855

874856
@unittest.skipIf(_trampoline_ehframe is None,
875857
"Tools/jit/_trampoline_ehframe.py not found")
876858
class TestTrampolineEhframeScript(unittest.TestCase):
877-
"""Tests for Tools/jit/_trampoline_ehframe.py."""
859+
"""The generator against this build's trampoline object. The parsers are
860+
tested with synthetic objects in test.test_tools.test_trampoline_ehframe."""
878861

879862
ehframe = _trampoline_ehframe
880863

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-
917864
def _build_trampoline_objects(self):
918865
"""The object(s) the Makefile fed to the generator."""
919866
builddir = sysconfig.get_config_var("abs_builddir") or "."
@@ -925,61 +872,6 @@ def _build_trampoline_objects(self):
925872
os.path.join(builddir, "Python", "asm_trampoline_*.o"))
926873
if "apple-darwin" not in os.path.basename(path))
927874

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-
983875
def test_generated_source_is_current(self):
984876
"""The C file in the build directory matches a fresh generation."""
985877
objects = self._build_trampoline_objects()
@@ -998,15 +890,64 @@ def test_generated_source_is_current(self):
998890

999891

1000892
class TestTrampolineEhframeData(unittest.TestCase):
1001-
"""Structural checks on the generated trampoline_ehframe.c data."""
893+
"""Checks on the linked trampoline_ehframe.c data and the runtime patching."""
1002894

1003-
def test_generated_data_structure(self):
1004-
_testinternalcapi = import_helper.import_module("_testinternalcapi")
1005-
check = getattr(_testinternalcapi, "test_trampoline_ehframe", None)
1006-
if check is None:
895+
def setUp(self):
896+
self.capi = import_helper.import_module("_testinternalcapi")
897+
if not hasattr(self.capi, "test_trampoline_ehframe"):
1007898
self.skipTest("_testinternalcapi built without the perf trampoline")
899+
900+
def test_generated_data_structure(self):
1008901
# Raises AssertionError describing the first failed check.
1009-
check()
902+
self.capi.test_trampoline_ehframe()
903+
904+
@unittest.skipIf(fake_cie is None, "test_tools.test_trampoline_ehframe not importable")
905+
def test_patch_both_widths(self):
906+
patch = self.capi.patch_trampoline_ehframe
907+
for encoding, field_size in ((DW_EH_PE_PCREL_SDATA4, 4),
908+
(DW_EH_PE_PCREL_ABSPTR, 8)):
909+
cie = fake_cie(encoding=encoding)
910+
data = cie + fake_fde(len(cie), field_size=field_size)
911+
pc = len(cie) + 8
912+
rng = pc + field_size
913+
for code_size in (1, 8, 9, 4096):
914+
with self.subTest(field_size=field_size, code_size=code_size):
915+
out = patch(data, pc, rng, field_size, code_size, 1024)
916+
self.assertEqual(len(out), len(data))
917+
rounded = (code_size + 7) & ~7
918+
self.assertEqual(
919+
int.from_bytes(out[pc:rng], sys.byteorder, signed=True),
920+
-(rounded + pc))
921+
self.assertEqual(
922+
int.from_bytes(out[rng:rng + field_size], sys.byteorder),
923+
code_size)
924+
self.assertEqual(out[:pc] + out[rng + field_size:],
925+
data[:pc] + data[rng + field_size:])
926+
927+
@unittest.skipIf(fake_cie is None, "test_tools.test_trampoline_ehframe not importable")
928+
def test_patch_rejects_bad_input(self):
929+
patch = self.capi.patch_trampoline_ehframe
930+
cie = fake_cie()
931+
data = cie + fake_fde(len(cie))
932+
pc = len(cie) + 8
933+
rng = pc + 4
934+
# The largest code size whose offsets still fit a signed 32-bit field.
935+
limit = 2**31 - 1 - 8 - len(data)
936+
self.assertIsNotNone(patch(data, pc, rng, 4, limit, 1024))
937+
cases = {
938+
"empty data, as in the bootstrap stub": (b"", 8, 12, 4, 8, 1024),
939+
"buffer too small": (data, pc, rng, 4, 8, len(data) - 1),
940+
"bad field size": (data, pc, rng, 2, 8, 1024),
941+
"fields not adjacent": (data, pc, rng + 4, 4, 8, 1024),
942+
"fields past the end": (data, len(data) - 4, len(data), 4, 8, 1024),
943+
"zero code size": (data, pc, rng, 4, 0, 1024),
944+
"code size past INT32_MAX": (data, pc, rng, 4, limit + 1, 1024),
945+
}
946+
for name, args in cases.items():
947+
with self.subTest(name):
948+
self.assertIsNone(patch(*args))
949+
with self.assertRaises(ValueError):
950+
patch(data, -1, rng, 4, 8, 1024)
1010951

1011952

1012953
if __name__ == "__main__":

0 commit comments

Comments
 (0)