blob: ef8ebe6eece18cccf8b1994587a34b12762bd918 [file] [log] [blame]
// Copyright 2019 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_into_thread_controller.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/symbols/input_location.h"
#include "src/developer/debug/zxdb/symbols/line_details.h"
namespace zxdb {
namespace {
// This override of MockModuleSymbols allows us to respond with different addresses depending on
// whether prologue skipping was requested or not (the normal mock doesn't provide this level of
// control).
class StepIntoMockModuleSymbols : public MockModuleSymbols {
public:
// IP of beginning of function where the prologue will be queried.
static const uint64_t kNestedBegin;
// IP of first non-prologue instruction of the function above.
static const uint64_t kNestedPrologueEnd;
// ModuleSymbols overrides.
std::vector<Location> ResolveInputLocation(const SymbolContext& symbol_context,
const InputLocation& input_location,
const ResolveOptions& options) const override {
if (input_location.type == InputLocation::Type::kAddress &&
input_location.address == kNestedBegin) {
// This is the address in question.
if (options.skip_function_prologue)
return {Location(Location::State::kSymbolized, kNestedPrologueEnd)};
return {Location(Location::State::kSymbolized, kNestedBegin)};
}
return MockModuleSymbols::ResolveInputLocation(symbol_context, input_location, options);
}
protected:
FRIEND_MAKE_REF_COUNTED(StepIntoMockModuleSymbols);
FRIEND_REF_COUNTED_THREAD_SAFE(StepIntoMockModuleSymbols);
StepIntoMockModuleSymbols() : MockModuleSymbols("file.so") {
AddLineDetails(
InlineThreadControllerTest::kTopInlineFunctionRange.begin(),
LineDetails(InlineThreadControllerTest::kTopInlineFileLine,
{LineDetails::LineEntry(InlineThreadControllerTest::kTopInlineFunctionRange)}));
}
~StepIntoMockModuleSymbols() override {}
};
const uint64_t StepIntoMockModuleSymbols::kNestedBegin =
InlineThreadControllerTest::kTopInlineFunctionRange.begin();
const uint64_t StepIntoMockModuleSymbols::kNestedPrologueEnd = kNestedBegin + 4;
class StepIntoThreadControllerTest : public InlineThreadControllerTest {
public:
void DoStepTest(bool skip_prologue) {
constexpr uint64_t kBeginAddr = kSymbolizedModuleAddress + 0x1000;
constexpr uint64_t kEndAddr = kSymbolizedModuleAddress + 0x1010;
constexpr uint64_t kStackFramePrevious = 0x5010;
constexpr uint64_t kStackFrameInitial = 0x5000;
constexpr uint64_t kStackFrameNested = 0x4090;
constexpr uint64_t kStackFrameNestedPrologueCall = 0x4080;
// 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, kStackFrameInitial, kStackFrameInitial);
exception.thread.frames.emplace_back(kBeginAddr - 10, kStackFramePrevious, kStackFramePrevious);
InjectException(exception);
// Start the "step into" over that range.
auto step_into = std::make_unique<StepIntoThreadController>(
AddressRanges(AddressRange(kBeginAddr, kEndAddr)));
bool continued = false;
step_into->set_should_skip_prologue(skip_prologue);
thread()->ContinueWith(std::move(step_into), [&continued](const Err& err) {
if (!err.has_error())
continued = true;
});
// That should have resumed the thread.
EXPECT_TRUE(continued);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount());
// Stop at the beginning of a new stack frame (this adds to the previous stack frame still in th
// exception record).
exception.thread.frames.emplace(exception.thread.frames.begin(),
StepIntoMockModuleSymbols::kNestedBegin, kStackFrameNested,
kStackFrameNested);
InjectException(exception);
if (!skip_prologue) {
// When not skipping prologues, the thread should stop since we're in a new frame.
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount());
return;
}
// When skipping prologues, it should continue through the prologue.
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount());
// Test a function call from within the prologue. This corresponds to things like asan
// bookkeeping functions that should be skipped. Here we generate some random unsymbolized
// code address for the prologue call.
exception.thread.frames.emplace(exception.thread.frames.begin(),
kEndAddr + 0x8, // Call address, arbitrary.
kStackFrameNestedPrologueCall, kStackFrameNestedPrologueCall);
InjectException(exception);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Skip prologue call.
// Delete the nested prologue call from the stack.
exception.thread.frames.erase(exception.thread.frames.begin());
// Report a stop at the end of the prologue. This just updates the same stack frame still in the
// exception record.
exception.thread.frames.front().ip = StepIntoMockModuleSymbols::kNestedPrologueEnd;
InjectException(exception);
// That should have stopped.
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount());
}
protected:
// ThreadControllerTest override:
fxl::RefPtr<MockModuleSymbols> MakeModuleSymbols() override {
return fxl::MakeRefCounted<StepIntoMockModuleSymbols>();
}
};
} // namespace
TEST_F(StepIntoThreadControllerTest, SkipPrologue) { DoStepTest(true); }
TEST_F(StepIntoThreadControllerTest, WithPrologue) { DoStepTest(false); }
// Inlines should never have prologues skipped. The prologue finder has a fallback that it will
// find a prologue even if one isn't explicitly noted to handle some GCC-generated code. If called
// on an inline routine, it will skip the first line.
TEST_F(StepIntoThreadControllerTest, Inline) {
// 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);
// Do the "step into".
auto step_into_controller = std::make_unique<StepIntoThreadController>(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.
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());
}
// If the program is killed out from under us, we can get an exception with no stack. This should
// stop and not crash.
TEST_F(StepIntoThreadControllerTest, NoStack) {
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep, {}, true);
auto step_into_controller = std::make_unique<StepIntoThreadController>(StepMode::kSourceLine);
std::optional<Err> result;
thread()->ContinueWith(std::move(step_into_controller),
[&result](const Err& err) { result = err; });
loop().RunUntilNoTasks();
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount());
EXPECT_TRUE(result);
EXPECT_EQ("Can't step, no frames.", result->msg());
}
} // namespace zxdb