@@ -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" )
876858class 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
1000892class 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
1012953if __name__ == "__main__" :
0 commit comments