Med Ismail Bennani b30a48c389 [lldb/test] Fix scripted frame provider tests on ARM32
On ARM32, FixCodeAddress unconditionally clears bit 0 (the Thumb bit)
from all code addresses, including synthetic frame PCs. This causes
test failures where synthetic PCs like 0xFFFF and 0xDEADBEEF become
0xFFFE and 0xDEADBEEE respectively.

This adjusts the tests to expect the modified PC values on ARM32.

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
2025-12-02 16:39:33 -08:00

428 lines
17 KiB
Python

"""
Test scripted frame provider functionality.
"""
import os
import lldb
import lldbsuite.test.lldbplatformutil as lldbplatformutil
from lldbsuite.test.lldbtest import TestBase
from lldbsuite.test import lldbutil
class ScriptedFrameProviderTestCase(TestBase):
NO_DEBUG_INFO_TESTCASE = True
def setUp(self):
TestBase.setUp(self)
self.source = "main.cpp"
def test_replace_all_frames(self):
"""Test that we can replace the entire stack."""
self.build()
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
)
# Import the test frame provider
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
self.runCmd("command script import " + script_path)
# Attach the Replace provider
error = lldb.SBError()
provider_id = target.RegisterScriptedFrameProvider(
"test_frame_providers.ReplaceFrameProvider",
lldb.SBStructuredData(),
error,
)
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
# Verify we have exactly 3 synthetic frames
self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
# Verify frame indices and PCs (dictionary-based frames don't have custom function names)
frame0 = thread.GetFrameAtIndex(0)
self.assertIsNotNone(frame0)
self.assertEqual(frame0.GetPC(), 0x1000)
frame1 = thread.GetFrameAtIndex(1)
self.assertIsNotNone(frame1)
self.assertIn("thread_func", frame1.GetFunctionName())
frame2 = thread.GetFrameAtIndex(2)
self.assertIsNotNone(frame2)
self.assertEqual(frame2.GetPC(), 0x3000)
def test_prepend_frames(self):
"""Test that we can add frames before real stack."""
self.build()
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
)
# Get original frame count and PC
original_frame_count = thread.GetNumFrames()
self.assertGreaterEqual(
original_frame_count, 2, "Should have at least 2 real frames"
)
# Import and attach Prepend provider
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
self.runCmd("command script import " + script_path)
error = lldb.SBError()
provider_id = target.RegisterScriptedFrameProvider(
"test_frame_providers.PrependFrameProvider",
lldb.SBStructuredData(),
error,
)
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
# Verify we have 2 more frames
new_frame_count = thread.GetNumFrames()
self.assertEqual(new_frame_count, original_frame_count + 2)
# Verify first 2 frames are synthetic (check PCs, not function names)
frame0 = thread.GetFrameAtIndex(0)
self.assertEqual(frame0.GetPC(), 0x9000)
frame1 = thread.GetFrameAtIndex(1)
self.assertEqual(frame1.GetPC(), 0xA000)
# Verify frame 2 is the original real frame 0
frame2 = thread.GetFrameAtIndex(2)
self.assertIn("thread_func", frame2.GetFunctionName())
def test_append_frames(self):
"""Test that we can add frames after real stack."""
self.build()
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
)
# Get original frame count
original_frame_count = thread.GetNumFrames()
# Import and attach Append provider
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
self.runCmd("command script import " + script_path)
error = lldb.SBError()
provider_id = target.RegisterScriptedFrameProvider(
"test_frame_providers.AppendFrameProvider",
lldb.SBStructuredData(),
error,
)
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
# Verify we have 1 more frame
new_frame_count = thread.GetNumFrames()
self.assertEqual(new_frame_count, original_frame_count + 1)
# Verify first frames are still real
frame0 = thread.GetFrameAtIndex(0)
self.assertIn("thread_func", frame0.GetFunctionName())
frame_n_plus_1 = thread.GetFrameAtIndex(new_frame_count - 1)
self.assertEqual(frame_n_plus_1.GetPC(), 0x10)
def test_scripted_frame_objects(self):
"""Test that provider can return ScriptedFrame objects."""
self.build()
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
)
# Import the provider that returns ScriptedFrame objects
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
self.runCmd("command script import " + script_path)
error = lldb.SBError()
provider_id = target.RegisterScriptedFrameProvider(
"test_frame_providers.ScriptedFrameObjectProvider",
lldb.SBStructuredData(),
error,
)
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
# Verify we have 5 frames
self.assertEqual(
thread.GetNumFrames(), 5, "Should have 5 custom scripted frames"
)
# Verify frame properties from CustomScriptedFrame
frame0 = thread.GetFrameAtIndex(0)
self.assertIsNotNone(frame0)
self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
self.assertEqual(frame0.GetPC(), 0x5000)
self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")
frame1 = thread.GetFrameAtIndex(1)
self.assertIsNotNone(frame1)
self.assertEqual(frame1.GetPC(), 0x6000)
frame2 = thread.GetFrameAtIndex(2)
self.assertIsNotNone(frame2)
self.assertEqual(frame2.GetFunctionName(), "custom_scripted_frame_2")
self.assertEqual(frame2.GetPC(), 0x7000)
self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic")
def test_applies_to_thread(self):
"""Test that applies_to_thread filters which threads get the provider."""
self.build()
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
)
# We should have at least 2 threads (worker threads) at the breakpoint
num_threads = process.GetNumThreads()
self.assertGreaterEqual(
num_threads, 2, "Should have at least 2 threads at breakpoint"
)
# Import the test frame provider
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
self.runCmd("command script import " + script_path)
# Collect original thread info before applying provider
thread_info = {}
for i in range(num_threads):
t = process.GetThreadAtIndex(i)
thread_info[t.GetIndexID()] = {
"frame_count": t.GetNumFrames(),
"pc": t.GetFrameAtIndex(0).GetPC(),
}
# Register the ThreadFilterFrameProvider which only applies to thread ID 1
error = lldb.SBError()
provider_id = target.RegisterScriptedFrameProvider(
"test_frame_providers.ThreadFilterFrameProvider",
lldb.SBStructuredData(),
error,
)
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
# Check each thread
thread_id_1_found = False
# On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified
is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
expected_synthetic_pc = 0xFFFE if is_arm_32bit else 0xFFFF
for i in range(num_threads):
t = process.GetThreadAtIndex(i)
thread_id = t.GetIndexID()
if thread_id == 1:
# Thread with ID 1 should have synthetic frame
thread_id_1_found = True
self.assertEqual(
t.GetNumFrames(),
1,
f"Thread with ID 1 should have 1 synthetic frame",
)
self.assertEqual(
t.GetFrameAtIndex(0).GetPC(),
expected_synthetic_pc,
f"Thread with ID 1 should have synthetic PC {expected_synthetic_pc:#x}",
)
else:
# Other threads should keep their original frames
self.assertEqual(
t.GetNumFrames(),
thread_info[thread_id]["frame_count"],
f"Thread with ID {thread_id} should not be affected by provider",
)
self.assertEqual(
t.GetFrameAtIndex(0).GetPC(),
thread_info[thread_id]["pc"],
f"Thread with ID {thread_id} should have its original PC",
)
# We should have found at least one thread with ID 1
self.assertTrue(
thread_id_1_found,
"Should have found a thread with ID 1 to test filtering",
)
def test_remove_frame_provider_by_id(self):
"""Test that RemoveScriptedFrameProvider removes a specific provider by ID."""
self.build()
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
)
# Import the test frame providers
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
self.runCmd("command script import " + script_path)
# Get original frame count
original_frame_count = thread.GetNumFrames()
original_pc = thread.GetFrameAtIndex(0).GetPC()
# Register the first provider and get its ID
error = lldb.SBError()
provider_id_1 = target.RegisterScriptedFrameProvider(
"test_frame_providers.ReplaceFrameProvider",
lldb.SBStructuredData(),
error,
)
self.assertTrue(error.Success(), f"Failed to register provider 1: {error}")
# Verify first provider is active (3 synthetic frames)
self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
self.assertEqual(
thread.GetFrameAtIndex(0).GetPC(), 0x1000, "Should have first provider's PC"
)
# Register a second provider and get its ID
provider_id_2 = target.RegisterScriptedFrameProvider(
"test_frame_providers.PrependFrameProvider",
lldb.SBStructuredData(),
error,
)
self.assertTrue(error.Success(), f"Failed to register provider 2: {error}")
# Verify IDs are different
self.assertNotEqual(
provider_id_1, provider_id_2, "Provider IDs should be unique"
)
# Now remove the first provider by ID
result = target.RemoveScriptedFrameProvider(provider_id_1)
self.assertSuccess(
result, f"Should successfully remove provider with ID {provider_id_1}"
)
# After removing the first provider, the second provider should still be active
# The PrependFrameProvider adds 2 frames before the real stack
# Since ReplaceFrameProvider had 3 frames, and we removed it, we should now
# have the original frames (from real stack) with PrependFrameProvider applied
new_frame_count = thread.GetNumFrames()
self.assertEqual(
new_frame_count,
original_frame_count + 2,
"Should have original frames + 2 prepended frames",
)
# First two frames should be from PrependFrameProvider
self.assertEqual(
thread.GetFrameAtIndex(0).GetPC(),
0x9000,
"First frame should be from PrependFrameProvider",
)
self.assertEqual(
thread.GetFrameAtIndex(1).GetPC(),
0xA000,
"Second frame should be from PrependFrameProvider",
)
# Remove the second provider
result = target.RemoveScriptedFrameProvider(provider_id_2)
self.assertSuccess(
result, f"Should successfully remove provider with ID {provider_id_2}"
)
# After removing both providers, frames should be back to original
self.assertEqual(
thread.GetNumFrames(),
original_frame_count,
"Should restore original frame count",
)
self.assertEqual(
thread.GetFrameAtIndex(0).GetPC(),
original_pc,
"Should restore original PC",
)
# Try to remove a provider that doesn't exist
result = target.RemoveScriptedFrameProvider(999999)
self.assertTrue(result.Fail(), "Should fail to remove non-existent provider")
def test_circular_dependency_fix(self):
"""Test that accessing input_frames in __init__ doesn't cause circular dependency.
This test verifies the fix for the circular dependency issue where:
1. Thread::GetStackFrameList() creates the frame provider
2. Provider's __init__ accesses input_frames and calls methods on frames
3. SBFrame methods trigger ExecutionContextRef::GetFrameSP()
4. Before the fix: GetFrameSP() would call Thread::GetStackFrameList() again -> circular dependency!
5. After the fix: GetFrameSP() uses the remembered frame list -> no circular dependency
The fix works by:
- StackFrame stores m_frame_list_wp (weak pointer to originating list)
- ExecutionContextRef stores m_frame_list_wp when created from a frame
- ExecutionContextRef::GetFrameSP() tries the remembered list first before asking the thread
"""
self.build()
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
)
# Get original frame count and PC
original_frame_count = thread.GetNumFrames()
original_pc = thread.GetFrameAtIndex(0).GetPC()
self.assertGreaterEqual(
original_frame_count, 2, "Should have at least 2 real frames"
)
# Import the provider that accesses input frames in __init__
script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
self.runCmd("command script import " + script_path)
# Register the CircularDependencyTestProvider
# Before the fix, this would crash or hang due to circular dependency
error = lldb.SBError()
provider_id = target.RegisterScriptedFrameProvider(
"test_frame_providers.CircularDependencyTestProvider",
lldb.SBStructuredData(),
error,
)
# If we get here without crashing, the fix is working!
self.assertTrue(error.Success(), f"Failed to register provider: {error}")
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
# Verify the provider worked correctly
# Should have 1 synthetic frame + all original frames
new_frame_count = thread.GetNumFrames()
self.assertEqual(
new_frame_count,
original_frame_count + 1,
"Should have original frames + 1 synthetic frame",
)
# On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified
is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
expected_synthetic_pc = 0xDEADBEEE if is_arm_32bit else 0xDEADBEEF
# First frame should be synthetic
frame0 = thread.GetFrameAtIndex(0)
self.assertIsNotNone(frame0)
self.assertEqual(
frame0.GetPC(),
expected_synthetic_pc,
f"First frame should be synthetic frame with PC {expected_synthetic_pc:#x}",
)
# Second frame should be the original first frame
frame1 = thread.GetFrameAtIndex(1)
self.assertIsNotNone(frame1)
self.assertEqual(
frame1.GetPC(),
original_pc,
"Second frame should be original first frame",
)
# Verify we can still call methods on frames (no circular dependency!)
for i in range(min(3, new_frame_count)):
frame = thread.GetFrameAtIndex(i)
self.assertIsNotNone(frame)
# These calls should not trigger circular dependency
pc = frame.GetPC()
self.assertNotEqual(pc, 0, f"Frame {i} should have valid PC")