blob: 2b0685c2123e6d2b54429ccace9c0c7de30596d0 [file] [log] [blame]
// 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/step_thread_controller.h"
#include "src/developer/debug/ipc/protocol.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/session.h"
#include "src/developer/debug/zxdb/client/setting_schema_definition.h"
#include "src/developer/debug/zxdb/client/system.h"
#include "src/developer/debug/zxdb/client/thread.h"
#include "src/developer/debug/zxdb/common/err.h"
#include "src/developer/debug/zxdb/symbols/elf_symbol.h"
#include "src/developer/debug/zxdb/symbols/function.h"
#include "src/developer/debug/zxdb/symbols/line_details.h"
#include "src/developer/debug/zxdb/symbols/mock_module_symbols.h"
namespace zxdb {
class StepThreadControllerTest : public InlineThreadControllerTest {
public:
// Sets the "on-unsymbolized" setting based on the input flag.
void SetUnsymbolizedSetting(bool stop_on_no_symbols);
// Does two tests that are very similar based on the flag.
//
// Steps with some instructions on a line, followed by an inline function.
//
// When |separate_line| is true, the inline function call will be on the following line. A step
// into command should step over the first line, leaving the stack about to call into the inline
// frame.
//
// When |separate_line| is false, the inline call will be on the same line being stepped and
// a step into command should stop at the first instruction inside the inline function. This
// will be the same physical instruction as the first case, but the stacks will be different.
void DoIntoInlineFunctionTest(bool separate_line);
};
// Software exceptions should always stop execution. These might be from something like a hardcoded
// breakpoint instruction in the code. Doing "step" shouldn't skip over these.
TEST_F(StepThreadControllerTest, SofwareException) {
// Step as long as we're in this range. Using the "code range" for stepping allows us to avoid
// dependencies on the symbol subsystem.
constexpr uint64_t kBeginAddr = 0x1000;
constexpr uint64_t kEndAddr = 0x1010;
// Set up the thread to be stopped at the beginning of our range.
debug_ipc::NotifyException exception;
exception.type = debug_ipc::ExceptionType::kSingleStep;
exception.thread.id = {.process = process()->GetKoid(), .thread = thread()->GetKoid()};
exception.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
exception.thread.frames.emplace_back(kBeginAddr, 0x5000);
InjectException(exception);
// Continue the thread with the controller stepping in range.
auto step_into =
std::make_unique<StepThreadController>(AddressRanges(AddressRange(kBeginAddr, kEndAddr)));
bool continued = false;
thread()->ContinueWith(std::move(step_into), [&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());
// Issue a software exception in the range.
exception.type = debug_ipc::ExceptionType::kSoftwareBreakpoint;
exception.thread.frames[0].ip += 4;
InjectException(exception);
// It should have stayed stopped despite being in range.
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stopped
EXPECT_EQ(debug_ipc::ThreadRecord::State::kBlocked, thread()->GetState());
}
// Some entries in the line table may have their line number set to zero. These indicate code
// generated by the compiler not associated with any line number. These should be transparently
// stepped over when stepping by line.
//
// This test tests the case where the line table has 10, 0, 10 11. Stepping from the first "10" line
// should end up on "11".
TEST_F(StepThreadControllerTest, Line0) {
FileLine line0;
FileLine line10("/path/file.cc", 10);
FileLine line11("/path/file.cc", 11);
const uint64_t kAddr1 = kSymbolizedModuleAddress + 0x100; // Line 10
const uint64_t kAddr2 = kAddr1 + 4; // Line 0
const uint64_t kAddr3 = kAddr2 + 4; // Line 10
const uint64_t kAddr4 = kAddr3 + 4; // Line 11
LineDetails line_details1(line10);
line_details1.entries().push_back({21, AddressRange(kAddr1, kAddr2)});
LineDetails line_details2(line0);
// Column 0 indicates the "whole line".
line_details2.entries().push_back({0, AddressRange(kAddr2, kAddr3)});
// Same line 10 as above but starts at a different column (this time 7).
LineDetails line_details3(line10);
line_details3.entries().push_back({7, AddressRange(kAddr3, kAddr4)});
LineDetails line_details4(line11);
line_details4.entries().push_back({0, AddressRange(kAddr4, kAddr4 + 4)});
module_symbols()->AddLineDetails(kAddr1, line_details1);
module_symbols()->AddLineDetails(kAddr2, line_details2);
module_symbols()->AddLineDetails(kAddr3, line_details3);
module_symbols()->AddLineDetails(kAddr4, line_details4);
// Set up the thread to be stopped at the beginning of our range.
std::vector<std::unique_ptr<MockFrame>> mock_frames;
mock_frames.push_back(GetTopFrame(kAddr1));
mock_frames[0]->SetFileLine(line10);
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// Continue the thread with the controller stepping in range.
auto step_into = std::make_unique<StepThreadController>(StepMode::kSourceLine);
bool continued = false;
thread()->ContinueWith(std::move(step_into), [&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());
// Stop on 2nd instruction (line 0). This should be automatically resumed.
mock_frames.push_back(GetTopFrame(kAddr2));
mock_frames[0]->SetFileLine(line0);
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount());
// Stop on 3rd instruction (line 10). Since this matches the original line, it should be
// automatically resumed.
mock_frames.push_back(GetTopFrame(kAddr3));
mock_frames[0]->SetFileLine(line10);
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount());
// Stop on 4th instruction. Since this is line 11, we should stay stopped.
mock_frames.push_back(GetTopFrame(kAddr4));
mock_frames[0]->SetFileLine(line11);
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stopped
EXPECT_EQ(debug_ipc::ThreadRecord::State::kBlocked, thread()->GetState());
}
void StepThreadControllerTest::SetUnsymbolizedSetting(bool stop_on_no_symbols) {
thread()->session()->system().settings().SetBool(ClientSettings::System::kSkipUnsymbolized,
!stop_on_no_symbols);
}
// Tests doing a "step" when the current instruction is about to execute the first instruction of
// an inline, with the current function being the caller. This location is ambiguous so the step
// operation should go into the inline without actually executing any code.
TEST_F(StepThreadControllerTest, AmbiguousInline) {
// Recall the top frame from GetStack() is inline.
auto mock_frames = GetStack();
// Stepping into the 0th frame from the first. These are the source locations.
FileLine file_line = mock_frames[1]->GetLocation().file_line();
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// Hide the inline frame at the top so we're about to step into it.
Stack& stack = thread()->GetStack();
stack.SetHideAmbiguousInlineFrameCount(1);
// Provide a line mapping for this address so the symbolic stepping doesn't think we're in code
// with no symbols. This maps the first address of the inline function to its code. This is
// specifically not the file/line above because the line table maps generated code which will be
// the inlined function.
module_symbols()->AddLineDetails(
kTopInlineFunctionRange.begin(),
LineDetails(kTopInlineFileLine, {LineDetails::LineEntry(kTopInlineFunctionRange)}));
// Do the "step into".
auto step_into_controller = std::make_unique<StepThreadController>(StepMode::kSourceLine);
bool continued = false;
thread()->ContinueWith(std::move(step_into_controller), [&continued](const Err& err) {
if (!err.has_error())
continued = true;
});
EXPECT_TRUE(continued);
// That should have requested a synthetic exception which will be sent out asynchronously. The
// Resume() call will cause the MockRemoteAPI to exit the message loop.
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Nothing yet.
loop().RunUntilNoTasks();
// The operation should have unhidden the inline stack frame rather than actually affecting the
// backend.
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount());
EXPECT_EQ(0u, stack.hide_ambiguous_inline_frame_count());
// Now that we're at the top of the inline stack, do a subsequent "step into" which this time
// should resume the backend.
step_into_controller = std::make_unique<StepThreadController>(StepMode::kSourceLine);
continued = false;
thread()->ContinueWith(std::move(step_into_controller), [&continued](const Err& err) {
if (!err.has_error())
continued = true;
});
EXPECT_TRUE(continued);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount());
EXPECT_EQ(0u, stack.hide_ambiguous_inline_frame_count());
}
void StepThreadControllerTest::DoIntoInlineFunctionTest(bool separate_line) {
// Recall the top frame from GetStack() is inline. We delete that and set the instruction pointer
// back a few so that it looks like it's stepping in the function before it gets to the inline.
auto mock_frames = GetStack();
mock_frames.erase(mock_frames.begin());
uint64_t kStepBegin = mock_frames[0]->GetAddress() - 4;
mock_frames[0]->SetAddress(kStepBegin);
FileLine file_line = mock_frames[0]->GetLocation().file_line();
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// Provide a line mapping for the code we step immediately before the inline call.
AddressRange before_inline_range(kStepBegin, kTopInlineFunctionRange.begin());
module_symbols()->AddLineDetails(
kStepBegin, LineDetails(file_line, {LineDetails::LineEntry(before_inline_range)}));
// Line information for the inline function (otherwise it will try to step through the
// unsymbolized function).
module_symbols()->AddLineDetails(
kTopInlineFunctionRange.begin(),
LineDetails(kTopInlineFileLine, {LineDetails::LineEntry(kTopInlineFunctionRange)}));
// Do the "step into".
auto step_into_controller = std::make_unique<StepThreadController>(StepMode::kSourceLine);
bool continued = false;
thread()->ContinueWith(std::move(step_into_controller), [&continued](const Err& err) {
if (!err.has_error())
continued = true;
});
EXPECT_TRUE(continued);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continued.
// Construct a stop at the beginning of the inline function. The reported stack will include all
// inline frames and the step controller will have to figure out what to do.
mock_frames = GetStack();
// Fix up the inline call location based on the type of test we're doing.
Location loc = mock_frames[0]->GetLocation();
Function* function = const_cast<Function*>(loc.symbol().Get()->As<Function>());
ASSERT_TRUE(function);
FileLine call_line;
if (separate_line)
call_line = FileLine(file_line.file(), file_line.line() + 1);
else
call_line = file_line;
function->set_call_line(call_line);
mock_frames[0]->set_location(
Location(loc.address(), loc.file_line(), loc.column(), loc.symbol_context(), function));
// The previous frame's location should match the inline call line (this will always be the
// case for inlines -- the Stack does this computation).
loc = mock_frames[1]->GetLocation();
mock_frames[1]->set_location(
Location(loc.address(), call_line, loc.column(), loc.symbol_context(), loc.symbol()));
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// The controller should have reported stop and left the stack at the first instruction of the
// inline, but whether it counts as being inside the inline or not depends on whether the inline
// was called on the line being stepped or not.
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stopped.
Stack& stack = thread()->GetStack();
if (separate_line)
EXPECT_EQ(1u, stack.hide_ambiguous_inline_frame_count());
else
EXPECT_EQ(0u, stack.hide_ambiguous_inline_frame_count());
}
TEST_F(StepThreadControllerTest, IntoInlineSameLine) { DoIntoInlineFunctionTest(false); }
TEST_F(StepThreadControllerTest, IntoInlineNextLine) { DoIntoInlineFunctionTest(true); }
// This tests when we're in the middle of a step and there's a "line 0" entry (indicating
// compiler-generated code with no corresponding line number) followed by a different line. This is
// an interesting case because the stack frame will "fix" the line 0 to the next entry to avoid
// showing the user "no line information" errors. But we want to continue over the "line 0" code and
// stop at the inline function.
TEST_F(StepThreadControllerTest, StepThroughLine0) {
SymbolContext symbol_context(kSymbolizedModuleAddress);
// Start location for the step.
constexpr uint64_t kStartIp = kSymbolizedModuleAddress + 0x1000;
constexpr uint64_t kSp = 0x100000;
FileLine step_file_line("file.cc", 10);
Location start_loc(kStartIp, step_file_line, 0, symbol_context);
// Line table entry for the initial location.
AddressRange start_range(kStartIp, kStartIp + 8);
module_symbols()->AddLineDetails(
kStartIp, LineDetails(step_file_line, {LineDetails::LineEntry(start_range)}));
// Inject stop for the initial location.
std::vector<std::unique_ptr<Frame>> stack;
stack.push_back(std::make_unique<MockFrame>(nullptr, nullptr, start_loc, kSp, kSp));
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep, std::move(stack), true);
// Create a "step" controller to get over this line. This will continue execution.
auto step_into = std::make_unique<StepThreadController>(StepMode::kSourceLine);
bool continued = false;
thread()->ContinueWith(std::move(step_into), [&continued](const Err& err) {
if (!err.has_error())
continued = true;
});
EXPECT_TRUE(continued);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continued.
// Provide a line mapping entry for the "line 0" location we're about to stop at. This follows the
// line stepped over above.
uint64_t kLine0Ip = start_range.end();
AddressRange line0_range(kLine0Ip, kLine0Ip + 8);
module_symbols()->AddLineDetails(kLine0Ip,
LineDetails(FileLine(), {LineDetails::LineEntry(line0_range)}));
// Stop at the "line 0" address. This stack's file_line will show the *next* entry in the line
// table. This should be ignored in favor of the data we inserted into the LineDetails above.
Location line0_loc(kLine0Ip, FileLine("file.cc", 11), 0, symbol_context);
stack.push_back(std::make_unique<MockFrame>(nullptr, nullptr, line0_loc, kSp, kSp));
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep, std::move(stack), true);
// It should automatically continue over the "line 0" entry.
EXPECT_TRUE(continued);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount());
}
// Does a step in range that results in finishing the current function.
TEST_F(StepThreadControllerTest, StepOut) {
SymbolContext symbol_context(kSymbolizedModuleAddress);
// Recall the top frame from GetStack() is inline which we don't want, so we remove it to leave
// "TopFunction" at the top of the stack.
auto mock_frames = GetStack();
mock_frames.erase(mock_frames.begin(), mock_frames.begin() + 1);
// Start with an exception in the function.
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;
// Step in the top function's range, logging the return_info.
auto step_into = std::make_unique<StepThreadController>(
AddressRanges(kTopFunctionRange),
[&return_info](const FunctionReturnInfo& info) { return_info = info; });
bool continued = false;
thread()->ContinueWith(std::move(step_into), [&continued](const Err& err) {
if (!err.has_error())
continued = true;
});
EXPECT_TRUE(continued);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continued.
// Issue an intermediate stop while in range (construct the stack the same was as above).
mock_frames = GetStack();
mock_frames.erase(mock_frames.begin(), mock_frames.begin() + 1);
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// The thread should be auto-resumed.
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continued.
EXPECT_FALSE(return_info);
// Issue a stop at the previous frame.
mock_frames = GetStack();
mock_frames.erase(mock_frames.begin(), mock_frames.begin() + 2); // Remove top two.
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// Should not have been resumed and the return callback should be issued.
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount());
ASSERT_TRUE(return_info);
EXPECT_EQ(thread(), return_info->thread);
EXPECT_EQ(GetTopFunction()->GetAssignedName(), return_info->symbol.Get()->GetAssignedName());
}
} // namespace zxdb