| // 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 |