| // 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. |
| |
| #ifndef SRC_DEVELOPER_DEBUG_IPC_RECORDS_H_ |
| #define SRC_DEVELOPER_DEBUG_IPC_RECORDS_H_ |
| |
| #include <stdint.h> |
| |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "src/developer/debug/ipc/automation_instruction.h" |
| #include "src/developer/debug/shared/address_range.h" |
| #include "src/developer/debug/shared/register_id.h" |
| #include "src/developer/debug/shared/register_value.h" |
| #include "src/developer/debug/shared/serialization.h" |
| |
| namespace debug_ipc { |
| |
| #pragma pack(push, 8) |
| |
| enum class ExceptionType : uint32_t { |
| // No current exception, used as placeholder or to indicate not set. |
| kNone = 0, |
| |
| // Zircon defines this as a sort of catch-all exception. |
| kGeneral, |
| |
| // The usual band of execution traps. |
| kPageFault, |
| kUndefinedInstruction, |
| kUnalignedAccess, |
| |
| // Indicates the process was killed due to misusing a syscall, e.g. passing a bad handle. |
| kPolicyError, |
| |
| // Synthetic exeptions used by zircon to communicated with the debugger. The debug agent generally |
| // shouldn't pass these on, but we should recognize them at least. |
| kThreadStarting, |
| kThreadExiting, |
| kProcessStarting, |
| |
| // Hardware breakpoints are issues by the CPU via debug registers. |
| kHardwareBreakpoint, |
| |
| // HW exceptions triggered on memory read/write. |
| kWatchpoint, |
| |
| // Single-step completion issued by the CPU. |
| kSingleStep, |
| |
| // Software breakpoint. This will be issued when a breakpoint is hit and when the debugged program |
| // manually issues a breakpoint instruction. |
| kSoftwareBreakpoint, |
| |
| // Indicates this exception is not a real CPU exception but was generated internally for the |
| // purposes of sending a stop notification. The frontend uses this value when the thread didn't |
| // actually do anything, but the should be updated as if it hit an exception. |
| kSynthetic, |
| |
| // For exception codes the debugger doesn't recognize. |
| kUnknown, |
| |
| kLast // Not an actual exception type, for range checking. |
| }; |
| const char* ExceptionTypeToString(ExceptionType); |
| bool IsDebug(ExceptionType); |
| |
| // Exception handling strategy. |
| enum class ExceptionStrategy : uint32_t { |
| // No current exception, used as placeholder or to indicate not set. |
| kNone = 0, |
| |
| // Indicates that the debugger only gets the first chance to handle the |
| // exception, before the thread and process-level handlers. |
| kFirstChance, |
| |
| // Indicates that the debugger also gets a second first chance to handle |
| // the exception after process-level handler. |
| kSecondChance, |
| |
| kLast, // Not an actual exception strategy, for range checking. |
| }; |
| |
| const char* ExceptionStrategyToString(ExceptionStrategy); |
| |
| std::optional<ExceptionStrategy> ToExceptionStrategy(uint32_t raw_value); |
| |
| std::optional<uint32_t> ToRawValue(ExceptionStrategy strategy); |
| |
| // A process+thread koid pair for referring to a thread. While a thread koid is globally unique and |
| // doesn't technically need a process koid to scope it, most code deals with a process/thread |
| // hierarchy so maintaining both is more convenient. |
| struct ProcessThreadId { |
| uint64_t process = 0; |
| uint64_t thread = 0; |
| |
| bool operator==(const ProcessThreadId& other) const { |
| return process == other.process && thread == other.thread; |
| } |
| bool operator!=(const ProcessThreadId& other) const { return !operator==(other); } |
| |
| // For ordered containers. |
| bool operator<(const ProcessThreadId& other) const { |
| return std::tie(process, thread) < std::tie(other.process, other.thread); |
| } |
| |
| void Serialize(Serializer& ser, uint32_t ver) { ser | process | thread; } |
| }; |
| |
| struct ExceptionRecord { |
| ExceptionRecord() { memset(&arch, 0, sizeof(Arch)); } |
| |
| // Race conditions or other errors can conspire to mean the exception records are not valid. In |
| // order to differentiate this case from "0" addresses, this flag indicates validity of the "arch" |
| // union. |
| bool valid = false; |
| |
| union Arch { |
| // Exception record for x64. |
| struct { |
| uint64_t vector; |
| uint64_t err_code; |
| uint64_t cr2; |
| } x64; |
| |
| // Exception record for ARM64. |
| struct { |
| uint32_t esr; |
| uint64_t far; |
| } arm64; |
| |
| // Exception record for RISC-V 64. |
| struct { |
| uint64_t cause; |
| uint64_t tval; |
| } riscv64; |
| } arch; |
| |
| ExceptionStrategy strategy = ExceptionStrategy::kNone; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { |
| ser | valid; |
| ser.SerializeBytes(&arch, sizeof(Arch)); |
| ser | strategy; |
| } |
| }; |
| |
| struct ComponentInfo { |
| std::string moniker; |
| std::string url; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { ser | moniker | url; } |
| }; |
| |
| // Note: see "ps" source: |
| // https://fuchsia.googlesource.com/fuchsia/+/HEAD/src/sys/bin/psutils/ps.c |
| struct ProcessTreeRecord { |
| enum class Type : uint32_t { kJob, kProcess }; |
| |
| Type type = Type::kJob; |
| uint64_t koid = 0; |
| std::string name; |
| |
| // The following fields are only valid on kJob and will be skipped if type is kProcess. |
| |
| // The component information if the process is running in a component. There could be many |
| // components for a single process. An empty vector means there was no component associated with |
| // the process. Order of components is not guaranteed. |
| std::vector<ComponentInfo> components; |
| |
| std::vector<ProcessTreeRecord> children; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { |
| ser | type | koid | name; |
| if (type == Type::kJob) { |
| if (ver < 57) { |
| // The component information if the current job is the root job of an ELF component. |
| // Deprecated in version 57 in favor of |components|. |
| std::optional<ComponentInfo> component = std::nullopt; |
| if (!components.empty()) { |
| component = components[0]; |
| } |
| components.clear(); |
| |
| ser | component; |
| |
| if (component) { |
| components = {*component}; |
| } |
| } else { |
| ser | components; |
| } |
| ser | children; |
| } |
| } |
| }; |
| |
| struct StackFrame { |
| StackFrame() = default; |
| StackFrame(uint64_t ip, uint64_t sp, uint64_t cfa = 0, std::vector<debug::RegisterValue> r = {}) |
| : ip(ip), sp(sp), cfa(cfa), regs(std::move(r)) {} |
| |
| // Comparisons (primarily for tests). |
| bool operator==(const StackFrame& other) const { |
| return ip == other.ip && sp == other.sp && cfa == other.cfa && regs == other.regs; |
| } |
| bool operator!=(const StackFrame& other) const { return !operator==(other); } |
| |
| // Instruction pointer. |
| uint64_t ip = 0; |
| |
| // Stack pointer. |
| uint64_t sp = 0; |
| |
| // Canonical frame address. This is the stack pointer of the previous |
| // frame at the time of the call. 0 if unknown. |
| uint64_t cfa = 0; |
| |
| // Known general registers for this stack frame. See IsGeneralRegister() for |
| // which registers are counted as "general". |
| // |
| // Every frame should contain the register for the IP and SP for the current |
| // architecture (duplicating the above two fields). |
| std::vector<debug::RegisterValue> regs; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { ser | ip | sp | cfa | regs; } |
| }; |
| |
| struct ThreadRecord { |
| enum class State : uint32_t { |
| kNew = 0, // The thread is newly created and running. |
| kRunning, |
| kSuspended, |
| kBlocked, |
| kDying, |
| kDead, |
| kCoreDump, |
| |
| kLast // Not an actual thread state, for range checking. |
| }; |
| static const char* StateToString(State); |
| |
| enum class BlockedReason : uint32_t { |
| kNotBlocked = 0, // Used when State isn't kBlocked. |
| |
| kException, |
| kSleeping, |
| kFutex, |
| kPort, |
| kChannel, |
| kWaitOne, |
| kWaitMany, |
| kInterrupt, |
| kPager, |
| |
| kLast // Not an actual blocked reason, for range checking. |
| }; |
| static const char* BlockedReasonToString(BlockedReason); |
| |
| // Indicates how much of the stack was attempted to be retrieved in this |
| // call. This doesn't indicate how many stack frames were actually retrieved. |
| // For example, there could be no stack frames because they weren't |
| // requested, or there could be no stack frames due to an error. |
| enum class StackAmount : uint32_t { |
| // A backtrace was not attempted. This will always be the case if the |
| // thread is neither suspended nor blocked in an exception. |
| kNone = 0, |
| |
| // The frames vector contains a minimal stack only (if available) which |
| // is defined as the top two frames. This is used when the stack frames |
| // have not been specifically requested since retrieving the full stack |
| // can be slow. The frames can still be less than 2 if there was an error |
| // or if there is only one stack frame. |
| kMinimal, |
| |
| // The frames are the full stack trace (up to some maximum). |
| kFull, |
| |
| kLast // Not an actual state, for range checking. |
| }; |
| |
| ProcessThreadId id; |
| std::string name; |
| State state = State::kNew; |
| // Only valid when state is kBlocked. |
| BlockedReason blocked_reason = BlockedReason::kNotBlocked; |
| StackAmount stack_amount = StackAmount::kNone; |
| |
| // The frames of the top of the stack when the thread is in suspended or blocked in an exception |
| // (if possible). See stack_amnount for how to interpret this. |
| // |
| // This could still be empty in the "kMinimal" or "kFull" cases if retrieval failed, which can |
| // happen in some valid race conditions if the thread was killed out from under the debug agent. |
| std::vector<StackFrame> frames; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { |
| ser | id | name | state | blocked_reason | stack_amount | frames; |
| } |
| }; |
| |
| struct ProcessRecord { |
| uint64_t process_koid = 0; |
| std::string process_name; |
| |
| // The component information if the process is running in a component. There could be many |
| // components for a single process. An empty vector means there was no component associated with |
| // the process. Order of components is not guaranteed. |
| std::vector<ComponentInfo> components; |
| |
| std::vector<ThreadRecord> threads; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { |
| ser | process_koid | process_name | threads; |
| if (ver < 57) { |
| // The component information if the process is running in a component. |
| // Deprecated in version 57 in favor of |components|. |
| std::optional<ComponentInfo> component = std::nullopt; |
| if (!components.empty()) { |
| component = components[0]; |
| } |
| components.clear(); |
| |
| ser | component; |
| |
| if (component) { |
| components = {*component}; |
| } |
| } else { |
| ser | components; |
| } |
| } |
| }; |
| |
| struct MemoryBlock { |
| // Begin address of this memory. |
| uint64_t address = 0; |
| |
| // When true, indicates this is valid memory, with the data containing the |
| // memory. False means that this range is not mapped in the process and the |
| // data will be empty. |
| bool valid = false; |
| |
| // Length of this range. When valid == true, this will be the same as |
| // data.size(). When valid == false, this will be whatever the length of |
| // the invalid region is, and data will be empty. |
| uint32_t size = 0; |
| |
| // The actual memory. Filled in only if valid == true. |
| std::vector<uint8_t> data; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { ser | address | valid | size | data; } |
| }; |
| |
| struct ProcessBreakpointSettings { |
| // The process is required to be nonzero. A zero thread ID indicates this is a process-wide |
| // breakpoint. Otherwise, this is the thread to break. |
| ProcessThreadId id; |
| |
| // Address to break at. |
| uint64_t address = 0; |
| |
| // Range is used for watchpoints. |
| debug::AddressRange address_range; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { ser | id | address | address_range; } |
| }; |
| |
| // What threads to stop when the breakpoint is hit. These are ordered such that the integer values |
| // increase for larger scopes. |
| enum class Stop : uint32_t { |
| kNone = 0, // Don't stop anything but accumulate hit counts. |
| kThread, // Stop only the thread that hit the breakpoint. |
| kProcess, // Stop all threads of the process that hit the breakpoint. |
| kAll // Stop all threads of all processes attached to the debugger. |
| }; |
| |
| // NOTE: read-only could be added in the future as arm64 supports them. They're not added today as |
| // x64 does not support them and presenting a common platform is cleaner for now. |
| enum class BreakpointType : uint32_t { |
| kSoftware = 0, // Software code execution. |
| kHardware, // Hardware code execution. |
| kReadWrite, // Hardware read/write. |
| kWrite, // Hardware write. |
| |
| kLast, // Not a real type, end marker. |
| }; |
| const char* BreakpointTypeToString(BreakpointType); |
| |
| // Read, ReadWrite and Write are considered watchpoint types. |
| bool IsWatchpointType(BreakpointType); |
| |
| constexpr uint32_t kDebugAgentInternalBreakpointId = static_cast<uint32_t>(-1); |
| |
| struct BreakpointSettings { |
| // The ID if this breakpoint. This is assigned by the client. This is different than the ID in |
| // the console frontend which can be across mutliple processes or may match several addresses in |
| // a single process. |
| // |
| // The ID kDebugAgentInternalBreakpointId is reserved for internal use by the backend. |
| uint32_t id = 0; |
| |
| BreakpointType type = BreakpointType::kSoftware; |
| |
| // Name used to recognize a breakpoint. Useful for debugging purposes. Optional. |
| std::string name; |
| |
| // When set, the breakpoint will automatically be removed as soon as it is |
| // hit. |
| bool one_shot = false; |
| |
| // What should stop when the breakpoint is hit. |
| Stop stop = Stop::kAll; |
| |
| // Processes to which this breakpoint applies. |
| // |
| // If any process specifies a nonzero thread_koid, it must be the only process (a breakpoint can |
| // apply either to all threads in a set of processes, or exactly one thread globally). |
| std::vector<ProcessBreakpointSettings> locations; |
| |
| // Handles the automatic collection of memory if it's requested. |
| bool has_automation = false; |
| |
| std::vector<debug_ipc::AutomationInstruction> instructions; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { |
| ser | id | type | name | one_shot | stop | locations | has_automation | instructions; |
| } |
| }; |
| |
| struct BreakpointStats { |
| uint32_t id = 0; |
| uint32_t hit_count = 0; |
| |
| // On a "breakpoint hit" message from the debug agent, if this flag is set, |
| // the agent has deleted the breakpoint because it was a one-shot breakpoint. |
| // Whenever a client gets a breakpoint hit with this flag set, it should |
| // clear the local state associated with the breakpoint. |
| bool should_delete = false; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { ser | id | hit_count | should_delete; } |
| }; |
| |
| // Information on one loaded module. |
| struct Module { |
| std::string name; // The main executable binary will normally have an empty name. |
| uint64_t base = 0; // Load address of this file. |
| uint64_t debug_address = 0; // Link map address for this module. |
| std::string build_id; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { ser | name | base | debug_address | build_id; } |
| }; |
| |
| struct AddressRegion { |
| std::string name; |
| uint64_t base = 0; |
| uint64_t size = 0; |
| uint64_t depth = 0; |
| uint64_t vmo_koid = 0; // Fuchsia only. |
| uint64_t vmo_offset = 0; |
| uint64_t committed_pages = 0; |
| |
| // MMU flags. |
| bool read = false; |
| bool write = false; |
| bool execute = false; |
| bool shared = false; // Linux only. |
| |
| void Serialize(Serializer& ser, uint32_t ver) { |
| if (ver < 60) { |
| // Previous to v60 the MMU flags were sent as a uint32_t bitfield. |
| uint32_t mmu_flags = 0; |
| if (read) { |
| mmu_flags = mmu_flags | (1u << 0); |
| } else if (write) { |
| mmu_flags = mmu_flags | (1u << 1); |
| } else if (execute) { |
| mmu_flags = mmu_flags | (1u << 2); |
| } |
| |
| ser | name | base | size | depth | mmu_flags | vmo_koid | vmo_offset | committed_pages; |
| |
| read = !!(mmu_flags & (1u << 0)); |
| write = !!(mmu_flags & (1u << 1)); |
| execute = !!(mmu_flags & (1u << 2)); |
| } else { |
| ser | name | base | size | depth | vmo_koid | vmo_offset | committed_pages | read | write | |
| execute | shared; |
| } |
| } |
| }; |
| |
| // LoadInfoHandleTable ----------------------------------------------------------------------------- |
| |
| // VMO-specific handle information from zx_info_vmo that's not in the InfoHandle structure. |
| struct InfoHandleVmo { |
| char name[32]; // Needs to be POD for use in union below, and 32 is the max from the kernel. |
| uint64_t size_bytes; |
| uint64_t parent_koid; |
| uint64_t num_children; |
| uint64_t num_mappings; |
| uint64_t share_count; |
| uint32_t flags; |
| uint64_t committed_bytes; |
| uint32_t cache_policy; |
| uint64_t metadata_bytes; |
| uint64_t committed_change_events; |
| }; |
| |
| // This structure is assumed to be entirely POD. |
| struct InfoHandle { |
| // Provide 0-init that covers the union. |
| InfoHandle() { memset(this, 0, sizeof(InfoHandle)); } |
| |
| // Standard information from zx_info_handle_extended. |
| // |
| // There is a special case for a VMO. It is possible to have a VMO mapped without a handle to it. |
| // These will appear here but the handle_value will be 0. |
| uint32_t type; |
| uint32_t handle_value; |
| uint32_t rights; |
| uint32_t reserved; |
| uint64_t koid; |
| uint64_t related_koid; |
| uint64_t peer_owner_koid; |
| |
| // Type-specific handle information. |
| union { |
| InfoHandleVmo vmo; // Valid when type == ZX_OBJ_TYPE_VMO. |
| // Other types go here. |
| } ext; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { ser.SerializeBytes(this, sizeof(*this)); } |
| }; |
| |
| // Filters ----------------------------------------------------------------------------------------- |
| constexpr uint32_t kInvalidFilterId = static_cast<uint32_t>(-1); |
| |
| struct Filter { |
| enum class Type : uint32_t { |
| kUnset, |
| kProcessNameSubstr, |
| kProcessName, |
| kComponentName, |
| kComponentUrl, |
| kComponentMoniker, |
| kComponentMonikerSuffix, |
| kComponentMonikerPrefix, |
| |
| kLast, |
| } type = Type::kUnset; |
| static const char* TypeToString(Type); |
| |
| std::string pattern; |
| uint64_t job_koid = 0; // must be 0 when type is kComponent*. |
| |
| // Set by the frontend. Different from the console's id for "active" filters in the ui. |
| uint32_t id = kInvalidFilterId; |
| |
| // Whether or not this is a weak filter. The backend needs to know this so when newly spawned |
| // processes that match a filter hit the loader breakpoint can know whether or not to eagerly send |
| // modules to the front end. |
| bool weak = false; |
| |
| // Indicates a recursive filter, which should match all child components spawned in the realm of |
| // this filter's matching component. |
| bool recursive = false; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { |
| ser | type | pattern | job_koid; |
| if (ver >= 61) { |
| ser | id | weak; |
| } |
| if (ver >= 62) { |
| ser | recursive; |
| } |
| } |
| }; |
| |
| // Reply indicating that a filter matched one or more processes. |
| struct FilterMatch { |
| FilterMatch() = default; |
| FilterMatch(uint32_t id, std::vector<uint64_t> pids) : id(id), matched_pids(std::move(pids)) {} |
| |
| // The frontend id of the filter that matched, -1 if invalid or unknown. |
| uint32_t id = kInvalidFilterId; |
| |
| // All of the pids that matched this filter. |
| std::vector<uint64_t> matched_pids; |
| |
| void Serialize(Serializer& ser, uint32_t ver) { ser | id | matched_pids; } |
| }; |
| |
| #pragma pack(pop) |
| |
| } // namespace debug_ipc |
| |
| #endif // SRC_DEVELOPER_DEBUG_IPC_RECORDS_H_ |