| // 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); | |
| // "Finish" from the top stack frame, which is an inline one. | |
| 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; | |
| }); | |
| // 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()); | |
| } | |
| // 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); | |
| // "Finish" frame 3, | |
| auto finish_controller = std::make_unique<FinishThreadController>(thread()->GetStack(), 3); | |
| 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.process_koid = process()->GetKoid(); | |
| exception.thread.thread_koid = 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); | |
| // 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. | |
| InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(), | |
| debug_ipc::ExceptionType::kSoftwareBreakpoint, | |
| MockFrameVectorToFrameVector(std::move(stack)), true); | |
| // 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 |