| // 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 <lib/syslog/cpp/macros.h> |
| #include <zircon/status.h> |
| |
| #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/ipc/message_reader.h" |
| #include "src/developer/debug/shared/logging/logging.h" |
| #include "src/developer/debug/shared/platform_message_loop.h" |
| #include "src/developer/debug/shared/zx_status.h" |
| |
| namespace debug_agent { |
| |
| namespace { |
| |
| // This test is an integration test to verify that the debug agent is able to successfully set |
| // breakpoints to Zircon and get the correct responses. This particular test does the following |
| // script: |
| // |
| // 1. Load a pre-made .so (debug_agent_test_so) and search for a particular exported function. By |
| // also getting the loaded base address of the .so, we can get the offset of the function within |
| // the module. |
| // |
| // 2. Launch a process (through RemoteAPI::OnLaunch) control by the debug agent. |
| // |
| // 3. Get the module notication (NotifyModules message) for the process launched in (2). We look |
| // over the modules for the same module (debug_agent_test_so) that was loaded by this newly |
| // created process. With the base address of this module, we can use the offset calculated in (1) |
| // and get the actual loaded address for the exported function within the process. |
| // |
| // 4. Set a breakpoint on that address and resume the process. The test program is written such that |
| // it will call the searched symbol, so should hit the breakpoint. |
| // |
| // 5. Verify that we get a breakpoint exception on that address. |
| // |
| // 6. Success! |
| |
| // The exported symbol we're going to put the breakpoint on. |
| const char* kExportedFunctionName = "InsertBreakpointFunction"; |
| const char* kExportedFunctionName2 = "InsertBreakpointFunction2"; |
| |
| // The test .so we load in order to search the offset of the exported symbol |
| // within it. |
| const char* kTestSo = "debug_agent_test_so.so"; |
| |
| // The test executable the debug agent is going to launch. This is linked with |kTestSo|, meaning |
| // that the offset within that .so will be valid into the loaded module of this executable. |
| /* const char* kTestExecutableName = "breakpoint_test_exe"; */ |
| const char* kTestExecutablePath = "/pkg/bin/breakpoint_test_exe"; |
| const char* kModuleToSearch = "libdebug_agent_test_so.so"; |
| |
| class BreakpointStreamBackend : public LocalStreamBackend { |
| public: |
| BreakpointStreamBackend(debug_ipc::MessageLoop* loop) : loop_(loop) {} |
| |
| uint64_t so_test_base_addr() const { return so_test_base_addr_; } |
| |
| bool thread_started() const { return thread_started_; } |
| bool thread_exited() const { return thread_exited_; } |
| |
| bool process_exited() const { return process_exited_; } |
| |
| zx_koid_t process_koid() const { return process_koid_; } |
| zx_koid_t thread_koid() const { return thread_koid_; } |
| |
| const std::vector<debug_ipc::NotifyException>& exceptions() const { return exceptions_; } |
| |
| // The messages we're interested in handling --------------------------------- |
| |
| // Searches the loaded modules for specific one. |
| void HandleNotifyModules(debug_ipc::NotifyModules modules) override { |
| for (auto& module : modules.modules) { |
| if (module.name == kModuleToSearch) { |
| so_test_base_addr_ = module.base; |
| break; |
| } |
| } |
| loop_->QuitNow(); |
| } |
| |
| void HandleNotifyThreadStarting(debug_ipc::NotifyThread thread) override { |
| ASSERT_FALSE(thread_started_); |
| thread_started_ = true; |
| loop_->QuitNow(); |
| } |
| |
| // Records the exception given from the debug agent. |
| void HandleNotifyException(debug_ipc::NotifyException exception) override { |
| exceptions_.push_back(std::move(exception)); |
| loop_->QuitNow(); |
| } |
| |
| void HandleNotifyThreadExiting(debug_ipc::NotifyThread thread) override { |
| ASSERT_FALSE(thread_exited_); |
| thread_exited_ = true; |
| |
| loop_->QuitNow(); |
| } |
| |
| void HandleNotifyProcessExiting(debug_ipc::NotifyProcessExiting exit) override { |
| ASSERT_FALSE(process_exited_); |
| process_exited_ = true; |
| |
| loop_->QuitNow(); |
| } |
| |
| private: |
| debug_ipc::MessageLoop* loop_; |
| uint64_t so_test_base_addr_ = 0; |
| |
| bool thread_started_ = false; |
| bool thread_exited_ = false; |
| |
| bool process_exited_ = false; |
| |
| zx_koid_t process_koid_ = 0; |
| zx_koid_t thread_koid_ = 0; |
| |
| std::vector<debug_ipc::NotifyException> exceptions_; |
| }; |
| |
| } // namespace |
| |
| // TODO(fxbug.dev/73422): This test fails, fix and re-enable. |
| TEST(BreakpointIntegration, DISABLED_SWBreakpoint) { |
| // Uncomment for debugging the test. |
| // debug_ipc::SetDebugMode(true); |
| |
| // We attempt to load the pre-made .so. |
| SoWrapper so_wrapper; |
| ASSERT_TRUE(so_wrapper.Init(kTestSo)) << "Could not load so " << kTestSo; |
| |
| // Obtain the offsets into the .so of the symbols we want. |
| uint64_t symbol_offset1 = so_wrapper.GetSymbolOffset(kTestSo, kExportedFunctionName); |
| ASSERT_NE(symbol_offset1, 0u); |
| uint64_t symbol_offset2 = so_wrapper.GetSymbolOffset(kTestSo, kExportedFunctionName2); |
| ASSERT_NE(symbol_offset2, 0u); |
| |
| MessageLoopWrapper loop_wrapper; |
| { |
| auto* loop = loop_wrapper.loop(); |
| // This stream backend will take care of intercepting the calls from the debug agent. |
| BreakpointStreamBackend mock_stream_backend(loop); |
| |
| DebugAgent agent(std::make_unique<ZirconSystemInterface>()); |
| RemoteAPI* remote_api = &agent; |
| |
| agent.Connect(&mock_stream_backend.stream()); |
| |
| // We launch the test binary. |
| debug_ipc::LaunchRequest launch_request = {}; |
| launch_request.argv.push_back(kTestExecutablePath); |
| launch_request.inferior_type = debug_ipc::InferiorType::kBinary; |
| debug_ipc::LaunchReply launch_reply; |
| remote_api->OnLaunch(launch_request, &launch_reply); |
| ASSERT_EQ(launch_reply.status, ZX_OK) << "Got: " << zx_status_get_string(launch_reply.status); |
| |
| // We run the loop which will stop at the new thread notification. |
| loop->Run(); |
| |
| // We should have only received a thread started notification. |
| ASSERT_TRUE(mock_stream_backend.thread_started()); |
| ASSERT_TRUE(mock_stream_backend.exceptions().empty()); |
| ASSERT_FALSE(mock_stream_backend.thread_exited()); |
| |
| // We resume the thread because the new thread will be stopped. |
| debug_ipc::ResumeRequest resume_request; |
| resume_request.process_koid = mock_stream_backend.process_koid(); |
| debug_ipc::ResumeReply resume_reply; |
| remote_api->OnResume(resume_request, &resume_reply); |
| |
| // We run the loop to get the notifications sent by the agent. |
| // The stream backend will stop the loop once it has received the modules notification. |
| loop->Run(); |
| |
| // We should have found the correct module by now. |
| ASSERT_NE(mock_stream_backend.so_test_base_addr(), 0u); |
| |
| DEBUG_LOG(Test) << "Modules found. Adding breakpoint."; |
| |
| // We get the offset of the loaded function within the process space. |
| uint64_t module_base = mock_stream_backend.so_test_base_addr(); |
| uint64_t module_function1 = module_base + symbol_offset1; |
| uint64_t module_function2 = module_base + symbol_offset2; |
| |
| // We add a breakpoint in the functions. |
| constexpr uint32_t kBreakpointId = 1234u; |
| debug_ipc::AddOrChangeBreakpointRequest breakpoint_request = {}; |
| breakpoint_request.breakpoint.id = kBreakpointId; |
| breakpoint_request.breakpoint.one_shot = false; |
| |
| debug_ipc::ProcessBreakpointSettings location1 = {}; |
| location1.process_koid = launch_reply.process_id; |
| location1.address = module_function1; |
| breakpoint_request.breakpoint.locations.push_back(location1); |
| debug_ipc::ProcessBreakpointSettings location2 = {}; |
| location2.process_koid = launch_reply.process_id; |
| location2.address = module_function2; |
| breakpoint_request.breakpoint.locations.push_back(location2); |
| |
| debug_ipc::AddOrChangeBreakpointReply breakpoint_reply; |
| remote_api->OnAddOrChangeBreakpoint(breakpoint_request, &breakpoint_reply); |
| ASSERT_EQ(breakpoint_reply.status, ZX_OK); |
| |
| // Resume the process now that the breakpoint is installed. |
| DEBUG_LOG(Test) << "Resuming thread."; |
| remote_api->OnResume(resume_request, &resume_reply); |
| loop->Run(); |
| |
| // We should have received a breakpoint exception by now. |
| ASSERT_EQ(mock_stream_backend.exceptions().size(), 1u); |
| debug_ipc::NotifyException exception = mock_stream_backend.exceptions()[0]; |
| EXPECT_EQ(exception.thread.process_koid, launch_reply.process_id); |
| EXPECT_EQ(exception.type, debug_ipc::ExceptionType::kSoftwareBreakpoint); |
| ASSERT_EQ(exception.hit_breakpoints.size(), 1u); |
| EXPECT_TRUE(exception.other_affected_threads.empty()); // Test has only one thread. |
| |
| // Verify that the correct breakpoint was hit. |
| auto& breakpoint = exception.hit_breakpoints[0]; |
| EXPECT_EQ(breakpoint.id, kBreakpointId); |
| EXPECT_EQ(breakpoint.hit_count, 1u); |
| EXPECT_FALSE(breakpoint.should_delete); |
| |
| // Resuming the thread. |
| DEBUG_LOG(Test) << "First breakpoint found, resuming thread."; |
| remote_api->OnResume(resume_request, &resume_reply); |
| loop->Run(); |
| |
| // We should've received a second breakpoint exception. |
| ASSERT_EQ(mock_stream_backend.exceptions().size(), 2u); |
| exception = mock_stream_backend.exceptions()[1]; |
| EXPECT_EQ(exception.thread.process_koid, launch_reply.process_id); |
| EXPECT_EQ(exception.type, debug_ipc::ExceptionType::kSoftwareBreakpoint); |
| ASSERT_EQ(exception.hit_breakpoints.size(), 1u); |
| |
| // Verify that the correct breakpoint was hit. |
| breakpoint = exception.hit_breakpoints[0]; |
| EXPECT_EQ(breakpoint.id, kBreakpointId); |
| EXPECT_EQ(breakpoint.hit_count, 2u); |
| EXPECT_FALSE(breakpoint.should_delete); |
| |
| // Resuming the thread. |
| DEBUG_LOG(Test) << "Second breakpoint found, resuming thread."; |
| remote_api->OnResume(resume_request, &resume_reply); |
| loop->Run(); |
| |
| // We verify that the thread exited or the process exited. |
| ASSERT_TRUE(mock_stream_backend.thread_exited() || mock_stream_backend.process_exited()); |
| } |
| } |
| |
| #if defined(__aarch64__) |
| // TODO(donosoc): Currently arm64 has a flake over this functionality. |
| // One of the objectives of test week is to fix this flake once |
| // and for all. |
| TEST(BreakpointIntegration, DISABLED_HWBreakpoint) { |
| #else |
| TEST(BreakpointIntegration, DISABLED_HWBreakpoint) { |
| #endif |
| |
| // We attempt to load the pre-made .so. |
| SoWrapper so_wrapper; |
| ASSERT_TRUE(so_wrapper.Init(kTestSo)) << "Could not load so " << kTestSo; |
| |
| uint64_t symbol_offset = so_wrapper.GetSymbolOffset(kTestSo, kExportedFunctionName); |
| ASSERT_NE(symbol_offset, 0u); |
| |
| MessageLoopWrapper loop_wrapper; |
| { |
| auto* loop = loop_wrapper.loop(); |
| |
| // This stream backend will take care of intercepting the calls from the debug agent. |
| BreakpointStreamBackend mock_stream_backend(loop); |
| |
| DebugAgent agent(std::make_unique<ZirconSystemInterface>()); |
| RemoteAPI* remote_api = &agent; |
| |
| agent.Connect(&mock_stream_backend.stream()); |
| |
| DEBUG_LOG(Test) << "Launching binary."; |
| |
| // We launch the test binary. |
| debug_ipc::LaunchRequest launch_request = {}; |
| launch_request.inferior_type = debug_ipc::InferiorType::kBinary; |
| launch_request.argv.push_back(kTestExecutablePath); |
| debug_ipc::LaunchReply launch_reply; |
| remote_api->OnLaunch(launch_request, &launch_reply); |
| ASSERT_EQ(launch_reply.status, ZX_OK) << "Got: " << zx_status_get_string(launch_reply.status); |
| |
| // We run the loop which will stop at the new thread notification. |
| loop->Run(); |
| |
| // We should have only received a thread started notification. |
| ASSERT_TRUE(mock_stream_backend.thread_started()); |
| ASSERT_TRUE(mock_stream_backend.exceptions().empty()); |
| ASSERT_FALSE(mock_stream_backend.thread_exited()); |
| |
| // We resume the thread because the new thread will be stopped. |
| debug_ipc::ResumeRequest resume_request; |
| resume_request.process_koid = mock_stream_backend.process_koid(); |
| debug_ipc::ResumeReply resume_reply; |
| remote_api->OnResume(resume_request, &resume_reply); |
| |
| // We run the loop to get the notifications sent by the agent. |
| // The stream backend will stop the loop once it has received the modules notification. |
| loop->Run(); |
| |
| // We should have found the correct module by now. |
| ASSERT_NE(mock_stream_backend.so_test_base_addr(), 0u); |
| |
| // We get the offset of the loaded function within the process space. |
| uint64_t module_base = mock_stream_backend.so_test_base_addr(); |
| uint64_t module_function = module_base + symbol_offset; |
| |
| DEBUG_LOG(Test) << "Setting breakpoint at 0x" << std::hex << module_function; |
| |
| // We add a breakpoint in that address. |
| constexpr uint32_t kBreakpointId = 1234u; |
| debug_ipc::ProcessBreakpointSettings location = {}; |
| location.process_koid = launch_reply.process_id; |
| location.address = module_function; |
| |
| debug_ipc::AddOrChangeBreakpointRequest breakpoint_request = {}; |
| breakpoint_request.breakpoint.id = kBreakpointId; |
| breakpoint_request.breakpoint.type = debug_ipc::BreakpointType::kHardware; |
| breakpoint_request.breakpoint.one_shot = true; |
| breakpoint_request.breakpoint.locations.push_back(location); |
| debug_ipc::AddOrChangeBreakpointReply breakpoint_reply; |
| remote_api->OnAddOrChangeBreakpoint(breakpoint_request, &breakpoint_reply); |
| ASSERT_EQ(breakpoint_reply.status, ZX_OK) |
| << "Received: " << debug_ipc::ZxStatusToString(breakpoint_reply.status); |
| |
| // Resume the process now that the breakpoint is installed. |
| remote_api->OnResume(resume_request, &resume_reply); |
| |
| // The loop will run until the stream backend receives an exception notification. |
| loop->Run(); |
| |
| DEBUG_LOG(Test) << "Hit breakpoint."; |
| |
| // We should have received an exception now. |
| ASSERT_EQ(mock_stream_backend.exceptions().size(), 1u); |
| debug_ipc::NotifyException exception = mock_stream_backend.exceptions()[0]; |
| EXPECT_EQ(exception.thread.process_koid, launch_reply.process_id); |
| EXPECT_EQ(exception.type, debug_ipc::ExceptionType::kHardwareBreakpoint) |
| << "Got: " << debug_ipc::ExceptionTypeToString(exception.type); |
| ASSERT_EQ(exception.hit_breakpoints.size(), 1u); |
| |
| // Verify that the correct breakpoint was hit. |
| auto& breakpoint = exception.hit_breakpoints[0]; |
| EXPECT_EQ(breakpoint.id, kBreakpointId); |
| EXPECT_EQ(breakpoint.hit_count, 1u); |
| EXPECT_TRUE(breakpoint.should_delete); |
| |
| // Resume the thread again. |
| remote_api->OnResume(resume_request, &resume_reply); |
| loop->Run(); |
| |
| DEBUG_LOG(Test) << "Verifyint thread exited correctly."; |
| |
| // We verify that the thread exited or the process exited. |
| ASSERT_TRUE(mock_stream_backend.thread_exited() || mock_stream_backend.process_exited()); |
| } |
| } |
| |
| } // namespace debug_agent |