blob: afa20a284aa54d8b22c30696424f46b7b4b4879b [file] [log] [blame]
// 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