| """ | |
| Test that libunwind correctly injects 'ret' instructions to rebalance execution flow | |
| when unwinding C++ exceptions. This is important for Apple Processor Trace analysis. | |
| """ | |
| import lldb | |
| import os | |
| from lldbsuite.test.decorators import * | |
| from lldbsuite.test.lldbtest import * | |
| from lldbsuite.test import lldbutil | |
| from lldbsuite.test import configuration | |
| class LibunwindRetInjectionTestCase(TestBase): | |
| @skipIf(archs=no_match(["arm64", "arm64e", "aarch64"])) | |
| @skipUnlessDarwin | |
| @skipIfOutOfTreeLibunwind | |
| def test_ret_injection_on_exception_unwind(self): | |
| """Test that __libunwind_Registers_arm64_jumpto receives correct walkedFrames count and injects the right number of ret instructions.""" | |
| self.build() | |
| exe = self.getBuildArtifact("a.out") | |
| target = self.dbg.CreateTarget(exe) | |
| self.assertTrue(target, VALID_TARGET) | |
| # Find the just-built libunwind, not the system one. | |
| # llvm_tools_dir is typically <build>/bin, so lib is a sibling. | |
| self.assertIsNotNone( | |
| configuration.llvm_tools_dir, | |
| "llvm_tools_dir must be set to find in-tree libunwind", | |
| ) | |
| llvm_lib_dir = os.path.join( | |
| os.path.dirname(configuration.llvm_tools_dir), "lib" | |
| ) | |
| # Find the libunwind library (platform-agnostic). | |
| libunwind_path = None | |
| for filename in os.listdir(llvm_lib_dir): | |
| if filename.startswith("libunwind.") or filename.startswith("unwind."): | |
| libunwind_path = os.path.join(llvm_lib_dir, filename) | |
| break | |
| self.assertIsNotNone( | |
| libunwind_path, f"Could not find libunwind in {llvm_lib_dir}" | |
| ) | |
| # Set breakpoint in __libunwind_Registers_arm64_jumpto. | |
| # This is the function that performs the actual jump and ret injection. | |
| bp = target.BreakpointCreateByName("__libunwind_Registers_arm64_jumpto") | |
| self.assertTrue(bp.IsValid()) | |
| self.assertGreater(bp.GetNumLocations(), 0) | |
| # Set up DYLD_INSERT_LIBRARIES to use the just-built libunwind. | |
| launch_info = lldb.SBLaunchInfo(None) | |
| env = target.GetEnvironment() | |
| env.Set("DYLD_INSERT_LIBRARIES", libunwind_path, True) | |
| launch_info.SetEnvironment(env, False) | |
| # Launch the process with our custom libunwind. | |
| error = lldb.SBError() | |
| process = target.Launch(launch_info, error) | |
| self.assertSuccess( | |
| error, f"Failed to launch process with libunwind at {libunwind_path}" | |
| ) | |
| self.assertTrue(process, PROCESS_IS_VALID) | |
| # We should hit the breakpoint in __libunwind_Registers_arm64_jumpto | |
| # during the exception unwinding phase 2. | |
| threads = lldbutil.get_threads_stopped_at_breakpoint(process, bp) | |
| self.assertEqual(len(threads), 1, "Should have stopped at breakpoint") | |
| thread = threads[0] | |
| frame = thread.GetFrameAtIndex(0) | |
| # Verify we're in __libunwind_Registers_arm64_jumpto. | |
| function_name = frame.GetFunctionName() | |
| self.assertTrue( | |
| "__libunwind_Registers_arm64_jumpto" in function_name, | |
| f"Expected to be in __libunwind_Registers_arm64_jumpto, got {function_name}", | |
| ) | |
| # On ARM64, the walkedFrames parameter should be in register x1 (second parameter). | |
| # According to the ARM64 calling convention, integer arguments are passed in x0-x7. | |
| # x0 = Registers_arm64* pointer. | |
| # x1 = unsigned walkedFrames. | |
| error = lldb.SBError() | |
| x1_value = frame.register["x1"].GetValueAsUnsigned(error) | |
| self.assertSuccess(error, "Failed to read x1 register") | |
| # According to the code in UnwindCursor.hpp, the walkedFrames value represents: | |
| # 1. The number of frames walked in unwind_phase2 to reach the landing pad. | |
| # 2. Plus _EXTRA_LIBUNWIND_FRAMES_WALKED = 5 - 1 = 4 additional libunwind frames. | |
| # | |
| # From the comment in the code: | |
| # frame #0: __libunwind_Registers_arm64_jumpto | |
| # frame #1: Registers_arm64::returnto | |
| # frame #2: UnwindCursor::jumpto | |
| # frame #3: __unw_resume | |
| # frame #4: __unw_resume_with_frames_walked | |
| # frame #5: unwind_phase2 | |
| # | |
| # Since __libunwind_Registers_arm64_jumpto returns to the landing pad, | |
| # we subtract 1, so _EXTRA_LIBUNWIND_FRAMES_WALKED = 4. | |
| # | |
| # For our test program: | |
| # - unwind_phase2 starts walking (frame 0 counted here). | |
| # - Walks through: func_d (throw site), func_c, func_b, func_a. | |
| # - Finds landing pad in main. | |
| # That's approximately 4-5 frames from the user code. | |
| # Plus the 4 extra libunwind frames. | |
| # | |
| # So we expect x1 to be roughly 8-10. | |
| expected_min_frames = 8 | |
| expected_max_frames = 13 # Allow some variation for libc++abi frames. | |
| self.assertGreaterEqual( | |
| x1_value, | |
| expected_min_frames, | |
| f"walkedFrames (x1) should be >= {expected_min_frames}, got {x1_value}. " | |
| "This is the number of 'ret' instructions that will be executed.", | |
| ) | |
| self.assertLessEqual( | |
| x1_value, | |
| expected_max_frames, | |
| f"walkedFrames (x1) should be <= {expected_max_frames}, got {x1_value}. " | |
| "Value seems too high.", | |
| ) | |
| # Now step through the ret injection loop and count the actual number of 'ret' executions. | |
| # The loop injects exactly x1_value ret instructions before continuing with register restoration. | |
| # We step until we hit the first 'ldp' instruction (register restoration starts with 'ldp x2, x3, [x0, #0x010]'). | |
| ret_executed_count = 0 | |
| max_steps = 100 # Safety limit to prevent infinite loops. | |
| for step_count in range(max_steps): | |
| # Get current instruction. | |
| pc = frame.GetPC() | |
| inst = process.ReadMemory(pc, 4, lldb.SBError()) | |
| # Disassemble current instruction. | |
| current_inst = target.GetInstructions(lldb.SBAddress(pc, target), inst)[0] | |
| mnemonic = current_inst.GetMnemonic(target) | |
| operands = current_inst.GetOperands(target) | |
| # Check if we've reached the register restoration part (first ldp after the loop). | |
| if mnemonic == "ldp": | |
| # We've exited the ret injection loop. | |
| break | |
| # Count 'ret' instructions that get executed. | |
| if mnemonic == "ret": | |
| self.assertEqual(operands, "x16") | |
| ret_executed_count += 1 | |
| # Step one instruction. | |
| thread.StepInstruction(False) # False = step over. | |
| # Update frame reference. | |
| frame = thread.GetFrameAtIndex(0) | |
| # Verify we didn't hit the safety limit. | |
| self.assertLess( | |
| step_count, | |
| max_steps - 1, | |
| f"Stepped {max_steps} times without reaching 'ldp' instruction. Something is wrong.", | |
| ) | |
| # The number of executed 'ret' instructions should match x1_value. | |
| # According to the implementation, the loop executes exactly x1_value times. | |
| self.assertEqual( | |
| ret_executed_count, | |
| x1_value, | |
| f"Expected {x1_value} 'ret' instructions to be executed (matching x1 register), " | |
| f"but counted {ret_executed_count} executed 'ret' instructions.", | |
| ) |