blob: 7c9d138c612110523d4bd5d31e54a61dfc4287a6 [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/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"
#include "src/developer/debug/zxdb/symbols/mock_module_symbols.h"
namespace zxdb {
class StepThreadControllerTest : public InlineThreadControllerTest {
public:
// Shared code for the shared lib thunk tests. There are two variants of this test, one where we
// want to skip the thunks, and one where we don't. The parameter controls which variant of the
// test to run.
void DoSharedLibThunkTest(bool stop_on_no_symbols);
// Backend that runs a test for stepping into an unsymbolized function, both for when we want it
// to stop (param = true) and continue (param = false).
void DoUnsymbolizedFunctionTest(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.process_koid = process()->GetKoid();
exception.thread.thread_koid = 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());
}
// Tests shared library thunks which have no symbol information in a module which otherwise has
// symbols.
//
// A cross module function call looks like
// 1. A call to an address in the same module.
// 2. That is an indirect jump to an address (the dynamic loader fills in the destination address
// when imports are resolved). This jump has no symbol information since it's generated by the
// linker.
// 3. Normal code in another module.
void StepThreadControllerTest::DoSharedLibThunkTest(bool stop_on_no_symbols) {
FileLine src_line("/path/src.cc", 1);
FileLine dest_line("/path/dest.cc", 2);
const uint64_t kAddrSrc = kSymbolizedModuleAddress + 0x100; // Line 1
const uint64_t kAddrThunk = kSymbolizedModuleAddress + 0x10000; // No symbols.
// This is technically in the same module (normally it would be in a different one) but it doesn't
// matter for this test and it simplifies things.
const uint64_t kAddrDest = kSymbolizedModuleAddress + 0x200;
const uint64_t kSrcSP = 0x5000;
const uint64_t kThunkSP = 0x4ff8;
LineDetails src_details(src_line);
src_details.entries().push_back({0, AddressRange(kAddrSrc, kAddrSrc + 1)});
module_symbols()->AddLineDetails(kAddrSrc, src_details);
LineDetails dest_details(dest_line);
dest_details.entries().push_back({0, AddressRange(kAddrDest, kAddrDest + 1)});
module_symbols()->AddLineDetails(kAddrDest, dest_details);
// 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.process_koid = process()->GetKoid();
exception.thread.thread_koid = thread()->GetKoid();
exception.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
exception.thread.frames.emplace_back(kAddrSrc, kSrcSP, 0);
InjectException(exception);
// Continue the thread with the controller stepping in range.
auto step_into = std::make_unique<StepThreadController>(StepMode::kSourceLine);
step_into->set_stop_on_no_symbols(stop_on_no_symbols);
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 the thunk instruction with no line info. This is a separate function so we push an
// entry on the stack.
exception.thread.frames.emplace(exception.thread.frames.begin(), kAddrThunk, kThunkSP, kSrcSP);
InjectException(exception);
if (stop_on_no_symbols) {
// For this variant of the test, the unsymbolized thunk should have stopped stepping.
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stopped.
EXPECT_EQ(debug_ipc::ThreadRecord::State::kBlocked, thread()->GetState());
return;
}
// The rest of this test is the "step over unsymbolized thunks" case. It should have automatically
// resumed from the previous exception.
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount());
// Stop on dest instruction. Since it's a different line, we should now stop.
exception.thread.frames[0].ip = kAddrDest;
InjectException(exception);
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stopped
EXPECT_EQ(debug_ipc::ThreadRecord::State::kBlocked, thread()->GetState());
}
TEST_F(StepThreadControllerTest, SharedLibThunksStepOver) { DoSharedLibThunkTest(false); }
TEST_F(StepThreadControllerTest, SharedLibThunksStepInto) { DoSharedLibThunkTest(true); }
void StepThreadControllerTest::DoUnsymbolizedFunctionTest(bool stop_on_no_symbols) {
FileLine src_line("/path/src.cc", 1);
// Jump from src to dest and return, then to kOutOfRange.
const uint64_t kAddrSrc = kSymbolizedModuleAddress + 0x100;
const uint64_t kAddrDest = kUnsymbolizedModuleAddress + 0x200;
const uint64_t kAddrReturn = kAddrSrc + 4;
const uint64_t kAddrOutOfRange = kAddrReturn + 4;
const uint64_t kSrcSP = 0x5000;
const uint64_t kDestSP = 0x4ff0;
LineDetails src_details(src_line);
src_details.entries().push_back({0, AddressRange(kAddrSrc, kAddrOutOfRange)});
module_symbols()->AddLineDetails(kAddrSrc, src_details);
// Set up the thread to be stopped at the beginning of our range.
debug_ipc::NotifyException src_exception;
src_exception.type = debug_ipc::ExceptionType::kSingleStep;
src_exception.thread.process_koid = process()->GetKoid();
src_exception.thread.thread_koid = thread()->GetKoid();
src_exception.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
src_exception.thread.frames.emplace_back(kAddrSrc, kSrcSP, 0x5008);
src_exception.thread.frames.emplace_back(0x10, 0x5008, 0);
InjectException(src_exception);
// Continue the thread with the controller stepping in range.
auto step_into = std::make_unique<StepThreadController>(StepMode::kSourceLine);
step_into->set_stop_on_no_symbols(stop_on_no_symbols);
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 the destination unsymbolized address.
debug_ipc::NotifyException dest_exception(src_exception);
dest_exception.thread.frames.clear();
dest_exception.thread.frames.emplace_back(kAddrDest, kDestSP, kSrcSP);
dest_exception.thread.frames.emplace_back(kAddrReturn, kSrcSP, 0);
InjectException(dest_exception);
if (stop_on_no_symbols) {
// For this variant of the test, the unsymbolized thunk should have stopped
// stepping.
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stopped
EXPECT_EQ(debug_ipc::ThreadRecord::State::kBlocked, thread()->GetState());
return;
}
// The rest of this test is the "step over unsymbolized thunks" case. It should have automatically
// resumed from the previous exception.
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount());
// Send a breakpoint completion notification at the previous stack frame. Breakpoint exceptions
// are "software".
src_exception.type = debug_ipc::ExceptionType::kSoftwareBreakpoint;
src_exception.hit_breakpoints.resize(1);
src_exception.hit_breakpoints[0].id = mock_remote_api()->last_breakpoint_id();
src_exception.hit_breakpoints[0].hit_count = 1;
src_exception.thread.frames[0].ip = kAddrReturn;
InjectException(src_exception);
// This should have continued since the return address is still in the original address range.
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount());
// Stop on dest instruction, this is still in range so we should continue.
src_exception.thread.frames[0].ip = kAddrOutOfRange;
InjectException(src_exception);
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stopped
EXPECT_EQ(debug_ipc::ThreadRecord::State::kBlocked, thread()->GetState());
}
TEST_F(StepThreadControllerTest, UnsymbolizedCallStepOver) { DoUnsymbolizedFunctionTest(false); }
TEST_F(StepThreadControllerTest, UnsymbolizedCallStepInto) { DoUnsymbolizedFunctionTest(true); }
// 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()->AsFunction());
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());
}
} // namespace zxdb