blob: cfc5d379494034a327610a837d8f76c298b19fb8 [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 "garnet/bin/zxdb/client/step_thread_controller.h"
#include "garnet/bin/zxdb/client/process.h"
#include "garnet/bin/zxdb/client/thread.h"
#include "garnet/bin/zxdb/client/thread_controller_test.h"
#include "garnet/bin/zxdb/common/err.h"
#include "garnet/bin/zxdb/symbols/line_details.h"
#include "garnet/bin/zxdb/symbols/mock_module_symbols.h"
#include "garnet/lib/debug_ipc/protocol.h"
namespace zxdb {
class StepThreadControllerTest : public ThreadControllerTest {
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);
};
// 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.process_koid = process()->GetKoid();
exception.type = debug_ipc::NotifyException::Type::kSingleStep;
exception.thread.koid = thread()->GetKoid();
exception.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
exception.thread.frames.resize(2);
exception.thread.frames[0].ip = kBeginAddr;
exception.thread.frames[0].sp = 0x5000;
exception.thread.frames[0].bp = 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()->resume_count());
// Issue a software exception in the range.
exception.type = debug_ipc::NotifyException::Type::kSoftware;
exception.thread.frames[0].ip += 4;
InjectException(exception);
// It should have stayed stopped despite being in range.
EXPECT_EQ(1, mock_remote_api()->resume_count()); // Same count as above.
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("/path/file.cc", 0);
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({20, AddressRange(kAddr1, kAddr2)});
LineDetails line_details2(line0);
line_details2.entries().push_back({0, AddressRange(kAddr2, kAddr3)});
LineDetails line_details3(line10);
line_details3.entries().push_back({10, 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.
debug_ipc::NotifyException exception;
exception.process_koid = process()->GetKoid();
exception.type = debug_ipc::NotifyException::Type::kSingleStep;
exception.thread.koid = thread()->GetKoid();
exception.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
exception.thread.frames.resize(2);
exception.thread.frames[0].ip = kAddr1;
exception.thread.frames[0].sp = 0x5000;
exception.thread.frames[0].bp = 0x5000;
InjectException(exception);
// 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()->resume_count());
// Stop on 2nd instruction (line 0). This should be automatically resumed.
exception.thread.frames[0].ip = kAddr2;
InjectException(exception);
EXPECT_EQ(2, mock_remote_api()->resume_count());
// Stop on 3rd instruction (line 10). Since this matches the original line,
// it should be automatically resumed.
exception.thread.frames[0].ip = kAddr3;
InjectException(exception);
EXPECT_EQ(3, mock_remote_api()->resume_count());
// Stop on 4th instruction. Since this is line 11, we should stay stopped.
exception.thread.frames[0].ip = kAddr4;
InjectException(exception);
EXPECT_EQ(3, mock_remote_api()->resume_count()); // Same count as above.
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;
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.process_koid = process()->GetKoid();
exception.type = debug_ipc::NotifyException::Type::kSingleStep;
exception.thread.koid = thread()->GetKoid();
exception.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
exception.thread.frames.resize(2);
exception.thread.frames[0].ip = kAddrSrc;
exception.thread.frames[0].sp = 0x5000;
exception.thread.frames[0].bp = 0x5000;
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()->resume_count());
// 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());
exception.thread.frames[0].ip = kAddrThunk;
exception.thread.frames[0].sp = 0x4ff0;
exception.thread.frames[0].bp = 0x5000;
InjectException(exception);
if (stop_on_no_symbols) {
// For this variant of the test, the unsymbolized thunk should have stopped
// stepping.
EXPECT_EQ(1, mock_remote_api()->resume_count());
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(2, mock_remote_api()->resume_count());
// Stop on dest instruction. Since it's a different line, we should now stop.
exception.thread.frames[0].ip = kAddrDest;
InjectException(exception);
EXPECT_EQ(2, mock_remote_api()->resume_count()); // Unchanged from previous.
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;
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.process_koid = process()->GetKoid();
src_exception.type = debug_ipc::NotifyException::Type::kSingleStep;
src_exception.thread.koid = thread()->GetKoid();
src_exception.thread.state = debug_ipc::ThreadRecord::State::kBlocked;
src_exception.thread.frames.resize(2);
src_exception.thread.frames[0].ip = kAddrSrc;
src_exception.thread.frames[0].sp = 0x5000;
src_exception.thread.frames[0].bp = 0x5000;
src_exception.thread.frames[1].ip = 0x10;
src_exception.thread.frames[1].sp = 0x5008;
src_exception.thread.frames[1].bp = 0x5008;
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()->resume_count());
// Stop on the destination unsymbolized address.
debug_ipc::NotifyException dest_exception(src_exception);
dest_exception.thread.frames.resize(2);
dest_exception.thread.frames[0].ip = kAddrDest;
dest_exception.thread.frames[0].sp = 0x4ff0;
dest_exception.thread.frames[0].bp = 0x4ff0;
dest_exception.thread.frames[1].ip = kAddrReturn;
dest_exception.thread.frames[1].sp = 0x5000;
dest_exception.thread.frames[1].bp = 0x5000;
InjectException(dest_exception);
if (stop_on_no_symbols) {
// For this variant of the test, the unsymbolized thunk should have stopped
// stepping.
EXPECT_EQ(1, mock_remote_api()->resume_count());
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(2, mock_remote_api()->resume_count());
// Send a breakpoint completion notification at the previous stack frame.
// Breakpoint exceptions are "software".
src_exception.type = debug_ipc::NotifyException::Type::kSoftware;
src_exception.hit_breakpoints.resize(1);
src_exception.hit_breakpoints[0].breakpoint_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(3, mock_remote_api()->resume_count());
// 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(3, mock_remote_api()->resume_count()); // Unchanged from previous.
EXPECT_EQ(debug_ipc::ThreadRecord::State::kBlocked, thread()->GetState());
}
TEST_F(StepThreadControllerTest, UnsymbolizedCallStepOver) {
DoUnsymbolizedFunctionTest(false);
}
TEST_F(StepThreadControllerTest, UnsymbolizedCallStepInto) {
DoUnsymbolizedFunctionTest(true);
}
} // namespace zxdb