blob: 60c713be18fb26c8e2e203cd4821aa0b70a1f117 [file] [log] [blame]
// Copyright 2022 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/zxdump/dump.h"
#include <lib/stdcompat/span.h>
#include <zircon/assert.h>
#include <zircon/syscalls/debug.h>
#include <zircon/syscalls/exception.h>
#include <array>
#include <cassert>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <map>
#include <optional>
#include <tuple>
#include <vector>
#include "core.h"
namespace zxdump {
namespace {
// This collects a bunch of note data, header and payload ByteView items. The
// actual data the items point to is stored in Collector::notes_.
class NoteData {
public:
using Vector = std::vector<ByteView>;
NoteData() = default;
NoteData(NoteData&&) = default;
NoteData& operator=(NoteData&&) = default;
size_t size() const { return data_.size(); }
size_t size_bytes() const { return size_bytes_; }
void push_back(ByteView data) {
ZX_DEBUG_ASSERT(data.size() % NoteAlign() == 0);
if (!data.empty()) {
data_.push_back(data);
size_bytes_ += data.size();
}
}
Vector take() && { return std::move(data_); }
private:
Vector data_;
size_t size_bytes_ = 0;
};
// This represents one note header, with name and padding but no desc.
template <size_t NameSize>
class NoteHeader {
public:
NoteHeader() = delete;
constexpr NoteHeader(const NoteHeader&) = default;
constexpr NoteHeader& operator=(const NoteHeader&) = default;
constexpr NoteHeader(std::string_view name, uint32_t descsz, uint32_t type)
: nhdr_{.namesz = NameSize + 1, .descsz = descsz, .type = type} {
assert(name.size() == NameSize);
name.copy(name_, NameSize);
}
ByteView bytes() const {
return {
reinterpret_cast<const std::byte*>(this),
sizeof(*this),
};
}
constexpr void set_size(uint32_t descsz) { nhdr_.descsz = descsz; }
private:
static constexpr size_t kAlignedSize_ = NoteAlign(NameSize + 1);
static_assert(kAlignedSize_ < std::numeric_limits<uint32_t>::max());
Elf::Nhdr nhdr_;
char name_[kAlignedSize_] = {};
};
// Each note format has an object in notes_ of a NoteBase type.
// The Type is the n_type field for the ELF note header.
// The Class represents a set of notes handled the same way.
// It provides the ELF note name, as well as other details used below.
template <typename Class, uint32_t Type>
class NoteBase {
public:
void AddToNoteData(NoteData& notes) const {
if (!data_.empty()) {
notes.push_back(header_.bytes());
notes.push_back(data_);
}
}
bool empty() const { return data_.empty(); }
const auto& header() const { return header_; }
auto& header() { return header_; }
size_t size_bytes() const { return empty() ? 0 : header_.bytes().size() + data_.size(); }
void clear() { data_ = {}; }
protected:
// The subclass Collect method calls this when it might have data.
void Emplace(ByteView data) {
if (!data.empty()) {
ZX_ASSERT(data.size() <= std::numeric_limits<uint32_t>::max());
header_.set_size(static_cast<uint32_t>(data.size()));
data_ = data;
}
}
private:
// This holds the storage for the note header that NoteData points into.
decltype(Class::MakeHeader(Type)) header_ = Class::MakeHeader(Type);
// This points into the subclass storage for the note contents.
// It's empty until the subclass calls Emplace.
ByteView data_;
};
// Each class derived from NoteBase uses a core.h kFooNoteName constant in:
// ```
// static constexpr auto MakeHeader = kMakeNote<kFooNoteName>;
// ```
template <const std::string_view& Name>
constexpr auto kMakeNote = [](uint32_t type) { return NoteHeader<Name.size()>(Name, 0, type); };
// This is called with each note (classes derived from NoteBase) when its
// information is required. It can be called more than once, so it does
// nothing if it's already collected the data. Each NoteBase subclass below
// has a Collect(const Handle&)->fitx::result<Error> method that should call
// NoteBase::Emplace when it has acquired data, and then won't be called again.
constexpr auto CollectNote = [](const auto& handle, auto& note) -> fitx::result<Error> {
if (note.empty()) {
return note.Collect(handle);
}
return fitx::ok();
};
// This doesn't need to be templated as specifically as InfoNote itself. The
// GetInfo work is the same for all get_info topics whose data have the same
// size and alignment, so it's factored out that way here.
template <size_t Size, size_t Align>
using AlignedStorageVector = std::vector<std::aligned_storage_t<Size, Align>>;
template <typename T>
using InfoVector = AlignedStorageVector<sizeof(T), alignof(T)>;
template <size_t Align, size_t Size>
fitx::result<Error, AlignedStorageVector<Size, Align>> GetInfo(
const zx::handle& task, zx_object_info_topic_t topic, AlignedStorageVector<Size, Align> data) {
// Start with a buffer of at least one but reuse any larger old buffer.
if (data.empty()) {
data.resize(1);
}
while (true) {
// Use as much space as is handy.
data.resize(data.capacity());
// See how much there is available and how much fits in the buffer.
size_t actual, avail;
zx_status_t status = task.get_info(topic, data.data(), data.size() * Size, &actual, &avail);
if (status != ZX_OK) {
return fitx::error(Error{"zx_object_get_info", status});
}
if (actual == avail) {
// This is all the data.
data.resize(avail);
data.shrink_to_fit();
return fitx::ok(std::move(data));
}
// There is more data. Make the buffer at least as big as is needed.
if (data.size() < avail) {
data.resize(avail);
}
}
}
// Notes based on zx_object_get_info calls use this.
// For some types, the size is variable.
// We treat them all as variable.
template <typename Class, zx_object_info_topic_t Topic, typename T>
class InfoNote : public NoteBase<Class, Topic> {
public:
fitx::result<Error> Collect(const typename Class::Handle& task) {
auto result =
GetInfo<alignof(T), sizeof(T)>(*zx::unowned_handle{task.get()}, Topic, std::move(data_));
if (result.is_error()) {
return result.take_error();
}
data_ = std::move(result).value();
this->Emplace({
reinterpret_cast<const std::byte*>(data_.data()),
data_.size() * sizeof(data_[0]),
});
return fitx::ok();
}
cpp20::span<const T> info() const {
static_assert(sizeof(T) == sizeof(data_.data()[0]));
return {reinterpret_cast<const T*>(data_.data()), data_.size()};
}
private:
InfoVector<T> data_;
};
// Notes based on the fixed-sized property/state calls use this.
template <typename Class, uint32_t Prop, typename T>
class PropertyNote : public NoteBase<Class, Prop> {
public:
fitx::result<Error> Collect(const typename Class::Handle& handle) {
zx_status_t status = (handle.*Class::kSyscall)(Prop, &data_, sizeof(T));
if (status != ZX_OK) {
return fitx::error(Error{Class::kCall_, status});
}
this->Emplace({reinterpret_cast<std::byte*>(&data_), sizeof(data_)});
return fitx::ok();
}
private:
T data_;
};
// Classes using zx_object_get_property use this.
struct PropertyBaseClass {
static constexpr std::string_view kCall_{"zx_object_get_property"};
static constexpr auto kSyscall = &zx::object_base::get_property;
};
// These are called via std::apply on ProcessNotes tuples.
constexpr auto CollectNoteData =
// For each note that hasn't already been fetched, try to fetch it now.
[](const auto& handle, auto&... note) -> fitx::result<Error, size_t> {
// This value is always replaced (or ignored), but the type is not
// default-constructible.
fitx::result<Error> result = fitx::ok();
size_t total = 0;
auto collect = [&handle, &result, &total](auto& note) -> bool {
result = CollectNote(handle, note);
if (result.is_ok()) {
total += note.size_bytes();
return true;
}
switch (result.error_value().status_) {
case ZX_ERR_NOT_SUPPORTED:
case ZX_ERR_BAD_STATE:
// These just mean the data is not available because it never
// existed or the thread is dead.
return true;
default:
return false;
}
};
if (!(collect(note) && ...)) {
// The fold expression bailed out early after setting result.
return result.take_error();
}
return fitx::ok(total);
};
constexpr auto DumpNoteData =
// Return a vector pointing at all the nonempty note data.
[](const auto&... note) -> NoteData::Vector {
NoteData data;
(note.AddToNoteData(data), ...);
return std::move(data).take();
};
} // namespace
// The public class is just a container for a std::unique_ptr to this private
// class, so no implementation details of the object need to be visible in the
// public header.
class ProcessDumpBase::Collector {
public:
// Only constructed by Emplace.
Collector() = delete;
// Only Emplace and clear call this. The process is mandatory and all other
// members are safely default-initialized.
explicit Collector(zx::unowned_process process) : process_(std::move(process)) {
ZX_ASSERT(process_->is_valid());
}
// Reset to initial state.
void clear() { *this = Collector{std::move(process_)}; }
// This collects information and other process-wide state. The return value
// gives the total size of the ET_CORE file to be written. Collection is cut
// short without error if the ET_CORE file would already exceed the size
// limit without even including the memory.
fitx::result<Error, size_t> CollectProcess(size_t limit) {
// Collect the process-wide note data.
auto collect = [this](auto&... note) -> fitx::result<Error, size_t> {
return CollectNoteData(*process_, note...);
};
if (auto result = std::apply(collect, notes_); result.is_error()) {
return result.take_error();
} else {
notes_size_bytes_ += result.value();
}
// Clear out from any previous use.
phdrs_.clear();
// The first phdr is the main note segment.
const Elf::Phdr note_phdr = {
.type = elfldltl::ElfPhdrType::kNote,
.flags = Elf::Phdr::kRead,
.filesz = notes_size_bytes_,
.align = NoteAlign(),
};
phdrs_.push_back(note_phdr);
// Now figure everything else out to write out a full ET_CORE file.
return fitx::ok(Layout());
}
// Accumulate header and note data to be written out, by calling
// `dump(offset, ByreView{...})` repeatedly.
// The views point to storage in this->notes and Thread::notes_.
size_t DumpHeaders(DumpCallback dump, size_t limit) {
// Layout has already been done.
ZX_ASSERT(ehdr_.type == elfldltl::ElfType::kCore);
size_t offset = 0;
auto append = [&](ByteView data) -> bool {
if (offset >= limit || limit - offset < data.size()) {
return false;
}
bool bail = dump(offset, data);
offset += data.size();
return bail;
};
// Generate the ELF headers.
if (append({reinterpret_cast<std::byte*>(&ehdr_), sizeof(ehdr_)})) {
return offset;
}
if (ehdr_.shnum > 0) {
ZX_DEBUG_ASSERT(ehdr_.shnum() == 1);
ZX_DEBUG_ASSERT(ehdr_.shoff == offset);
if (append({reinterpret_cast<std::byte*>(&shdr_), sizeof(shdr_)})) {
return offset;
}
}
if (append({reinterpret_cast<std::byte*>(phdrs_.data()), phdrs_.size() * sizeof(phdrs_[0])})) {
return offset;
}
// Generate the process-wide note data.
for (ByteView data : notes()) {
if (append(data)) {
return offset;
}
}
ZX_DEBUG_ASSERT(offset % NoteAlign() == 0);
return offset;
}
private:
struct ProcessInfoClass {
using Handle = zx::process;
static constexpr auto MakeHeader = kMakeNote<kProcessInfoNoteName>;
};
template <zx_object_info_topic_t Topic, typename T>
using ProcessInfo = InfoNote<ProcessInfoClass, Topic, T>;
struct ProcessPropertyClass : public PropertyBaseClass {
using Handle = zx::process;
static constexpr auto MakeHeader = kMakeNote<kProcessPropertyNoteName>;
};
template <uint32_t Prop, typename T>
using ProcessProperty = PropertyNote<ProcessPropertyClass, Prop, T>;
using ProcessNotes = std::tuple<
// This lists all the notes for process-wide state. Ordering of the
// notes after the first two is not specified and can change.
ProcessInfo<ZX_INFO_HANDLE_BASIC, zx_info_handle_basic_t>,
ProcessProperty<ZX_PROP_NAME, char[ZX_MAX_NAME_LEN]>,
ProcessInfo<ZX_INFO_PROCESS, zx_info_process_t>,
ProcessInfo<ZX_INFO_PROCESS_THREADS, zx_koid_t>,
ProcessInfo<ZX_INFO_TASK_STATS, zx_info_task_stats_t>,
ProcessInfo<ZX_INFO_TASK_RUNTIME, zx_info_task_runtime_t>,
ProcessInfo<ZX_INFO_PROCESS_MAPS, zx_info_maps_t>,
ProcessInfo<ZX_INFO_PROCESS_VMOS, zx_info_vmo_t>,
ProcessInfo<ZX_INFO_PROCESS_HANDLE_STATS, zx_info_process_handle_stats_t>,
ProcessInfo<ZX_INFO_HANDLE_TABLE, zx_info_handle_extended_t>,
ProcessProperty<ZX_PROP_PROCESS_DEBUG_ADDR, uintptr_t>,
ProcessProperty<ZX_PROP_PROCESS_BREAK_ON_LOAD, uintptr_t>,
ProcessProperty<ZX_PROP_PROCESS_VDSO_BASE_ADDRESS, uintptr_t>,
ProcessProperty<ZX_PROP_PROCESS_HW_TRACE_CONTEXT_ID, uintptr_t>>;
// Returns a vector of views into the storage held in this->notes_.
NoteData::Vector notes() const { return std::apply(DumpNoteData, notes_); }
// Some of the process-wide state is needed in Suspend anyway, so pre-collect
// it directly in the notes.
auto& process_threads() {
return std::get<ProcessInfo<ZX_INFO_PROCESS_THREADS, zx_koid_t>>(notes_);
}
auto& process_maps() {
return std::get<ProcessInfo<ZX_INFO_PROCESS_MAPS, zx_info_maps_t>>(notes_);
}
auto& process_vmos() {
return std::get<ProcessInfo<ZX_INFO_PROCESS_VMOS, zx_info_vmo_t>>(notes_);
}
// Populate the header fields and reify phdrs_ with p_offset values.
// This chooses where everything will go in the ET_CORE file.
size_t Layout() {
// Fill in the file header boilerplate.
ehdr_.magic = Elf::Ehdr::kMagic;
ehdr_.elfclass = elfldltl::ElfClass::k64;
ehdr_.elfdata = elfldltl::ElfData::k2Lsb;
ehdr_.ident_version = elfldltl::ElfVersion::kCurrent;
ehdr_.type = elfldltl::ElfType::kCore;
ehdr_.machine = elfldltl::ElfMachine::kNative;
ehdr_.version = elfldltl::ElfVersion::kCurrent;
size_t offset = ehdr_.phoff = ehdr_.ehsize = sizeof(ehdr_);
ehdr_.phentsize = sizeof(phdrs_[0]);
offset += phdrs_.size() * sizeof(phdrs_[0]);
if (phdrs_.size() < Elf::Ehdr::kPnXnum) {
ehdr_.phnum = static_cast<uint16_t>(phdrs_.size());
} else {
shdr_.info = static_cast<uint32_t>(phdrs_.size());
ehdr_.phnum = Elf::Ehdr::kPnXnum;
ehdr_.shnum = 1;
ehdr_.shentsize = sizeof(shdr_);
ehdr_.shoff = offset;
offset += sizeof(shdr_);
}
// Now assign offsets to all the segments.
auto place = [&offset](Elf::Phdr& phdr) {
if (phdr.filesz == 0) {
phdr.offset = 0;
} else {
offset = (offset + phdr.align - 1) & -size_t{phdr.align};
phdr.offset = offset;
offset += phdr.filesz;
}
};
ZX_DEBUG_ASSERT(phdrs_.size() == 1);
// First is the initial note segment.
ZX_DEBUG_ASSERT(phdrs_[0].type == elfldltl::ElfPhdrType::kNote);
place(phdrs_[0]);
ZX_DEBUG_ASSERT(offset % NoteAlign() == 0);
return offset;
}
zx::unowned_process process_;
zx::suspend_token process_suspended_;
ProcessNotes notes_;
std::vector<Elf::Phdr> phdrs_;
Elf::Ehdr ehdr_ = {};
Elf::Shdr shdr_ = {}; // Only used for the PN_XNUM case.
// This collects the totals for process-wide notes.
size_t notes_size_bytes_ = 0;
};
ProcessDumpBase::~ProcessDumpBase() = default;
void ProcessDumpBase::clear() { collector_->clear(); }
fitx::result<Error, size_t> ProcessDumpBase::CollectProcess(size_t limit) {
return collector_->CollectProcess(limit);
}
size_t ProcessDumpBase::DumpHeadersImpl(DumpCallback dump, size_t limit) {
return collector_->DumpHeaders(std::move(dump), limit);
}
// The Collector borrows the process handle. A single Collector cannot be
// used for a different process later. It can be clear()'d to reset all
// state other than the process handle.
void ProcessDumpBase::Emplace(zx::unowned_process process) {
collector_ = std::make_unique<Collector>(std::move(process));
}
template <>
ProcessDump<zx::process>::ProcessDump(zx::process process) noexcept : process_{std::move(process)} {
Emplace(zx::unowned_process{process_});
}
template class ProcessDump<zx::unowned_process>;
template <>
ProcessDump<zx::unowned_process>::ProcessDump(zx::unowned_process process) noexcept
: process_{std::move(process)} {
Emplace(zx::unowned_process{process_});
}
} // namespace zxdump