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