| // 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 "src/developer/debug/zxdb/console/analyze_memory.h" |
| |
| #include <inttypes.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <map> |
| |
| #include "src/developer/debug/ipc/records.h" |
| #include "src/developer/debug/shared/message_loop.h" |
| #include "src/developer/debug/shared/register_info.h" |
| #include "src/developer/debug/zxdb/client/frame.h" |
| #include "src/developer/debug/zxdb/client/memory_dump.h" |
| #include "src/developer/debug/zxdb/client/process.h" |
| #include "src/developer/debug/zxdb/client/thread.h" |
| #include "src/developer/debug/zxdb/common/err.h" |
| #include "src/developer/debug/zxdb/common/string_util.h" |
| #include "src/developer/debug/zxdb/console/command_utils.h" |
| #include "src/developer/debug/zxdb/console/format_location.h" |
| #include "src/developer/debug/zxdb/console/format_register.h" |
| #include "src/developer/debug/zxdb/console/format_table.h" |
| #include "src/developer/debug/zxdb/console/output_buffer.h" |
| #include "src/developer/debug/zxdb/symbols/input_location.h" |
| #include "src/developer/debug/zxdb/symbols/process_symbols.h" |
| #include "src/developer/debug/zxdb/symbols/symbol_utils.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| |
| namespace zxdb { |
| |
| namespace { |
| |
| // Rounds the beginning and size to sizeof(uint64_t) which we assume all pointers are on the |
| // debugged platform. This may need to be configurable in the future. |
| constexpr uint64_t kAlign = sizeof(uint64_t); |
| |
| // Aspace entries this size or larger will be ignored for annotation purposes. These large regions |
| // generally represent the process's available address space rather than actually used memory. |
| constexpr uint64_t kMaxAspaceRegion = 128000000000; // 128GB |
| |
| } // namespace |
| |
| namespace internal { |
| |
| MemoryAnalysis::MemoryAnalysis(const AnalyzeMemoryOptions& opts, Callback cb) |
| : callback_(std::move(cb)) { |
| process_ = opts.process->GetWeakPtr(); |
| |
| // This doesn't store the Thread because it may go out-of-scope during the asynchronous requests. |
| // We'd need a weak pointer but its better avoided. |
| begin_address_ = opts.begin_address / kAlign * kAlign; |
| |
| uint64_t end = opts.begin_address + opts.bytes_to_read; |
| end += kAlign - 1; |
| end = end / kAlign * kAlign; |
| bytes_to_read_ = static_cast<uint32_t>(end - begin_address_); |
| } |
| |
| void MemoryAnalysis::Schedule(const AnalyzeMemoryOptions& opts) { |
| // Copies are passed to the callbacks to keep this object in scope until all are complete. |
| fxl::RefPtr<MemoryAnalysis> this_ref(this); |
| |
| if (opts.thread) { |
| // Request stack dump. |
| if (!have_frames_) { |
| if (opts.thread->GetStack().has_all_frames()) { |
| OnFrames(opts.thread->GetWeakPtr()); |
| } else { |
| opts.thread->GetStack().SyncFrames( |
| [this_ref, weak_thread = opts.thread->GetWeakPtr()](const Err&) { |
| // Can ignore the error, the frames will be re-queried from the thread and we'll check |
| // the weak pointer in case its destroyed. |
| this_ref->OnFrames(weak_thread); |
| }); |
| } |
| } |
| } else { |
| // Mark these as complete so we can continue when everything else is done. |
| have_frames_ = true; |
| } |
| |
| // Request memory dump. |
| if (!have_memory_) { |
| opts.process->ReadMemory( |
| begin_address_, bytes_to_read_, |
| [this_ref](const Err& err, MemoryDump dump) { this_ref->OnMemory(err, std::move(dump)); }); |
| } |
| |
| // Request address space dump. |
| if (!have_aspace_) { |
| opts.process->GetAspace( |
| 0, [this_ref](const Err& err, std::vector<debug_ipc::AddressRegion> aspace) { |
| this_ref->OnAspace(err, std::move(aspace)); |
| }); |
| } |
| |
| // Test code could have set everything, in which case trigger a run. |
| if (HasEverything()) { |
| debug::MessageLoop::Current()->PostTask(FROM_HERE, [this_ref]() { this_ref->DoAnalysis(); }); |
| } |
| } |
| |
| void MemoryAnalysis::SetAspace(std::vector<debug_ipc::AddressRegion> aspace) { |
| FX_DCHECK(!have_aspace_); |
| have_aspace_ = true; |
| aspace_ = std::move(aspace); |
| } |
| |
| void MemoryAnalysis::SetStack(const Stack& stack) { |
| FX_DCHECK(!have_frames_); |
| have_frames_ = true; |
| |
| for (size_t i = 0; i < stack.size(); i++) { |
| // Only add the registers once per inline function call sequence. It makes the most sense for |
| // the frames to reference the topmost frame of an inline call sequence so this skips everything |
| // with an inline frame immediately above it. |
| if (i > 0 && stack[i - 1]->IsInline()) |
| continue; |
| |
| const std::vector<debug::RegisterValue>* regs = |
| stack[i]->GetRegisterCategorySync(debug::RegisterCategory::kGeneral); |
| FX_DCHECK(regs); // Always expect general registers to be available. |
| AddRegisters(i, *regs); |
| |
| // TODO(brettw) make this work when the frame base is asynchronous. |
| if (auto bp = stack[i]->GetBasePointer()) |
| AddAnnotation(*bp, fxl::StringPrintf("frame %zu base", i)); |
| } |
| } |
| |
| void MemoryAnalysis::SetMemory(MemoryDump dump) { |
| FX_DCHECK(!have_memory_); |
| have_memory_ = true; |
| memory_ = std::move(dump); |
| } |
| |
| void MemoryAnalysis::DoAnalysis() { |
| std::vector<std::vector<OutputBuffer>> rows; |
| rows.reserve(bytes_to_read_ / kAlign); |
| for (uint64_t offset = 0; offset < bytes_to_read_; offset += kAlign) { |
| rows.emplace_back(); |
| auto& row = rows.back(); |
| |
| uint64_t address = begin_address_ + offset; |
| |
| // Address. |
| row.emplace_back(Syntax::kComment, to_hex_string(address)); |
| |
| // Data |
| uint64_t data_value = 0; |
| bool has_data = GetData(address, &data_value); |
| if (has_data) { |
| row.emplace_back(to_hex_string(data_value, 16)); |
| } else { |
| row.emplace_back("<invalid memory>"); |
| } |
| |
| OutputBuffer annotation = GetAnnotationsBetween(address, address + kAlign); |
| OutputBuffer pointed_to; |
| if (has_data) |
| pointed_to = GetPointedToAnnotation(data_value); |
| |
| if (!pointed_to.empty()) { |
| if (!annotation.empty()) |
| annotation.Append(". "); // Separator between sections. |
| annotation.Append(std::move(pointed_to)); |
| } |
| row.push_back(annotation); |
| } |
| |
| OutputBuffer out; |
| FormatTable({ColSpec(Align::kRight, 0, "Address"), ColSpec(Align::kRight, 0, "Data"), ColSpec()}, |
| rows, &out); |
| callback_(Err(), std::move(out), begin_address_ + bytes_to_read_); |
| } |
| |
| void MemoryAnalysis::OnAspace(const Err& err, std::vector<debug_ipc::AddressRegion> aspace) { |
| if (aborted_) |
| return; |
| |
| // This function can continue without address space annotations so ignore errors. |
| SetAspace(std::move(aspace)); |
| |
| if (HasEverything()) |
| DoAnalysis(); |
| } |
| |
| void MemoryAnalysis::OnMemory(const Err& err, MemoryDump dump) { |
| if (aborted_) |
| return; |
| if (err.has_error()) { |
| IssueError(err); |
| return; |
| } |
| |
| SetMemory(std::move(dump)); |
| |
| if (HasEverything()) |
| DoAnalysis(); |
| } |
| |
| void MemoryAnalysis::OnFrames(fxl::WeakPtr<Thread> thread) { |
| if (aborted_) |
| return; |
| |
| // This function can continue even if the thread is gone, it just won't get the frame annotations. |
| if (thread) |
| SetStack(thread->GetStack()); |
| else |
| have_frames_ = true; // Mark fetching is complete. |
| |
| if (HasEverything()) |
| DoAnalysis(); |
| } |
| |
| bool MemoryAnalysis::HasEverything() const { return have_memory_ && have_frames_ && have_aspace_; } |
| |
| void MemoryAnalysis::IssueError(const Err& err) { |
| aborted_ = true; |
| callback_(err, OutputBuffer(), 0); |
| |
| // Reset so we notice if there's an accidental double-call. |
| callback_ = Callback(); |
| } |
| |
| void MemoryAnalysis::AddRegisters(int frame_no, const std::vector<debug::RegisterValue>& regs) { |
| // Frames can have saved registers. Sometimes these will be the same as frame 0 (the current CPU |
| // state). We want to make them say, e.g. "rax" if the value matches the top frame, but if the |
| // current frame's register value is different, we want e.g. "frame 5's rax". |
| for (const auto& r : regs) { |
| if (r.data.size() > sizeof(uint64_t)) |
| continue; // Weird register, don't bother. |
| |
| uint64_t value = r.GetValue(); |
| std::string reg_desc; |
| |
| if (frame_no == 0) { |
| // Frame 0 always gets added with no frame annotation. |
| reg_desc = debug::RegisterIDToString(r.id); |
| frame_0_regs_[r.id] = value; |
| } else { |
| // Later frames get an annotation and only get added if they're different than frame 0. |
| // Duplicates for inline frames should have been filtered out by the caller. |
| auto found_frame_0 = frame_0_regs_.find(r.id); |
| if (found_frame_0 != frame_0_regs_.end() && found_frame_0->second == value) |
| continue; // Matches frame 0, don't add a record. |
| |
| reg_desc = fxl::StringPrintf("frame %d %s", frame_no, debug::RegisterIDToString(r.id)); |
| } |
| |
| AddAnnotation(value, reg_desc); |
| } |
| } |
| |
| void MemoryAnalysis::AddAnnotation(uint64_t address, const std::string& str) { |
| auto found = annotations_.find(address); |
| if (found == annotations_.end()) { |
| annotations_[address] = str; |
| } else { |
| found->second.append(", "); |
| found->second.append(str); |
| } |
| } |
| |
| bool MemoryAnalysis::GetData(uint64_t address, uint64_t* out_value) const { |
| // Need to handle invalid memory. The easiest thing is to read a byte at a time. This doesn't |
| // handle invalid regions spanning a pointer; that shouldn't happen because valid memory regions |
| // should always be aligned more coarsely than the size of a pointer. |
| uint64_t data = 0; |
| for (uint64_t i = 0; i < kAlign; i++) { |
| uint8_t byte; |
| if (!memory_.GetByte(address + i, &byte)) |
| return false; |
| data |= static_cast<uint64_t>(byte) << (i * 8); |
| } |
| |
| *out_value = data; |
| return true; |
| } |
| |
| OutputBuffer MemoryAnalysis::GetAnnotationsBetween(uint64_t address_begin, |
| uint64_t address_end) const { |
| auto lower = annotations_.lower_bound(address_begin); |
| auto upper = annotations_.upper_bound(address_end - 1); |
| if (lower == upper) |
| return OutputBuffer(); // No annotations in this range. |
| |
| // Mark "pointing to here" annotations as special since they can get drowned out by all of the |
| // other pointer stuff. |
| OutputBuffer result(Syntax::kSpecial, "◁ "); |
| for (auto cur = lower; cur != upper; ++cur) { |
| if (cur != lower) { |
| // Not the first annotation, needs a separator. |
| result.Append("; "); |
| } |
| if (cur->first != address_begin) { |
| // Not at the address but inside of the range. Annotate that carefully. |
| result.Append(Syntax::kSpecial, fxl::StringPrintf("@ 0x%" PRIx64 ": ", cur->first)); |
| } |
| result.Append(Syntax::kSpecial, cur->second); |
| } |
| return result; |
| } |
| |
| OutputBuffer MemoryAnalysis::GetPointedToAnnotation(uint64_t data) const { |
| if (!process_) |
| return OutputBuffer(); |
| auto locations = process_->GetSymbols()->ResolveInputLocation(InputLocation(data)); |
| FX_DCHECK(locations.size() == 1); |
| |
| if (!locations[0].symbol()) { |
| // Check if this points into any relevant aspace entries. Want the deepest one smaller than the |
| // max size threshold. |
| int max_depth = -1; |
| size_t found_entry = aspace_.size(); // Indicates not found. |
| for (size_t i = 0; i < aspace_.size(); i++) { |
| const auto& region = aspace_[i]; |
| if (region.size < kMaxAspaceRegion && data >= region.base && |
| data < region.base + region.size && max_depth < static_cast<int>(region.depth)) { |
| max_depth = static_cast<int>(region.depth); |
| found_entry = i; |
| } |
| } |
| |
| if (found_entry == aspace_.size()) |
| return OutputBuffer(); // Not found. |
| return fxl::StringPrintf("▷ inside map \"%s\"", aspace_[found_entry].name.c_str()); |
| } |
| |
| FormatLocationOptions opts; |
| opts.func.name.show_global_qual = false; |
| opts.func.name.elide_templates = true; |
| opts.func.name.bold_last = true; |
| opts.func.params = FormatFunctionNameOptions::kNoParams; |
| opts.always_show_addresses = false; |
| opts.show_file_line = false; |
| opts.show_file_path = false; |
| |
| OutputBuffer out("▷ "); |
| out.Append(FormatLocation(locations[0], opts)); |
| return out; |
| } |
| |
| } // namespace internal |
| |
| void AnalyzeMemory( |
| const AnalyzeMemoryOptions& opts, |
| fit::callback<void(const Err& err, OutputBuffer analysis, uint64_t next_addr)> cb) { |
| auto analysis = fxl::MakeRefCounted<zxdb::internal::MemoryAnalysis>(opts, std::move(cb)); |
| analysis->Schedule(opts); |
| } |
| |
| } // namespace zxdb |