Merge pull request #100 from haberman/dwarf-form
Simplified DWARF attribute reading code.
diff --git a/src/dwarf.cc b/src/dwarf.cc
index 51844a0..69843ba 100644
--- a/src/dwarf.cc
+++ b/src/dwarf.cc
@@ -28,6 +28,7 @@
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "bloaty.h"
+#include "bloaty.pb.h"
#include "dwarf_constants.h"
#include "re2/re2.h"
@@ -525,9 +526,6 @@
// Each DIE contains a tag and a set of attribute/value pairs. We rely on the
// abbreviations in an AbbrevTable to decode the DIEs.
-template <class T, class Enable = void>
-class FormReader;
-
class DIEReader {
public:
// Constructs a new DIEReader. Cannot be used until you call one of the
@@ -599,7 +597,7 @@
private:
BLOATY_DISALLOW_COPY_AND_ASSIGN(DIEReader);
- template<typename...> friend class FixedAttrReader;
+ template<typename> friend class AttrReader;
// APIs for our friends to use to update our state.
@@ -777,711 +775,169 @@
}
-// FormReader //////////////////////////////////////////////////////////////////
+// DWARF form parsing //////////////////////////////////////////////////////////
-// A mapping of DWARF "forms" into C++ datatypes, and code to parse an attribute
-// into those C++ types. This is the main parsing code for parsing DIE
-// attributes, and there's a lot going on here because DWARF specifies a lot of
-// forms/encodings with ambiguous/overloaded semantics in some cases.
-//
-// Note that this code is only concerned with mapping DWARF data into C++. It
-// is not concerned with any possible *semantic* differences between the forms.
-// For example, DW_FORM_block and DW_FORM_exprloc both represent delimited
-// sections of the input, so this code treats them identically (both map to
-// string_view) even though DW_FORM_exprloc carries extra semantic meaning about
-// the *interpretation* of those bytes.
-
-// The type of the decoding function yielded from all GetFunctionForForm()
-// functions. The return value indicates the data that remains after we parsed
-// our value out.
-typedef string_view FormDecodeFunc(const DIEReader& reader, string_view data,
- void* val);
-
-// Helper to get decoding function as a function pointer.
-template <class T>
-FormDecodeFunc* GetFormDecodeFunc(uint8_t form, CompilationUnitSizes sizes) {
- FormDecodeFunc* func = nullptr;
- FormReader<T>::GetFunctionForForm(sizes, form, [&func](FormDecodeFunc* f) {
- func = f;
- });
- return func;
-}
-
-template <class Derived>
-class FormReaderBase {
+class AttrValue {
public:
- FormReaderBase(const DIEReader& reader, string_view data)
- : reader_(reader), data_(data) {}
+ AttrValue(uint64_t val) : uint_(val), type_(Type::kUint) {}
+ AttrValue(string_view val) : string_(val), type_(Type::kString) {}
- string_view data() const { return data_; }
-
- protected:
- const DIEReader& reader_;
- string_view data_;
-
- // Function for parsing a specific, known form. This function compiles into
- // extremely tight/optimized code for parsing this specific form into one
- // specific C++ type.
- template <void (Derived::*mf)()>
- static string_view ReadAttr(const DIEReader& reader, string_view data,
- void* val) {
- Derived form_reader(reader, data,
- static_cast<typename Derived::type*>(val));
- (form_reader.*mf)();
- return form_reader.data();
- }
-
- // Function for parsing the "indirect" form, which only gives you the concrete
- // form when you see the data. This compiles into a switch() statement based
- // on the form we parse.
- static string_view ReadIndirect(const DIEReader& reader, string_view data,
- void* value) {
- uint16_t form = ReadLEB128<uint16_t>(&data);
- if (form == DW_FORM_indirect) {
- THROW("indirect attribute has indirect form type");
- }
- Derived::GetFunctionForForm(
- reader.unit_sizes(), form,
- [&](FormDecodeFunc* func) { data = func(reader, data, value); });
- return data;
- }
-};
-
-// FormReader for void. For skipping the data instead of reading it somewhere.
-template <>
-class FormReader<void> : public FormReaderBase<FormReader<void>> {
- public:
- typedef FormReader ME;
- typedef FormReaderBase<ME> Base;
- typedef void type;
- using Base::data_;
-
- FormReader(const DIEReader& reader, string_view data, void* /*val*/)
- : Base(reader, data) {}
-
- template <class Func>
- static void GetFunctionForForm(CompilationUnitSizes sizes, uint8_t form,
- Func func) {
- switch (form) {
- case DW_FORM_flag_present:
- func(&Base::template ReadAttr<&ME::DoNothing>);
- return;
- case DW_FORM_data1:
- case DW_FORM_ref1:
- case DW_FORM_flag:
- func(&Base::template ReadAttr<&ME::SkipFixed<1>>);
- return;
- case DW_FORM_data2:
- case DW_FORM_ref2:
- func(&Base::template ReadAttr<&ME::SkipFixed<2>>);
- return;
- case DW_FORM_data4:
- case DW_FORM_ref4:
- func(&Base::template ReadAttr<&ME::SkipFixed<4>>);
- return;
- case DW_FORM_data8:
- case DW_FORM_ref8:
- case DW_FORM_ref_sig8:
- func(&Base::template ReadAttr<&ME::SkipFixed<8>>);
- return;
- case DW_FORM_addr:
- case DW_FORM_ref_addr:
- if (sizes.address_size == 8) {
- func(&Base::template ReadAttr<&ME::SkipFixed<8>>);
- } else if (sizes.address_size == 4) {
- func(&Base::template ReadAttr<&ME::SkipFixed<4>>);
- } else {
- THROWF("don't know how to skip address size $0", sizes.address_size);
- }
- return;
- case DW_FORM_sec_offset:
- if (sizes.dwarf64) {
- func(&Base::template ReadAttr<&ME::SkipFixed<8>>);
- } else {
- func(&Base::template ReadAttr<&ME::SkipFixed<4>>);
- }
- return;
- case DW_FORM_strp:
- if (sizes.dwarf64) {
- func(&Base::template ReadAttr<&ME::SkipIndirectString<uint64_t>>);
- } else {
- func(&Base::template ReadAttr<&ME::SkipIndirectString<uint32_t>>);
- }
- return;
- case DW_FORM_sdata:
- case DW_FORM_udata:
- case DW_FORM_ref_udata:
- func(&Base::template ReadAttr<&ME::SkipVariable>);
- return;
- case DW_FORM_block1:
- func(&Base::template ReadAttr<&ME::SkipBlock<uint8_t>>);
- return;
- case DW_FORM_block2:
- func(&Base::template ReadAttr<&ME::SkipBlock<uint16_t>>);
- return;
- case DW_FORM_block4:
- func(&Base::template ReadAttr<&ME::SkipBlock<uint32_t>>);
- return;
- case DW_FORM_block:
- case DW_FORM_exprloc:
- func(&Base::template ReadAttr<&ME::SkipVariableBlock>);
- return;
- case DW_FORM_string:
- func(&Base::template ReadAttr<&ME::SkipString>);
- return;
- case DW_FORM_indirect:
- func(&ME::ReadIndirect);
- return;
- default:
- THROWF("don't know how to skip DWARF form $0", form);
- }
- }
-
- private:
- void DoNothing() {}
-
- template <size_t N>
- void SkipFixed() {
- SkipBytes(N, &data_);
- }
-
- void SkipVariable() {
- SkipLEB128(&data_);
- }
-
- template <class D>
- void SkipBlock() {
- D len = ReadMemcpy<D>(&data_);
- SkipBytes(len, &data_);
- }
-
- void SkipVariableBlock() {
- uint64_t len = ReadLEB128<uint64_t>(&data_);
- SkipBytes(len, &data_);
- }
-
- void SkipString() {
- SkipNullTerminated(&data_);
- }
-
- template <class D>
- void SkipIndirectString() {
- D ofs = ReadMemcpy<D>(&data_);
- StringTable table(reader_.dwarf().debug_str);
- string_view str = table.ReadEntry(ofs);
- reader_.AddIndirectString(str);
- }
-};
-
-// FormReader for string_view. We accept the true string forms (DW_FORM_string
-// and DW_FORM_strp) as well as a number of other forms that contain delimited
-// string data. We also accept the generic/opaque DW_FORM_data* types; the
-// string_view can store the uninterpreted data which can then be interpreted by
-// a higher layer.
-template <>
-class FormReader<string_view> : public FormReaderBase<FormReader<string_view>> {
- public:
- typedef FormReader ME;
- typedef FormReaderBase<ME> Base;
- typedef string_view type;
- using Base::data_;
-
- FormReader(const DIEReader& reader, string_view data, string_view* val)
- : Base(reader, data), val_(val) {}
-
- template <class Func>
- static void GetFunctionForForm(CompilationUnitSizes sizes, uint8_t form,
- Func func) {
- switch (form) {
- case DW_FORM_block1:
- func(&ReadAttr<&FormReader::ReadBlock<uint8_t>>);
- return;
- case DW_FORM_block2:
- func(&ReadAttr<&FormReader::ReadBlock<uint16_t>>);
- return;
- case DW_FORM_block4:
- func(&ReadAttr<&FormReader::ReadBlock<uint32_t>>);
- return;
- case DW_FORM_block:
- case DW_FORM_exprloc:
- func(&ReadAttr<&FormReader::ReadVariableBlock>);
- return;
- case DW_FORM_string:
- func(&ReadAttr<&FormReader::ReadString>);
- return;
- case DW_FORM_strp:
- if (sizes.dwarf64) {
- func(&ReadAttr<&FormReader::ReadIndirectString<uint64_t>>);
- } else {
- func(&ReadAttr<&FormReader::ReadIndirectString<uint32_t>>);
- }
- return;
- case DW_FORM_data1:
- func(&ReadAttr<&FormReader::ReadFixed<1>>);
- return;
- case DW_FORM_data2:
- func(&ReadAttr<&FormReader::ReadFixed<2>>);
- return;
- case DW_FORM_data4:
- func(&ReadAttr<&FormReader::ReadFixed<4>>);
- return;
- case DW_FORM_data8:
- func(&ReadAttr<&FormReader::ReadFixed<8>>);
- return;
- case DW_FORM_indirect:
- func(&FormReader::ReadIndirect);
- return;
- default:
- // Skip it.
- FormReader<void>::GetFunctionForForm(sizes, form, func);
- return;
- }
- }
-
- private:
- string_view* val_;
-
- template <size_t N>
- void ReadFixed() {
- *val_ = ReadPiece(N, &data_);
- }
-
- template <class D>
- void ReadBlock() {
- D len = ReadMemcpy<D>(&data_);
- *val_ = ReadPiece(len, &data_);
- }
-
- void ReadVariableBlock() {
- uint64_t len = ReadLEB128<uint64_t>(&data_);
- *val_ = ReadPiece(len, &data_);
- }
-
- void ReadString() {
- *val_ = ReadNullTerminated(&data_);
- }
-
- template <class D>
- void ReadIndirectString() {
- D ofs = ReadMemcpy<D>(&data_);
- StringTable table(reader_.dwarf().debug_str);
- *val_ = table.ReadEntry(ofs);
- reader_.AddIndirectString(*val_);
- }
-};
-
-// FormReader for all integral types. We accept any DW_FORM_data* forms (sign
-// or zero-extending as necessary), as well as the true integer and address
-// types.
-template <class T>
-class FormReader<T, typename std::enable_if<std::is_integral<T>::value>::type>
- : public FormReaderBase<FormReader<T>> {
- public:
- typedef FormReader ME;
- typedef FormReaderBase<ME> Base;
- typedef T type;
- using Base::data_;
-
- FormReader(const DIEReader& reader, string_view data, T* val)
- : Base(reader, data), val_(val) {}
-
- template <class Func>
- static void GetFunctionForForm(CompilationUnitSizes sizes, uint8_t form,
- Func func) {
- switch (form) {
- case DW_FORM_data1:
- case DW_FORM_ref1:
- func(&Base::template ReadAttr<&ME::ReadFixed<int8_t>>);
- return;
- case DW_FORM_data2:
- case DW_FORM_ref2:
- if (sizeof(T) < 2) {
- THROW("can't fit data2/ref2 into this type");
- }
- func(&Base::template ReadAttr<&ME::ReadFixed<int16_t>>);
- return;
- case DW_FORM_data4:
- case DW_FORM_ref4:
- if (sizeof(T) < 4) {
- THROW("can't fit data4/ref4 into this type");
- }
- func(&Base::template ReadAttr<&ME::ReadFixed<int32_t>>);
- return;
- case DW_FORM_data8:
- case DW_FORM_ref8:
- if (sizeof(T) < 8) {
- THROW("can't fit data8/ref8 into this type");
- }
- func(&Base::template ReadAttr<&ME::ReadFixed<int64_t>>);
- return;
- case DW_FORM_addr:
- // We require FORM_addr to be parsed into 8 bytes, since there is always
- // the possibility of running into 64-bit files.
- if (sizeof(T) < 8 || std::is_signed<T>::value) {
- THROW("can't fit addr into this type");
- }
- if (sizes.address_size == 8) {
- func(&Base::template ReadAttr<&ME::ReadFixed<int64_t>>);
- } else if (sizes.address_size == 4) {
- func(&Base::template ReadAttr<&ME::ReadFixed<int32_t>>);
- } else {
- THROWF("don't know how to handle address size: $0",
- sizes.address_size);
- }
- return;
-
- case DW_FORM_sec_offset:
- // We require FORM_addr to be parsed into 8 bytes, since there is always
- // the possibility of running into 64-bit files.
- if (sizeof(T) < 8 || std::is_signed<T>::value) {
- THROW("can't fit sec_offset into this type");
- }
- if (sizes.dwarf64) {
- func(&Base::template ReadAttr<&ME::ReadFixed<int64_t>>);
- } else {
- func(&Base::template ReadAttr<&ME::ReadFixed<int32_t>>);
- }
- return;
- case DW_FORM_sdata:
- if (!std::is_signed<T>::value) {
- THROW("sdata must be parsed into signed type");
- }
- func(&Base::template ReadAttr<&ME::ReadVariable>);
- return;
- case DW_FORM_udata:
- if (std::is_signed<T>::value) {
- THROW("udata must be parsed into unsigned type");
- }
- func(&Base::template ReadAttr<&ME::ReadVariable>);
- return;
- case DW_FORM_indirect:
- func(&Base::ReadIndirect);
- return;
- default:
- // Skip it.
- FormReader<void>::GetFunctionForForm(sizes, form, func);
- return;
- }
- }
-
- private:
- T* val_;
-
- template <class U>
- void ReadFixed() {
- U val = ReadMemcpy<U>(&data_);
- // If the user is reading as signed, then perform sign extension.
- // TODO(haberman): is this the right behavior?
- if (std::is_signed<T>::value) {
- *val_ = static_cast<typename std::make_signed<U>::type>(val);
- } else {
- *val_ = static_cast<typename std::make_unsigned<U>::type>(val);
- }
- }
-
- void ReadVariable() {
- *val_ = ReadLEB128<T>(&data_);
- }
-};
-
-// FormReader for bool. The only types we expect for a bool field are
-// DW_FORM_flag and DW_FORM_flag_present.
-template <>
-class FormReader<bool> : public FormReaderBase<FormReader<bool>> {
- public:
- typedef FormReader ME;
- typedef FormReaderBase<ME> Base;
- typedef bool type;
- using Base::data_;
-
- FormReader(const DIEReader& reader, string_view data, bool* val)
- : Base(reader, data), val_(val) {}
-
- template <class Func>
- static void GetFunctionForForm(const DIEReader& /*reader*/, uint8_t form,
- Func func) {
- switch (form) {
- case DW_FORM_flag:
- func(&Base::template ReadAttr<&FormReader::ReadFlag>);
- return;
- case DW_FORM_flag_present:
- func(&Base::template ReadAttr<&FormReader::ReadFlagPresent>);
- return;
- case DW_FORM_indirect:
- func(&ME::ReadIndirect);
- return;
- default:
- THROWF("don't know how to translate DWARF form $0 into bool", form);
- }
- }
-
- private:
- bool* val_;
-
- void ReadFlag() {
- *val_ = ReadMemcpy<uint8_t>(&data_);
- }
-
- void ReadFlagPresent() {
- *val_ = true;
- }
-};
-
-
-// ActionBuf ///////////////////////////////////////////////////////////////////
-
-// ActionBuf is an optimized list of decoding functions to call (and pointers to
-// where to store the data) when a particular abbreviation is seen. It is used
-// by the attribute readers.
-
-class ActionBuf {
- private:
- struct AttrAction {
- AttrAction(FormDecodeFunc* func_, void* data_, bool* has_)
- : func(func_), data(data_), has(has_) {}
- FormDecodeFunc* func;
- void* data;
- bool* has;
+ enum class Type {
+ kUint,
+ kString
};
- struct IndexedAction {
- IndexedAction(size_t index_, FormDecodeFunc* func_, void* data_, bool* has_)
- : index(index_), action(func_, data_, has_) {}
- size_t index; // The index where this action should go.
- AttrAction action; // The action, but func will be nullptr if invalid.
- };
+ Type type() const { return type_; }
+ bool IsUint() const { return type_ == Type::kUint; }
+ bool IsString() const { return type_ == Type::kString; }
- public:
- // Build a list of actions to perform for the given abbreviation in a
- // compilation unit with the given sizes. Any attributes you want to parse
- // should be listed in "actions" (which came from calling GetAction()).
- ActionBuf(const AbbrevTable::Abbrev& abbrev, CompilationUnitSizes sizes,
- std::initializer_list<IndexedAction> actions);
+ uint64_t GetUint() const {
+ assert(type_ == Type::kUint);
+ return uint_;
+ }
- // For the given |attr_name| and destination type |T|, destination data |data|
- // and |has| bool locations, returns an action suitable for passing to the
- // ActionBuf constructor.
- template <class T>
- static IndexedAction GetAction(uint16_t attr_name,
- const AbbrevTable::Abbrev& abbrev,
- CompilationUnitSizes sizes, void* data,
- bool* has);
-
- string_view ReadAttributes(const DIEReader& reader, string_view data) const;
+ string_view GetString() const {
+ assert(type_ == Type::kString);
+ return string_;
+ }
private:
- std::vector<AttrAction> action_list_;
+ union {
+ uint64_t uint_;
+ string_view string_;
+ };
+
+ Type type_;
};
-ActionBuf::ActionBuf(const AbbrevTable::Abbrev& abbrev,
- CompilationUnitSizes sizes,
- std::initializer_list<IndexedAction> indexed_actions) {
- // Initialize list with functions that will just skip the fields.
- for (size_t i = 0; i < abbrev.attr.size(); i++) {
- const auto& attr = abbrev.attr[i];
- auto func = GetFormDecodeFunc<void>(attr.form, sizes);
+template <class D>
+string_view ReadBlock(string_view* data) {
+ D len = ReadMemcpy<D>(data);
+ return ReadPiece(len, data);
+}
- if (!func) {
- THROWF("don't know how to skip DWARF form $0", attr.form);
- }
+string_view ReadVariableBlock(string_view* data) {
+ uint64_t len = ReadLEB128<uint64_t>(data);
+ return ReadPiece(len, data);
+}
- action_list_.push_back(AttrAction(func, nullptr, nullptr));
- }
+template <class D>
+string_view ReadIndirectString(const DIEReader& reader, string_view* data) {
+ D ofs = ReadMemcpy<D>(data);
+ StringTable table(reader.dwarf().debug_str);
+ string_view ret = table.ReadEntry(ofs);
+ reader.AddIndirectString(ret);
+ return ret;
+}
- // Overwrite any entries for attributes we actually want to store somewhere.
- for (const auto& action : indexed_actions) {
- const auto& attr = abbrev.attr[action.index];
- if (action.action.func &&
- action.action.func != GetFormDecodeFunc<void>(attr.form, sizes)) {
- assert(action.index < action_list_.size());
- if (action_list_[action.index].data) {
- THROW(
- "internal error, specified same DWARF attribute more "
- "than once");
+AttrValue ParseAttr(const DIEReader& reader, uint8_t form, string_view* data) {
+ switch (form) {
+ case DW_FORM_indirect: {
+ uint16_t indirect_form = ReadLEB128<uint16_t>(data);
+ if (indirect_form == DW_FORM_indirect) {
+ THROW("indirect attribute has indirect form type");
}
- action_list_[action.index] = action.action;
+ return ParseAttr(reader, indirect_form, data);
}
+ case DW_FORM_ref1:
+ return AttrValue(ReadMemcpy<uint8_t>(data));
+ case DW_FORM_ref2:
+ return AttrValue(ReadMemcpy<uint16_t>(data));
+ case DW_FORM_ref4:
+ return AttrValue(ReadMemcpy<uint32_t>(data));
+ case DW_FORM_ref8:
+ return AttrValue(ReadMemcpy<uint64_t>(data));
+ case DW_FORM_addr:
+ case DW_FORM_ref_addr:
+ switch (reader.unit_sizes().address_size) {
+ case 4:
+ return AttrValue(ReadMemcpy<uint32_t>(data));
+ case 8:
+ return AttrValue(ReadMemcpy<uint64_t>(data));
+ default:
+ BLOATY_UNREACHABLE();
+ }
+ case DW_FORM_sec_offset:
+ if (reader.unit_sizes().dwarf64) {
+ return AttrValue(ReadMemcpy<uint64_t>(data));
+ } else {
+ return AttrValue(ReadMemcpy<uint32_t>(data));
+ }
+ case DW_FORM_udata:
+ return AttrValue(ReadLEB128<uint64_t>(data));
+ case DW_FORM_block1:
+ return AttrValue(ReadBlock<uint8_t>(data));
+ case DW_FORM_block2:
+ return AttrValue(ReadBlock<uint16_t>(data));
+ case DW_FORM_block4:
+ return AttrValue(ReadBlock<uint32_t>(data));
+ case DW_FORM_block:
+ case DW_FORM_exprloc:
+ return AttrValue(ReadVariableBlock(data));
+ case DW_FORM_string:
+ return AttrValue(ReadNullTerminated(data));
+ case DW_FORM_strp:
+ if (reader.unit_sizes().dwarf64) {
+ return AttrValue(ReadIndirectString<uint64_t>(reader, data));
+ } else {
+ return AttrValue(ReadIndirectString<uint32_t>(reader, data));
+ }
+ case DW_FORM_data1:
+ return AttrValue(ReadPiece(1, data));
+ case DW_FORM_data2:
+ return AttrValue(ReadPiece(2, data));
+ case DW_FORM_data4:
+ return AttrValue(ReadPiece(4, data));
+ case DW_FORM_data8:
+ return AttrValue(ReadPiece(8, data));
+
+ // Bloaty doesn't currently care about any bool or signed data.
+ // So we fudge it a bit and just stuff these in a uint64.
+ case DW_FORM_flag_present:
+ return AttrValue(1);
+ case DW_FORM_flag:
+ return AttrValue(ReadMemcpy<uint8_t>(data));
+ case DW_FORM_sdata:
+ return AttrValue(ReadLEB128<uint64_t>(data));
+ default:
+ THROWF("Don't know how to parse DWARF form: $0", form);
}
}
+
+// AttrReader //////////////////////////////////////////////////////////////////
+
+// Parses a DIE's attributes, calling user callbacks with the parsed values.
+
template <class T>
-ActionBuf::IndexedAction ActionBuf::GetAction(uint16_t attr_name,
- const AbbrevTable::Abbrev& abbrev,
- CompilationUnitSizes sizes,
- void* data, bool* has) {
- for (size_t i = 0; i < abbrev.attr.size(); i++) {
- if (attr_name == abbrev.attr[i].name) {
- FormDecodeFunc* func = GetFormDecodeFunc<T>(abbrev.attr[i].form, sizes);
-
- if (!func) {
- THROWF("don't know how to convert form $0 to type $1",
- abbrev.attr[i].form, typeid(T).name());
- }
-
- return IndexedAction(i, func, data, has);
- }
- }
-
- // This attribute doesn't occur.
- return IndexedAction(0, nullptr, nullptr, nullptr);
-}
-
-// The fast path function that reads all attributes by simply calling a list of
-// function pointers to super-specialized functions.
-string_view ActionBuf::ReadAttributes(const DIEReader& reader,
- string_view data) const {
- for (const auto& action : action_list_) {
- data = action.func(reader, data, action.data);
- if (action.has) {
- *action.has = true;
- }
- }
- return data;
-}
-
-
-// FixedAttrReader /////////////////////////////////////////////////////////////
-
-// Parses a DIE's attributes into a tuple of values. The user specifies the
-// attributes they are expecting to see and the C++ types they want to parse
-// into. Any attributes that we don't list, or that have a type that doesn't
-// fit our expected type, are skipped/ignored. This is more convenient and more
-// efficient than parsing all attributes into a generic representation and then
-// selecting/converting them in a second phase.
-//
-// For the moment we don't distinguish between "data was not present" and "data
-// was present but in a bad form."
-
-template <class... Args>
-class FixedAttrReader {
+class AttrReader {
public:
- typedef std::tuple<Args...> ValueTuple;
+ typedef void CallbackFunc(T* container, AttrValue val);
- // Constructs a decoder for the given attributes. We will accept any
- // attribute forms that can decode to our target types (the template params on
- // this class). If we want to be more restrictive about this later, we could
- // let users specify that only certain forms should be allowed.
- template <size_t N>
- FixedAttrReader(DIEReader* /*reader*/, const DwarfAttribute (&attributes)[N]) {
- static_assert(N == sizeof...(Args), "must match number of template params");
- std::copy(std::begin(attributes), std::end(attributes),
- std::begin(attributes_));
- }
-
- FixedAttrReader(DIEReader* /*reader*/,
- std::initializer_list<DwarfAttribute> attributes) {
- assert(attributes.size() == sizeof...(Args));
- std::copy(std::begin(attributes), std::end(attributes),
- std::begin(attributes_));
+ void OnAttribute(DwarfAttribute attr, CallbackFunc* func) {
+ attributes_[attr] = func;
}
// Reads all attributes for this DIE, storing the ones we were expecting.
- void ReadAttributes(DIEReader* reader) {
+ void ReadAttributes(DIEReader* reader, T* container) {
string_view data = reader->ReadAttributesBegin();
+ const AbbrevTable::Abbrev& abbrev = reader->GetAbbrev();
- // Clear all existing attributes.
- values_ = std::tuple<Args...>();
- memset(&has_attr_, 0, sizeof...(Args));
+ for (auto attr : abbrev.attr) {
+ AttrValue value = ParseAttr(*reader, attr.form, &data);
+ auto it = attributes_.find(attr.name);
+ if (it != attributes_.end()) {
+ it->second(container, value);
+ }
+ }
- // Parse all attributes.
- sibling_ = 0;
- data = GetActionBuf(*reader).ReadAttributes(*reader, data);
- reader->ReadAttributesEnd(data, sibling_);
+ reader->ReadAttributesEnd(data, 0);
}
- template <size_t N>
- bool HasAttribute() const {
- static_assert(N < sizeof...(Args), "index too large");
- return has_attr_[N];
- }
-
- template <size_t N>
- typename std::tuple_element<N, ValueTuple>::type GetAttribute() const {
- return std::get<N>(values_);
- }
-
- const ValueTuple& values() const { return values_; }
-
private:
- static const size_t kCount = sizeof...(Args);
-
- // Template to generate a compile-time sequence of integers, so we can do
- // "foreach element in the tuple".
- //
- // With C++14 we'll be able to use simple std:index_sequence instead of these
- // custom sequence-making templates.
- template <size_t... Indexes>
- struct IndexSequence {};
-
- template <size_t N, size_t... Indexes>
- struct MakeIndexSequence : MakeIndexSequence<N - 1, N - 1, Indexes...> {};
-
- template <size_t... Indexes>
- struct MakeIndexSequence<0, Indexes...> : IndexSequence<Indexes...> {};
-
- // Keyed by abbrev code, this stores a list of attribute actions and
- // associated data pointers.
- typedef std::unordered_map<uint32_t, ActionBuf> AbbrevCodeMap;
-
- const ActionBuf& GetActionBuf(const DIEReader& reader) {
- if (actions_.size() <= reader.abbrev_version()) {
- actions_.resize(reader.abbrev_version() + 1);
- }
-
- auto code = reader.GetAbbrev().code;
- auto& map = actions_[reader.abbrev_version()];
- auto it = map.find(code);
- auto sizes = reader.unit_sizes();
-
- if (it == map.end()) {
- return BuildActionBuf(reader.GetAbbrev(), sizes,
- MakeIndexSequence<sizeof...(Args)>(), &map);
- } else {
- return it->second;
- }
- }
-
- template <size_t... I>
- const ActionBuf& BuildActionBuf(const AbbrevTable::Abbrev& abbrev,
- CompilationUnitSizes sizes,
- IndexSequence<I...>, AbbrevCodeMap* map);
-
- // Specifies for each attribute whether it was present or not.
- bool has_attr_[sizeof...(Args)];
-
- // The set of attributes we are expecting.
- DwarfAttribute attributes_[sizeof...(Args)];
-
- // The slots where we store the values we have parsed.
- ValueTuple values_;
-
- // We always store the sibling if we see one.
- uint64_t sibling_;
-
- // Indexed by DIEReader::abbrev_version(), so we have a different code map
- // when the abbreviation table or compilation unit sizes change.
- std::vector<AbbrevCodeMap> actions_;
+ std::unordered_map<int, CallbackFunc*> attributes_;
};
-template <class... Args>
-template <size_t... I>
-const ActionBuf& FixedAttrReader<Args...>::BuildActionBuf(
- const AbbrevTable::Abbrev& abbrev, CompilationUnitSizes sizes,
- FixedAttrReader<Args...>::IndexSequence<I...>, AbbrevCodeMap* map) {
- auto actions = {
- ActionBuf::GetAction<Args>(attributes_[I], abbrev, sizes,
- &std::get<I>(values_), &has_attr_[I])...,
- ActionBuf::GetAction<uint64_t>(DW_AT_sibling, abbrev, sizes, &sibling_,
- nullptr)};
- auto pair =
- map->emplace(std::piecewise_construct, std::make_tuple(abbrev.code),
- std::make_tuple(abbrev, sizes, actions));
-
- // Must have inserted.
- assert(pair.second);
- return pair.first->second;
-}
-
// From DIEReader, defined here because it depends on FixedAttrReader.
bool DIEReader::SkipChildren() {
assert(state_ == State::kReadyToNext);
@@ -1490,19 +946,18 @@
}
int target_depth = depth_ - 1;
- dwarf::FixedAttrReader<uint64_t> attr_reader(this, {DW_AT_sibling});
+ dwarf::AttrReader<void> attr_reader;
while (depth_ > target_depth) {
// TODO(haberman): use DW_AT_sibling to optimize skipping when it is
// available.
if (!NextDIE()) {
return false;
}
- attr_reader.ReadAttributes(this);
+ attr_reader.ReadAttributes(this, nullptr);
}
return true;
}
-
// LineInfoReader //////////////////////////////////////////////////////////////
// Code to read the .line_info programs in a DWARF file.
@@ -1585,12 +1040,12 @@
string_view remaining_;
- // Whether we are in a "shadow" part of the bytecode program. Sometimes parts
- // of the line info program make it into the final binary even though the
- // corresponding code was stripped. We can tell when this happened by looking
- // for DW_LNE_set_address ops where the operand is 0. This indicates that a
- // relocation for that argument never got applied, which probably means that
- // the code got stripped.
+ // Whether we are in a "shadow" part of the bytecode program. Sometimes
+ // parts of the line info program make it into the final binary even though
+ // the corresponding code was stripped. We can tell when this happened by
+ // looking for DW_LNE_set_address ops where the operand is 0. This
+ // indicates that a relocation for that argument never got applied, which
+ // probably means that the code got stripped.
//
// While this is true, we don't yield any LineInfo entries, because the
// "address" value is garbage.
@@ -1606,8 +1061,8 @@
void Advance(uint64_t amount) {
if (params_.maximum_operations_per_instruction == 1) {
- // This is by far the common case (only false on VLIW architectuers), and
- // this inlining/specialization avoids a costly division.
+ // This is by far the common case (only false on VLIW architectuers),
+ // and this inlining/specialization avoids a costly division.
DoAdvance(amount, 1);
} else {
DoAdvance(amount, params_.maximum_operations_per_instruction);
@@ -1828,8 +1283,7 @@
}
}
-} // namespace dwarf
-
+} // namespace dwarf
// Bloaty DWARF Data Sources ///////////////////////////////////////////////////
@@ -1845,8 +1299,13 @@
public:
FilenameMap(const dwarf::File& file)
: die_reader_(file),
- attr_reader_(&die_reader_, {DW_AT_name}),
- missing_("[DWARF is missing filename]") {}
+ missing_("[DWARF is missing filename]") {
+ attr_reader_.OnAttribute(
+ DW_AT_name, [](string_view* s, dwarf::AttrValue data) {
+ if (!data.IsString()) return;
+ *s = data.GetString();
+ });
+ }
std::string GetFilename(uint64_t compilation_unit_offset) {
auto& name = map_[compilation_unit_offset];
@@ -1859,18 +1318,19 @@
private:
std::string LookupFilename(uint64_t compilation_unit_offset) {
auto section = dwarf::DIEReader::Section::kDebugInfo;
+ string_view name;
if (die_reader_.SeekToCompilationUnit(section, compilation_unit_offset) &&
die_reader_.GetTag() == DW_TAG_compile_unit &&
- (attr_reader_.ReadAttributes(&die_reader_),
- attr_reader_.HasAttribute<0>())) {
- return std::string(attr_reader_.GetAttribute<0>());
+ (attr_reader_.ReadAttributes(&die_reader_, &name),
+ !name.empty())) {
+ return std::string(name);
} else {
return missing_;
}
}
dwarf::DIEReader die_reader_;
- dwarf::FixedAttrReader<string_view> attr_reader_;
+ dwarf::AttrReader<string_view> attr_reader_;
std::unordered_map<uint64_t, std::string> map_;
std::string missing_;
} map(file);
@@ -1891,30 +1351,127 @@
return true;
}
-void AddDIE(const dwarf::File& file, const std::string& name,
- const dwarf::FixedAttrReader<string_view, string_view, uint64_t,
- uint64_t, string_view, uint64_t,
- uint64_t, uint64_t, uint64_t>& attr,
- const SymbolTable& symtab, const DualMap& symbol_map,
- const dwarf::CompilationUnitSizes& sizes, RangeSink* sink) {
- uint64_t low_pc = attr.GetAttribute<2>();
- uint64_t high_pc = attr.GetAttribute<3>();
+// TODO(haberman): make these into real protobufs once proto supports
+// string_view.
+class GeneralDIE {
+ public:
+ bool has_name() const { return has_name_; }
+ bool has_linkage_name() const { return has_linkage_name_; }
+ bool has_location_string() const { return has_location_string_; }
+ bool has_low_pc() const { return has_low_pc_; }
+ bool has_high_pc() const { return has_high_pc_; }
+ bool has_location_uint64() const { return has_location_uint64_; }
+ bool has_stmt_list() const { return has_stmt_list_; }
+ bool has_ranges() const { return has_ranges_; }
+ bool has_start_scope() const { return has_start_scope_; }
+ string_view name() const { return name_; }
+ string_view linkage_name() const { return linkage_name_; }
+ string_view location_string() const { return location_string_; }
+ uint64_t low_pc() const { return low_pc_; }
+ uint64_t high_pc() const { return high_pc_; }
+ uint64_t location_uint64() const { return location_uint64_; }
+ uint64_t stmt_list() const { return stmt_list_; }
+ uint64_t ranges() const { return ranges_; }
+ uint64_t start_scope() const { return start_scope_; }
+
+ void set_name(string_view val) {
+ has_name_ = true;
+ name_ = val;
+ }
+ void set_linkage_name(string_view val) {
+ has_linkage_name_ = true;
+ location_string_ = val;
+ }
+ void set_location_string(string_view val) {
+ has_location_string_ = true;
+ location_string_ = val;
+ }
+ void set_low_pc(uint64_t val) {
+ has_low_pc_ = true;
+ low_pc_ = val;
+ }
+ void set_high_pc(uint64_t val) {
+ has_high_pc_ = true;
+ high_pc_ = val;
+ }
+ void set_location_uint64(uint64_t val) {
+ has_location_uint64_ = true;
+ location_uint64_ = val;
+ }
+ void set_stmt_list(uint64_t val) {
+ has_stmt_list_ = true;
+ stmt_list_ = val;
+ }
+ void set_ranges(uint64_t val) {
+ has_ranges_ = true;
+ ranges_ = val;
+ }
+ void set_start_scope(uint64_t val) {
+ has_start_scope_ = true;
+ start_scope_ = val;
+ }
+
+ private:
+ bool has_name_ = false;
+ bool has_linkage_name_ = false;
+ bool has_location_string_ = false;
+ bool has_low_pc_ = false;
+ bool has_high_pc_ = false;
+ bool has_location_uint64_ = false;
+ bool has_stmt_list_ = false;
+ bool has_ranges_ = false;
+ bool has_start_scope_ = false;
+
+ string_view name_;
+ string_view linkage_name_;
+ string_view location_string_;
+ uint64_t low_pc_ = 0;
+ uint64_t high_pc_ = 0;
+ uint64_t location_uint64_ = 0;
+ uint64_t stmt_list_ = 0;
+ uint64_t ranges_ = 0;
+ uint64_t start_scope_ = 0;
+};
+
+class InlinesDIE {
+ public:
+ bool has_stmt_list() const { return has_stmt_list_; }
+
+ uint64_t stmt_list() const { return stmt_list_; }
+
+ void set_stmt_list(uint64_t val) {
+ has_stmt_list_ = true;
+ stmt_list_ = val;
+ }
+
+ private:
+ bool has_stmt_list_ = false;
+ uint64_t stmt_list_ = 0;
+};
+
+void AddDIE(const dwarf::File& file, const std::string& name,
+ const GeneralDIE& die, const SymbolTable& symtab,
+ const DualMap& symbol_map, const dwarf::CompilationUnitSizes& sizes,
+ RangeSink* sink) {
// Some DIEs mark address ranges with high_pc/low_pc pairs (especially
// functions).
- if (attr.HasAttribute<2>() && attr.HasAttribute<3>() && low_pc != 0) {
+ if (die.has_low_pc() && die.has_high_pc() && die.low_pc() != 0) {
+ uint64_t high_pc = die.high_pc();
+
// It appears that some compilers make high_pc a size, and others make it an
// address.
- if (high_pc >= low_pc) {
- high_pc -= low_pc;
+ if (high_pc >= die.low_pc()) {
+ high_pc -= die.low_pc();
}
- sink->AddVMRangeIgnoreDuplicate("dwarf_pcpair", low_pc, high_pc, name);
+ sink->AddVMRangeIgnoreDuplicate("dwarf_pcpair", die.low_pc(), high_pc,
+ name);
}
// Sometimes a DIE has a linkage_name, which we can look up in the symbol
// table.
- if (attr.HasAttribute<1>()) {
- auto it = symtab.find(attr.GetAttribute<1>());
+ if (die.has_linkage_name()) {
+ auto it = symtab.find(die.linkage_name());
if (it != symtab.end()) {
sink->AddVMRangeIgnoreDuplicate("dwarf_linkagename", it->second.first,
it->second.second, name);
@@ -1923,8 +1480,8 @@
// Sometimes the DIE has a "location", which gives the location as an address.
// This parses a very small subset of the overall DWARF expression grammar.
- if (attr.HasAttribute<4>()) {
- string_view location = attr.GetAttribute<4>();
+ if (die.has_location_string()) {
+ string_view location = die.location_string();
if (location.size() == sizes.address_size + 1 &&
location[0] == DW_OP_addr) {
location.remove_prefix(1);
@@ -1954,18 +1511,21 @@
}
}
- if (attr.HasAttribute<5>()) {
- absl::string_view loc_range = file.debug_loc.substr(attr.GetAttribute<5>());
+ // Sometimes a location is given as an offset into debug_loc.
+ if (die.has_location_uint64()) {
+ absl::string_view loc_range = file.debug_loc.substr(die.location_uint64());
loc_range = GetLocationListRange(sizes, loc_range);
sink->AddFileRange("dwarf_locrange", name, loc_range);
}
uint64_t ranges_offset = UINT64_MAX;
- if (attr.HasAttribute<7>()) {
- ranges_offset = attr.GetAttribute<7>();
- } else if (attr.HasAttribute<8>()) {
- ranges_offset = attr.GetAttribute<8>();
+ // There are two different attributes that sometimes contain an offset into
+ // debug_ranges.
+ if (die.has_ranges()) {
+ ranges_offset = die.ranges();
+ } else if (die.has_start_scope()) {
+ ranges_offset = die.start_scope();
}
if (ranges_offset != UINT64_MAX) {
@@ -1978,9 +1538,16 @@
static void ReadDWARFPubNames(const dwarf::File& file, string_view section,
RangeSink* sink) {
dwarf::DIEReader die_reader(file);
- dwarf::FixedAttrReader<string_view> attr_reader(&die_reader, {DW_AT_name});
+ dwarf::AttrReader<string_view> attr_reader;
string_view remaining = section;
+ attr_reader.OnAttribute(
+ DW_AT_name, [](string_view* s, dwarf::AttrValue data) {
+ if (data.type() == dwarf::AttrValue::Type::kString) {
+ *s = data.GetString();
+ }
+ });
+
while (remaining.size() > 0) {
dwarf::CompilationUnitSizes sizes;
string_view full_unit = remaining;
@@ -1994,8 +1561,8 @@
if (!ok) {
THROW("Couldn't seek to debug_info section");
}
- attr_reader.ReadAttributes(&die_reader);
- std::string compileunit_name = std::string(attr_reader.GetAttribute<0>());
+ string_view compileunit_name;
+ attr_reader.ReadAttributes(&die_reader, &compileunit_name);
if (!compileunit_name.empty()) {
sink->AddFileRange("dwarf_pubnames", compileunit_name, full_unit);
}
@@ -2005,7 +1572,7 @@
uint64_t ReadEncodedPointer(uint8_t encoding, bool is_64bit, string_view* data,
const char* data_base, RangeSink* sink) {
uint64_t value;
- const char *ptr = data->data();
+ const char* ptr = data->data();
uint8_t format = encoding & DW_EH_PE_FORMAT_MASK;
switch (format) {
@@ -2048,7 +1615,7 @@
uint8_t application = encoding & DW_EH_PE_APPLICATION_MASK;
- switch(application) {
+ switch (application) {
case 0:
break;
case DW_EH_PE_pcrel:
@@ -2084,7 +1651,8 @@
//
// The best documentation I can find for this format comes from:
//
-// * http://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html
+// *
+// http://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html
// * https://www.airs.com/blog/archives/460
//
// However these are both under-specified. Some details are not mentioned in
@@ -2259,23 +1827,63 @@
std::unordered_map<uint64_t, std::string>* stmt_list_map) {
dwarf::DIEReader die_reader(file);
die_reader.set_strp_sink(sink);
- dwarf::FixedAttrReader<string_view, string_view, uint64_t, uint64_t,
- string_view, uint64_t, uint64_t, uint64_t, uint64_t>
- attr_reader(&die_reader,
- {DW_AT_name, DW_AT_linkage_name, DW_AT_low_pc, DW_AT_high_pc,
- DW_AT_location, DW_AT_location, DW_AT_stmt_list,
- DW_AT_ranges, DW_AT_start_scope});
+ dwarf::AttrReader<GeneralDIE> attr_reader;
+
+ attr_reader.OnAttribute(DW_AT_name,
+ [](GeneralDIE* die, dwarf::AttrValue val) {
+ if (!val.IsString()) return;
+ die->set_name(val.GetString());
+ });
+ attr_reader.OnAttribute(DW_AT_linkage_name,
+ [](GeneralDIE* die, dwarf::AttrValue val) {
+ if (!val.IsString()) return;
+ die->set_linkage_name(val.GetString());
+ });
+ attr_reader.OnAttribute(DW_AT_location,
+ [](GeneralDIE* die, dwarf::AttrValue val) {
+ if (val.IsString()) {
+ die->set_location_string(val.GetString());
+ } else {
+ die->set_location_uint64(val.GetUint());
+ }
+ });
+ attr_reader.OnAttribute(DW_AT_low_pc,
+ [](GeneralDIE* die, dwarf::AttrValue val) {
+ if (!val.IsUint()) return;
+ die->set_low_pc(val.GetUint());
+ });
+ attr_reader.OnAttribute(DW_AT_high_pc,
+ [](GeneralDIE* die, dwarf::AttrValue val) {
+ if (!val.IsUint()) return;
+ die->set_high_pc(val.GetUint());
+ });
+ attr_reader.OnAttribute(DW_AT_stmt_list,
+ [](GeneralDIE* die, dwarf::AttrValue val) {
+ if (!val.IsUint()) return;
+ die->set_stmt_list(val.GetUint());
+ });
+ attr_reader.OnAttribute(DW_AT_ranges,
+ [](GeneralDIE* die, dwarf::AttrValue val) {
+ if (!val.IsUint()) return;
+ die->set_ranges(val.GetUint());
+ });
+ attr_reader.OnAttribute(DW_AT_start_scope,
+ [](GeneralDIE* die, dwarf::AttrValue val) {
+ if (!val.IsUint()) return;
+ die->set_start_scope(val.GetUint());
+ });
if (!die_reader.SeekToStart(section)) {
return;
}
do {
- attr_reader.ReadAttributes(&die_reader);
- std::string compileunit_name = std::string(attr_reader.GetAttribute<0>());
+ GeneralDIE compileunit_die;
+ attr_reader.ReadAttributes(&die_reader, &compileunit_die);
+ std::string compileunit_name = std::string(compileunit_die.name());
- if (attr_reader.HasAttribute<6>()) {
- uint64_t stmt_list = attr_reader.GetAttribute<6>();
+ if (compileunit_die.has_stmt_list()) {
+ uint64_t stmt_list = compileunit_die.stmt_list();
if (compileunit_name.empty()) {
auto iter = stmt_list_map->find(stmt_list);
if (iter != stmt_list_map->end()) {
@@ -2293,11 +1901,11 @@
die_reader.set_compileunit_name(compileunit_name);
sink->AddFileRange("dwarf_debuginfo", compileunit_name,
die_reader.unit_range());
- AddDIE(file, compileunit_name, attr_reader, symtab, symbol_map,
+ AddDIE(file, compileunit_name, compileunit_die, symtab, symbol_map,
die_reader.unit_sizes(), sink);
- if (attr_reader.HasAttribute<6>()) {
- uint64_t offset = attr_reader.GetAttribute<6>();
+ if (compileunit_die.has_stmt_list()) {
+ uint64_t offset = compileunit_die.stmt_list();
ReadDWARFStmtListRange(file, offset, compileunit_name, sink);
}
@@ -2307,16 +1915,16 @@
abbrev_data = unit_abbrev.ReadAbbrevs(abbrev_data);
sink->AddFileRange("dwarf_abbrev", compileunit_name, abbrev_data);
-
while (die_reader.NextDIE()) {
- attr_reader.ReadAttributes(&die_reader);
+ GeneralDIE die;
+ attr_reader.ReadAttributes(&die_reader, &die);
// low_pc == 0 is a signal that this routine was stripped out of the
// final binary. Skip this DIE and all of its children.
- if (attr_reader.HasAttribute<2>() && attr_reader.GetAttribute<2>() == 0) {
+ if (die.has_low_pc() && die.low_pc() == 0) {
die_reader.SkipChildren();
} else {
- AddDIE(file, compileunit_name, attr_reader, symtab, symbol_map,
+ AddDIE(file, compileunit_name, die, symtab, symbol_map,
die_reader.unit_sizes(), sink);
}
}
@@ -2369,7 +1977,7 @@
if (!span_startaddr) {
span_startaddr = addr;
} else if (line_info.end_sequence ||
- (!last_source.empty() && name != last_source)) {
+ (!last_source.empty() && name != last_source)) {
sink->AddVMRange("dwarf_stmtlist", span_startaddr, addr - span_startaddr,
last_source);
if (line_info.end_sequence) {
@@ -2390,17 +1998,24 @@
dwarf::DIEReader die_reader(file);
dwarf::LineInfoReader line_info_reader(file);
- dwarf::FixedAttrReader<uint64_t> attr_reader(&die_reader, {DW_AT_stmt_list});
+ dwarf::AttrReader<InlinesDIE> attr_reader;
+
+ attr_reader.OnAttribute(
+ DW_AT_stmt_list, [](InlinesDIE* die, dwarf::AttrValue data) {
+ if (!data.IsUint()) return;
+ die->set_stmt_list(data.GetUint());
+ });
if (!die_reader.SeekToStart(dwarf::DIEReader::Section::kDebugInfo)) {
THROW("debug info is present, but empty");
}
while (true) {
- attr_reader.ReadAttributes(&die_reader);
+ InlinesDIE die;
+ attr_reader.ReadAttributes(&die_reader, &die);
- if (attr_reader.HasAttribute<0>()) {
- uint64_t offset = attr_reader.GetAttribute<0>();
+ if (die.has_stmt_list()) {
+ uint64_t offset = die.stmt_list();
line_info_reader.SeekToOffset(offset,
die_reader.unit_sizes().address_size);
ReadDWARFStmtList(include_line, &line_info_reader, sink);
@@ -2412,4 +2027,4 @@
}
}
-} // namespace bloaty
+} // namespace bloaty