blob: e03234d1b5077f799118596ea66c2fd7a2813d07 [file] [log] [blame]
"""
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.",
)