blob: 85b56305598c3a6ab5e10184d773241bf71349a5 [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 <lib/zxdump/dump.h>
#include <lib/zxdump/elf-search.h>
#include <zircon/assert.h>
#include <zircon/syscalls/debug.h>
#include <zircon/syscalls/exception.h>
#include <algorithm>
#include <array>
#include <cassert>
#include <charconv>
#include <cinttypes>
#include <cstdlib>
#include <limits>
#include <map>
#include <optional>
#include <tuple>
#include <vector>
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#ifdef __Fuchsia__
#include <lib/zx/process.h>
#endif
#include "core.h"
#include "job-archive.h"
#include "rights.h"
namespace zxdump {
namespace {
// This collects a bunch of note data, header and payload ByteView items.
// There's one of these for each thread, and one for the process. The actual
// data the items point to is stored in Collector::notes_ and
// ThreadCollector::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) {
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_] = {};
};
constexpr std::byte kZeroBytes[NoteAlign() - 1] = {};
// This returns a ByteView of as many zero bytes are needed for alignment
// padding after the given ELF note payload data.
constexpr ByteView PadForElfNote(ByteView data) {
return {kZeroBytes, NoteAlign(data.size()) - data.size()};
}
class FlexNoteHeader {
public:
FlexNoteHeader() = default;
FlexNoteHeader(const FlexNoteHeader&) = delete;
FlexNoteHeader(FlexNoteHeader&& other) { std::swap(data_, other.data_); }
FlexNoteHeader& operator=(FlexNoteHeader&& other) {
std::swap(data_, other.data_);
return *this;
}
ByteView bytes() const {
if (!data_) {
return {};
}
return {
reinterpret_cast<const std::byte*>(data_),
sizeof(data_->nhdr_) + NoteAlign(data_->nhdr_.namesz),
};
}
void InitAccumulate(std::string_view name) {
ZX_DEBUG_ASSERT(!data_); // Not already called.
ZX_DEBUG_ASSERT(!name.empty()); // Name must be nonempty.
const size_t namesz = name.size() + 1;
auto buf = new std::byte[sizeof(data_->nhdr_) + NoteAlign(namesz)]();
static_assert(alignof(Data) <= alignof(std::max_align_t));
data_ = reinterpret_cast<Data*>(buf);
data_->nhdr_.namesz = static_cast<uint32_t>(namesz);
name.copy(data_->name_, name.size());
}
void set_size(uint32_t descsz) { data_->nhdr_.descsz = descsz; }
~FlexNoteHeader() {
if (data_) {
delete[] reinterpret_cast<std::byte*>(data_);
}
}
private:
struct Data {
Elf::Nhdr nhdr_;
char name_[];
};
Data* data_ = nullptr;
};
// This represents one archive member header.
//
// The name field in the traditional header is only 16 characters. So the
// modern protocol is to use a name of "/%u" to encode an offset into the
// name table, which is a special member at the beginning of the archive,
// itself named "//".
class ArchiveMemberHeader {
public:
ArchiveMemberHeader() {
// Initialize the header. All fields are left-justified and padded with
// spaces. There are no separators between fields.
memset(&header_, ' ', sizeof(header_));
static_assert(ar_hdr::kMagic.size() == sizeof(header_.ar_fmag));
ar_hdr::kMagic.copy(header_.ar_fmag, sizeof(header_.ar_fmag));
}
// The name is copied directly into the header, truncated if necessary.
// The size must be filled in later, and the date may be.
explicit ArchiveMemberHeader(std::string_view name) : ArchiveMemberHeader() {
name.copy(header_.ar_name, sizeof(header_.ar_name));
Init();
}
// The name is stored here to go into the name table later. The name
// table offset and size must be filled in later, and the date may be.
// This constructor
void InitAccumulate(std::string name) {
// Each name in the table is terminated by a slash and newline.
name_ = std::move(name);
name_ += "/\n";
Init();
}
// This sets up the state for the special name table member.
void InitNameTable(size_t size) {
Check();
header_.ar_name[0] = '/';
header_.ar_name[1] = '/';
set_size(size);
}
void set_name_offset(size_t name_offset) {
Check();
ZX_DEBUG_ASSERT(header_.ar_name[0] == ' ');
header_.ar_name[0] = '/';
auto [ptr, ec] = std::to_chars(&header_.ar_name[1], std::end(header_.ar_name), name_offset);
ZX_ASSERT_MSG(ec == std::errc{}, "archive member name offset %zu too large for header",
name_offset);
}
void set_size(size_t size) {
Check();
auto [ptr, ec] = std::to_chars(header_.ar_size, std::end(header_.ar_size), size);
ZX_ASSERT_MSG(ec == std::errc{}, "archive member size %zu too large for header", size);
}
void set_date(time_t mtime) {
Check();
auto [ptr, ec] = std::to_chars(header_.ar_date, std::end(header_.ar_date), mtime);
ZX_ASSERT_MSG(ec == std::errc{}, "archive member timestamp %zu too large for header", mtime);
}
ByteView bytes() const {
Check();
return {reinterpret_cast<const std::byte*>(&header_), sizeof(header_)};
}
ByteView name_bytes() const {
Check();
ZX_DEBUG_ASSERT(!name_.empty());
return {reinterpret_cast<const std::byte*>(name_.data()), name_.size()};
}
private:
void Check() const {
[[maybe_unused]] std::string_view magic{
header_.ar_fmag,
sizeof(header_.ar_fmag),
};
ZX_DEBUG_ASSERT(magic == ar_hdr::kMagic);
}
void Init() {
Check();
// The mode field is encoded in octal, but we always emit a constant value
// anyway. Other integer fields are encoded in decimal.
kZero_.copy(header_.ar_date, sizeof(header_.ar_date));
kZero_.copy(header_.ar_uid, sizeof(header_.ar_uid));
kZero_.copy(header_.ar_gid, sizeof(header_.ar_gid));
kMode_.copy(header_.ar_mode, sizeof(header_.ar_mode));
}
static constexpr std::string_view kZero_{"0"};
static constexpr std::string_view kMode_{"400"}; // octal
std::string name_;
ar_hdr header_;
};
constexpr const std::byte kArchiveMemberPadByte = std::byte{'\n'};
constexpr ByteView kArchiveMemberPad{&kArchiveMemberPadByte, 1};
// This returns any necessary padding after the given member contents.
constexpr ByteView PadForArchive(ByteView data) {
if (data.size() % 2 != 0) {
return kArchiveMemberPad;
}
return {};
}
// 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:
NoteBase() = default;
NoteBase(NoteBase&&) noexcept = default;
NoteBase& operator=(NoteBase&&) noexcept = default;
void AddToNoteData(NoteData& notes) const {
if (!data_.empty()) {
notes.push_back(header_.bytes());
notes.push_back(data_);
notes.push_back(Class::Pad(data_));
}
}
bool empty() const { return data_.empty(); }
const auto& header() const { return header_; }
auto& header() { return header_; }
size_t size_bytes() const {
if (empty()) {
return 0;
}
return header_.bytes().size() + data_.size() + Class::Pad(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:
using HeaderType = decltype(Class::MakeHeader(Type));
static_assert(std::is_move_constructible_v<HeaderType>);
static_assert(std::is_move_assignable_v<HeaderType>);
// This holds the storage for the note header that NoteData points into.
HeaderType 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>;
// static constexpr auto Pad = PadForElfNote;
// ```
template <const std::string_view& Name>
constexpr auto kMakeNote = [](uint32_t type) { return NoteHeader<Name.size()>(Name, 0, type); };
// Each job-specific class uses a job-archive.h kFooName constant in:
// ```
// static constexpr auto MakeHeader = kMakeMember<kFooName>;
// static constexpr auto Pad = PadForArchive;
// ```
template <const std::string_view& Name, bool NoType = false>
constexpr auto kMakeMember = [](uint32_t type) {
std::string name{Name};
if constexpr (NoType) {
ZX_DEBUG_ASSERT(type == 0);
} else {
name += '.';
name += std::to_string(type);
}
ArchiveMemberHeader header;
header.InitAccumulate(std::move(name));
return header;
};
// 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&)->fit::result<Error> method that should call
// NoteBase::Emplace when it has acquired data, and then won't be called again.
constexpr auto CollectNote = [](auto& handle, auto& note) -> fit::result<Error> {
if (note.empty()) {
return note.Collect(handle);
}
return fit::ok();
};
// Notes based on zx_object_get_info calls use this.
template <typename Class, zx_object_info_topic_t Topic>
class InfoNote : public NoteBase<Class, Topic> {
public:
fit::result<Error> Collect(Object& object) {
Object& handle =
std::is_same_v<typename Class::Handle, Resource> ? object.root_resource() : object;
if constexpr (kIsSpan<T>) {
// Cache the data for iteration. This tells zxdump::Object to refresh
// live data when this is used for the thread list so it will catch up
// with racing new threads.
auto result = handle.get_info<Topic>(Topic == ZX_INFO_PROCESS_THREADS);
if (result.is_error()) {
return result.take_error();
}
info_ = result.value();
}
// This gets the same data just cached, but in generic ByteView form.
auto result = handle.get_info(Topic);
if (result.is_error()) {
return result.take_error();
}
this->Emplace(result.value());
return fit::ok();
}
auto info() const { return info_; }
private:
using T = typename InfoTraits<Topic>::type;
struct Unused {};
std::conditional_t<kIsSpan<T>, T, Unused> info_;
};
// Notes based on the fixed-sized property calls use this.
template <typename Class, uint32_t Property>
class PropertyNote : public NoteBase<Class, Property> {
public:
fit::result<Error> Collect(typename Class::Handle& handle) {
auto result = handle.template get_property<Property>();
if (result.is_error()) {
return result.take_error();
}
data_ = result.value();
ByteView bytes{reinterpret_cast<std::byte*>(&data_), sizeof(data_)};
if constexpr (Property == ZX_PROP_NAME) {
bytes = {reinterpret_cast<std::byte*>(data_.data()), data_.size()};
}
this->Emplace(bytes);
return fit::ok();
}
private:
typename PropertyTraits<Property>::type data_;
};
// Notes based on the fixed-sized read_state calls use this.
template <typename Class, uint32_t Kind>
class ThreadStateNote : public NoteBase<Class, Kind> {
public:
fit::result<Error> Collect(Thread& thread) {
auto result = thread.read_state(Kind);
if (result.is_error()) {
return result.take_error();
}
this->Emplace(result.value());
return fit::ok();
}
};
template <typename Class>
class JsonNote : public NoteBase<Class, 0> {
public:
// The writer object holds a reference to the note object. When the writer
// is destroyed, everything it wrote will be reified into the note contents.
class JsonWriter {
public:
auto& writer() { return writer_; }
~JsonWriter() {
note_.Emplace({
reinterpret_cast<const std::byte*>(note_.data_.GetString()),
note_.data_.GetSize(),
});
}
private:
friend JsonNote;
explicit JsonWriter(JsonNote& note) : writer_(note.data_), note_(note) {}
rapidjson::Writer<rapidjson::StringBuffer> writer_;
JsonNote& note_;
};
[[nodiscard]] auto MakeJsonWriter() { return JsonWriter{*this}; }
[[nodiscard]] bool Set(const rapidjson::Value& value) {
return value.Accept(MakeJsonWriter().writer());
}
// CollectNoteData will call this, but it has nothing to do.
static constexpr auto Collect = [](const auto& handle) -> fit::result<Error> {
return fit::ok();
};
private:
rapidjson::StringBuffer data_;
};
// These are called via std::apply on ProcessNotes and ThreadNotes tuples.
constexpr auto CollectNoteData =
// For each note that hasn't already been fetched, try to fetch it now.
[](auto& handle, auto&... note) -> fit::result<Error, size_t> {
// This value is always replaced (or ignored), but the type is not
// default-constructible.
fit::result<Error> result = fit::ok();
size_t total = 0;
auto collect = [&handle, &result, &total](auto& note) -> bool {
result = CollectNote(handle, note);
if (result.is_ok()) {
ZX_DEBUG_ASSERT(note.size_bytes() % 2 == 0);
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 fit::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();
};
template <size_t N, typename T>
void CollectJson(rapidjson::Document& dom, const char (&name)[N], T value) {
rapidjson::Value json_value{value};
dom.AddMember(name, json_value, dom.GetAllocator());
}
template <size_t N>
void CollectJson(rapidjson::Document& dom, const char (&name)[N], std::string_view value) {
rapidjson::Value json_value{
value.data(),
static_cast<rapidjson::SizeType>(value.size()),
};
dom.AddMember(name, json_value, dom.GetAllocator());
}
fit::result<Error, rapidjson::Document> CollectSystemJson(const TaskHolder& holder) {
std::string_view version = holder.system_get_version_string();
if (version.empty()) {
return fit::error{Error{"no system data available", ZX_ERR_NOT_SUPPORTED}};
}
rapidjson::Document dom;
dom.SetObject();
{
rapidjson::Value value(version.data(), static_cast<rapidjson::SizeType>(version.size()));
dom.AddMember("version_string", value, dom.GetAllocator());
}
{
rapidjson::Value value{holder.system_get_dcache_line_size()};
dom.AddMember("dcache_line_size", value, dom.GetAllocator());
}
{
rapidjson::Value value{holder.system_get_num_cpus()};
dom.AddMember("num_cpus", value, dom.GetAllocator());
}
{
rapidjson::Value value{holder.system_get_page_size()};
dom.AddMember("page_size", value, dom.GetAllocator());
}
{
rapidjson::Value value{holder.system_get_physmem()};
dom.AddMember("physmem", value, dom.GetAllocator());
}
return fit::ok(std::move(dom));
}
constexpr auto CollectSystemNote = [](const TaskHolder& holder, auto& note) -> fit::result<Error> {
auto result = CollectSystemJson(holder);
if (result.is_error()) {
return result.take_error();
}
bool ok = note.Set(std::move(result).value());
ZX_ASSERT(ok);
return fit::ok();
};
template <typename Class, typename... Notes>
using KernelNotes = std::tuple< // The whole tuple of all note types:
Notes..., // First the process or job notes.
// Now the kernel note types.
InfoNote<Class, ZX_INFO_HANDLE_BASIC>, // Identifies root resource KOID.
InfoNote<Class, ZX_INFO_CPU_STATS>, //
InfoNote<Class, ZX_INFO_KMEM_STATS>, //
InfoNote<Class, ZX_INFO_GUEST_STATS>>;
constexpr auto CollectKernelNoteData = [](auto&& handle, auto& notes) -> fit::result<Error> {
auto collect = [&](auto&... note) -> fit::result<Error, size_t> {
return CollectNoteData(handle, note...);
};
if (auto result = std::apply(collect, notes); result.is_error()) {
return result.take_error();
}
return fit::ok();
};
template <typename Class>
class Remark : public NoteBase<Class, 0> {
public:
using Base = NoteBase<Class, 0>;
Remark() = default;
Remark(Remark&& other) { *this = std::move(other); }
Remark& operator=(Remark&& other) {
Base::operator=(static_cast<Base&&>(other));
data_ = std::move(other.data_);
this->Emplace(data_);
return *this;
}
Remark(std::string_view name, ByteView data) : data_(data.begin(), data.end()) {
ZX_ASSERT(!name.empty());
ZX_ASSERT(!data.empty());
std::string remark_name{kRemarkNotePrefix};
remark_name += name;
this->header().InitAccumulate(std::move(remark_name));
this->Emplace(data_);
}
private:
std::vector<std::byte> data_;
};
struct JobRemarkClass {
static ArchiveMemberHeader MakeHeader(uint32_t type) { return {}; }
static constexpr auto Pad = PadForArchive;
};
struct ProcessRemarkClass {
static FlexNoteHeader MakeHeader(uint32_t type) { return {}; }
static constexpr auto Pad = PadForElfNote;
};
template <class RemarkClass>
class CollectorBase {
public:
static_assert(std::is_move_constructible_v<Remark<RemarkClass>>);
static_assert(std::is_move_assignable_v<Remark<RemarkClass>>);
void AddRemarks(std::string_view name, ByteView data) { remarks_.emplace_back(name, data); }
// Returns a vector of views into the storage held in this->remarks_.
NoteData::Vector GetRemarks() const {
NoteData data;
for (const auto& note : remarks_) {
note.AddToNoteData(data);
}
ZX_DEBUG_ASSERT(data.size_bytes() == remarks_size_bytes());
return std::move(data).take();
}
template <typename T>
bool OnRemarkHeaders(T&& call) {
for (auto& note : remarks_) {
if (!call(note.header())) {
return false;
}
}
return true;
}
size_t remarks_size_bytes() const {
size_t total = 0;
for (const auto& remark : remarks_) {
total += remark.size_bytes();
}
return total;
}
private:
std::vector<Remark<RemarkClass>> remarks_;
};
} // 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 ProcessDump::Collector : public CollectorBase<ProcessRemarkClass> {
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. The suspend token handle is held
// solely to be held, i.e. to be released on destruction of the collector.
// It's expected to stay std::nullopt until SuspendAndCollectThreads is
// called, and then not to be std::nullopt any more when CollectThreads
// runs, indicating that the process is suspended. We use std::optional
// rather than just an invalid handle so that the asserts can distinguish
// the "not suspended" state in our logic from the non-Fuchsia case's fake
// handles that are all always invalid. (When dumping a postmortem process,
// the logic goes through all the paces of suspension but everything reports
// as already ready already by dint of being dead and there are no real
// suspend tokens.)
Collector(Process& process, std::optional<LiveHandle> suspended)
: process_(process), process_suspended_(std::move(suspended)) {}
Process& process() const { return process_; }
// Reset to initial state, except that if the process is already suspended,
// it stays that way.
void clear() { *this = Collector{process_, std::move(process_suspended_)}; }
// This can be called at most once and must be called first if at all. If
// this is not called, then threads may be allowed to run while the dump
// takes place, yielding an inconsistent memory image; and CollectProcess
// will report only about memory and process-wide state, nothing about
// threads. Afterwards the process remains suspended until the Collector is
// destroyed.
fit::result<Error> SuspendAndCollectThreads() {
ZX_ASSERT(!process_suspended_);
ZX_DEBUG_ASSERT(notes_size_bytes_ == 0);
auto result = process_.get().suspend();
if (result.is_ok()) {
process_suspended_ = std::move(result).value();
} else if (result.error_value().status_ == ZX_ERR_NOT_SUPPORTED) {
// Suspending yourself isn't supported, but that's OK.
ZX_DEBUG_ASSERT(ProcessIsSelf());
} else {
return result.take_error();
}
return CollectThreads();
}
fit::result<Error> CollectSystem(const TaskHolder& holder) {
return CollectSystemNote(holder, std::get<SystemNote>(notes_));
}
fit::result<Error> CollectKernel();
// This collects information about memory 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.
fit::result<Error, size_t> CollectProcess(SegmentCallback prune, size_t limit) {
// Collect the process-wide note data.
auto collect = [this](auto&... note) -> fit::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_ + remarks_size_bytes(),
.align = NoteAlign(),
};
phdrs_.push_back(note_phdr);
// Find the memory segments and build IDs. This fills the phdrs_ table.
if (auto result = FindMemory(std::move(prune)); result.is_error()) {
return result.take_error();
}
// Now figure everything else out to write out a full ET_CORE file.
return fit::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 ThreadCollector::notes_.
fit::result<Error, 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 fit::ok(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 fit::ok(offset);
}
}
if (append({reinterpret_cast<std::byte*>(phdrs_.data()), phdrs_.size() * sizeof(phdrs_[0])})) {
return fit::ok(offset);
}
// Returns true early if any append call returns true.
auto append_notes = [&](auto&& notes) -> bool {
return std::any_of(notes.begin(), notes.end(), append);
};
// Generate the process-wide note data.
if (append_notes(notes())) {
return fit::ok(offset);
}
// Generate the note data for each thread.
for (const auto& [koid, thread] : threads_) {
if (append_notes(thread.notes())) {
return fit::ok(offset);
}
}
ZX_DEBUG_ASSERT(offset % NoteAlign() == 0);
ZX_DEBUG_ASSERT(offset == headers_size_bytes() + notes_size_bytes());
append_notes(GetRemarks());
ZX_DEBUG_ASSERT(offset % NoteAlign() == 0);
ZX_DEBUG_ASSERT(offset == headers_size_bytes() + notes_size_bytes() + remarks_size_bytes());
return fit::ok(offset);
}
// Dump the memory data by calling `dump(size_t offset, ByteView data)` with
// the data meant for the given offset into the ET_CORE file. The data is in
// storage only available during the callback. The `dump` function returns
// some fit::result<error_type> type. DumpMemory returns an "error" result
// for errors reading the memory in. The "success" result holds the results
// from the callback. If `dump` returns an error result, that is returned
// immediately. If it returns success, additional callbacks will be made
// until all the data has been dumped, and the final `dump` callback's return
// value will be the "success" return value.
fit::result<Error, size_t> DumpMemory(DumpCallback dump, size_t limit) {
size_t offset = headers_size_bytes() + notes_size_bytes() + remarks_size_bytes();
for (const auto& segment : phdrs_) {
if (segment.type == elfldltl::ElfPhdrType::kLoad) {
uintptr_t vaddr = segment.vaddr;
if (segment.offset >= limit) {
break;
}
const size_t size = std::min(segment.filesz(), limit - segment.offset);
if (size == 0) {
continue;
}
size_t left = size;
offset = segment.offset;
do {
// This yields some nonempty subset of the requested range, and
// possibly more than requested.
auto read =
process_.get().read_memory<std::byte, ByteView>(vaddr, size, ReadMemorySize::kLess);
if (read.is_error()) {
return read.take_error();
}
// Note the buffer is still owned by read.value().
ByteView chunk = read.value()->subspan(0, std::min(size, read.value()->size()));
// TODO(mcgrathr): subset dump must be detected in layout phase
ZX_DEBUG_ASSERT(!chunk.empty());
// Send it to the callback to write it out.
if (dump(offset, chunk)) {
return fit::ok(offset);
}
vaddr += chunk.size();
offset += chunk.size();
left -= chunk.size();
} while (left > 0);
ZX_DEBUG_ASSERT_MSG(offset == segment.offset + size, "%#zx != %#" PRIx64 " + %#zx", offset,
segment.offset(), size);
}
}
return fit::ok(offset);
}
void set_date(time_t date) { std::get<DateNote>(notes_).Set(date); }
// This can be used from a Collect `prune_segment` callback function.
fit::result<Error, std::optional<SegmentDisposition::Note>> FindBuildIdNote(
const zx_info_maps_t& segment) {
auto elf = DetectElf(process_, segment);
if (elf.is_error()) {
return elf.take_error();
}
if (!elf->empty()) {
auto id = DetectElfIdentity(process_, segment, **elf);
if (id.is_error()) {
return id.take_error();
}
if (id->build_id.size > 0) {
return fit::ok(id->build_id);
}
}
return fit::ok(std::nullopt);
}
private:
struct ProcessInfoClass {
using Handle = Process;
static constexpr auto MakeHeader = kMakeNote<kProcessInfoNoteName>;
static constexpr auto Pad = PadForElfNote;
};
template <zx_object_info_topic_t Topic>
using ProcessInfo = InfoNote<ProcessInfoClass, Topic>;
struct ProcessPropertyClass {
using Handle = Process;
static constexpr auto MakeHeader = kMakeNote<kProcessPropertyNoteName>;
static constexpr auto Pad = PadForElfNote;
};
template <uint32_t Prop>
using ProcessProperty = PropertyNote<ProcessPropertyClass, Prop>;
struct ThreadInfoClass {
using Handle = Thread;
static constexpr auto MakeHeader = kMakeNote<kThreadInfoNoteName>;
static constexpr auto Pad = PadForElfNote;
};
template <zx_object_info_topic_t Topic>
using ThreadInfo = InfoNote<ThreadInfoClass, Topic>;
struct ThreadPropertyClass {
using Handle = Thread;
static constexpr auto MakeHeader = kMakeNote<kThreadPropertyNoteName>;
static constexpr auto Pad = PadForElfNote;
};
template <uint32_t Prop>
using ThreadProperty = PropertyNote<ThreadPropertyClass, Prop>;
// Classes using zx_thread_read_state use this.
struct ThreadStateClass {
using Handle = Thread;
static constexpr auto MakeHeader = kMakeNote<kThreadStateNoteName>;
static constexpr auto Pad = PadForElfNote;
};
template <zx_thread_state_topic_t Topic>
using ThreadState = ThreadStateNote<ThreadStateClass, Topic>;
struct SystemClass {
static constexpr auto MakeHeader = kMakeNote<kSystemNoteName>;
static constexpr auto Pad = PadForElfNote;
};
using SystemNote = JsonNote<SystemClass>;
struct DateClass {
static constexpr auto MakeHeader = kMakeNote<kDateNoteName>;
static constexpr auto Pad = PadForElfNote;
};
class DateNote : public NoteBase<DateClass, 0> {
public:
fit::result<Error> Collect(Process& process) { return fit::ok(); }
void Set(time_t date) {
date_ = date;
Emplace({reinterpret_cast<const std::byte*>(&date_), sizeof(date_)});
}
private:
time_t date_ = 0;
};
struct KernelInfoClass {
using Handle = Resource;
static constexpr auto MakeHeader = kMakeNote<kKernelInfoNoteName>;
static constexpr auto Pad = PadForElfNote;
};
template <typename... Notes>
using WithKernelNotes = KernelNotes<KernelInfoClass, Notes...>;
using ThreadNotes = std::tuple<
// This lists all the notes that can be extracted from a thread.
ThreadInfo<ZX_INFO_HANDLE_BASIC>, ThreadProperty<ZX_PROP_NAME>,
// Ordering of the notes after the first two is not specified and can
// change. Nothing separates the notes for one thread from the notes for
// the next thread, but consumers recognize the zx_info_handle_basic_t
// note as the key for a new thread's notes. Whatever subset of these
// notes that is available for the given thread is present in the dump.
ThreadInfo<ZX_INFO_THREAD>, //
ThreadInfo<ZX_INFO_THREAD_EXCEPTION_REPORT>, //
ThreadInfo<ZX_INFO_THREAD_STATS>, //
ThreadInfo<ZX_INFO_TASK_RUNTIME>, //
ThreadState<ZX_THREAD_STATE_GENERAL_REGS>, //
ThreadState<ZX_THREAD_STATE_FP_REGS>, //
ThreadState<ZX_THREAD_STATE_VECTOR_REGS>, //
ThreadState<ZX_THREAD_STATE_DEBUG_REGS>, //
ThreadState<ZX_THREAD_STATE_SINGLE_STEP>>;
using ProcessNotes = WithKernelNotes<
// This lists all the notes for process-wide state.
ProcessInfo<ZX_INFO_HANDLE_BASIC>, ProcessProperty<ZX_PROP_NAME>,
// Ordering of the notes after the first two is not specified and can
// change.
DateNote, // Self-elides.
SystemNote, // Optional.
ProcessInfo<ZX_INFO_PROCESS>, //
ProcessInfo<ZX_INFO_PROCESS_THREADS>, //
ProcessInfo<ZX_INFO_TASK_STATS>, //
ProcessInfo<ZX_INFO_TASK_RUNTIME>, //
ProcessInfo<ZX_INFO_PROCESS_MAPS>, //
ProcessInfo<ZX_INFO_PROCESS_VMOS>, //
ProcessInfo<ZX_INFO_PROCESS_HANDLE_STATS>, //
ProcessInfo<ZX_INFO_HANDLE_TABLE>, //
ProcessProperty<ZX_PROP_PROCESS_DEBUG_ADDR>, //
ProcessProperty<ZX_PROP_PROCESS_BREAK_ON_LOAD>, //
ProcessProperty<ZX_PROP_PROCESS_VDSO_BASE_ADDRESS>, //
ProcessProperty<ZX_PROP_PROCESS_HW_TRACE_CONTEXT_ID>>;
class ThreadCollector {
public:
ThreadCollector() = delete;
ThreadCollector(ThreadCollector&&) = default;
explicit ThreadCollector(zx_koid_t koid) : koid_(koid) {}
// Acquire the thread handle if possible.
fit::result<Error> Acquire(Process& process) {
if (!handle_) {
auto result = process.get_child(koid_);
if (result.is_error()) {
// It's not an error if the thread has simply died already so the
// KOID is no longer valid.
if (result.error_value().status_ != ZX_ERR_NOT_FOUND) {
return result.take_error();
}
handle_ = nullptr;
} else {
zxdump::Object& child = result.value();
handle_ = static_cast<zxdump::Thread*>(&child);
}
}
return fit::ok();
}
// Return the item to wait for this thread if it needs to be waited for.
[[nodiscard]] std::optional<Object::WaitItem> wait() const {
if (handle_ && *handle_) {
return Object::WaitItem{.handle = **handle_, .waitfor = kWaitFor_};
}
return std::nullopt;
}
// This can be called after the wait() item has been used in wait_many.
// If it still needs to be waited for, it returns success but zero size.
// The next call to wait() will show whether collection is finished.
fit::result<Error, size_t> Collect(zx_signals_t pending) {
ZX_DEBUG_ASSERT(handle_);
ZX_DEBUG_ASSERT(*handle_);
if (pending & kWaitFor_) {
// Now that this thread is quiescent, collect its data.
// Reset *handle_ so wait() will say no next time.
// It's only needed for the collection being done right now.
Thread& thread = **handle_;
handle_ = nullptr;
auto collect = [&thread](auto&... note) { return CollectNoteData(thread, note...); };
return std::apply(collect, notes_);
}
// Still need to wait for this one.
return fit::ok(0);
}
// Returns a vector of views into the storage held in this->notes_.
NoteData::Vector notes() const {
ZX_DEBUG_ASSERT(handle_);
ZX_DEBUG_ASSERT(!*handle_);
return std::apply(DumpNoteData, notes_);
}
private:
static constexpr zx_signals_t kWaitFor_ = // Suspension or death is fine.
ZX_THREAD_SUSPENDED | ZX_THREAD_TERMINATED;
zx_koid_t koid_;
// This is std::nullopt before the thread has been acquired. Once the
// thread has been acquired, this holds its thread handle until it's been
// collected. Once it's been collected, this holds nullptr.
std::optional<Thread*> handle_;
ThreadNotes notes_;
};
// 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>>(notes_); }
auto& process_maps() { return std::get<ProcessInfo<ZX_INFO_PROCESS_MAPS>>(notes_); }
auto& process_vmos() { return std::get<ProcessInfo<ZX_INFO_PROCESS_VMOS>>(notes_); }
// Acquire all the threads. Then collect all their data as soon as they are
// done suspending, waiting as necessary.
fit::result<Error> CollectThreads() {
ZX_DEBUG_ASSERT(process_suspended_ || ProcessIsSelf());
threads_.clear();
while (true) {
// We need fresh data each time through to see if there are new threads.
// Since the process is suspended, no new threads will run in user mode.
// But threads already running might not have finished suspension yet,
// and while not suspended they may create and/or start new threads that
// will "start suspended" but their suspension is asynchronous too.
// Hence, don't use CollectNote here, because it caches old data.
//
// Also a third party with the process handle could be creating and
// starting new suspended threads too. We don't really care about that
// or about the racing new threads, because they won't have any user
// state that's interesting to dump yet. So if we overlook those threads
// the dump will just appear to be from before they existed.
if (auto get = process_threads().Collect(process_); get.is_error()) {
return get;
}
std::vector<zxdump::Object::WaitItem> wait_for;
std::vector<ThreadCollector*> wait_for_threads;
// Look for new threads or unfinished threads.
for (zx_koid_t koid : process_threads().info()) {
auto& thread = threads_.try_emplace(threads_.end(), koid, koid)->second;
// Make sure we have the thread handle if possible.
// If this is not a new thread, this is a no-op.
if (auto result = thread.Acquire(process_); result.is_error()) {
return result.take_error();
}
if (auto wait = thread.wait()) {
// This thread hasn't been collected yet. Wait for it to finish
// suspension (or die).
wait_for.push_back(*wait);
wait_for_threads.push_back(&thread);
}
}
// If there are no unfinished threads, collection is all done.
if (wait_for.empty()) {
return fit::ok();
}
// Wait for a thread to finish its suspension (or death).
if (auto result = Object::wait_many(wait_for); result.is_error()) {
return result.take_error();
}
ZX_DEBUG_ASSERT(wait_for.size() == wait_for_threads.size());
for (size_t i = 0; i < wait_for.size(); ++i) {
auto result = wait_for_threads[i]->Collect(wait_for[i].pending);
if (result.is_error()) {
return result.take_error();
}
notes_size_bytes_ += result.value();
}
// Even if all known threads are quiescent now, another iteration is
// needed to be sure that no new threads were created by these threads
// before they went quiescent.
}
}
// Populate phdrs_. The p_offset fields are filled in later by Layout.
fit::result<Error> FindMemory(SegmentCallback prune_segment) {
// Make sure we have the relevant information to scan.
if (auto result = CollectNote(process_, process_maps()); result.is_error()) {
if (result.error_value().status_ == ZX_ERR_NOT_SUPPORTED) {
// This just means there is no information in the dump.
return fit::ok();
}
return result;
}
if (auto result = CollectNote(process_, process_vmos()); result.is_error()) {
if (result.error_value().status_ == ZX_ERR_NOT_SUPPORTED) {
// This just means there is no information in the dump.
return fit::ok();
}
return result;
}
// The mappings give KOID and some info but the VMO info is also needed.
// So make a quick cross-reference table to find one from the other.
std::map<zx_koid_t, const zx_info_vmo_t&> vmos;
for (const zx_info_vmo_t& info : process_vmos().info()) {
vmos.emplace(info.koid, info);
}
constexpr auto elf_flags = [](zx_vm_option_t mmu_flags) -> Elf::Word {
return (((mmu_flags & ZX_VM_PERM_READ) ? Elf::Phdr::kRead : 0) |
((mmu_flags & ZX_VM_PERM_WRITE) ? Elf::Phdr::kWrite : 0) |
((mmu_flags & ZX_VM_PERM_EXECUTE) ? Elf::Phdr::kExecute : 0));
};
// Go through each mapping. They are in ascending address order.
uintptr_t address_limit = 0;
for (const zx_info_maps_t& info : process_maps().info()) {
if (info.type == ZX_INFO_MAPS_TYPE_MAPPING) {
ZX_ASSERT(info.base >= address_limit);
address_limit = info.base + info.size;
ZX_ASSERT(info.base < address_limit);
// Add a PT_LOAD segment for the mapping no matter what.
// It will be present with p_filesz==0 if the memory is elided.
{
const Elf::Phdr new_phdr = {
.type = elfldltl::ElfPhdrType::kLoad,
.flags = elf_flags(info.u.mapping.mmu_flags),
.vaddr = info.base,
.filesz = info.size,
.memsz = info.size,
.align = process_.get().dump_page_size(),
};
phdrs_.push_back(new_phdr);
}
Elf::Phdr& segment = phdrs_.back();
const zx_info_vmo_t& vmo = vmos.at(info.u.mapping.vmo_koid);
ZX_DEBUG_ASSERT(vmo.koid == info.u.mapping.vmo_koid);
// The default-constructed state elides the whole segment.
SegmentDisposition dump;
// Default choice: dump the whole thing. But never dump device memory,
// which could cause side effects on memory-mapped devices just from
// reading the physical address.
if (ZX_INFO_VMO_TYPE(vmo.flags) != ZX_INFO_VMO_TYPE_PHYSICAL) {
dump.filesz = segment.filesz;
}
// If this mapping covers past the end of the VMO, trim the excess away. Note that we have
// to handle overflows because the vmo_offset could be greater than the VMO size.
uint64_t vmo_size_after_offset;
if (sub_overflow(vmo.size_bytes, info.u.mapping.vmo_offset, &vmo_size_after_offset)) {
vmo_size_after_offset = 0;
}
if (dump.filesz > vmo_size_after_offset) {
dump.filesz = vmo_size_after_offset;
}
// Let the callback decide about this segment.
if (auto result = prune_segment(dump, info, vmo); result.is_error()) {
return result.take_error();
} else {
dump = result.value();
}
ZX_ASSERT(dump.filesz <= info.size);
segment.filesz = dump.filesz;
// The callback can choose to emit an ELF note whose contents are
// embedded within this PT_LOAD segment. This becomes a PT_NOTE
// segment directly in the ET_CORE file, pointing at the dumped memory
// expected to be in ELF note format.
if (dump.note) {
// The note's vaddr and size are a subset of the segment's. Add an
// extra PT_NOTE segment pointing to the chosen memory area. The
// callback sets p_vaddr and p_filesz but we sanitize the rest.
ZX_ASSERT(dump.note->vaddr >= segment.vaddr);
ZX_ASSERT(dump.note->vaddr - segment.vaddr <= segment.filesz);
ZX_ASSERT(dump.note->size <= segment.filesz - (dump.note->vaddr - segment.vaddr));
const Elf::Phdr note_phdr = {
.type = elfldltl::ElfPhdrType::kNote,
.flags = Elf::Phdr::kRead,
.vaddr = dump.note->vaddr,
.filesz = dump.note->size,
.memsz = dump.note->size,
.align = NoteAlign(),
};
phdrs_.push_back(note_phdr);
}
}
}
return fit::ok();
}
// 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_);
}
ZX_DEBUG_ASSERT(offset == headers_size_bytes());
// 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;
}
};
// First is the initial note segment.
ZX_DEBUG_ASSERT(!phdrs_.empty());
ZX_DEBUG_ASSERT(phdrs_[0].type == elfldltl::ElfPhdrType::kNote);
place(phdrs_[0]);
// Now place the remaining segments, if any.
for (auto& phdr : cpp20::span(phdrs_).subspan(1)) {
switch (phdr.type) {
case elfldltl::ElfPhdrType::kLoad:
place(phdr);
break;
case elfldltl::ElfPhdrType::kNote: {
// This is an ELF note segment.
// It lies within the preceding PT_LOAD segment.
const auto& load = (&phdr)[-1];
ZX_DEBUG_ASSERT(load.type == elfldltl::ElfPhdrType::kLoad);
ZX_DEBUG_ASSERT(phdr.vaddr >= load.vaddr);
phdr.offset = phdr.vaddr - load.vaddr + load.offset;
break;
}
default:
ZX_ASSERT_MSG(false, "generated p_type %#x ???", static_cast<unsigned int>(phdr.type()));
}
}
ZX_DEBUG_ASSERT(offset % NoteAlign() == 0);
return offset;
}
size_t headers_size_bytes() const {
return sizeof(ehdr_) + (sizeof(phdrs_[0]) * phdrs_.size()) +
(ehdr_.phnum == Elf::Ehdr::kPnXnum ? sizeof(shdr_) : 0);
}
size_t notes_size_bytes() const { return notes_size_bytes_; }
bool ProcessIsSelf() {
#ifdef __Fuchsia__
zx_info_handle_basic_t info;
zx_status_t status =
zx::process::self()->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr);
ZX_ASSERT_MSG(status == ZX_OK, "ZX_INFO_HANDLE_BASIC on self: %d", status);
return info.koid == process_.get().koid();
#endif
return false;
}
std::reference_wrapper<Process> process_;
std::optional<zxdump::LiveHandle> process_suspended_;
ProcessNotes notes_;
std::map<zx_koid_t, ThreadCollector> threads_;
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 and thread notes.
size_t notes_size_bytes_ = 0;
};
ProcessDump::ProcessDump(ProcessDump&&) noexcept = default;
ProcessDump& ProcessDump::operator=(ProcessDump&&) noexcept = default;
ProcessDump::~ProcessDump() = default;
Process& ProcessDump::process() const { return collector_->process(); }
void ProcessDump::clear() { collector_->clear(); }
fit::result<Error> ProcessDump::SuspendAndCollectThreads() {
return collector_->SuspendAndCollectThreads();
}
fit::result<Error, size_t> ProcessDump::CollectProcess(SegmentCallback prune, size_t limit) {
return collector_->CollectProcess(std::move(prune), limit);
}
fit::result<Error> ProcessDump::CollectKernel() { return collector_->CollectKernel(); }
fit::result<Error> ProcessDump::CollectSystem(const TaskHolder& holder) {
return collector_->CollectSystem(holder);
}
fit::result<Error, std::optional<SegmentDisposition::Note>> ProcessDump::FindBuildIdNote(
const zx_info_maps_t& segment) {
return collector_->FindBuildIdNote(segment);
}
fit::result<Error, size_t> ProcessDump::DumpHeadersImpl(DumpCallback dump, size_t limit) {
return collector_->DumpHeaders(std::move(dump), limit);
}
fit::result<Error, size_t> ProcessDump::DumpMemoryImpl(DumpCallback callback, size_t limit) {
return collector_->DumpMemory(std::move(callback), limit);
}
void ProcessDump::set_date(time_t date) { collector_->set_date(date); }
fit::result<Error> ProcessDump::Remarks(std::string_view name, ByteView data) {
collector_->AddRemarks(name, data);
return fit::ok();
}
// 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 and the process
// being suspended.
ProcessDump::ProcessDump(Process& process) noexcept : collector_{new Collector{process, {}}} {}
class JobDump::Collector : public CollectorBase<JobRemarkClass> {
public:
Collector() = delete;
// Only Emplace and clear call this. The job is mandatory and all other
// members are safely default-initialized.
explicit Collector(Job& job) : job_(job) {}
Job& job() const { return job_; }
// Reset to initial state.
void clear() { *this = Collector{job_}; }
// This collects information about job-wide state.
fit::result<Error, size_t> CollectJob() {
// Collect the job-wide note data.
auto collect = [this](auto&... note) -> fit::result<Error, size_t> {
return CollectNoteData(job_, note...);
};
auto result = std::apply(collect, notes_);
if (result.is_error()) {
return result.take_error();
}
ZX_DEBUG_ASSERT(result.value() % 2 == 0);
// Each note added its name to the name table inside CollectNoteData.
auto count_job_note_names = [](const auto&... note) -> size_t {
return (note.header().name_bytes().size() + ...);
};
size_t name_table_size = std::apply(count_job_note_names, notes_);
OnRemarkHeaders([&name_table_size](const ArchiveMemberHeader& header) {
name_table_size += header.name_bytes().size();
return true;
});
name_table_.InitNameTable(name_table_size);
// The name table member will be padded on the way out.
name_table_size += name_table_size % 2;
return fit::ok(kArchiveMagic.size() + // Archive header +
name_table_.bytes().size() + // name table member header +
name_table_size + // name table contents +
result.value()); // note members & headers.
}
auto CollectChildren() { return job_.get().children(); }
auto CollectProcesses() { return job_.get().processes(); }
fit::result<Error> CollectSystem(const TaskHolder& holder) {
return CollectSystemNote(holder, std::get<SystemNote>(notes_));
}
fit::result<Error> CollectKernel();
fit::result<Error, size_t> DumpHeaders(DumpCallback dump, time_t mtime) {
size_t offset = 0;
auto append = [&](ByteView data) -> bool {
bool bail = dump(offset, data);
offset += data.size();
return bail;
};
// Generate the archive header.
if (append(ArchiveMagic())) {
return fit::ok(offset);
}
ZX_DEBUG_ASSERT(offset % 2 == 0);
// The name table member header has been initialized. Write it out now.
if (append(name_table_.bytes())) {
return fit::ok(offset);
}
ZX_DEBUG_ASSERT(offset % 2 == 0);
// Finalize each note by setting its name and date fields, and stream
// out the contents of the name table at the same time. Additional
// members streamed out later can only use the truncated name field in
// the member header.
size_t name_table_pos = 0;
auto finish_note_header = [&](ArchiveMemberHeader& header) -> bool {
header.set_date(mtime);
header.set_name_offset(name_table_pos);
ByteView name = header.name_bytes();
name_table_pos += name.size();
return append(name);
};
if (!OnRemarkHeaders(finish_note_header)) {
return fit::ok(offset);
}
auto finalize = [&](auto&... note) { return (finish_note_header(note.header()) || ...); };
if (std::apply(finalize, notes_)) {
return fit::ok(offset);
}
ZX_DEBUG_ASSERT(offset % 2 == name_table_pos % 2);
if (name_table_pos % 2 != 0 && append(kArchiveMemberPad)) {
return fit::ok(offset);
}
ZX_DEBUG_ASSERT(offset % 2 == 0);
// Generate the job-wide note data.
for (ByteView data : notes()) {
if (append(data)) {
return fit::ok(offset);
}
}
ZX_DEBUG_ASSERT(offset % 2 == 0);
// Generate the remarks note data.
for (ByteView data : GetRemarks()) {
if (append(data)) {
return fit::ok(offset);
}
}
ZX_DEBUG_ASSERT(offset % 2 == 0);
return fit::ok(offset);
}
private:
struct JobInfoClass {
using Handle = Job;
static constexpr auto MakeHeader = kMakeMember<kJobInfoName>;
static constexpr auto Pad = PadForArchive;
};
template <zx_object_info_topic_t Topic>
using JobInfo = InfoNote<JobInfoClass, Topic>;
struct JobPropertyClass {
using Handle = Job;
static constexpr auto MakeHeader = kMakeMember<kJobPropertyName>;
static constexpr auto Pad = PadForArchive;
};
template <uint32_t Prop>
using JobProperty = PropertyNote<JobPropertyClass, Prop>;
struct SystemClass {
static constexpr auto MakeHeader = kMakeMember<kSystemNoteName, true>;
static constexpr auto Pad = PadForArchive;
};
using SystemNote = JsonNote<SystemClass>;
struct KernelInfoClass {
using Handle = Resource;
static constexpr auto MakeHeader = kMakeMember<kKernelInfoNoteName>;
static constexpr auto Pad = PadForArchive;
};
template <typename... Notes>
using WithKernelNotes = KernelNotes<KernelInfoClass, Notes...>;
// These are named for use by CollectChildren and CollectProcesses.
using Children = JobInfo<ZX_INFO_JOB_CHILDREN>;
using Processes = JobInfo<ZX_INFO_JOB_PROCESSES>;
using JobNotes = WithKernelNotes<
// This lists all the notes for job-wide state.
JobInfo<ZX_INFO_HANDLE_BASIC>, JobProperty<ZX_PROP_NAME>,
// Ordering of the other notes is not specified and can change.
SystemNote, // Optionally included in any given job.
JobInfo<ZX_INFO_JOB>, //
JobInfo<ZX_INFO_JOB_CHILDREN>, //
JobInfo<ZX_INFO_JOB_PROCESSES>, //
JobInfo<ZX_INFO_TASK_RUNTIME>>;
// Returns a vector of views into the storage held in this->notes_.
NoteData::Vector notes() const { return std::apply(DumpNoteData, notes_); }
std::reference_wrapper<Job> job_;
ArchiveMemberHeader name_table_;
JobNotes notes_;
};
ByteView JobDump::ArchiveMagic() { return cpp20::as_bytes(cpp20::span(kArchiveMagic)); }
JobDump::JobDump(JobDump&&) noexcept = default;
JobDump& JobDump::operator=(JobDump&&) noexcept = default;
JobDump::~JobDump() = default;
Job& JobDump::job() const { return collector_->job(); }
fit::result<Error> JobDump::CollectKernel() { return collector_->CollectKernel(); }
fit::result<Error> JobDump::CollectSystem(const TaskHolder& holder) {
return collector_->CollectSystem(holder);
}
fit::result<Error, size_t> JobDump::CollectJob() { return collector_->CollectJob(); }
fit::result<Error, std::reference_wrapper<Job::JobMap>> JobDump::CollectChildren() {
return collector_->CollectChildren();
}
fit::result<Error, std::reference_wrapper<Job::ProcessMap>> JobDump::CollectProcesses() {
return collector_->CollectProcesses();
}
fit::result<Error, size_t> JobDump::DumpHeadersImpl(DumpCallback callback, time_t mtime) {
return collector_->DumpHeaders(std::move(callback), mtime);
}
fit::result<Error, size_t> JobDump::DumpMemberHeaderImpl(DumpCallback callback, size_t offset,
std::string_view name, size_t size,
time_t mtime) {
ArchiveMemberHeader header{name};
header.set_size(size);
header.set_date(mtime);
callback(offset, header.bytes());
return fit::ok(offset + header.bytes().size());
}
size_t JobDump::MemberHeaderSize() { return sizeof(ar_hdr); }
fit::result<Error> ProcessDump::Collector::CollectKernel() {
return CollectKernelNoteData(process_, notes_);
}
fit::result<Error> JobDump::Collector::CollectKernel() {
return CollectKernelNoteData(job_, notes_);
}
fit::result<Error> JobDump::Remarks(std::string_view name, ByteView data) {
collector_->AddRemarks(name, data);
return fit::ok();
}
// A single Collector cannot be used for a different job later. It can be
// clear()'d to reset all state other than the job handle.
JobDump::JobDump(Job& job) noexcept : collector_{new Collector{job}} {}
} // namespace zxdump