178 lines
7.3 KiB
Python
178 lines
7.3 KiB
Python
"""
|
|
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.",
|
|
)
|