Page MenuHomeFreeBSD

D48199.diff
No OneTemporary

D48199.diff

diff --git a/usr.bin/Makefile b/usr.bin/Makefile
--- a/usr.bin/Makefile
+++ b/usr.bin/Makefile
@@ -26,6 +26,7 @@
comm \
compress \
csplit \
+ ctfdiff \
ctlstat \
cut \
diff \
diff --git a/usr.bin/ctfdiff/Makefile b/usr.bin/ctfdiff/Makefile
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/Makefile
@@ -0,0 +1,33 @@
+.include <Makefile.inc>
+
+.PATH: ${SRCTOP}/cddl/contrib/opensolaris/tools/ctf/common
+
+PACKAGE= ctf-tools
+PROG_CXX= ctfdiff
+SRCS= ctfdiff.cc \
+ ctfdata.cc \
+ ctftype.cc \
+ metadata.cc\
+ utility.cc \
+
+CFLAGS+= -DIN_BASE
+CFLAGS+= -I${SRCTOP}/sys/contrib/openzfs/include
+CFLAGS+= -I${SRCTOP}/sys/contrib/openzfs/lib/libspl/include/
+CFLAGS+= -I${SRCTOP}/sys/contrib/openzfs/lib/libspl/include/os/freebsd
+CFLAGS+= -I${SRCTOP}/sys
+CFLAGS+= -I${SRCTOP}/cddl/compat/opensolaris/include
+CFLAGS+= -I${OPENSOLARIS_USR_DISTDIR} \
+ -I${OPENSOLARIS_SYS_DISTDIR} \
+ -I${OPENSOLARIS_USR_DISTDIR}/head \
+ -I${OPENSOLARIS_USR_DISTDIR}/cmd/mdb/tools/common \
+ -I${SRCTOP}/sys/cddl/compat/opensolaris \
+ -I${SRCTOP}/cddl/compat/opensolaris/include \
+ -I${OPENSOLARIS_USR_DISTDIR}/tools/ctf/common \
+ -I${OPENSOLARIS_SYS_DISTDIR}/uts/common
+
+CXXFLAGS+= -std=c++17
+CFLAGS+= -DHAVE_ISSETUGID
+
+LIBADD= elf z
+
+.include <bsd.prog.mk>
diff --git a/usr.bin/ctfdiff/Makefile.inc b/usr.bin/ctfdiff/Makefile.inc
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/Makefile.inc
@@ -0,0 +1,12 @@
+OPENSOLARIS_USR_DISTDIR= ${.CURDIR}/../../cddl/contrib/opensolaris
+OPENSOLARIS_SYS_DISTDIR= ${.CURDIR}/../../sys/cddl/contrib/opensolaris
+
+IGNORE_PRAGMA= YES
+
+CFLAGS+= -DNEED_SOLARIS_BOOLEAN
+CFLAGS+= -DHAVE_STRLCAT -DHAVE_STRLCPY
+
+# Do not lint the CDDL stuff. It is all externally maintained and
+# lint output is wasteful noise here.
+
+NO_LINT=
diff --git a/usr.bin/ctfdiff/ctfdata.hpp b/usr.bin/ctfdiff/ctfdata.hpp
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/ctfdata.hpp
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <sys/types.h>
+
+#include <gelf.h>
+#include <stdint.h>
+
+#include "ctf_headers.h"
+#include "sys/ctf.h"
+
+#include "ctftype.hpp"
+#include "metadata.hpp"
+#include <cstdint>
+#include <memory>
+#include <string_view>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+struct CtfDiff;
+struct CtfData;
+struct CtfType;
+
+using ShrCtfData = std::shared_ptr<CtfData>;
+using ShrCtfType = std::shared_ptr<CtfType>;
+
+struct CtfData {
+ public:
+ /* typedef */
+ template <typename T> struct CtfObjEntry {
+ using ty_type = T;
+ std::string_view name; /* name of the variable */
+ T type; /* type of the variable */
+ uint32_t id; /* id of the variable */
+ };
+
+ using CtfFuncTypeEntry = CtfObjEntry<std::vector<ShrCtfType>>;
+ using CtfFuncIdEntry = CtfObjEntry<std::vector<uint32_t>>;
+ using CtfVarTypeEntry = CtfObjEntry<ShrCtfType>;
+ using CtfVarIdEntry = CtfObjEntry<uint32_t>;
+
+ private:
+ /* members */
+ CtfMetaData metadata;
+ size_t ctf_id_width;
+ ctf_header_t *header;
+ std::unordered_map<uint32_t, ShrCtfType> id_to_types;
+ std::vector<CtfVarIdEntry> static_variables;
+ std::vector<CtfFuncIdEntry> functions;
+
+ /* member function */
+ CtfTypeFactory get_type_factory();
+ bool zlib_decompress();
+
+ std::string_view find_next_symbol_with_type(int &idx, uchar_t type);
+ std::string_view get_str_from_ref(uint_t ref);
+ bool ignore_symbol(GElf_Sym *sym, const char *name);
+
+ std::pair<std::vector<CtfFuncTypeEntry>, std::vector<CtfFuncTypeEntry>>
+ do_diff_func(const CtfData &rhs,
+ std::unordered_map<uint64_t, bool> &cache) const;
+ std::pair<std::vector<CtfVarTypeEntry>, std::vector<CtfVarTypeEntry>>
+ do_diff_var(const CtfData &rhs,
+ std::unordered_map<uint64_t, bool> &cache) const;
+ CtfData(CtfMetaData &&metadata);
+
+ /* static function */
+ static bool do_parse_types(ShrCtfData info);
+ static bool do_parse_data(ShrCtfData info);
+ static bool do_parse_func(ShrCtfData info);
+
+ public:
+ std::pair<CtfDiff, CtfDiff> compare_and_get_diff(
+ const CtfData &rhs) const;
+
+ bool is_available();
+ inline const std::unordered_map<uint32_t, ShrCtfType> &id_mapper() const
+ {
+ return id_to_types;
+ }
+
+ static std::shared_ptr<CtfData> create_ctf_info(CtfMetaData &&metadata);
+};
+
+struct CtfDiff {
+ std::vector<CtfData::CtfVarTypeEntry> variables;
+ std::vector<CtfData::CtfFuncTypeEntry> functions;
+};
diff --git a/usr.bin/ctfdiff/ctfdata.cc b/usr.bin/ctfdiff/ctfdata.cc
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/ctfdata.cc
@@ -0,0 +1,735 @@
+
+#include <sys/types.h>
+#include <sys/param.h>
+#include <sys/sysmacros.h>
+
+#include <gelf.h>
+#include <libelf.h>
+#include <skein_port.h>
+#include <stdint.h>
+#include <strings.h>
+#include <unistd.h>
+#include <zconf.h>
+#include <zlib.h>
+
+#include "contrib/openzfs/lib/libspl/include/sys/stdtypes.h"
+#include "ctf_headers.h"
+#include "sys/ctf.h"
+#include "sys/elf_common.h"
+
+#include "ctfdata.hpp"
+#include "ctftype.hpp"
+#include "metadata.hpp"
+#include "utility.hpp"
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <string_view>
+#include <type_traits>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+bool
+CtfData::is_available()
+{
+ return (this->header != nullptr);
+}
+
+bool
+CtfData::ignore_symbol(GElf_Sym *sym, const char *name)
+{
+ u_char type = GELF_ST_TYPE(sym->st_info);
+
+ /* when symbol is anomous or undefined */
+ if (sym->st_shndx == SHN_UNDEF || sym->st_name == 0)
+ return (true);
+
+ if (strcmp(name, "_START_") == 0 || strcmp(name, "_END_") == 0)
+ return (true);
+
+ /* ignore address == 0 and abs) */
+ if (type == STT_OBJECT && sym->st_shndx == SHN_ABS &&
+ sym->st_value == 0)
+ return (true);
+
+ return (false);
+}
+
+bool
+CtfData::zlib_decompress()
+{
+ z_stream zs;
+ std::byte *buffer;
+ int rc;
+ size_t buffer_size = header->cth_stroff + header->cth_strlen;
+
+ buffer = new std::byte[buffer_size];
+
+ bzero((void *)&zs, sizeof(zs));
+ zs.next_in = reinterpret_cast<Bytef *>(metadata.ctfdata.data);
+ zs.avail_in = metadata.ctfdata.size;
+ zs.next_out = reinterpret_cast<Bytef *>(buffer);
+ zs.avail_out = buffer_size;
+
+ if ((rc = inflateInit(&zs)) != Z_OK) {
+ std::cout << "failed to initialize zlib: " << zError(rc)
+ << '\n';
+ return (false);
+ }
+
+ if ((rc = inflate(&zs, Z_FINISH)) != Z_STREAM_END) {
+ std::cout << "failed to decompress CTF data: " << zError(rc)
+ << '\n';
+ return (false);
+ }
+
+ if ((rc = inflateEnd(&zs)) != Z_OK) {
+ std::cout << "failed to finish decompress: " << zError(rc)
+ << '\n';
+ return (false);
+ }
+
+ if (zs.total_out != buffer_size) {
+ std::cout << "CTF data is corrupted\n";
+ return (false);
+ }
+
+ metadata.ctfdata.data = buffer;
+ metadata.ctfdata.size = buffer_size;
+
+ return (true);
+}
+
+CtfTypeFactory
+CtfData::get_type_factory()
+{
+ return header->cth_version == CTF_VERSION_2 ?
+ &CtfTypeParser_V2::create_symbol :
+ &CtfTypeParser_V3::create_symbol;
+}
+
+CtfData::CtfData(CtfMetaData &&metadata)
+ : metadata(metadata)
+{
+ Buffer &ctf_buffer = this->metadata.ctfdata;
+ this->header = nullptr;
+
+ if (ctf_buffer.size < sizeof(ctf_preamble_t)) {
+ std::cout << metadata.file_name()
+ << " does not contain a CTF preamble\n";
+ return;
+ }
+
+ const ctf_preamble_t *preamble = reinterpret_cast<ctf_preamble_t *>(
+ ctf_buffer.data);
+
+ if (preamble->ctp_magic != CTF_MAGIC) {
+ std::cout << metadata.file_name()
+ << " does not contain a valid ctf data\n";
+ return;
+ }
+
+ if (preamble->ctp_version != CTF_VERSION_2 &&
+ preamble->ctp_version != CTF_VERSION_3) {
+ std::cout << "CTF version " << preamble->ctp_version
+ << " is not available\n";
+ return;
+ }
+
+ if (ctf_buffer.size < sizeof(ctf_header_t)) {
+ std::cout << "File " << metadata.file_name()
+ << " contains invalid CTF header\n";
+ return;
+ }
+
+ this->ctf_id_width = preamble->ctp_version == CTF_VERSION_2 ? 2 : 4;
+ this->header = reinterpret_cast<ctf_header_t *>(ctf_buffer.data);
+ ctf_buffer.data += sizeof(ctf_header_t);
+
+ if (header->cth_flags & CTF_F_COMPRESS) {
+ if (!zlib_decompress()) {
+ this->header = nullptr;
+ return;
+ }
+ }
+
+ return;
+}
+
+std::shared_ptr<CtfData>
+CtfData::create_ctf_info(CtfMetaData &&metadata)
+{
+ auto res = std::shared_ptr<CtfData>(
+ new CtfData(std::forward<CtfMetaData &&>(metadata)));
+
+ if (!res->is_available()) {
+ return nullptr;
+ }
+
+ do_parse_types(res);
+ do_parse_data(res);
+ do_parse_func(res);
+
+ std::sort(res->functions.begin(), res->functions.end(),
+ [](const auto &lhs, const auto &rhs) {
+ return lhs.name < rhs.name;
+ });
+ std::sort(res->static_variables.begin(), res->static_variables.end(),
+ [](const auto &lhs, const auto &rhs) {
+ return lhs.name < rhs.name;
+ });
+
+ return (res);
+}
+
+std::string_view
+CtfData::find_next_symbol_with_type(int &idx, uchar_t type)
+{
+ size_t i;
+ uchar_t sym_type;
+ GElf_Sym sym;
+ const char *name;
+ Elf_Data *sym_sec = metadata.symdata.elfdata;
+
+ for (i = idx + 1; i < metadata.symdata.entries; ++i) {
+ if (gelf_getsym(sym_sec, i, &sym) == 0)
+ return ("");
+
+ name = (const char *)(metadata.strdata.data + sym.st_name);
+ sym_type = GELF_ST_TYPE(sym.st_info);
+
+ if (type != sym_type || ignore_symbol(&sym, name))
+ continue;
+ idx = i;
+ return (name);
+ }
+
+ return ("");
+}
+
+bool
+CtfData::do_parse_data(ShrCtfData info)
+{
+ auto &header = info->header;
+ auto &metadata = info->metadata;
+ auto &static_variables = info->static_variables;
+ auto ctf_id_width = info->ctf_id_width;
+
+ const std::byte *iter = metadata.ctfdata.data + header->cth_objtoff;
+ ulong_t n = (header->cth_funcoff - header->cth_objtoff) / ctf_id_width;
+
+ int symidx, id;
+ uint32_t type_id;
+ std::string_view name;
+
+ for (symidx = -1, id = 0; id < (int)n; ++id) {
+ if (metadata.symdata.data != nullptr)
+ name = info->find_next_symbol_with_type(symidx,
+ STT_OBJECT);
+ else
+ name = "";
+
+ memcpy(&type_id, iter, ctf_id_width);
+ iter += ctf_id_width;
+ if (name != "")
+ static_variables.push_back(
+ { name, type_id, static_cast<uint32_t>(id) });
+ }
+
+ return (true);
+}
+
+bool
+CtfData::do_parse_types(ShrCtfData info)
+{
+ auto &header = info->header;
+ auto &metadata = info->metadata;
+ auto &id_to_types = info->id_to_types;
+ const std::byte *iter = metadata.ctfdata.data + header->cth_typeoff;
+ const std::byte *end = metadata.ctfdata.data + header->cth_stroff;
+ auto ctf_id_width = info->ctf_id_width;
+ uint64_t id;
+ CtfTypeParser *(*type_factory)(const std::byte *);
+ size_t vlen, increment;
+
+ if (header->cth_typeoff & 3) {
+ std::cout << "cth_typeoff is not aligned porperly\n";
+ return (false);
+ }
+
+ if (header->cth_typeoff >= metadata.ctfdata.size) {
+ std::cout << "file is truncated or cth_typeoff is corrupt\n";
+ return (false);
+ }
+
+ if (header->cth_stroff >= metadata.ctfdata.size) {
+ std::cout << "file is truncated or cth_stroff is corrupt\n";
+ return (false);
+ }
+
+ if (header->cth_typeoff > header->cth_stroff) {
+ std::cout << "file is corrupt -- cth_typeoff > cth_stroff\n";
+ return (false);
+ }
+
+ uint32_t version = header->cth_version;
+ id = 1;
+ if (header->cth_parname)
+ id += 1ul << (header->cth_version == CTF_VERSION_2 ?
+ CTF_V2_PARENT_SHIFT :
+ CTF_V3_PARENT_SHIFT);
+
+ type_factory = info->get_type_factory();
+
+ id_to_types[0] = std::make_shared<CtfTypeVaArg>(nullptr, 0, "va_arg",
+ info);
+
+ for (/* */; iter < end; ++id) {
+ CtfTypeParser *sym = type_factory(iter);
+ vlen = 0;
+
+ union {
+ const std::byte *ptr;
+ struct ctf_array_v2 *ap2;
+ struct ctf_array_v3 *ap3;
+ const struct ctf_member_v2 *mp2;
+ const struct ctf_member_v3 *mp3;
+ const struct ctf_lmember_v2 *lmp2;
+ const struct ctf_lmember_v3 *lmp3;
+ const ctf_enum_t *ep;
+ } u;
+
+ increment = sym->increment();
+ u.ptr = iter + increment;
+
+ switch (sym->kind()) {
+ case CTF_K_INTEGER: {
+ uint_t encoding = *(
+ reinterpret_cast<const uint_t *>(u.ptr));
+ vlen = sizeof(uint32_t);
+ id_to_types[id] =
+ std::make_shared<CtfTypeInteger>(encoding, sym, id,
+ info->get_str_from_ref(sym->name()), info);
+ break;
+ }
+
+ case CTF_K_FLOAT: {
+ uint_t encoding = *(
+ reinterpret_cast<const uint_t *>(u.ptr));
+ vlen = sizeof(uint32_t);
+ id_to_types[id] =
+ std::make_shared<CtfTypeFloat>(encoding, sym, id,
+ info->get_str_from_ref(sym->name()), info);
+ break;
+ }
+
+ case CTF_K_POINTER: {
+ uint_t type = sym->type();
+ id_to_types[id] = std::make_shared<CtfTypePtr>(type,
+ sym, id, info->get_str_from_ref(sym->name()), info);
+ break;
+ }
+
+ case CTF_K_ARRAY:
+ id_to_types[id] = std::make_shared<CtfTypeArray>(u.ptr,
+ sym, id, info->get_str_from_ref(sym->name()), info);
+ if (version == CTF_VERSION_2)
+ vlen = sizeof(struct ctf_array_v2);
+ else
+ vlen = sizeof(struct ctf_array_v3);
+ break;
+
+ case CTF_K_FUNCTION: {
+ uint_t ret = sym->type();
+ uint_t arg = 0;
+ int n = sym->vlen();
+ std::vector<uint_t> args;
+
+ for (int i = 0; i < n; ++i, u.ptr += ctf_id_width) {
+ memcpy(&arg, u.ptr, ctf_id_width);
+ args.push_back(arg);
+ }
+
+ id_to_types[id] = std::make_shared<CtfTypeFunc>(ret,
+ std::move(args), sym, id,
+ info->get_str_from_ref(sym->name()), info);
+ vlen = roundup2(ctf_id_width * n, 4);
+ break;
+ }
+
+ case CTF_K_STRUCT: {
+ auto [size, members] = sym->do_struct(u.ptr,
+ std::bind(&CtfData::get_str_from_ref, info.get(),
+ std::placeholders::_1));
+ id_to_types[id] = std::make_shared<CtfTypeStruct>(
+ sym->size(), std::move(members), sym, id,
+ info->get_str_from_ref(sym->name()), info);
+ vlen = size;
+ break;
+ }
+
+ case CTF_K_UNION: {
+ auto [size, members] = sym->do_struct(u.ptr,
+ std::bind(&CtfData::get_str_from_ref, info.get(),
+ std::placeholders::_1));
+ id_to_types[id] = std::make_shared<CtfTypeUnion>(
+ sym->size(), std::move(members), sym, id,
+ info->get_str_from_ref(sym->name()), info);
+ vlen = size;
+ break;
+ }
+
+ case CTF_K_ENUM: {
+ std::vector<std::pair<std::string_view, uint32_t>> vec;
+ int n = sym->vlen(), i;
+
+ for (i = 0; i < n; ++i, u.ep++)
+ vec.push_back(
+ { info->get_str_from_ref(u.ep->cte_name),
+ u.ep->cte_value });
+
+ id_to_types[id] =
+ std::make_shared<CtfTypeEnum>(std::move(vec), sym,
+ id, info->get_str_from_ref(sym->name()), info);
+ vlen = sizeof(ctf_enum_t) * n;
+ break;
+ }
+
+ case CTF_K_FORWARD:
+ id_to_types[id] = std::make_shared<CtfTypeForward>(sym,
+ id, info->get_str_from_ref(sym->name()), info);
+ break;
+ case CTF_K_TYPEDEF:
+ id_to_types[id] =
+ std::make_shared<CtfTypeTypeDef>(sym->type(), sym,
+ id, info->get_str_from_ref(sym->name()), info);
+ break;
+ case CTF_K_VOLATILE:
+ id_to_types[id] =
+ std::make_shared<CtfTypeVolatile>(sym->type(), sym,
+ id, info->get_str_from_ref(sym->name()), info);
+ break;
+ case CTF_K_CONST:
+ id_to_types[id] =
+ std::make_shared<CtfTypeConst>(sym->type(), sym, id,
+ info->get_str_from_ref(sym->name()), info);
+ break;
+ case CTF_K_RESTRICT:
+ id_to_types[id] =
+ std::make_shared<CtfTypeRestrict>(sym->type(), sym,
+ id, info->get_str_from_ref(sym->name()), info);
+ break;
+ case CTF_K_UNKNOWN:
+ id_to_types[id] = std::make_shared<CtfTypeUnknown>(sym,
+ id, info);
+ break;
+ default:
+ std::cout << "Unexpected kind: " << sym->kind() << '\n';
+ return (false);
+ }
+
+ iter += increment + vlen;
+ }
+
+ return (true);
+}
+
+bool
+CtfData::do_parse_func(ShrCtfData info)
+{
+ auto &header = info->header;
+ auto &metadata = info->metadata;
+ auto &functions = info->functions;
+ auto ctf_id_width = info->ctf_id_width;
+
+ const std::byte *iter = metadata.ctfdata.data + header->cth_funcoff;
+ const std::byte *end = metadata.ctfdata.data + header->cth_typeoff;
+
+ std::string_view name;
+
+ int32_t id;
+ int symidx;
+ uint_t ctf_sym_info;
+
+ for (symidx = -1, id = 0; iter < end; ++id) {
+ memcpy(&ctf_sym_info, iter, ctf_id_width);
+ iter += ctf_id_width;
+ ushort_t kind = header->cth_version == CTF_VERSION_2 ?
+ CTF_V2_INFO_KIND(ctf_sym_info) :
+ CTF_V3_INFO_KIND(ctf_sym_info);
+ ushort_t n = header->cth_version == CTF_VERSION_2 ?
+ CTF_V2_INFO_VLEN(ctf_sym_info) :
+ CTF_V3_INFO_VLEN(ctf_sym_info);
+
+ uint_t i, arg;
+
+ if (metadata.strdata.data != nullptr)
+ name = info->find_next_symbol_with_type(symidx,
+ STT_FUNC);
+ else
+ name = "";
+
+ if (kind == CTF_K_UNKNOWN && n == 0)
+ continue; /* padding, skip it */
+
+ if (kind != CTF_K_FUNCTION)
+ std::cout << "incorrect type for function: " << name
+ << '\n';
+
+ if (iter + n * ctf_id_width > end)
+ std::cout << "function out of bound: " << name << '\n';
+
+ if (name != "") {
+ /* Return value */
+ std::vector<uint_t> args;
+ memcpy(&arg, iter, ctf_id_width);
+ iter += ctf_id_width;
+ args.push_back(arg);
+
+ for (i = 0; i < n; ++i) {
+ memcpy(&arg, iter, ctf_id_width);
+ iter += ctf_id_width;
+ args.push_back(arg);
+ }
+
+ functions.push_back({ name, std::move(args),
+ static_cast<uint32_t>(id) });
+ } else
+ iter += n * ctf_id_width + 1;
+ }
+
+ return (true);
+}
+
+std::string_view
+CtfData::get_str_from_ref(uint_t ref)
+{
+ size_t offset = CTF_NAME_OFFSET(ref);
+
+ const char *s = reinterpret_cast<const char *>(
+ metadata.ctfdata.data + header->cth_stroff + offset);
+
+ if (CTF_NAME_STID(ref) != CTF_STRTAB_0)
+ return ("<< ??? - name in external strtab >>");
+
+ if (offset >= header->cth_strlen)
+ return ("<< ??? - name exceeds strlab len >>");
+
+ if (header->cth_stroff + offset >= metadata.ctfdata.size)
+ return ("<< ??? - file truncated >>");
+
+ if (s[0] == '\n')
+ return ("(anon)");
+
+ return (s);
+}
+
+#define L_DIFF 0
+#define R_DIFF 1
+
+template <typename Ret, typename T>
+std::pair<std::vector<Ret>, std::vector<Ret>>
+do_diff_generic(const std::vector<T> &lhs, const std::vector<T> &rhs,
+ const std::function<bool(const typename Ret::ty_type &,
+ const typename Ret::ty_type &)> &compare,
+ const std::function<std::optional<typename Ret::ty_type>(
+ const typename T::ty_type &, int LR)> &id_to_syms)
+{
+ size_t l_idx = 0, r_idx = 0;
+ int name_diff;
+ std::vector<Ret> l_diff, r_diff;
+
+ while (l_idx < lhs.size() && r_idx < rhs.size()) {
+ name_diff = lhs[l_idx].name.compare(rhs[r_idx].name);
+
+ if (name_diff < 0) {
+ auto syms = id_to_syms(lhs[l_idx].type, L_DIFF);
+ if (syms != std::nullopt) {
+ std::cout << "< [" << lhs[l_idx].id << "] "
+ << lhs[l_idx].name << '\n';
+ l_diff.push_back(
+ { lhs[l_idx].name, *syms, lhs[l_idx].id });
+ }
+ ++l_idx;
+ } else if (name_diff > 0) {
+ auto syms = id_to_syms(rhs[r_idx].type, R_DIFF);
+ if (syms != std::nullopt) {
+ std::cout << "> [" << rhs[r_idx].id << "] "
+ << rhs[r_idx].name << '\n';
+ r_diff.push_back(
+ { rhs[r_idx].name, *syms, rhs[r_idx].id });
+ }
+ ++r_idx;
+ } else {
+ auto l_syms = id_to_syms(lhs[l_idx].type, L_DIFF);
+ auto r_syms = id_to_syms(rhs[r_idx].type, R_DIFF);
+ bool sym_diff = true;
+
+ /*
+ * TODO: elaborate on detailed compare diff for each
+ * type
+ */
+ if (l_syms != std::nullopt && r_syms != std::nullopt) {
+ sym_diff = !compare(*l_syms, *r_syms);
+ }
+
+ if (sym_diff) {
+ if (l_syms != std::nullopt) {
+ std::cout << "< [" << lhs[l_idx].id
+ << "] " << lhs[l_idx].name
+ << '\n';
+
+ l_diff.push_back({ lhs[l_idx].name,
+ *l_syms, lhs[l_idx].id });
+ }
+ if (r_syms != std::nullopt) {
+ std::cout << "> [" << rhs[r_idx].id
+ << "] " << rhs[r_idx].name
+ << '\n';
+ r_diff.push_back({ rhs[r_idx].name,
+ *r_syms, rhs[r_idx].id });
+ }
+ }
+ ++l_idx;
+ ++r_idx;
+ }
+ }
+
+ while (l_idx < lhs.size()) {
+ auto syms = id_to_syms(lhs[l_idx].type, L_DIFF);
+ if (syms != std::nullopt) {
+ std::cout << "< [" << lhs[l_idx].id << "] "
+ << lhs[l_idx].name << '\n';
+ l_diff.push_back(
+ { lhs[l_idx].name, *syms, lhs[l_idx].id });
+ }
+ ++l_idx;
+ }
+
+ while (l_idx < lhs.size()) {
+ auto syms = id_to_syms(rhs[r_idx].type, R_DIFF);
+ if (syms != std::nullopt) {
+ std::cout << "< [" << rhs[r_idx].id << "] "
+ << rhs[r_idx].name << '\n';
+
+ r_diff.push_back(
+ { rhs[r_idx].name, *syms, rhs[r_idx].id });
+ }
+ ++r_idx;
+ }
+
+ return (std::make_pair(l_diff, r_diff));
+}
+
+std::pair<std::vector<CtfData::CtfFuncTypeEntry>,
+ std::vector<CtfData::CtfFuncTypeEntry>>
+CtfData::do_diff_func(const CtfData &rhs,
+ std::unordered_map<uint64_t, bool> &cache) const
+{
+ const auto &lhs = *this;
+
+ auto get_symbol =
+ [&](const std::vector<uint32_t> &ids,
+ int LR) -> std::optional<std::vector<ShrCtfType>> {
+ std::vector<ShrCtfType> res;
+ const std::unordered_map<uint32_t, ShrCtfType> *converter;
+
+ switch (LR) {
+ case L_DIFF:
+ converter = &(lhs.id_to_types);
+ break;
+ case R_DIFF:
+ converter = &(rhs.id_to_types);
+ break;
+ }
+
+ for (const auto id : ids) {
+ auto iter = converter->find(id);
+ if (iter == converter->end())
+ return (std::nullopt);
+ res.push_back(iter->second);
+ }
+
+ return (std::make_optional(res));
+ };
+
+ auto compare = [&](const std::vector<ShrCtfType> &lhs,
+ const std::vector<ShrCtfType> &rhs) {
+ size_t idx = 0;
+
+ if (lhs.size() != rhs.size())
+ return (false);
+
+ for (; idx < lhs.size(); ++idx) {
+ if (!lhs[idx]->compare(*rhs[idx], cache))
+ return (false);
+ }
+
+ return (true);
+ };
+
+ return do_diff_generic<CtfFuncTypeEntry, CtfFuncIdEntry>(
+ this->functions, rhs.functions, compare, get_symbol);
+}
+
+std::pair<std::vector<CtfData::CtfVarTypeEntry>,
+ std::vector<CtfData::CtfVarTypeEntry>>
+CtfData::do_diff_var(const CtfData &rhs,
+ std::unordered_map<uint64_t, bool> &cache) const
+{
+ const auto &lhs = *this;
+
+ auto get_symbol = [&](const uint32_t &id,
+ int LR) -> std::optional<ShrCtfType> {
+ ShrCtfType res;
+ const std::unordered_map<uint32_t, ShrCtfType> *converter;
+
+ switch (LR) {
+ case L_DIFF:
+ converter = &(lhs.id_to_types);
+ break;
+ case R_DIFF:
+ converter = &(rhs.id_to_types);
+ break;
+ }
+
+ auto iter = converter->find(id);
+ if (iter == converter->end())
+ return (std::nullopt);
+ res = iter->second;
+
+ return (std::make_optional(res));
+ };
+
+ auto compare = [&](const ShrCtfType &lhs, const ShrCtfType &rhs) {
+ if (!lhs->compare(*rhs, cache))
+ return (false);
+
+ return (true);
+ };
+
+ return do_diff_generic<CtfVarTypeEntry, CtfVarIdEntry>(
+ this->static_variables, rhs.static_variables, compare, get_symbol);
+}
+
+/*
+ * cache work as following:
+ * id_pair = lhs.id << 31 | rhs.id
+ * if id_pair found in map, means two types have compared
+ * return the result directly, compare it vice versa
+ */
+std::pair<CtfDiff, CtfDiff>
+CtfData::compare_and_get_diff(const CtfData &rhs) const
+{
+ std::unordered_map<uint64_t, bool> cache;
+ auto [l_diff_funcs, r_diff_funcs] = this->do_diff_func(rhs, cache);
+ auto [l_diff_syms, r_diff_syms] = this->do_diff_var(rhs, cache);
+
+ return std::make_pair(CtfDiff { l_diff_syms, l_diff_funcs },
+ CtfDiff { r_diff_syms, r_diff_funcs });
+}
diff --git a/usr.bin/ctfdiff/ctfdiff.1 b/usr.bin/ctfdiff/ctfdiff.1
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/ctfdiff.1
@@ -0,0 +1,68 @@
+.\"-
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2024 ShengYi Hung <aokblast@FreeBSD.org>
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+
+.Dd December 25, 2024
+.Dt CTFDIFF 1
+.Os
+.Sh NAME
+.Nm ctfdiff
+.Nd compare the SUNW_ctf section of two ELF files
+.Sh SYNOPSIS
+.Nm
+.Op Fl f-ignore-const
+.Fl u Ar file
+file
+.Sh DESCRIPTION
+The
+.Nm
+utility comapre the contents of the CTF (Compact C Type Format) data section
+(SUNW_ctf) present in two ELF binary files.
+This section was previously created with
+.Xr ctfconvert 1
+or
+.Xr ctfmerge 1 .
+.Pp
+The following options are available:
+.Bl -tag -width indent
+.It Fl f-ignore-const
+Show the statistic output by libxo
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh SEE ALSO
+.Xr ctfconvert 1 ,
+.Xr ctfmerge 1 ,
+.Xr ctfdump 1 ,
+.Xr ctf 5
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Fx 15.0 .
+.Sh AUTHORS
+ShengYi Hung <aokblast@FreeBSD.org>
+The CTF utilities came from OpenSolaris.
diff --git a/usr.bin/ctfdiff/ctfdiff.cc b/usr.bin/ctfdiff/ctfdiff.cc
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/ctfdiff.cc
@@ -0,0 +1,94 @@
+#include <getopt.h>
+#include <libelf.h>
+
+#include "sys/elf_common.h"
+
+#include "ctfdata.hpp"
+#include "metadata.hpp"
+#include "utility.hpp"
+#include <iostream>
+
+static struct option longopts[] = {
+ { "f-ignore-const", no_argument, NULL, 'c' }, { NULL, 0, NULL, 0 }
+};
+
+static void
+print_usage()
+{
+ std::cout << "ctfdiff compare the SUNW_ctf section of two ELF files\n";
+ std::cout << "usage: ctfdiff <options> <file1> <file2>\n";
+ std::cout << "options:\n";
+ std::cout << "-f-ignore-const: ignore const decorator";
+}
+
+static void
+do_compare_inplace(const CtfData &lhs, const CtfData &rhs)
+{
+ lhs.compare_and_get_diff(rhs);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *l_filename = nullptr, *r_filename = nullptr;
+
+ (void)elf_version(EV_CURRENT);
+ int c = 0;
+
+ if (argc < 0)
+ exit(EXIT_FAILURE);
+
+ for (opterr = 0; optind < argc; ++optind) {
+ while ((c = getopt_long_only(argc, argv, "c", longopts,
+ NULL)) != (int)EOF) {
+ switch (c) {
+ case 'c':
+ flags |= F_IGNORE_CONST;
+ break;
+ }
+ }
+
+ if (optind < argc) {
+ if (l_filename != nullptr && r_filename != nullptr) {
+ print_usage();
+ return (0);
+ } else if (l_filename == nullptr) {
+ l_filename = argv[optind];
+ } else {
+ r_filename = argv[optind];
+ }
+ }
+ }
+
+ if (l_filename == nullptr || r_filename == nullptr) {
+ print_usage();
+ return (1);
+ }
+
+ CtfMetaData lhs(l_filename);
+
+ if (!lhs.is_available()) {
+ std::cout << "Cannot parse file " << argv[1] << '\n';
+ return (1);
+ }
+
+ CtfMetaData rhs(r_filename);
+
+ if (!rhs.is_available()) {
+ std::cout << "Cannot parse file " << argv[1] << '\n';
+ return (1);
+ }
+
+ auto l_info = CtfData::create_ctf_info(std::move(lhs));
+ if (l_info == nullptr)
+ return (1);
+
+ auto r_info = CtfData::create_ctf_info(std::move(rhs));
+ if (r_info == nullptr)
+ return (1);
+
+ if ((flags & F_IGNORE_CONST) != 0)
+ ignore_ids.push_back(&typeid(CtfTypeConst));
+
+ do_compare_inplace(*l_info.get(), *r_info.get());
+}
diff --git a/usr.bin/ctfdiff/ctftype.hpp b/usr.bin/ctfdiff/ctftype.hpp
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/ctftype.hpp
@@ -0,0 +1,394 @@
+#pragma once
+
+#include <sys/cdefs.h>
+
+#include "ctf_headers.h"
+#include "sys/ctf.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <string_view>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+struct CtfDiff;
+struct CtfData;
+
+using ShrCtfData = std::shared_ptr<CtfData>;
+
+struct ArrayEntry {
+ uint32_t contents, index, nelems;
+};
+
+struct MemberEntry {
+ std::string_view name; /* name of the member */
+ uint32_t type_id; /* type ref of the member */
+ uint64_t offset; /* offset in the member */
+};
+
+struct CtfTypeParser {
+ /* virtual function */
+ virtual ~CtfTypeParser() = default;
+ virtual bool is_root() const = 0;
+ virtual int kind() const = 0;
+ virtual ulong_t vlen() const = 0;
+ virtual uint_t name() const = 0;
+ virtual uint_t type() const = 0;
+ virtual size_t increment() const = 0;
+ virtual size_t size() const = 0;
+ virtual ArrayEntry do_array(const std::byte *bytes) const = 0;
+ virtual std::pair<size_t, std::vector<MemberEntry>>
+ do_struct(const std::byte *,
+ const std::function<std::string_view(uint)> &) const = 0;
+};
+
+struct CtfTypeParser_V2 : CtfTypeParser {
+ private:
+ struct ctf_type_v2 t;
+
+ public:
+ /* virtual function */
+ virtual bool is_root() const override;
+ virtual int kind() const override;
+ virtual ulong_t vlen() const override;
+ virtual uint_t name() const override;
+ virtual uint_t type() const override;
+ virtual size_t increment() const override;
+ virtual size_t size() const override;
+ virtual ArrayEntry do_array(const std::byte *bytes) const override;
+ virtual std::pair<size_t, std::vector<MemberEntry>>
+ do_struct(const std::byte *,
+ const std::function<std::string_view(uint)> &) const override;
+
+ /* static function */
+ static CtfTypeParser *create_symbol(const std::byte *data);
+};
+
+struct CtfTypeParser_V3 : CtfTypeParser {
+ private:
+ struct ctf_type_v3 t;
+
+ public:
+ /* virtual function */
+ virtual bool is_root() const override;
+ virtual int kind() const override;
+ virtual ulong_t vlen() const override;
+ virtual uint_t name() const override;
+ virtual uint_t type() const override;
+ virtual size_t increment() const override;
+ virtual size_t size() const override;
+ virtual ArrayEntry do_array(const std::byte *bytes) const override;
+ virtual std::pair<size_t, std::vector<MemberEntry>>
+ do_struct(const std::byte *,
+ const std::function<std::string_view(uint)> &) const override;
+
+ /* static function */
+ static CtfTypeParser *create_symbol(const std::byte *data);
+};
+
+struct CtfType {
+ protected:
+ using CompareFunc = std::function<bool(const CtfType &lhs,
+ const CtfType &rhs, uint32_t l_child_id, uint32_t r_child_id)>;
+ CtfTypeParser *parser;
+ std::string_view name_str;
+ ShrCtfData owned_ctf;
+ uint32_t id;
+
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp)
+ const = 0; /* virtual function to implement each type comparasion
+ function */
+
+ /* static function */
+ static bool do_compare(const CtfType &lhs, const CtfType &rhs,
+ std::unordered_set<uint64_t> &visited,
+ std::unordered_map<uint64_t, bool>
+ &cache); /* internal function for compare two types */
+ static bool do_compare_child(const CtfType &lhs, const CtfType &rhs,
+ uint32_t l_child_id, uint32_t r_child_id,
+ std::unordered_set<uint64_t> &visited,
+ std::unordered_map<uint64_t, bool> &cache);
+
+ public:
+ /* constructor */
+ CtfType(CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name, ShrCtfData owned_ctf)
+ : parser(parser)
+ , name_str(name)
+ , owned_ctf(owned_ctf)
+ , id(id) {};
+ virtual ~CtfType();
+
+ /* member function */
+ inline const std::string_view &name() const { return name_str; }
+ inline ShrCtfData get_owned() const { return owned_ctf; }
+ bool compare(const CtfType &rhs,
+ std::unordered_map<uint64_t, bool> &cache)
+ const; /* compare two ctftype with type cache */
+};
+
+/*
+ * a dummpy type for va_arg as ctf record it as id 0
+ */
+struct CtfTypeVaArg : CtfType {
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+
+ /* constructor */
+ CtfTypeVaArg(CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfType(parser, id, name, owned_ctf) {};
+ virtual ~CtfTypeVaArg() = default;
+};
+
+struct CtfTypePrimitive : CtfType {
+ protected:
+ /* members */
+ uint32_t data;
+
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+ virtual uint32_t encoding() const = 0;
+ virtual uint32_t offset() const = 0;
+ virtual uint32_t width() const = 0;
+
+ /* constructor */
+ CtfTypePrimitive(uint32_t data, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfType(parser, id, name, owned_ctf)
+ , data(data) {};
+ virtual ~CtfTypePrimitive() = default;
+};
+
+struct CtfTypeInteger : CtfTypePrimitive {
+ /* virtual function */
+ virtual uint32_t encoding() const override;
+ virtual uint32_t offset() const override;
+ virtual uint32_t width() const override;
+
+ /* cosntructor */
+ CtfTypeInteger(uint32_t data, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfTypePrimitive(data, parser, id, name, owned_ctf) {};
+};
+
+struct CtfTypeFloat : CtfTypePrimitive {
+ /* virtual function */
+ virtual uint32_t encoding() const override;
+ virtual uint32_t offset() const override;
+ virtual uint32_t width() const override;
+
+ /* constructor */
+ CtfTypeFloat(uint32_t data, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfTypePrimitive(data, parser, id, name, owned_ctf) {};
+};
+
+struct CtfTypeArray : CtfType {
+ private:
+ /* members */
+ ArrayEntry entry;
+
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+
+ /* constructor */
+ CtfTypeArray(const std::byte *data, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfType(parser, id, name, owned_ctf)
+ , entry(parser->do_array(data)) {};
+
+ /* member function */
+ uint32_t members() const { return entry.nelems; };
+};
+
+struct CtfTypeFunc : CtfType {
+ protected:
+ /* members */
+ uint32_t ret_id;
+ std::vector<uint32_t> args_vec;
+
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+
+ /* constructor */
+ CtfTypeFunc(uint32_t ret_id, std::vector<uint32_t> &&args_vec,
+ CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfType(parser, id, name, owned_ctf)
+ , ret_id(ret_id)
+ , args_vec(args_vec) {};
+
+ /* member function */
+ const std::vector<uint32_t> &args() const { return args_vec; };
+ uint32_t ret() const { return ret_id; };
+};
+
+struct CtfTypeEnum : CtfType {
+ private:
+ /* member */
+ std::vector<std::pair<std::string_view, uint32_t>> members;
+
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+
+ /* constructor */
+ CtfTypeEnum(
+ std::vector<std::pair<std::string_view, uint32_t>> &&members,
+ CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfType(parser, id, name, owned_ctf)
+ , members(members) {};
+};
+
+struct CtfTypeForward : CtfType {
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+
+ /* constructor */
+ CtfTypeForward(CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfType(parser, id, name, owned_ctf) {};
+};
+
+struct CtfTypeQualifier : CtfType {
+ protected:
+ /* members */
+ uint32_t ref_id;
+
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+
+ /* constructor */
+ CtfTypeQualifier(uint32_t ref_id, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfType(parser, id, name, owned_ctf)
+ , ref_id(ref_id) {};
+
+ virtual ~CtfTypeQualifier() = default;
+
+ /* member function */
+ uint32_t ref() const { return this->ref_id; }
+};
+
+struct CtfTypePtr : CtfTypeQualifier {
+ public:
+ /* constructor */
+ CtfTypePtr(uint32_t ref_id, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfTypeQualifier(ref_id, parser, id, name, owned_ctf) {};
+};
+
+struct CtfTypeTypeDef : CtfTypeQualifier {
+ public:
+ /* constructor */
+ CtfTypeTypeDef(uint32_t ref_id, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfTypeQualifier(ref_id, parser, id, name, owned_ctf) {};
+};
+
+struct CtfTypeVolatile : CtfTypeQualifier {
+ public:
+ /* constructor */
+ CtfTypeVolatile(uint32_t ref_id, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfTypeQualifier(ref_id, parser, id, name, owned_ctf) {};
+};
+
+struct CtfTypeConst : CtfTypeQualifier {
+ public:
+ /* constructor */
+ CtfTypeConst(uint32_t ref_id, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfTypeQualifier(ref_id, parser, id, name, owned_ctf) {};
+};
+
+struct CtfTypeRestrict : CtfTypeQualifier {
+ public:
+ /* constructor */
+ CtfTypeRestrict(uint32_t ref_id, CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfTypeQualifier(ref_id, parser, id, name, owned_ctf) {};
+};
+
+struct CtfTypeUnknown : CtfType {
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+
+ /* constructor */
+ CtfTypeUnknown(CtfTypeParser *parser, uint32_t id,
+ ShrCtfData owned_ctf = nullptr)
+ : CtfType(parser, id, "", owned_ctf) {};
+};
+
+struct CtfTypeComplex : CtfType {
+ protected:
+ /* members */
+ uint32_t size;
+ std::vector<MemberEntry> args;
+
+ public:
+ /* constructor */
+ CtfTypeComplex(uint32_t size, std::vector<MemberEntry> &&args,
+ CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfType(parser, id, name, owned_ctf)
+ , size(size)
+ , args(args) {};
+ virtual ~CtfTypeComplex() = default;
+};
+
+struct CtfTypeStruct : CtfTypeComplex {
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+
+ /* constructor */
+ CtfTypeStruct(uint32_t size, std::vector<MemberEntry> &&args,
+ CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfTypeComplex(size,
+ std::forward<std::vector<MemberEntry> &&>(args), parser, id,
+ name, owned_ctf) {};
+};
+
+struct CtfTypeUnion : CtfTypeComplex {
+ public:
+ /* virtual function */
+ virtual bool do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const override;
+
+ /* constructor */
+ CtfTypeUnion(uint32_t size, std::vector<MemberEntry> &&args,
+ CtfTypeParser *parser, uint32_t id,
+ const std::string_view &name = "", ShrCtfData owned_ctf = nullptr)
+ : CtfTypeComplex(size,
+ std::forward<std::vector<MemberEntry> &&>(args), parser, id,
+ name, owned_ctf) {};
+};
+
+using CtfTypeFactory = CtfTypeParser *(*)(const std::byte *);
+using ShrCtfType = std::shared_ptr<CtfType>;
diff --git a/usr.bin/ctfdiff/ctftype.cc b/usr.bin/ctfdiff/ctftype.cc
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/ctftype.cc
@@ -0,0 +1,476 @@
+#include <sys/cdefs.h>
+#include <sys/types.h>
+
+#include "sys/bio.h"
+#include "sys/ctf.h"
+#include "sys/sx.h"
+#include "sys/systm.h"
+
+#include "ctfdata.hpp"
+#include "ctftype.hpp"
+#include "utility.hpp"
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <iostream>
+#include <string_view>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+bool
+CtfTypeParser_V2::is_root() const
+{
+ return (CTF_V2_INFO_ISROOT(t.ctt_info));
+}
+
+int
+CtfTypeParser_V2::kind() const
+{
+ return (CTF_V2_INFO_KIND(t.ctt_info));
+}
+
+ulong_t
+CtfTypeParser_V2::vlen() const
+{
+ return (CTF_V2_INFO_VLEN(t.ctt_info));
+}
+
+uint_t
+CtfTypeParser_V2::name() const
+{
+ return (t.ctt_name);
+}
+
+uint_t
+CtfTypeParser_V2::type() const
+{
+ return (t.ctt_type);
+}
+
+size_t
+CtfTypeParser_V2::increment() const
+{
+ if (t.ctt_size == CTF_V2_LSIZE_SENT) {
+ return (sizeof(ctf_type_v2));
+ } else {
+ return (sizeof(ctf_stype_v2));
+ }
+}
+
+size_t
+CtfTypeParser_V2::size() const
+{
+ if (t.ctt_size == CTF_V2_LSIZE_SENT) {
+ return (sizeof(ctf_type_v2));
+ } else {
+ return (sizeof(ctf_stype_v2));
+ }
+}
+
+ArrayEntry
+CtfTypeParser_V2::do_array(const std::byte *bytes) const
+{
+ const ctf_array_v2 *arr = reinterpret_cast<const ctf_array_v2 *>(bytes);
+ return { arr->cta_contents, arr->cta_index, arr->cta_nelems };
+}
+
+std::pair<size_t, std::vector<MemberEntry>>
+CtfTypeParser_V2::do_struct(const std::byte *bytes,
+ const std::function<std::string_view(uint)> &get_str_by_ref) const
+{
+ std::vector<MemberEntry> res;
+ int n = vlen(), i;
+ size_t size;
+ if (this->size() >= CTF_V2_LSTRUCT_THRESH) {
+ const ctf_lmember_v2 *iter =
+ reinterpret_cast<const ctf_lmember_v2 *>(bytes);
+ for (i = 0; i < n; ++i, ++iter)
+ res.push_back({ get_str_by_ref(iter->ctlm_name),
+ iter->ctlm_type, CTF_LMEM_OFFSET(iter) });
+ size = n * sizeof(struct ctf_lmember_v2);
+ } else {
+ const ctf_member_v2 *iter =
+ reinterpret_cast<const ctf_member_v2 *>(bytes);
+ for (i = 0; i < n; ++i, ++iter)
+ res.push_back({ get_str_by_ref(iter->ctm_name),
+ iter->ctm_type, iter->ctm_offset });
+ size = n * sizeof(struct ctf_member_v2);
+ }
+
+ return { size, res };
+}
+
+CtfTypeParser *
+CtfTypeParser_V2::create_symbol(const std::byte *data)
+{
+ CtfTypeParser_V2 *res = new CtfTypeParser_V2;
+ memcpy(&res->t, data, sizeof(res->t));
+ return (res);
+}
+
+bool
+CtfTypeParser_V3::is_root() const
+{
+ return (CTF_V3_INFO_ISROOT(t.ctt_info));
+}
+
+int
+CtfTypeParser_V3::kind() const
+{
+ return (CTF_V3_INFO_KIND(t.ctt_info));
+}
+
+ulong_t
+CtfTypeParser_V3::vlen() const
+{
+ return (CTF_V3_INFO_VLEN(t.ctt_info));
+}
+
+uint_t
+CtfTypeParser_V3::name() const
+{
+ return (t.ctt_name);
+}
+
+uint_t
+CtfTypeParser_V3::type() const
+{
+ return (t.ctt_type);
+}
+
+size_t
+CtfTypeParser_V3::increment() const
+{
+ if (t.ctt_size == CTF_V3_LSIZE_SENT) {
+ return (sizeof(ctf_type_v3));
+ } else {
+ return (sizeof(ctf_stype_v3));
+ }
+}
+
+size_t
+CtfTypeParser_V3::size() const
+{
+ if (t.ctt_size == CTF_V3_LSIZE_SENT) {
+ return (sizeof(ctf_type_v3));
+ } else {
+ return (sizeof(ctf_stype_v3));
+ }
+}
+
+ArrayEntry
+CtfTypeParser_V3::do_array(const std::byte *bytes) const
+{
+ const ctf_array_v3 *arr = reinterpret_cast<const ctf_array_v3 *>(bytes);
+ return { arr->cta_contents, arr->cta_index, arr->cta_nelems };
+}
+
+std::pair<size_t, std::vector<MemberEntry>>
+CtfTypeParser_V3::do_struct(const std::byte *bytes,
+ const std::function<std::string_view(uint)> &get_str_by_ref) const
+{
+ std::vector<MemberEntry> res;
+ int n = vlen(), i;
+ size_t size;
+ if (this->size() >= CTF_V3_LSTRUCT_THRESH) {
+ const ctf_lmember_v3 *iter =
+ reinterpret_cast<const ctf_lmember_v3 *>(bytes);
+ for (i = 0; i < n; ++i, ++iter)
+ res.push_back({ get_str_by_ref(iter->ctlm_name),
+ iter->ctlm_type, CTF_LMEM_OFFSET(iter) });
+
+ size = n * sizeof(ctf_lmember_v3);
+ } else {
+ const ctf_member_v3 *iter =
+ reinterpret_cast<const ctf_member_v3 *>(bytes);
+ for (i = 0; i < n; ++i, ++iter)
+ res.push_back({ get_str_by_ref(iter->ctm_name),
+ iter->ctm_type, iter->ctm_offset });
+
+ size = n * sizeof(ctf_member_v3);
+ }
+
+ return { size, res };
+}
+
+CtfTypeParser *
+CtfTypeParser_V3::create_symbol(const std::byte *data)
+{
+ CtfTypeParser_V3 *res = new CtfTypeParser_V3;
+ memcpy(&res->t, data, sizeof(res->t));
+ return (res);
+}
+
+bool
+CtfType::compare(const CtfType &rhs,
+ std::unordered_map<uint64_t, bool> &cache) const
+{
+ std::unordered_set<uint64_t> visited;
+
+ return do_compare_child(*this, rhs, this->id, rhs.id, visited, cache);
+}
+
+bool
+CtfType::do_compare(const CtfType &_lhs, const CtfType &_rhs,
+ std::unordered_set<uint64_t> &visited,
+ std::unordered_map<uint64_t, bool> &cache)
+{
+ const CtfType *lhs = &_lhs;
+ const CtfType *rhs = &_rhs;
+
+ auto ignored = [&](const auto &id) {
+ return (std::find(ignore_ids.begin(), ignore_ids.end(), id) !=
+ ignore_ids.end());
+ };
+
+ /* we don't compare the type in ignore list */
+ while (ignored(&typeid(*lhs))) {
+ const CtfTypeQualifier *t =
+ dynamic_cast<const CtfTypeQualifier *>(lhs);
+ lhs =
+ lhs->get_owned()->id_mapper().find(t->ref())->second.get();
+ }
+
+ while (ignored(&typeid(*rhs))) {
+ const CtfTypeQualifier *t =
+ dynamic_cast<const CtfTypeQualifier *>(rhs);
+ rhs =
+ rhs->get_owned()->id_mapper().find(t->ref())->second.get();
+ }
+
+ /* it guarentee all type should be same, so we can cast to specified
+ * cast in each do_compare_impl */
+ if (typeid(*lhs) != typeid(*rhs))
+ return (false);
+
+ /* A type can be mutual refernce so that it will create a circle in the
+ * graph */
+
+ uint64_t visited_pair = lhs->id << 31 | rhs->id;
+
+ bool is_visited = visited.find(visited_pair) != visited.end();
+
+ if (is_visited)
+ return (true);
+
+ if (cache.find(visited_pair) != cache.end())
+ return (cache[visited_pair]);
+
+ visited.insert(visited_pair);
+ bool comp_res = (lhs->do_compare_impl(*rhs,
+ std::bind(CtfType::do_compare_child, std::placeholders::_1,
+ std::placeholders::_2, std::placeholders::_3,
+ std::placeholders::_4, std::ref(visited), std::ref(cache))));
+
+ cache[visited_pair] = comp_res;
+ visited.erase(visited_pair);
+
+ return (comp_res);
+}
+
+bool
+CtfType::do_compare_child(const CtfType &lhs, const CtfType &rhs,
+ uint32_t l_child_id, uint32_t r_child_id,
+ std::unordered_set<uint64_t> &visited,
+ std::unordered_map<uint64_t, bool> &cache)
+{
+ auto &l_id_map = lhs.get_owned()->id_mapper();
+ auto &r_id_map = rhs.get_owned()->id_mapper();
+ auto l_child_iter = l_id_map.find(l_child_id);
+ auto r_child_iter = r_id_map.find(r_child_id);
+
+ /* In CTF, va_args record the ... as type 0, which is not contained in
+ * the id_table */
+ if (l_child_iter == l_id_map.end() || r_child_iter == r_id_map.end())
+ return (false);
+
+ return do_compare(*(l_child_iter->second), *(r_child_iter->second),
+ visited, cache);
+}
+
+CtfType::~CtfType()
+{
+ delete this->parser;
+}
+
+uint32_t
+CtfTypeInteger::encoding() const
+{
+ return (CTF_INT_ENCODING(this->data));
+}
+
+uint32_t
+CtfTypeInteger::offset() const
+{
+ return (CTF_INT_OFFSET(this->data));
+}
+
+uint32_t
+CtfTypeInteger::width() const
+{
+ return (CTF_INT_BITS(this->data));
+}
+
+uint32_t
+CtfTypeFloat::encoding() const
+{
+ return (CTF_FP_ENCODING(this->data));
+}
+
+uint32_t
+CtfTypeFloat::offset() const
+{
+ return (CTF_FP_OFFSET(this->data));
+}
+
+uint32_t
+CtfTypeFloat::width() const
+{
+ return (CTF_FP_BITS(this->data));
+}
+
+bool
+CtfTypeVaArg::do_compare_impl(const CtfType &rhs __unused,
+ const CompareFunc &comp __unused) const
+{
+ return (true);
+}
+
+bool
+CtfTypePrimitive::do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp __unused) const
+{
+ const CtfTypePrimitive *d = dynamic_cast<const CtfTypePrimitive *>(
+ &rhs);
+ return (this->data == d->data);
+}
+
+bool
+CtfTypeArray::do_compare_impl(const CtfType &rhs, const CompareFunc &comp) const
+{
+ const CtfTypeArray *d = dynamic_cast<const CtfTypeArray *>(&rhs);
+ const ArrayEntry &l_ent = this->entry, &r_ent = d->entry;
+
+ return (l_ent.nelems == r_ent.nelems &&
+ comp(*this, rhs, l_ent.index, r_ent.index) &&
+ comp(*this, rhs, l_ent.contents, r_ent.contents));
+}
+
+bool
+CtfTypeFunc::do_compare_impl(const CtfType &rhs, const CompareFunc &comp) const
+{
+ const CtfTypeFunc *d = dynamic_cast<const CtfTypeFunc *>(&rhs);
+
+ if (this->args_vec.size() != d->args_vec.size())
+ return (false);
+
+ if (!comp(*this, rhs, this->ret_id, d->ret_id))
+ return (false);
+
+ int n = d->args_vec.size();
+
+ for (int i = 0; i < n; ++i) {
+ if (!comp(*this, rhs, this->args_vec[i], d->args_vec[i]))
+ return (false);
+ }
+
+ return (true);
+}
+
+bool
+CtfTypeEnum::do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp __unused) const
+{
+ const CtfTypeEnum *d = dynamic_cast<const CtfTypeEnum *>(&rhs);
+
+ if (this->members.size() != d->members.size())
+ return (false);
+
+ int n = d->members.size();
+
+ for (int i = 0; i < n; ++i) {
+ if (this->members[i].first != d->members[i].first ||
+ this->members[i].second != d->members[i].second)
+ return (false);
+ }
+
+ return (true);
+}
+
+bool
+CtfTypeForward::do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp __unused) const
+{
+ return (this->name() == rhs.name());
+}
+
+bool
+CtfTypeQualifier::do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const
+{
+ const CtfTypeQualifier *d = dynamic_cast<const CtfTypeQualifier *>(
+ &rhs);
+
+ return (comp(*this, rhs, this->ref_id, d->ref_id));
+}
+
+bool
+CtfTypeUnknown::do_compare_impl(const CtfType &rhs __unused,
+ const CompareFunc &comp __unused) const
+{
+ /* TODO: add unknown checker */
+ return (false);
+}
+
+bool
+CtfTypeStruct::do_compare_impl(const CtfType &rhs,
+ const CompareFunc &comp) const
+{
+ const CtfTypeStruct *d = dynamic_cast<const CtfTypeStruct *>(&rhs);
+ const auto &l_memb = this->args, &r_memb = d->args;
+
+ if (this->size != d->size)
+ return (false);
+
+ if (l_memb.size() != r_memb.size())
+ return (false);
+
+ int n = l_memb.size(), i;
+
+ for (i = 0; i < n; ++i) {
+ if (l_memb[i].offset != r_memb[i].offset)
+ return (false);
+
+ if (!comp(*this, rhs, l_memb[i].type_id, r_memb[i].type_id))
+ return (false);
+ }
+
+ return (true);
+}
+
+bool
+CtfTypeUnion::do_compare_impl(const CtfType &rhs, const CompareFunc &comp) const
+{
+ const CtfTypeUnion *d = dynamic_cast<const CtfTypeUnion *>(&rhs);
+ const auto &l_memb = this->args, &r_memb = d->args;
+
+ if (this->size != d->size)
+ return (false);
+
+ if (l_memb.size() != r_memb.size())
+ return (false);
+
+ int n = l_memb.size(), i;
+
+ for (i = 0; i < n; ++i) {
+ if (l_memb[i].offset != r_memb[i].offset)
+ return (false);
+
+ if (!comp(*this, rhs, l_memb[i].type_id, r_memb[i].type_id))
+ return (false);
+ }
+
+ return (true);
+}
diff --git a/usr.bin/ctfdiff/metadata.hpp b/usr.bin/ctfdiff/metadata.hpp
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/metadata.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <libelf.h>
+
+#include "utility.hpp"
+#include <string>
+#include <string_view>
+
+struct CtfMetaData {
+ private:
+ int data_fd;
+ std::string filename;
+ Elf *elf;
+
+ bool from_elf_file();
+ bool from_raw_file();
+
+ public:
+ Buffer ctfdata{};
+ Buffer symdata{};
+ Buffer strdata{};
+
+ CtfMetaData(const std::string &filename);
+ ~CtfMetaData();
+
+ std::string_view file_name() { return this->filename; }
+ bool is_available();
+};
diff --git a/usr.bin/ctfdiff/metadata.cc b/usr.bin/ctfdiff/metadata.cc
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/metadata.cc
@@ -0,0 +1,148 @@
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+#include <fcntl.h>
+#include <gelf.h>
+#include <libelf.h>
+#include <unistd.h>
+
+#include "ctfdata.hpp"
+#include "metadata.hpp"
+#include <cstddef>
+#include <iostream>
+
+static Elf_Scn *
+find_section_by_name(Elf *elf, GElf_Ehdr *ehdr, const std::string &sec_name)
+{
+ GElf_Shdr shdr;
+ Elf_Scn *scn = NULL;
+ const char *name;
+
+#define NEXTSCN(elf, scn) elf_nextscn(elf, scn)
+ for (scn = NEXTSCN(elf, scn); scn != NULL; scn = NEXTSCN(elf, scn)) {
+ if (gelf_getshdr(scn, &shdr) != NULL &&
+ (name = elf_strptr(elf, ehdr->e_shstrndx, shdr.sh_name)) &&
+ sec_name == name) {
+ return (scn);
+ }
+ }
+#undef NEXTSCN
+
+ return (nullptr);
+}
+
+bool
+CtfMetaData::from_elf_file()
+{
+ static constexpr char ctfscn_name[] = ".SUNW_ctf";
+ static constexpr char symscn_name[] = ".symtab";
+ GElf_Ehdr ehdr;
+ GElf_Shdr ctfshdr;
+
+ if ((this->elf = elf_begin(this->data_fd, ELF_C_READ, NULL)) == NULL ||
+ gelf_getehdr(elf, &ehdr) == NULL) {
+ return (false);
+ }
+
+ Elf_Scn *ctfscn = find_section_by_name(this->elf, &ehdr, ctfscn_name);
+ Elf_Data *ctfscn_data;
+
+ if (ctfscn == NULL ||
+ (ctfscn_data = elf_getdata(ctfscn, NULL)) == NULL) {
+ std::cout << "Cannot find " << ctfscn_name
+ << " in file: " << this->filename << '\n';
+ return (false);
+ }
+
+ this->ctfdata = Buffer(static_cast<std::byte *>(ctfscn_data->d_buf),
+ ctfscn_data->d_size);
+
+ Elf_Scn *symscn;
+
+ if (gelf_getshdr(ctfscn, &ctfshdr) != NULL && ctfshdr.sh_link != 0)
+ symscn = elf_getscn(elf, ctfshdr.sh_link);
+ else
+ symscn = find_section_by_name(elf, &ehdr, symscn_name);
+
+ if (symscn != NULL) {
+ GElf_Shdr symshdr;
+ Elf_Data *symsecdata;
+ Elf_Data *symstrdata;
+ Elf_Scn *symstrscn;
+
+ if (gelf_getshdr(symscn, &symshdr) != NULL) {
+ symstrscn = elf_getscn(elf, symshdr.sh_link);
+ symsecdata = elf_getdata(symscn, NULL);
+ symstrdata = elf_getdata(symstrscn, NULL);
+
+ this->symdata = Buffer(static_cast<std::byte *>(
+ symsecdata->d_buf),
+ symsecdata->d_size,
+ symshdr.sh_size / symshdr.sh_entsize);
+ this->strdata = Buffer(static_cast<std::byte *>(
+ symstrdata->d_buf),
+ symstrdata->d_size);
+ this->symdata.elfdata = symsecdata;
+ this->strdata.elfdata = symstrdata;
+ }
+ }
+
+ return (true);
+}
+
+bool
+CtfMetaData::from_raw_file()
+{
+ struct stat st;
+ std::byte *bytes;
+
+ if (fstat(this->data_fd, &st) == -1) {
+ std::cout << "Failed to do fstat\n";
+ close(this->data_fd);
+ return (false);
+ }
+
+ bytes = static_cast<std::byte *>(
+ mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, this->data_fd, 0));
+
+ if (bytes == MAP_FAILED) {
+ std::cout << "Failed to do mmap\n";
+ close(this->data_fd);
+ return (false);
+ }
+
+ this->ctfdata = Buffer(bytes, st.st_size);
+ return (true);
+}
+
+CtfMetaData::CtfMetaData(const std::string &filename)
+ : filename(filename)
+{
+ this->data_fd = open(filename.c_str(), O_RDONLY);
+ this->elf = nullptr;
+
+ if (this->data_fd == -1) {
+ return;
+ }
+
+ if (!this->from_elf_file()) {
+ if (!this->from_raw_file()) {
+ close(this->data_fd);
+ this->data_fd = -1;
+ }
+ }
+}
+
+bool
+CtfMetaData::is_available()
+{
+ return (this->data_fd != -1);
+}
+
+CtfMetaData::~CtfMetaData()
+{
+ if (this->elf)
+ elf_end(this->elf);
+ if (this->data_fd != -1)
+ close(this->data_fd);
+}
diff --git a/usr.bin/ctfdiff/utility.hpp b/usr.bin/ctfdiff/utility.hpp
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/utility.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <libelf.h>
+
+#include <cstddef>
+#include <typeinfo>
+#include <vector>
+
+extern int flags;
+extern std::vector<const std::type_info *> ignore_ids;
+
+enum CtfFlag {
+ F_IGNORE_CONST = 1,
+};
+
+struct Buffer {
+ std::byte *data;
+ size_t size;
+ size_t entries;
+ Elf_Data *elfdata;
+
+ Buffer(std::byte *data, size_t size)
+ : data(data)
+ , size(size)
+ , entries(0)
+ , elfdata(nullptr) {};
+ Buffer(std::byte *data, size_t size, size_t entries)
+ : data(data)
+ , size(size)
+ , entries(entries)
+ , elfdata(nullptr) {};
+
+ Buffer() = default;
+};
diff --git a/usr.bin/ctfdiff/utility.cc b/usr.bin/ctfdiff/utility.cc
new file mode 100644
--- /dev/null
+++ b/usr.bin/ctfdiff/utility.cc
@@ -0,0 +1,7 @@
+#include "ctftype.hpp"
+#include "utility.hpp"
+#include <cstdint>
+#include <typeinfo>
+
+int flags = 0;
+std::vector<const std::type_info *> ignore_ids = { &typeid(CtfTypeTypeDef) };

File Metadata

Mime Type
text/plain
Expires
Fri, Jan 10, 5:59 AM (12 h, 28 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
15740003
Default Alt Text
D48199.diff (55 KB)

Event Timeline