blob: 61aa2e57e53c4d97001552a459467a6d89587b7d [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_over_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/address_ranges.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 StepOverThreadControllerTest : public InlineThreadControllerTest {
public:
// Returns the stack with the "middle inline 2" frame at the top. This removes the top and "top
// inline 2" frames from the default mock inline stack.
auto GetStackAtMiddleInline2() const {
auto frames = GetStack();
frames.erase(frames.begin(), frames.begin() + 2);
return frames;
}
};
// Tests "step over" stepping from before an inline function to the call of the inline function.
// This is tricky because that call is actually the first instruction of the inline function so
// needs special handling. The code being tested would look like this:
//
// void Top() {
// foo();
// > NonInlinedFunction(TopInlineFunction(), SecondInlineFunction());
// bar();
// }
//
// Since we're testing "step over", the location after the step should be on the next line:
//
// void Top() {
// foo();
// NonInlinedFunction(TopInlineFunction(), SecondInlineFunction());
// > bar();
// }
//
// To do this, it steps into and out of TopInlineFunction(), then into and out of
// SecondInlineFunction(), then into and out of NonInlinedFunction().
//
// Code layout:
//
// +-----------------------------------------------------+
// | Top() |
// | <code for foo() call> |
// | +------------------------------------------+ |
// | | Inlined code for TopInlineFunction() | | <- (1)
// | | | | <- (2)
// | +------------------------------------------+ |
// | | Inlined code for SecondInlineFunction() | | <- (3)
// | | | |
// | +------------------------------------------+ |
// | <code for NonInlinedFunction() call> | <- (4)
// | <code for bar() call> | <- (5)
// | |
// +-----------------------------------------------------+
TEST_F(StepOverThreadControllerTest, Inline) {
// Add line information required for the stepping. The first instruction of the inlined function
// is two places:
// stack[0] = first instruction of inline @ kTopInlineFileLine.
// stack[1] = first instruction of inline @ kTopFileLine
auto mock_frames = GetStack();
FileLine step_line = kTopFileLine; // Line being stepped over.
// The line table holds the mapping for the inlined code (kTopInlineFileLine) at the ambiguous
// address so that's what we add here. The stepper should handle the fact that stack[1]'s
// file_line is different but at the same address.
module_symbols()->AddLineDetails(
kTopInlineFunctionRange.begin(),
LineDetails(kTopInlineFileLine, {LineDetails::LineEntry(kTopInlineFunctionRange)}));
// The SecondInlineFunction() immediately following the first.
FileLine second_inline_line("random.cc", 3746);
AddressRange second_inline_range(kTopInlineFunctionRange.end(),
kTopInlineFunctionRange.end() + 4);
module_symbols()->AddLineDetails(
second_inline_range.begin(),
LineDetails(second_inline_line, {LineDetails::LineEntry(second_inline_range)}));
// Line information for the address following the inlined function but on the same line (this is
// the code for the NonInlinedFunction() call).
const uint64_t kNonInlinedAddress = second_inline_range.end();
AddressRange non_inlined_call_range(kNonInlinedAddress, kNonInlinedAddress + 4);
module_symbols()->AddLineDetails(
kNonInlinedAddress, LineDetails(step_line, {LineDetails::LineEntry(non_inlined_call_range)}));
// Code for the line after (the "bar()" call in the example). This maps to a different line
// (immediately following) which is how we know to stop.
const uint64_t kFollowingAddress = non_inlined_call_range.end();
AddressRange following_range(kFollowingAddress, kFollowingAddress + 4);
FileLine following_line(kTopFileLine.file(), kTopFileLine.line() + 1);
module_symbols()->AddLineDetails(
kFollowingAddress, LineDetails(following_line, {LineDetails::LineEntry(following_range)}));
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// -----------------------------------------------------------------------------------------------
// Done with setup, actual test following.
//
// Current stack is:
// TopInline
// Top
// ...
Stack& stack = thread()->GetStack();
// The first instruction of the inlined function should be ambiguous.
ASSERT_EQ(1u, stack.GetAmbiguousInlineFrameCount());
// Hide the inline frame because we want to step over the inlined function.
stack.SetHideAmbiguousInlineFrameCount(1);
// Start to step over the top stack frame's line.
//
// Current code is at position (1) in the diagram above. Stack:
// [hidden w/ ambiguous address: TopInline]
// Top
// ...
EXPECT_EQ(step_line, stack[0]->GetLocation().file_line());
thread()->ContinueWith(std::make_unique<StepOverThreadController>(StepMode::kSourceLine),
[](const Err& err) {});
// 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().PostTask(FROM_HERE, [loop = &loop()]() { loop->QuitNow(); });
loop().Run();
// The synthetic exception will trigger the step over controller to exit the inline frame. It will
// single step the CPU to get out of the inline function so the thread should be resumed now.
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continued.
// Issue an exception in the middle of the inline function. Since we're stepping over it, the
// controller should continue.
//
// Current code is at position (2) in the diagram above. Stack:
// TopInline
// Top
// ...
mock_frames = GetStack();
mock_frames[0]->SetAddress(kTopInlineFunctionRange.begin() + 1);
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continue.
// Make the 2nd inline function.
auto second_inline_func = fxl::MakeRefCounted<Function>(DwarfTag::kInlinedSubroutine);
second_inline_func->set_assigned_name("SecondInlineFunction");
second_inline_func->set_code_ranges(AddressRanges(second_inline_range));
Location second_inline_loc(second_inline_range.begin(), second_inline_line, 0,
SymbolContext::ForRelativeAddresses(), second_inline_func);
// The code exits the first inline function and is now at the first instruction of the second
// inline function. This is an ambiguous location.
//
// Sets to position (3) in the diagram above. Stack:
// SecondInline (ambiguous address @ beginning of inline block)
// Top
mock_frames = GetStack();
mock_frames[0] = std::make_unique<MockFrame>(nullptr, nullptr, second_inline_loc, kTopSP, 0,
std::vector<debug::RegisterValue>(), kTopSP,
mock_frames[1].get(), true);
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// That should have hidden the top ambiguous inline frame, the StepOver controller should have
// decided to keep going since it's still on the same line, and then the step controller should
// have unhidden the top frame to step into the inline function.
// As of this writing, the "step over" controller delegates to the step controller which steps
// into the inline routine. This skips the "Continue" call on the thread since we're already in
// the middle of stepping and is not asynchronous (unlike when we do a "step into" at the
// beginning of a step operation). This is an implementation detail, however, and may change, so
// this test code doesn't make assumptions about asynchronous or not for this step.
loop().PostTask(FROM_HERE, [loop = &loop()]() { loop->QuitNow(); });
loop().Run();
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continue.
EXPECT_EQ(0u, stack.hide_ambiguous_inline_frame_count());
// Issue a step after the 2nd inline function. But this still has the same line as the callers for
// both the inlines, so it should continue.
//
// Sets to position (4) in the diagram above. Stack:
// Top (same line we were on before)
mock_frames = GetStack();
mock_frames.erase(mock_frames.begin()); // Remove inline we finished.
mock_frames[0]->SetAddress(kNonInlinedAddress);
mock_frames[0]->SetFileLine(step_line);
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continue.
// Issue a step for a different line, this should finally stop.
//
// Sets to position (5) in the diagram above. Stack:
// Top (different line)
mock_frames = GetStack();
mock_frames.erase(mock_frames.begin()); // Remove inline we finished.
mock_frames[0]->SetAddress(kFollowingAddress);
mock_frames[0]->SetFileLine(following_line);
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stop.
}
// The line table can contain entries with "line 0" that correspond to compiler-generated code.
// These should be transparently stepped over as if they're part of the original line being stepped.
// Most of the logic around "0 lines" is handled by the StepThreadController.
//
// This test covers the case where where it steps over a call, and the return address of that call
// maps to one of these 0 lines. Execution should resume from that point as if it was part of the
// original line being stepped.
TEST_F(StepOverThreadControllerTest, OutToZeroLine) {
// The location we're stepping from is the middle frame.
const uint64_t kFromAddress = kMiddleFunctionRange.begin();
FileLine step_line = kMiddleFileLine; // Line being stepped over.
const uint64_t kBottomAddress = 0x1000;
std::vector<std::unique_ptr<MockFrame>> mock_frames;
mock_frames.push_back(GetMiddleFrame(kFromAddress));
mock_frames.push_back(GetBottomFrame(kBottomAddress));
// Source line table information. This is a one-byte range for the instruction where the "step
// over" beings.
module_symbols()->AddLineDetails(
kFromAddress,
LineDetails(kMiddleFileLine,
{LineDetails::LineEntry(AddressRange(kFromAddress, kFromAddress + 1))}));
// Line info for the top function call.
const uint64_t kTopAddress = kTopFunctionRange.begin();
module_symbols()->AddLineDetails(
kTopAddress, LineDetails(kTopFileLine, {LineDetails::LineEntry(kTopFunctionRange)}));
// The function call returns to the next instruction which gives a "0" line number. Note that the
// file name is still present because this is how DWARF usually encodes things.
const uint64_t kReturnAddress = kFromAddress + 1;
const FileLine kZeroFileLine;
module_symbols()->AddLineDetails(
kReturnAddress,
LineDetails(kZeroFileLine,
{LineDetails::LineEntry(AddressRange(kReturnAddress, kReturnAddress + 1))}));
// The third byte is a new line number. This is where stepping should stop.
const uint64_t kFinalAddress = kReturnAddress + 1;
const FileLine kFinalFileLine(kMiddleFileLine.file(), kMiddleFileLine.line() + 1);
module_symbols()->AddLineDetails(
kFinalAddress,
LineDetails(kFinalFileLine,
{LineDetails::LineEntry(AddressRange(kFinalAddress, kFinalAddress + 1))}));
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// -----------------------------------------------------------------------------------------------
// Done with setup, actual test following.
//
// Current stack is:
// Middle (top of stack)
// Bottom
// Step over the "from" address.
thread()->ContinueWith(std::make_unique<StepOverThreadController>(StepMode::kSourceLine),
[](const Err& err) {});
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continue.
// Stop in a new stack frame called by the previous execution. It should continue.
mock_frames.push_back(GetTopFrame(kTopAddress));
mock_frames.push_back(GetMiddleFrame(kFromAddress));
mock_frames.push_back(GetBottomFrame(kBottomAddress));
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continue.
// Execution returns to the original frame at the next instruction. This is the instruction with
// the "line 0" annotation and it should be resumed. We can't use GetMiddleFrame() here because
// we need to supply a specific FileLine.
//
// This is a software breakpoint set by the "until" controller we have to emulate.
debug_ipc::BreakpointStats breakpoint{
.id = static_cast<uint32_t>(mock_remote_api()->last_breakpoint_id()), .hit_count = 1};
mock_frames.push_back(std::make_unique<MockFrame>(
nullptr, nullptr,
Location(kReturnAddress, kZeroFileLine, 0, SymbolContext::ForRelativeAddresses(),
GetMiddleFunction()),
kMiddleSP, kBottomSP, std::vector<debug::RegisterValue>(), kMiddleSP));
mock_frames.push_back(GetBottomFrame(kBottomAddress));
InjectExceptionWithStack(
process()->GetKoid(), thread()->GetKoid(), debug_ipc::ExceptionType::kSoftwareBreakpoint,
MockFrameVectorToFrameVector(std::move(mock_frames)), true, {breakpoint});
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continue.
// The next instruction is on a different line, reporting a stop there should finish stepping.
mock_frames.push_back(std::make_unique<MockFrame>(
nullptr, nullptr,
Location(kFinalAddress, kFinalFileLine, 0, SymbolContext::ForRelativeAddresses(),
GetMiddleFunction()),
kMiddleSP, kBottomSP, std::vector<debug::RegisterValue>(), kMiddleSP));
mock_frames.push_back(GetBottomFrame(kBottomAddress));
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stop.
}
TEST_F(StepOverThreadControllerTest, Range) {
auto mock_frames = GetStackAtMiddleInline2();
// The location we're stepping from is the middle frame.
const uint64_t kFromAddress = mock_frames[0]->GetAddress();
const uint64_t kToAddress = kFromAddress + 8;
AddressRange range(kFromAddress, kToAddress); // Range being stepped over.
// Inject an exception at the top inline of the middle frame. It's about to call the top frame.
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
// Step over the range.
thread()->ContinueWith(std::make_unique<StepOverThreadController>(AddressRanges(range)),
[](const Err& err) {});
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continue.
// Stop in a new stack frame called by the previous execution. It should continue.
mock_frames = GetStack();
mock_frames.erase(mock_frames.begin()); // Delete top inline to leave us at top (we don't need
// the top inline for this test).
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continue.
// Execution returns to the original "middle" frame at the next instruction. This is done via a
// software breakpoint set by the "until" controller which we need to emulate hitting.
debug_ipc::BreakpointStats breakpoint{
.id = static_cast<uint32_t>(mock_remote_api()->last_breakpoint_id()), .hit_count = 1};
mock_frames = GetStackAtMiddleInline2();
mock_frames[0]->SetAddress(kFromAddress + 1); // Set to next instruction.
InjectExceptionWithStack(
process()->GetKoid(), thread()->GetKoid(), debug_ipc::ExceptionType::kSoftwareBreakpoint,
MockFrameVectorToFrameVector(std::move(mock_frames)), true, {breakpoint});
EXPECT_EQ(1, mock_remote_api()->GetAndResetResumeCount()); // Continue.
// Now exit the range.
mock_frames = GetStackAtMiddleInline2();
mock_frames[0]->SetAddress(kToAddress); // End of range (is non-inclusive).
InjectExceptionWithStack(process()->GetKoid(), thread()->GetKoid(),
debug_ipc::ExceptionType::kSingleStep,
MockFrameVectorToFrameVector(std::move(mock_frames)), true);
EXPECT_EQ(0, mock_remote_api()->GetAndResetResumeCount()); // Stop.
}
} // namespace zxdb