blob: ca2a3b1a86c5815377cb76d264981dd1b295d106 [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 <gtest/gtest.h>
#include "src/developer/debug/debug_agent/integration_tests/message_loop_wrapper.h"
#include "src/developer/debug/debug_agent/integration_tests/so_wrapper.h"
#include "src/developer/debug/debug_agent/local_stream_backend.h"
#include "src/developer/debug/debug_agent/zircon_system_interface.h"
#include "src/developer/debug/shared/logging/logging.h"
#include "src/developer/debug/shared/zx_status.h"
#include "src/lib/fxl/strings/string_printf.h"
namespace debug_agent {
using namespace debug_ipc;
// This tests verifies that in a multithreaded program the debugger is able to
// setup a breakpoint that will only affect a single thread and let the others
// run without stopping in it.
namespace {
// Receives the notification from the DebugAgent.
// The implementation is at the end of the file.
class BreakpointStreamBackend : public LocalStreamBackend {
public:
// In what part of the test we currently are.
// This will determine when we quit the loop to let the test verify state.
enum class TestStage {
// Waits for the first thread start and modules.
kWaitingForThreadToStart,
// Waits for the other thread starting notifications.
kCreatingOtherThreads,
// Waits for the one thread to hit the breakpoint and all other threads
// to exit.
kExpectingBreakpointAndTerminations,
// Waiting for the last thread to exit.
kWaitingForFinalExit,
// Waiting for the process to exit.
kDone,
kInvalid,
};
BreakpointStreamBackend(debug::MessageLoop* loop, size_t thread_count)
: loop_(loop), thread_count_(thread_count) {}
void set_remote_api(RemoteAPI* remote_api) { remote_api_ = remote_api; }
// API -----------------------------------------------------------------------
// Will send a resume notification to all threads and run the loop.
void ResumeAllThreadsAndRunLoop();
// The messages we're interested in handling ---------------------------------
// Searches the loaded modules for specific one.
void HandleNotifyModules(NotifyModules) override;
void HandleNotifyProcessExiting(NotifyProcessExiting) override;
void HandleNotifyThreadStarting(NotifyThread) override;
void HandleNotifyThreadExiting(NotifyThread) override;
void HandleNotifyException(NotifyException) override;
// Getters -------------------------------------------------------------------
debug::MessageLoop* loop() const { return loop_; }
uint64_t so_test_base_addr() const { return so_test_base_addr_; }
zx_koid_t process_koid() const { return process_koid_; }
bool process_exited() const { return process_exited_; }
int64_t return_code() const { return return_code_; }
size_t thread_count() const { return thread_count_; }
const auto& thread_koids() const { return thread_koids_; }
const auto& thread_starts() const { return thread_starts_; }
const auto& thread_excp() const { return thread_excp_; }
const auto& thread_exits() const { return thread_exits_; }
private:
// Every exception should ask whether it should stop the loop and let the test
// verify if what happened is correct. This function holds the "script" that
// the tests follows in order to work properly.
void ShouldQuitLoop();
// Similar to ResumeAllThreadsAndRunLoop, but doesn't run the loop.
void ResumeAllThreads();
debug::MessageLoop* loop_ = nullptr;
RemoteAPI* remote_api_ = nullptr;
uint64_t so_test_base_addr_ = 0;
zx_koid_t process_koid_ = 0;
bool process_exited_ = false;
int64_t return_code_ = 0;
size_t thread_count_ = 0;
std::vector<zx_koid_t> thread_koids_;
std::vector<NotifyThread> thread_starts_;
std::vector<NotifyException> thread_excp_;
std::vector<NotifyThread> thread_exits_;
bool initial_thread_check_passed_ = false;
bool got_modules_check_passed_ = false;
bool process_finished_check_passed_ = false;
TestStage test_stage_ = TestStage::kWaitingForThreadToStart;
};
std::pair<LaunchRequest, LaunchReply> GetLaunchRequest(const BreakpointStreamBackend& backend,
std::string exe) {
LaunchRequest launch_request = {};
launch_request.argv = {exe, fxl::StringPrintf("%lu", backend.thread_count())};
launch_request.inferior_type = InferiorType::kBinary;
return {launch_request, {}};
}
constexpr uint32_t kBreakpointId = 1234u;
std::pair<AddOrChangeBreakpointRequest, AddOrChangeBreakpointReply> GetBreakpointRequest(
zx_koid_t process_koid, zx_koid_t thread_koid, uint64_t address) {
// We add a breakpoint in that address.
debug_ipc::ProcessBreakpointSettings location = {};
location.id = {.process = process_koid, .thread = thread_koid};
location.address = address;
debug_ipc::AddOrChangeBreakpointRequest breakpoint_request = {};
breakpoint_request.breakpoint.id = kBreakpointId;
breakpoint_request.breakpoint.locations.push_back(location);
DEBUG_LOG(Test) << "Setting breakpoint for [P: " << process_koid << ", T: " << thread_koid
<< "] on 0x" << std::hex << address;
return {breakpoint_request, {}};
}
} // namespace
#if defined(__x86_64__)
// TODO(fxbug.dev/6298): This is flaky on X64 for an unknown reason.
TEST(MultithreadedBreakpoint, DISABLED_SWBreakpoint) {
#elif defined(__aarch64__)
// TODO(fxbug.dev/6248): Arm64 has an instruction cache that makes a thread sometimes
// hit a thread that has been removed, making this test flake.
// This has to be fixed in zircon.
TEST(MultithreadedBreakpoint, DISABLED_SWBreakpoint) {
#endif
// Uncomment these is the test is giving you trouble.
// Only uncomment SetDebugMode if the test is giving you *real* trouble.
// debug::SetDebugMode(true);
// debug::SetLogCategories({LogCategory::kTest});
// We attempt to load the pre-made .so.
static constexpr const char kTestSo[] = "debug_agent_test_so.so";
SoWrapper so_wrapper;
ASSERT_TRUE(so_wrapper.Init(kTestSo)) << "Could not load so " << kTestSo;
uint64_t symbol_offset = so_wrapper.GetSymbolOffset(kTestSo, "MultithreadedFunctionToBreakOn");
ASSERT_NE(symbol_offset, 0u);
MessageLoopWrapper loop_wrapper;
{
auto* loop = loop_wrapper.loop();
// The stream backend will intercept the calls from the debug agent.
// Second arguments is the amount of threads to create.
BreakpointStreamBackend backend(loop, 5);
DebugAgent agent(std::make_unique<ZirconSystemInterface>());
RemoteAPI* remote_api = &agent;
agent.Connect(&backend.stream());
backend.set_remote_api(remote_api);
static constexpr const char kExecutable[] = "/pkg/bin/multithreaded_breakpoint_test_exe";
auto [lnch_request, lnch_reply] = GetLaunchRequest(backend, kExecutable);
remote_api->OnLaunch(lnch_request, &lnch_reply);
ASSERT_TRUE(lnch_reply.status.ok());
backend.ResumeAllThreadsAndRunLoop();
// We should have the correct module by now.
ASSERT_NE(backend.so_test_base_addr(), 0u);
// We let the main thread spin up all the other threads.
backend.ResumeAllThreadsAndRunLoop();
// At this point all sub-threads should have started.
ASSERT_EQ(backend.thread_starts().size(), backend.thread_count() + 1);
// Set a breakpoint
auto& thread_koids = backend.thread_koids();
auto thread_koid = thread_koids[1];
// We get the offset of the loaded function within the process space.
uint64_t module_base = backend.so_test_base_addr();
uint64_t module_function = module_base + symbol_offset;
DEBUG_LOG(Test) << std::hex << "BASE: 0x" << module_base << ", OFFSET: 0x" << symbol_offset
<< ", FINAL: 0x" << module_function;
auto [brk_request, brk_reply] =
GetBreakpointRequest(backend.process_koid(), thread_koid, module_function);
remote_api->OnAddOrChangeBreakpoint(brk_request, &brk_reply);
ASSERT_TRUE(brk_reply.status.ok());
backend.ResumeAllThreadsAndRunLoop();
// At this point all threads should've exited except one in breakpoint and
// the initial thread.
auto& thread_exits = backend.thread_exits();
ASSERT_EQ(thread_exits.size(), thread_koids.size() - 2);
auto& thread_excp = backend.thread_excp();
ASSERT_EQ(thread_excp.size(), 1u);
auto& brk_notify = thread_excp.front();
EXPECT_EQ(brk_notify.thread.id.thread, thread_koid);
EXPECT_EQ(brk_notify.type, debug_ipc::ExceptionType::kSoftwareBreakpoint);
ASSERT_EQ(brk_notify.hit_breakpoints.size(), 1u);
auto& hit_brk = brk_notify.hit_breakpoints.front();
EXPECT_EQ(hit_brk.id, kBreakpointId);
EXPECT_EQ(hit_brk.hit_count, 1u);
EXPECT_EQ(hit_brk.should_delete, false);
backend.ResumeAllThreadsAndRunLoop();
// At this point all threads and processes should've exited.
EXPECT_EQ(backend.thread_exits().size(), backend.thread_starts().size());
ASSERT_TRUE(backend.process_exited());
EXPECT_EQ(backend.return_code(), 0);
}
}
// BreakpointStreamBackend Implementation --------------------------------------
void BreakpointStreamBackend::ResumeAllThreadsAndRunLoop() {
ResumeAllThreads();
loop()->Run();
}
void BreakpointStreamBackend::ResumeAllThreads() {
debug_ipc::ResumeRequest resume_request;
resume_request.ids.push_back({.process = process_koid(), .thread = 0});
debug_ipc::ResumeReply resume_reply;
remote_api_->OnResume(resume_request, &resume_reply);
}
// Records the exception given from the debug agent.
void BreakpointStreamBackend::HandleNotifyException(NotifyException exception) {
DEBUG_LOG(Test) << "Received " << ExceptionTypeToString(exception.type)
<< " on Thread: " << exception.thread.id.thread;
thread_excp_.push_back(exception);
ShouldQuitLoop();
}
// Searches the loaded modules for specific one.
void BreakpointStreamBackend::HandleNotifyModules(NotifyModules modules) {
for (auto& module : modules.modules) {
DEBUG_LOG(Test) << "Received module " << module.name;
if (module.name == "libdebug_agent_test_so.so") {
so_test_base_addr_ = module.base;
break;
}
}
ShouldQuitLoop();
}
void BreakpointStreamBackend::HandleNotifyProcessExiting(NotifyProcessExiting process) {
DEBUG_LOG(Test) << "Process " << process.process_koid
<< " exiting with return code: " << process.return_code;
FX_DCHECK(process.process_koid == process_koid_);
process_exited_ = true;
return_code_ = process.return_code;
ShouldQuitLoop();
}
void BreakpointStreamBackend::HandleNotifyThreadStarting(NotifyThread thread) {
if (process_koid_ == 0) {
process_koid_ = thread.record.id.process;
DEBUG_LOG(Test) << "Process starting: " << process_koid_;
}
DEBUG_LOG(Test) << "Thread starting: " << thread.record.id.thread;
thread_starts_.push_back(thread);
thread_koids_.push_back(thread.record.id.thread);
ShouldQuitLoop();
}
void BreakpointStreamBackend::HandleNotifyThreadExiting(NotifyThread thread) {
DEBUG_LOG(Test) << "Thread exiting: " << thread.record.id.thread;
thread_exits_.push_back(thread);
ShouldQuitLoop();
}
void BreakpointStreamBackend::ShouldQuitLoop() {
if (test_stage_ == TestStage::kWaitingForThreadToStart) {
// The first thread started, we need to resume it.
if (initial_thread_check_passed_ == 0 && thread_starts_.size() == 1u) {
initial_thread_check_passed_ = true;
ResumeAllThreads();
return;
}
if (!got_modules_check_passed_ && so_test_base_addr_ != 0u) {
got_modules_check_passed_ = true;
loop()->QuitNow();
test_stage_ = TestStage::kCreatingOtherThreads;
DEBUG_LOG(Test) << "Stage change to CREATING OTHER THREADS";
return;
}
FX_NOTREACHED() << "Didn't get thread start or modules.";
}
if (test_stage_ == TestStage::kCreatingOtherThreads) {
if (thread_starts_.size() < thread_count_ + 1) {
return;
} else if (thread_starts_.size() == thread_count_ + 1) {
// We received all the threads we expected for, quit the loop.
loop()->QuitNow();
test_stage_ = TestStage::kExpectingBreakpointAndTerminations;
DEBUG_LOG(Test) << "Stage change to EXPECTING BREAKPOINT";
return;
}
FX_NOTREACHED() << "Didn't get all the thread startups.";
}
if (test_stage_ == TestStage::kExpectingBreakpointAndTerminations) {
// We should only get one breakpoint.
if (thread_excp_.size() > 1u)
FX_NOTREACHED() << "Got more than 1 exception.";
// All subthreads should exit but one.
if (thread_exits_.size() < thread_count_ - 1)
return;
if (thread_excp_.size() != 1u)
FX_NOTREACHED() << "Should've gotten one breakpoint exception.";
if (thread_exits_.size() != thread_count_ - 1)
FX_NOTREACHED() << "All subthreads but one should've exited.";
loop()->QuitNow();
test_stage_ = TestStage::kWaitingForFinalExit;
DEBUG_LOG(Test) << "Stage change to WAITING FOR FINAL EXIT.";
return;
}
if (test_stage_ == TestStage::kWaitingForFinalExit) {
// This is the breakpoint thread.
if (thread_exits_.size() < thread_starts_.size())
return;
if (thread_exits_.size() == thread_starts_.size()) {
test_stage_ = TestStage::kDone;
DEBUG_LOG(Test) << "Stage change to DONE.";
return;
}
FX_NOTREACHED() << "Unexpected thread exit.";
}
if (test_stage_ == TestStage::kDone) {
if (!process_finished_check_passed_ && process_exited_) {
process_finished_check_passed_ = true;
loop()->QuitNow();
test_stage_ = TestStage::kInvalid;
return;
}
FX_NOTREACHED() << "Should've only received process exit notification.";
}
FX_NOTREACHED() << "Invalid stage.";
}
} // namespace debug_agent