| // 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" |
| |
| namespace debug_agent { |
| |
| using namespace debug_ipc; |
| |
| namespace { |
| |
| constexpr int kInvalidReturnCode = 0xdeadbeef; |
| |
| // Receives the notification from the DebugAgent. |
| // The implementation is at the end of the file. |
| class WatchpointStreamBackend : public LocalStreamBackend { |
| public: |
| WatchpointStreamBackend(MessageLoop* loop) : loop_(loop) {} |
| |
| void set_remote_api(RemoteAPI* remote_api) { remote_api_ = remote_api; } |
| |
| // API ----------------------------------------------------------------------- |
| |
| void ResumeAllThreads(); |
| void ResumeAllThreadsAndRunLoop(); |
| |
| // Notification Interception ------------------------------------------------- |
| |
| void HandleNotifyModules(NotifyModules) override; |
| void HandleNotifyException(NotifyException) override; |
| void HandleNotifyProcessExiting(NotifyProcessExiting) override; |
| void HandleNotifyThreadStarting(NotifyThread) override; |
| |
| // Getters ------------------------------------------------------------------- |
| |
| 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_; } |
| zx_koid_t thread_koid() const { return thread_koid_; } |
| int return_code() const { return return_code_; } |
| |
| const auto& exceptions() const { return exceptions_; } |
| |
| private: |
| // Each trapped notification will forward the decision whether to quit the |
| // loop to this call according to the TestStage enum. |
| void ShouldQuitLoop(); |
| |
| enum class TestStage { |
| kWaitingForThread, |
| kWaitingForModules, |
| kWaitingForException, |
| kWaitingForExit, |
| kDone, |
| }; |
| |
| TestStage test_stage_ = TestStage::kWaitingForThread; |
| |
| MessageLoop* loop_ = nullptr; |
| RemoteAPI* remote_api_ = nullptr; |
| |
| uint64_t so_test_base_addr_ = 0; |
| |
| zx_koid_t process_koid_ = 0; |
| zx_koid_t thread_koid_ = 0; |
| |
| std::vector<NotifyException> exceptions_; |
| int return_code_ = kInvalidReturnCode; |
| }; |
| |
| constexpr uint32_t kWatchpointId = 0x1234; |
| |
| std::pair<LaunchRequest, LaunchReply> GetLaunchRequest(const WatchpointStreamBackend& backend, |
| std::string exe); |
| |
| std::pair<AddOrChangeBreakpointRequest, AddOrChangeBreakpointReply> GetWatchpointRequest( |
| const WatchpointStreamBackend& backend, uint64_t address); |
| |
| #if defined(__x86_64__) |
| TEST(Watchpoint, DISABLED_DefaultCase) { |
| // Arm64 implementation is not done yet. |
| #elif defined(__aarch64__) |
| TEST(Watchpoint, DISABLED_DefaultCase) { |
| #endif |
| // Activate this is the test is giving you trouble. |
| // debug_ipc::SetDebugMode(true); |
| |
| 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 variable_offset = so_wrapper.GetSymbolOffset(kTestSo, "gWatchpointVariable"); |
| ASSERT_NE(variable_offset, 0u); |
| |
| MessageLoopWrapper loop_wrapper; |
| { |
| auto* loop = loop_wrapper.loop(); |
| |
| WatchpointStreamBackend backend(loop); |
| 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/watchpoint_test_exe"; |
| auto [lnch_request, lnch_reply] = GetLaunchRequest(backend, kExecutable); |
| remote_api->OnLaunch(lnch_request, &lnch_reply); |
| ASSERT_EQ(lnch_reply.status, ZX_OK) << ZxStatusToString(lnch_reply.status); |
| |
| backend.ResumeAllThreadsAndRunLoop(); |
| |
| // The first thread should've started. |
| ASSERT_NE(backend.process_koid(), 0u); |
| ASSERT_NE(backend.thread_koid(), 0u); |
| |
| // We should have the correct module by now. |
| ASSERT_NE(backend.so_test_base_addr(), 0u); |
| uint64_t address = backend.so_test_base_addr() + variable_offset; |
| |
| DEBUG_LOG(Test) << std::hex << "Base: 0x" << backend.so_test_base_addr() << ", Offset: 0x" |
| << variable_offset << ", Actual Address: 0x" << address; |
| |
| auto [wp_request, wp_reply] = GetWatchpointRequest(backend, address); |
| remote_api->OnAddOrChangeBreakpoint(wp_request, &wp_reply); |
| ASSERT_EQ(wp_reply.status, ZX_OK) << ZxStatusToString(wp_reply.status); |
| |
| backend.ResumeAllThreadsAndRunLoop(); |
| |
| // We should've gotten an exception. |
| auto& exceptions = backend.exceptions(); |
| ASSERT_EQ(exceptions.size(), 1u); |
| |
| auto& exception = exceptions[0]; |
| EXPECT_EQ(exception.type, ExceptionType::kWatchpoint) << ExceptionTypeToString(exception.type); |
| EXPECT_EQ(exception.thread.process_koid, backend.process_koid()); |
| EXPECT_EQ(exception.thread.thread_koid, backend.thread_koid()); |
| |
| ASSERT_EQ(exception.hit_breakpoints.size(), 1u); |
| auto& wp = exception.hit_breakpoints[0]; |
| EXPECT_EQ(wp.id, kWatchpointId); |
| EXPECT_EQ(wp.hit_count, 1u); |
| EXPECT_EQ(wp.should_delete, true); |
| |
| backend.ResumeAllThreadsAndRunLoop(); |
| |
| // The process should've exited correctly. |
| EXPECT_EQ(backend.return_code(), 0); |
| } |
| } |
| |
| // Helpers --------------------------------------------------------------------- |
| |
| std::pair<LaunchRequest, LaunchReply> GetLaunchRequest(const WatchpointStreamBackend& backend, |
| std::string exe) { |
| LaunchRequest launch_request = {}; |
| launch_request.argv = {exe}; |
| launch_request.inferior_type = InferiorType::kBinary; |
| return {launch_request, {}}; |
| } |
| |
| std::pair<AddOrChangeBreakpointRequest, AddOrChangeBreakpointReply> GetWatchpointRequest( |
| const WatchpointStreamBackend& backend, uint64_t address) { |
| // We add a breakpoint in that address. |
| debug_ipc::ProcessBreakpointSettings location = {}; |
| location.process_koid = backend.process_koid(); |
| location.thread_koid = backend.thread_koid(); |
| location.address_range = {address, address}; |
| |
| debug_ipc::AddOrChangeBreakpointRequest watchpoint_request = {}; |
| watchpoint_request.breakpoint.id = kWatchpointId; |
| watchpoint_request.breakpoint.type = BreakpointType::kWrite; |
| watchpoint_request.breakpoint.one_shot = true; |
| watchpoint_request.breakpoint.locations.push_back(location); |
| |
| return {watchpoint_request, {}}; |
| } |
| |
| // WatchpointStreamBackend Implementation -------------------------------------- |
| |
| void WatchpointStreamBackend::ResumeAllThreadsAndRunLoop() { |
| ResumeAllThreads(); |
| loop()->Run(); |
| } |
| |
| void WatchpointStreamBackend::ResumeAllThreads() { |
| debug_ipc::ResumeRequest resume_request; |
| resume_request.process_koid = process_koid(); |
| debug_ipc::ResumeReply resume_reply; |
| remote_api_->OnResume(resume_request, &resume_reply); |
| } |
| |
| // Searches the loaded modules for specific one. |
| void WatchpointStreamBackend::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(); |
| } |
| |
| // Records the exception given from the debug agent. |
| void WatchpointStreamBackend::HandleNotifyException(NotifyException exception) { |
| DEBUG_LOG(Test) << "Received " << ExceptionTypeToString(exception.type) |
| << " on Thread: " << exception.thread.thread_koid; |
| exceptions_.push_back(std::move(exception)); |
| ShouldQuitLoop(); |
| } |
| |
| void WatchpointStreamBackend::HandleNotifyThreadStarting(NotifyThread thread) { |
| process_koid_ = thread.record.process_koid; |
| thread_koid_ = thread.record.thread_koid; |
| ShouldQuitLoop(); |
| } |
| |
| void WatchpointStreamBackend::HandleNotifyProcessExiting(NotifyProcessExiting process) { |
| DEBUG_LOG(Test) << "Process " << process.process_koid |
| << " exiting with return code: " << process.return_code; |
| FX_DCHECK(process.process_koid == process_koid_); |
| return_code_ = process.return_code; |
| ShouldQuitLoop(); |
| } |
| |
| void WatchpointStreamBackend::ShouldQuitLoop() { |
| if (test_stage_ == TestStage::kWaitingForThread) { |
| if (process_koid_ != 0u && thread_koid_ != 0u) { |
| test_stage_ = TestStage::kWaitingForModules; |
| DEBUG_LOG(Test) << "Stage changed to WAITING FOR MODULES."; |
| |
| // In this case we resume the thread. |
| ResumeAllThreads(); |
| return; |
| } |
| } else if (test_stage_ == TestStage::kWaitingForModules) { |
| if (so_test_base_addr_ != 0u) { |
| test_stage_ = TestStage::kWaitingForException; |
| DEBUG_LOG(Test) << "State changed to WAITING FOR EXCEPTION."; |
| loop_->QuitNow(); |
| return; |
| } |
| } else if (test_stage_ == TestStage::kWaitingForException) { |
| if (exceptions_.size() == 1u) { |
| test_stage_ = TestStage::kWaitingForExit; |
| DEBUG_LOG(Test) << "State changed to WAITING FOR EXIT."; |
| loop_->QuitNow(); |
| return; |
| } |
| } else if (test_stage_ == TestStage::kWaitingForExit) { |
| if (return_code_ != kInvalidReturnCode) { |
| test_stage_ = TestStage::kDone; |
| DEBUG_LOG(Test) << "State changed to DONE."; |
| loop_->QuitNow(); |
| return; |
| } |
| } |
| |
| FX_NOTREACHED() << "Invalid test state."; |
| } |
| |
| } // namespace |
| |
| } // namespace debug_agent |