| // 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 "garnet/bin/debug_agent/process_info.h" |
| |
| // link.h leaks Elf-related #defines all over the place, so this has to be |
| // included early or we don't compile. C++ was a mistake. |
| #include "garnet/lib/elflib/elflib.h" |
| |
| #include <inttypes.h> |
| #include <lib/zx/thread.h> |
| #include <link.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/syscalls/object.h> |
| |
| #include <algorithm> |
| |
| #include "garnet/bin/debug_agent/arch.h" |
| #include "garnet/bin/debug_agent/object_util.h" |
| #include "garnet/bin/debug_agent/unwind.h" |
| #include "garnet/lib/debug_ipc/helper/elf.h" |
| #include "lib/fxl/logging.h" |
| |
| namespace debug_agent { |
| |
| namespace { |
| |
| using elflib::ElfLib; |
| |
| class ProcessMemoryAccessor : public ElfLib::MemoryAccessor { |
| public: |
| ProcessMemoryAccessor(const zx::process* process, uint64_t base) |
| : process_(process), base_(base) {} |
| virtual ~ProcessMemoryAccessor() = default; |
| |
| std::optional<std::vector<uint8_t>> GetMemory(uint64_t offset, |
| size_t size) override { |
| std::vector<uint8_t> out; |
| out.resize(size); |
| size_t got; |
| |
| if (process_->read_memory(base_ + offset, out.data(), out.size(), &got) != |
| ZX_OK) { |
| return std::nullopt; |
| } |
| |
| if (got != size) { |
| return std::nullopt; |
| } |
| |
| return out; |
| } |
| |
| std::optional<std::vector<uint8_t>> GetMappedMemory( |
| uint64_t offset, uint64_t file_size, uint64_t mapped_address, |
| uint64_t mapped_size) override { |
| return GetMemory(mapped_address, mapped_size); |
| } |
| |
| private: |
| const zx::process* process_; |
| uint64_t base_; |
| }; |
| |
| zx_status_t WalkModules( |
| const zx::process& process, uint64_t dl_debug_addr, |
| std::function<bool(const zx::process&, uint64_t, uint64_t)> cb) { |
| size_t num_read = 0; |
| uint64_t lmap = 0; |
| zx_status_t status = process.read_memory( |
| dl_debug_addr + offsetof(r_debug, r_map), &lmap, sizeof(lmap), &num_read); |
| if (status != ZX_OK) |
| return status; |
| |
| size_t module_count = 0; |
| |
| // Walk the linked list. |
| constexpr size_t kMaxObjects = 512; // Sanity threshold. |
| while (lmap != 0) { |
| if (module_count++ >= kMaxObjects) |
| return ZX_ERR_BAD_STATE; |
| |
| uint64_t base; |
| if (process.read_memory(lmap + offsetof(link_map, l_addr), &base, |
| sizeof(base), &num_read) != ZX_OK) |
| break; |
| |
| uint64_t next; |
| if (process.read_memory(lmap + offsetof(link_map, l_next), &next, |
| sizeof(next), &num_read) != ZX_OK) |
| break; |
| |
| if (!cb(process, base, lmap)) |
| break; |
| |
| lmap = next; |
| } |
| |
| return ZX_OK; |
| } |
| |
| debug_ipc::ThreadRecord::BlockedReason ThreadStateBlockedReasonToEnum( |
| uint32_t state) { |
| FXL_DCHECK(ZX_THREAD_STATE_BASIC(state) == ZX_THREAD_STATE_BLOCKED); |
| |
| switch (state) { |
| case ZX_THREAD_STATE_BLOCKED_EXCEPTION: |
| return debug_ipc::ThreadRecord::BlockedReason::kException; |
| case ZX_THREAD_STATE_BLOCKED_SLEEPING: |
| return debug_ipc::ThreadRecord::BlockedReason::kSleeping; |
| case ZX_THREAD_STATE_BLOCKED_FUTEX: |
| return debug_ipc::ThreadRecord::BlockedReason::kFutex; |
| case ZX_THREAD_STATE_BLOCKED_PORT: |
| return debug_ipc::ThreadRecord::BlockedReason::kPort; |
| case ZX_THREAD_STATE_BLOCKED_CHANNEL: |
| return debug_ipc::ThreadRecord::BlockedReason::kChannel; |
| case ZX_THREAD_STATE_BLOCKED_WAIT_ONE: |
| return debug_ipc::ThreadRecord::BlockedReason::kWaitOne; |
| case ZX_THREAD_STATE_BLOCKED_WAIT_MANY: |
| return debug_ipc::ThreadRecord::BlockedReason::kWaitMany; |
| case ZX_THREAD_STATE_BLOCKED_INTERRUPT: |
| return debug_ipc::ThreadRecord::BlockedReason::kInterrupt; |
| default: |
| FXL_NOTREACHED(); |
| return debug_ipc::ThreadRecord::BlockedReason::kNotBlocked; |
| } |
| } |
| |
| debug_ipc::ThreadRecord::State ThreadStateToEnums( |
| uint32_t state, debug_ipc::ThreadRecord::BlockedReason* blocked_reason) { |
| struct Mapping { |
| uint32_t int_state; |
| debug_ipc::ThreadRecord::State enum_state; |
| }; |
| static const Mapping mappings[] = { |
| {ZX_THREAD_STATE_NEW, debug_ipc::ThreadRecord::State::kNew}, |
| {ZX_THREAD_STATE_RUNNING, debug_ipc::ThreadRecord::State::kRunning}, |
| {ZX_THREAD_STATE_SUSPENDED, debug_ipc::ThreadRecord::State::kSuspended}, |
| {ZX_THREAD_STATE_BLOCKED, debug_ipc::ThreadRecord::State::kBlocked}, |
| {ZX_THREAD_STATE_DYING, debug_ipc::ThreadRecord::State::kDying}, |
| {ZX_THREAD_STATE_DEAD, debug_ipc::ThreadRecord::State::kDead}}; |
| |
| const uint32_t basic_state = ZX_THREAD_STATE_BASIC(state); |
| *blocked_reason = debug_ipc::ThreadRecord::BlockedReason::kNotBlocked; |
| |
| for (const Mapping& mapping : mappings) { |
| if (mapping.int_state == basic_state) { |
| if (mapping.enum_state == debug_ipc::ThreadRecord::State::kBlocked) { |
| *blocked_reason = ThreadStateBlockedReasonToEnum(state); |
| } |
| return mapping.enum_state; |
| } |
| } |
| FXL_NOTREACHED(); |
| return debug_ipc::ThreadRecord::State::kDead; |
| } |
| |
| // Reads a null-terminated string from the given address of the given process. |
| zx_status_t ReadNullTerminatedString(const zx::process& process, |
| zx_vaddr_t vaddr, std::string* dest) { |
| // Max size of string we'll load as a sanity check. |
| constexpr size_t kMaxString = 32768; |
| |
| dest->clear(); |
| |
| constexpr size_t kBlockSize = 256; |
| char block[kBlockSize]; |
| while (dest->size() < kMaxString) { |
| size_t num_read = 0; |
| zx_status_t status = |
| process.read_memory(vaddr, block, kBlockSize, &num_read); |
| if (status != ZX_OK) |
| return status; |
| |
| for (size_t i = 0; i < num_read; i++) { |
| if (block[i] == 0) |
| return ZX_OK; |
| dest->push_back(block[i]); |
| } |
| |
| if (num_read < kBlockSize) |
| return ZX_OK; // Partial read: hit the mapped memory boundary. |
| vaddr += kBlockSize; |
| } |
| return ZX_OK; |
| } |
| |
| } // namespace |
| |
| zx_status_t GetProcessInfo(zx_handle_t process, zx_info_process* info) { |
| return zx_object_get_info(process, ZX_INFO_PROCESS, info, |
| sizeof(zx_info_process), nullptr, nullptr); |
| } |
| |
| zx_status_t GetProcessThreads(const zx::process& process, |
| uint64_t dl_debug_addr, |
| std::vector<debug_ipc::ThreadRecord>* threads) { |
| auto koids = GetChildKoids(process.get(), ZX_INFO_PROCESS_THREADS); |
| threads->resize(koids.size()); |
| for (size_t i = 0; i < koids.size(); i++) { |
| (*threads)[i].koid = koids[i]; |
| |
| zx_handle_t handle; |
| if (zx_object_get_child(process.get(), koids[i], ZX_RIGHT_SAME_RIGHTS, |
| &handle) == ZX_OK) { |
| FillThreadRecord(process, dl_debug_addr, zx::thread(handle), |
| debug_ipc::ThreadRecord::StackAmount::kMinimal, nullptr, |
| &(*threads)[i]); |
| } |
| } |
| return ZX_OK; |
| } |
| |
| void FillThreadRecord(const zx::process& process, uint64_t dl_debug_addr, |
| const zx::thread& thread, |
| debug_ipc::ThreadRecord::StackAmount stack_amount, |
| const zx_thread_state_general_regs* optional_regs, |
| debug_ipc::ThreadRecord* record) { |
| record->koid = KoidForObject(thread); |
| record->name = NameForObject(thread); |
| |
| zx_info_thread info; |
| if (thread.get_info(ZX_INFO_THREAD, &info, sizeof(info), nullptr, nullptr) == |
| ZX_OK) { |
| record->state = ThreadStateToEnums(info.state, &record->blocked_reason); |
| } else { |
| FXL_NOTREACHED(); |
| record->state = debug_ipc::ThreadRecord::State::kDead; |
| } |
| |
| // The registers are available when suspended or blocked in an exception. |
| if ((info.state == ZX_THREAD_STATE_SUSPENDED || |
| info.state == ZX_THREAD_STATE_BLOCKED_EXCEPTION) && |
| stack_amount != debug_ipc::ThreadRecord::StackAmount::kNone) { |
| // Only record this when we actually attempt to query the stack. |
| record->stack_amount = stack_amount; |
| |
| // The registers are required, fetch them if the caller didn't provide. |
| zx_thread_state_general_regs queried_regs; // Storage for fetched regs. |
| zx_thread_state_general_regs* regs = nullptr; |
| if (!optional_regs) { |
| if (thread.read_state(ZX_THREAD_STATE_GENERAL_REGS, &queried_regs, |
| sizeof(queried_regs)) == ZX_OK) |
| regs = &queried_regs; |
| } else { |
| // We don't change the values here but *InRegs below returns mutable |
| // references so we need a mutable pointer. |
| regs = const_cast<zx_thread_state_general_regs*>(optional_regs); |
| } |
| |
| if (regs) { |
| // Minimal stacks are 2 (current frame and calling one). Full stacks max |
| // out at 256 to prevent edge cases, especially around corrupted stacks. |
| uint32_t max_stack_depth = |
| stack_amount == debug_ipc::ThreadRecord::StackAmount::kMinimal ? 2 |
| : 256; |
| |
| UnwindStack(process, dl_debug_addr, thread, |
| *arch::ArchProvider::Get().IPInRegs(regs), |
| *arch::ArchProvider::Get().SPInRegs(regs), |
| *arch::ArchProvider::Get().BPInRegs(regs), max_stack_depth, |
| &record->frames); |
| } |
| } else { |
| // Didn't bother querying the stack. |
| record->stack_amount = debug_ipc::ThreadRecord::StackAmount::kNone; |
| } |
| } |
| |
| zx_status_t GetModulesForProcess(const zx::process& process, |
| uint64_t dl_debug_addr, |
| std::vector<debug_ipc::Module>* modules) { |
| return WalkModules( |
| process, dl_debug_addr, |
| [modules](const zx::process& process, uint64_t base, uint64_t lmap) { |
| debug_ipc::Module module; |
| module.base = base; |
| |
| uint64_t str_addr; |
| size_t num_read; |
| if (process.read_memory(lmap + offsetof(link_map, l_name), &str_addr, |
| sizeof(str_addr), &num_read) != ZX_OK) |
| return false; |
| |
| if (ReadNullTerminatedString(process, str_addr, &module.name) != ZX_OK) |
| return false; |
| |
| module.build_id = debug_ipc::ExtractBuildID(process, module.base); |
| |
| modules->push_back(std::move(module)); |
| return true; |
| }); |
| } |
| |
| zx_status_t GetSymbolTableFromProcess( |
| const zx::process& process, uint64_t elf_addr, const std::string& build_id, |
| std::vector<debug_ipc::ElfSymbol>* symbols) { |
| auto elf = ElfLib::Create( |
| std::make_unique<ProcessMemoryAccessor>(&process, elf_addr)); |
| |
| if (!elf) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| auto got_build_id = debug_ipc::ExtractBuildID(process, elf_addr); |
| |
| if (got_build_id != build_id) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| auto syms = elf->GetAllSymbols(); |
| |
| if (!syms) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| for (const auto& sym : *syms) { |
| debug_ipc::ElfSymbol ipc_sym; |
| |
| ipc_sym.name = sym.first; |
| ipc_sym.value = sym.second.st_value; |
| |
| symbols->push_back(std::move(ipc_sym)); |
| } |
| |
| return ZX_OK; |
| } |
| |
| std::vector<zx_info_maps_t> GetProcessMaps(const zx::process& process) { |
| const size_t kRegionsCountGuess = 64u; |
| const size_t kNewRegionsCountGuess = 4u; |
| |
| size_t count_guess = kRegionsCountGuess; |
| |
| std::vector<zx_info_maps_t> map; |
| size_t actual; |
| size_t avail; |
| |
| while (true) { |
| map.resize(count_guess); |
| |
| zx_status_t status = |
| process.get_info(ZX_INFO_PROCESS_MAPS, &map[0], |
| sizeof(zx_info_maps) * map.size(), &actual, &avail); |
| |
| if (status != ZX_OK) { |
| fprintf(stderr, "error %d for zx_object_get_info\n", status); |
| actual = 0; |
| break; |
| } else if (actual == avail) { |
| break; |
| } |
| |
| count_guess = avail + kNewRegionsCountGuess; |
| } |
| |
| map.resize(actual); |
| return map; |
| } |
| |
| bool ReadProcessMemoryBlock(const zx::process& process, uint64_t address, |
| uint32_t size, debug_ipc::MemoryBlock* block) { |
| block->address = address; |
| block->size = size; |
| block->data.resize(size); |
| |
| size_t bytes_read = 0; |
| if (process.read_memory(address, &block->data[0], block->size, &bytes_read) == |
| ZX_OK && |
| bytes_read == size) { |
| block->valid = true; |
| return true; |
| } |
| block->valid = false; |
| block->data.resize(0); |
| return false; |
| } |
| |
| void ReadProcessMemoryBlocks(const zx::process& process, uint64_t address, |
| uint32_t size, |
| std::vector<debug_ipc::MemoryBlock>* blocks) { |
| // Optimistically assume the read will work which will be faster in the |
| // common case. |
| blocks->resize(1); |
| if (ReadProcessMemoryBlock(process, address, size, &(*blocks)[0])) |
| return; |
| |
| // Failure reading, this memory is either not mapped or it may cross mapping |
| // boundaries. To solve the multiple boundary problem, get the memory mapping |
| // and compute all mapping boundaries in the requested region. Then try to |
| // read each of the resulting blocks (which may be valid or invalid). |
| // |
| // This computed boundaries array will contain all boundaries (including the |
| // end address and some duplicates) except the begin address (this will be |
| // implicit in the later computation). |
| std::vector<uint64_t> boundaries; |
| for (const zx_info_maps_t& map : GetProcessMaps(process)) { |
| // The returned maps should be sorted so any mapping region starting past |
| // our region means all relevant boundaries have been found. |
| if (map.base > address + size) |
| break; |
| if (map.base > address) |
| boundaries.push_back(map.base); |
| uint64_t end = map.base + map.size; |
| if (end > address && end < address + size) |
| boundaries.push_back(end); |
| } |
| boundaries.push_back(address + size); |
| std::sort(boundaries.begin(), boundaries.end()); |
| |
| blocks->clear(); |
| uint64_t begin = address; |
| for (uint64_t end : boundaries) { |
| // There will be some duplicates in the boundaries array so skip anything |
| // that's empty. These duplicates are caused by a range which a child |
| // inside it that is coincident with one of the parent boundaries, or |
| // two regions that abut each other. |
| if (end == begin) |
| continue; |
| blocks->emplace_back(); |
| ReadProcessMemoryBlock(process, begin, static_cast<uint32_t>(end - begin), |
| &blocks->back()); |
| begin = end; |
| } |
| } |
| |
| } // namespace debug_agent |