1- import glob
21import struct
32import unittest
43import string
1514)
1615from test .support import import_helper
1716from test .support .os_helper import temp_dir
18- from test import test_tools
1917
2018
2119if 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\n sys.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-
1000848class TestTrampolineEhframeHeader (unittest .TestCase ):
1001849 """Structural checks on the generated trampoline_ehframe.h data."""
1002850
0 commit comments