brintos

brintos / llvm-project-archived public Read only

0
0
Text · 7.3 KiB · e03234d Raw
178 lines · python
1"""2Test that libunwind correctly injects 'ret' instructions to rebalance execution flow3when unwinding C++ exceptions. This is important for Apple Processor Trace analysis.4"""5 6import lldb7import os8from lldbsuite.test.decorators import *9from lldbsuite.test.lldbtest import *10from lldbsuite.test import lldbutil11from lldbsuite.test import configuration12 13 14class LibunwindRetInjectionTestCase(TestBase):15    @skipIf(archs=no_match(["arm64", "arm64e", "aarch64"]))16    @skipUnlessDarwin17    @skipIfOutOfTreeLibunwind18    def test_ret_injection_on_exception_unwind(self):19        """Test that __libunwind_Registers_arm64_jumpto receives correct walkedFrames count and injects the right number of ret instructions."""20        self.build()21 22        exe = self.getBuildArtifact("a.out")23        target = self.dbg.CreateTarget(exe)24        self.assertTrue(target, VALID_TARGET)25 26        # Find the just-built libunwind, not the system one.27        # llvm_tools_dir is typically <build>/bin, so lib is a sibling.28        self.assertIsNotNone(29            configuration.llvm_tools_dir,30            "llvm_tools_dir must be set to find in-tree libunwind",31        )32 33        llvm_lib_dir = os.path.join(34            os.path.dirname(configuration.llvm_tools_dir), "lib"35        )36 37        # Find the libunwind library (platform-agnostic).38        libunwind_path = None39        for filename in os.listdir(llvm_lib_dir):40            if filename.startswith("libunwind.") or filename.startswith("unwind."):41                libunwind_path = os.path.join(llvm_lib_dir, filename)42                break43 44        self.assertIsNotNone(45            libunwind_path, f"Could not find libunwind in {llvm_lib_dir}"46        )47 48        # Set breakpoint in __libunwind_Registers_arm64_jumpto.49        # This is the function that performs the actual jump and ret injection.50        bp = target.BreakpointCreateByName("__libunwind_Registers_arm64_jumpto")51        self.assertTrue(bp.IsValid())52        self.assertGreater(bp.GetNumLocations(), 0)53 54        # Set up DYLD_INSERT_LIBRARIES to use the just-built libunwind.55        launch_info = lldb.SBLaunchInfo(None)56        env = target.GetEnvironment()57        env.Set("DYLD_INSERT_LIBRARIES", libunwind_path, True)58        launch_info.SetEnvironment(env, False)59 60        # Launch the process with our custom libunwind.61        error = lldb.SBError()62        process = target.Launch(launch_info, error)63        self.assertSuccess(64            error, f"Failed to launch process with libunwind at {libunwind_path}"65        )66        self.assertTrue(process, PROCESS_IS_VALID)67 68        # We should hit the breakpoint in __libunwind_Registers_arm64_jumpto69        # during the exception unwinding phase 2.70        threads = lldbutil.get_threads_stopped_at_breakpoint(process, bp)71        self.assertEqual(len(threads), 1, "Should have stopped at breakpoint")72 73        thread = threads[0]74        frame = thread.GetFrameAtIndex(0)75 76        # Verify we're in __libunwind_Registers_arm64_jumpto.77        function_name = frame.GetFunctionName()78        self.assertTrue(79            "__libunwind_Registers_arm64_jumpto" in function_name,80            f"Expected to be in __libunwind_Registers_arm64_jumpto, got {function_name}",81        )82 83        # On ARM64, the walkedFrames parameter should be in register x1 (second parameter).84        # According to the ARM64 calling convention, integer arguments are passed in x0-x7.85        # x0 = Registers_arm64* pointer.86        # x1 = unsigned walkedFrames.87        error = lldb.SBError()88        x1_value = frame.register["x1"].GetValueAsUnsigned(error)89        self.assertSuccess(error, "Failed to read x1 register")90 91        # According to the code in UnwindCursor.hpp, the walkedFrames value represents:92        # 1. The number of frames walked in unwind_phase2 to reach the landing pad.93        # 2. Plus _EXTRA_LIBUNWIND_FRAMES_WALKED = 5 - 1 = 4 additional libunwind frames.94        #95        # From the comment in the code:96        #   frame #0: __libunwind_Registers_arm64_jumpto97        #   frame #1: Registers_arm64::returnto98        #   frame #2: UnwindCursor::jumpto99        #   frame #3: __unw_resume100        #   frame #4: __unw_resume_with_frames_walked101        #   frame #5: unwind_phase2102        #103        # Since __libunwind_Registers_arm64_jumpto returns to the landing pad,104        # we subtract 1, so _EXTRA_LIBUNWIND_FRAMES_WALKED = 4.105        #106        # For our test program:107        # - unwind_phase2 starts walking (frame 0 counted here).108        # - Walks through: func_d (throw site), func_c, func_b, func_a.109        # - Finds landing pad in main.110        # That's approximately 4-5 frames from the user code.111        # Plus the 4 extra libunwind frames.112        #113        # So we expect x1 to be roughly 8-10.114        expected_min_frames = 8115        expected_max_frames = 13  # Allow some variation for libc++abi frames.116 117        self.assertGreaterEqual(118            x1_value,119            expected_min_frames,120            f"walkedFrames (x1) should be >= {expected_min_frames}, got {x1_value}. "121            "This is the number of 'ret' instructions that will be executed.",122        )123 124        self.assertLessEqual(125            x1_value,126            expected_max_frames,127            f"walkedFrames (x1) should be <= {expected_max_frames}, got {x1_value}. "128            "Value seems too high.",129        )130 131        # Now step through the ret injection loop and count the actual number of 'ret' executions.132        # The loop injects exactly x1_value ret instructions before continuing with register restoration.133        # We step until we hit the first 'ldp' instruction (register restoration starts with 'ldp x2, x3, [x0, #0x010]').134        ret_executed_count = 0135        max_steps = 100  # Safety limit to prevent infinite loops.136 137        for step_count in range(max_steps):138            # Get current instruction.139            pc = frame.GetPC()140            inst = process.ReadMemory(pc, 4, lldb.SBError())141 142            # Disassemble current instruction.143            current_inst = target.GetInstructions(lldb.SBAddress(pc, target), inst)[0]144            mnemonic = current_inst.GetMnemonic(target)145            operands = current_inst.GetOperands(target)146 147            # Check if we've reached the register restoration part (first ldp after the loop).148            if mnemonic == "ldp":149                # We've exited the ret injection loop.150                break151 152            # Count 'ret' instructions that get executed.153            if mnemonic == "ret":154                self.assertEqual(operands, "x16")155                ret_executed_count += 1156 157            # Step one instruction.158            thread.StepInstruction(False)  # False = step over.159 160            # Update frame reference.161            frame = thread.GetFrameAtIndex(0)162 163        # Verify we didn't hit the safety limit.164        self.assertLess(165            step_count,166            max_steps - 1,167            f"Stepped {max_steps} times without reaching 'ldp' instruction. Something is wrong.",168        )169 170        # The number of executed 'ret' instructions should match x1_value.171        # According to the implementation, the loop executes exactly x1_value times.172        self.assertEqual(173            ret_executed_count,174            x1_value,175            f"Expected {x1_value} 'ret' instructions to be executed (matching x1 register), "176            f"but counted {ret_executed_count} executed 'ret' instructions.",177        )178