| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/developer/debug/zxdb/client/finish_thread_controller.h" |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/developer/debug/zxdb/client/inline_thread_controller_test.h" |
| #include "src/developer/debug/zxdb/client/process.h" |
| #include "src/developer/debug/zxdb/client/thread.h" |
| #include "src/developer/debug/zxdb/common/err.h" |
| #include "src/developer/debug/zxdb/symbols/function.h" |
| #include "src/developer/debug/zxdb/symbols/line_details.h" |
| |
| namespace zxdb { |
| |
| namespace { |
| |
| class FinishThreadControllerTest : public InlineThreadControllerTest {}; |
| |
| } // namespace |
| |
| // See also the FinishPhysicalFrameThreadController tests. |
| |
| // Tests finishing a single inline frame. This finishes the top frame of the stack which is an |
| // inline function (see InlineThreadControllerTest for what the returned stack layout is). |
| TEST_F(FinishThreadControllerTest, FinishInline) { |
| auto mock_frames = GetStack(); |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(mock_frames)), true); |
| |
| // Since this never steps over a non-inline frame, the function return callback should never |
| // be called. |
| bool function_completion_called = false; |
| |
| // "Finish" from the top stack frame, which is an inline one. |
| auto finish_controller = std::make_unique<FinishThreadController>( |
| thread()->GetStack(), 0, [&function_completion_called](const FunctionReturnInfo&) { |
| function_completion_called = true; |
| }); |
| bool continued = false; |
| thread()->ContinueWith(std::move(finish_controller), [&continued](const Err& err) { |
| if (!err.has_error()) |
| continued = true; |
| }); |
| |
| // It should have been able to step without doing any further async work. |
| EXPECT_TRUE(continued); |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| |
| // Do one step inside the inline function (add 4 to the address). |
| mock_frames = GetStack(); |
| mock_frames[0]->SetAddress(mock_frames[0]->GetAddress() + 4); |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(mock_frames)), true); |
| |
| // That's still inside the frame's range, so it should continue. |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| |
| // Set exception at the first instruction after the inline frame. |
| mock_frames = GetStack(); |
| uint64_t after_inline = kTopInlineFunctionRange.end(); |
| mock_frames.erase(mock_frames.begin()); // Remove the inline function. |
| mock_frames[0]->SetAddress(after_inline); |
| |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(mock_frames)), true); |
| |
| // Should not have resumed. |
| EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); |
| EXPECT_EQ(debug_ipc::ThreadRecord::State::kBlocked, thread()->GetState()); |
| |
| // None of the above stepping should have triggered a non-inline function return. |
| EXPECT_FALSE(function_completion_called); |
| } |
| |
| // Finishes multiple frames, consisting of one physical frame finish followed by two inline frame |
| // finishes. This finishes to frame 4 (see InlineThreadControllerTest) which is the "middle" |
| // physical frame. It requires doing a "finish" of the top physical frame, then stepping through |
| // both middle inline frames. |
| TEST_F(FinishThreadControllerTest, FinishPhysicalAndInline) { |
| auto mock_frames = GetStack(); |
| uint64_t frame_2_ip = mock_frames[2]->GetAddress(); |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(mock_frames)), true); |
| |
| // Holds the result of any seen non-inline function returns. |
| std::optional<FunctionReturnInfo> return_info; |
| |
| // "Finish" frame 3, |
| auto finish_controller = std::make_unique<FinishThreadController>( |
| thread()->GetStack(), 3, |
| [&return_info](const FunctionReturnInfo& info) { return_info = info; }); |
| bool continued = false; |
| thread()->ContinueWith(std::move(finish_controller), [&continued](const Err& err) { |
| if (!err.has_error()) |
| continued = true; |
| }); |
| |
| // That should have sent a resume + a breakpoint set at the frame 2 IP (this breakpoint is |
| // implementing the "finish" to step out of the frame 1 physical frame). |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| EXPECT_EQ(0, mock_remote_api()->breakpoint_remove_count()); |
| EXPECT_EQ(frame_2_ip, mock_remote_api()->last_breakpoint_address()); |
| |
| // Simulate a breakpoint hit of that breakpoint (breakpoint exceptions are "software"). |
| debug_ipc::NotifyException exception; |
| exception.type = debug_ipc::ExceptionType::kSoftwareBreakpoint; |
| exception.thread.id.process = process()->GetKoid(); |
| exception.thread.id.thread = thread()->GetKoid(); |
| exception.thread.state = debug_ipc::ThreadRecord::State::kBlocked; |
| exception.hit_breakpoints.emplace_back(); |
| exception.hit_breakpoints[0].id = mock_remote_api()->last_breakpoint_id(); |
| |
| // Create a stack now showing frame 2 as the top (new frame 0). |
| mock_frames = GetStack(); |
| mock_frames.erase(mock_frames.begin(), mock_frames.begin() + 3); |
| InjectExceptionWithStack(exception, MockFrameVectorToFrameVector(std::move(mock_frames)), true); |
| |
| // That should have triggered the function return call indicating the top function returned. |
| ASSERT_TRUE(return_info); |
| EXPECT_EQ(thread(), return_info->thread); |
| EXPECT_EQ(GetTopFunction()->GetAssignedName(), return_info->symbol.Get()->GetAssignedName()); |
| |
| // The breakpoint should have been cleared and the thread should have been resumed. |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| EXPECT_EQ(1, mock_remote_api()->breakpoint_remove_count()); |
| |
| // Do another stop 4 bytes later in the inline frame 2 which should get continued. |
| mock_frames = GetStack(); |
| mock_frames.erase(mock_frames.begin(), mock_frames.begin() + 3); |
| mock_frames[0]->SetAddress(mock_frames[0]->GetAddress() + 4); |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(mock_frames)), true); |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| |
| // Stop in inline frame 1. This leaves inline frame 2 (right after its address range) but should |
| // still continue since we haven't reached the target. |
| mock_frames = GetStack(); |
| mock_frames.erase(mock_frames.begin(), mock_frames.begin() + 4); |
| mock_frames[0]->SetAddress(kMiddleInline2FunctionRange.end()); |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(mock_frames)), true); |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| |
| // Stop in middle frame which is the target (right after the inline 1 range). |
| mock_frames = GetStack(); |
| mock_frames.erase(mock_frames.begin(), mock_frames.begin() + 5); |
| mock_frames[0]->SetAddress(kMiddleInline1FunctionRange.end()); |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(mock_frames)), true); |
| EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stopped. |
| } |
| |
| // This sets up a situation where the finish controller creates a "step over" controller in response |
| // to a breakpoint hit exception. The step over controller should not see the breakpoint hit and |
| // should continue as if it was not created from within a breakpoint hit. |
| // |
| // The situation where this can happen is: |
| // |
| // FinishThreadController (FINISH#1) creates a new StepOverThreadController (OVER#1). |
| // OVER finds a physical function call and |
| // Creates a FinishThreadController (FINISH#2) to get out of it. |
| // FINISH#2 creates a FinishPhysicalFrameThreadController (PHYSICAL) to get out of it. |
| // The breakpoint for PHYSICAL is hit. |
| // FINISH#2 completes. |
| // OVER#1 completes. |
| // FINISH#1 notices a new inline subframe immediately following the first. |
| // FINISH#1 creates a new StepOverThreadController (OVER#2) |
| TEST_F(FinishThreadControllerTest, FinishPhysicalAndInline2) { |
| // Stack: |
| // [0] MiddleInline2 <- OVER#1 |
| // [1] MiddleInline1 <- finishing this one. |
| // [2] Middle |
| // [3] Bottom |
| auto stack = GetStack(); |
| stack.erase(stack.begin(), stack.begin() + 2); // Drop the "top" frames from the mock input. |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(stack)), true); |
| |
| // Create FINISH#1 from above. This should notice we're in an inline frame, create OVER#1, and |
| // continue. |
| auto finish_controller = std::make_unique<FinishThreadController>(thread()->GetStack(), 0); |
| bool continued = false; |
| thread()->ContinueWith(std::move(finish_controller), [&continued](const Err& err) { |
| if (!err.has_error()) |
| continued = true; |
| }); |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| EXPECT_EQ(0, mock_remote_api()->breakpoint_remove_count()); |
| |
| // Simulate a physical frame call. |
| // |
| // Stack: |
| // [0] Top <- PHYSICAL |
| // [1] MiddleInline2 <- OVER#1 |
| // [2] MiddleInline1 <- finishing this one. |
| // [3] Middle |
| // [4] Bottom |
| stack = GetStack(); |
| stack.erase(stack.begin(), stack.begin() + 1); // Drop the top inline frame from the mock input. |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(stack)), true); |
| // That should have created PHYSICAL which will set a breakpoint on the return addr. |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| EXPECT_EQ(1, mock_remote_api()->breakpoint_add_count()); |
| |
| // Simulate a return from the physical frame call to a new inline frame |
| // |
| // Stack: |
| // [1] MiddleInline2.2 <- OVER#2 |
| // [2] MiddleInline1 <- finishing this one. |
| // [3] Middle |
| // [4] Bottom |
| stack = GetStack(); |
| stack.erase(stack.begin(), stack.begin() + 2); // Drop both "top" frames. |
| |
| // Fix up the location so the MiddleInlin2 becomes MiddleInline2.2, a different inline function |
| // immediately following it. |
| const AddressRange middle_2_2_range(kMiddleInline2FunctionRange.end(), |
| kMiddleInline2FunctionRange.end() + 2); |
| auto middle2_2_func = fxl::MakeRefCounted<Function>(DwarfTag::kInlinedSubroutine); |
| middle2_2_func->set_assigned_name("MiddleInline2.2"); |
| middle2_2_func->set_code_ranges(AddressRanges(middle_2_2_range)); |
| stack[0]->set_location(Location(middle_2_2_range.begin(), kMiddleInline2FileLine, 0, |
| SymbolContext::ForRelativeAddresses(), middle2_2_func)); |
| |
| // Send the software breakpoint exception for PHYSICAL to finish. |
| debug_ipc::BreakpointStats breakpoint{ |
| .id = static_cast<uint32_t>(mock_remote_api()->last_breakpoint_id()), .hit_count = 1}; |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSoftwareBreakpoint, |
| MockFrameVectorToFrameVector(std::move(stack)), true, {breakpoint}); |
| // That should have finished PHYSICAL (deleting the temporary breakpoint) and OVER#1. Then started |
| // stepping over OVER#2 which should continue. |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| EXPECT_EQ(1, mock_remote_api()->breakpoint_remove_count()); |
| } |
| |
| // Tests that compiler generated ("line 0") code immediately following a function call is skipped |
| // when finishing a frame. |
| TEST_F(FinishThreadControllerTest, FinishToCompilerGenerated) { |
| // This finishes the top inline frame of the default stack because it's the most convenient |
| // thing to do. |
| |
| // Full stack for the starting point. |
| auto stack = GetStack(); |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(stack)), true); |
| |
| // Finish the top frame. This should continue through the inline. |
| auto finish_controller = std::make_unique<FinishThreadController>(thread()->GetStack(), 0); |
| bool continued = false; |
| thread()->ContinueWith(std::move(finish_controller), [&continued](const Err& err) { |
| if (!err.has_error()) |
| continued = true; |
| }); |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| |
| // Set up line table information for the location immediately after the inline. It consists of |
| // a "line 0" region followed by a regular region. |
| const uint64_t kLine0Begin = kTopInlineFunctionRange.end(); |
| const uint64_t kNormalLineBegin = kLine0Begin + 4; |
| module_symbols()->AddLineDetails( |
| kLine0Begin, |
| LineDetails(FileLine("", 0), |
| {LineDetails::LineEntry(AddressRange(kLine0Begin, kNormalLineBegin))})); |
| FileLine normal_file_line("file.cc", 27); |
| module_symbols()->AddLineDetails( |
| kNormalLineBegin, |
| LineDetails(normal_file_line, |
| {LineDetails::LineEntry(AddressRange(kNormalLineBegin, kNormalLineBegin + 4))})); |
| |
| // Inject an exception at the end of the inline frame. The controller should continue from here. |
| stack = GetStack(); |
| stack.erase(stack.begin()); // Remove inline frame to leave the "top" physical frame at the top. |
| Location old_top_location = stack[0]->GetLocation(); |
| stack[0]->set_location(Location(kLine0Begin, FileLine("", 0), 0, |
| old_top_location.symbol_context(), old_top_location.symbol())); |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(stack)), true); |
| EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); |
| |
| // Now do an exception at the normal line region following it. The controller should stop. |
| stack = GetStack(); |
| stack.erase(stack.begin()); // Remove inline frame to leave the "top" physical frame at the top. |
| old_top_location = stack[0]->GetLocation(); |
| stack[0]->set_location(Location(kNormalLineBegin, normal_file_line, 0, |
| old_top_location.symbol_context(), old_top_location.symbol())); |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), |
| debug_ipc::ExceptionType::kSingleStep, |
| MockFrameVectorToFrameVector(std::move(stack)), true); |
| EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stopped. |
| } |
| |
| } // namespace zxdb |