In the C++ interface, we represent a reference-class attribute with a debug_info_entry::iterator. This works nicely for purposes of navigation. You can use it like a pointer to the debug_info_entry.
But that doesn't help for the "semantic equality" check that we want in attribute::operator== as used by e.g. dwarfcmp. Pointer equality is not the right test when comparing references in two parallel DIE trees.
So, what does "reference equality" mean for a comparison between two trees from two different DWARF files? This is not only part of making dwarfcmp do something useful, but also essential at the crux of the identification of duplicate trees that is what the compression plan is all about.
At first blush, a simple answer is that two references are identical if the subtrees they point to are identical (by deep comparison). But for important semantic purposes, that definition is too generous.
Consider a trivial C++ example:
main.cc: namespace A { #include "foo.cc" }; namespace B { #include "foo.cc" }; A::foo var; foo.cc: struct foo { int x, y; };
This yields the DIE tree (omitting lots of the irrelevant detail):
#a10: compile_unit{name="main.cc", ...} #a20: namespace{name="A", decl_file=["main.cc"], decl_line=1} #a30: structure_type{name="foo", decl_file=["foo.cc"], decl_line=1} ... #a40: namespace{name="B", decl_file=["main.cc"], decl_line=4} #a50: structure_type{name="foo", decl_file=["foo.cc"], decl_line=1} ... #a60: variable{name="var", location=..., type=#a50}
Note the #a30 and #a50 subtrees are identical. Now compare a second file:
main.cc: namespace A { #include "foo.cc" }; namespace B { #include "foo.cc" }; B::foo var; // <--- B:: vs A:: above (foo.cc same as above)
This yields the corresponding DIE tree:
#b10: compile_unit{name="main.cc", ...} #b20: namespace{name="A", decl_file=["main.cc"], decl_line=1} #b30: structure_type{name="foo", decl_file=["foo.cc"], decl_line=1} ... #b40: namespace{name="B", decl_file=["main.cc"], decl_line=4} #b50: structure_type{name="foo", decl_file=["foo.cc"], decl_line=1} ... #b60: variable{name="var", location=..., type=#b30}
(The only difference from s/a/b/ is #b60:type.) Since #a30, #a50, #b30, and #b50 are all identical, #b60's type=#b30 matches #a60's type=#a50 and we would call #a10 and #b10 identical.
But we know this is wrong. In #a60 type=#a50 is required and type=#a30 would not do, not because the subtrees pointed to differ, but because their contexts differ. Context matters: here it is the difference between resolving to A::foo and resolving to B::foo.
We have to get this right for dwarfcmp to be right at its job of semantic equality comparison. Moreover, we need to get it right in deciding where we can really consolidate redundant DIE trees to effect compression via DW_AT_imported_unit pointers. Whenever there are refs pointing to both identical copies of a duplicated DIE, we have to be sure that those two refs should be equal before we can unify those two DIEs into one in the output.
So, what should count as "equal context" for two DIEs? What comes to mind is the sequence of parent DIEs back to (but not including) the compile_unit. One thought was just to compare tags and names (or lack thereof). Another is to compare tags and all attributes. Or maybe somewhere in between. Are there attributes that are going to give us mismatches where we would like to consolidate? (DW_AT_sibling of course does not count.)
This presupposes that one thing that does not matter is the ordering of a DIE among its parent's other children. DIE children certainly are ordered in DWARF, so it could very well matter in the abstract. I have the impression there are some arcane cases with C++ namespace and using declarations where that ordering is relevant to decoding the context. But I don't have any clear sense of that or any other case where this kind of ordering matters.
That is, of course order matters all over, e.g. "void foo(int x, int y)" vs "void foo(int y, int x)" produces DIE trees that only differ (and importantly so) in the order of the two DW_TAG_formal_parameter children. But I don't see how you could have two {formal_parameter "x" child of subprogram "foo" child of CU} (say) with all matching attrs, to each of which different things had a ref attr, yet there could be both of them in the same (logical) CU such that a tree walk from the base of the CU to find the one the ref points to would be ambiguous. But I fear for some hairy corners about that.
Clearly we would hamstring useful compression enormously if we count "equal context" only when the containing DIE was completely identical. That is probably in practice far worse than the most simple and naively conservative plan: considering two DIEs identical only when they are both immediate children of a compile_unit. That simple plan is definitely fine for C, and might even be OK for C++ though perhaps only in all but its most obscure corners.
My inclination is to start out with the plan to ignore ordering for the "context equality" part of reference equality. Conversely, be conservative in excluding none of the rest of context: compare all attributes. In dwarfcmp, this means the references will pass equality comparison, but then the trees as a whole still won't pass when there is a child order difference. For compression, it might mean foregoing some cases (and leaving duplicate DIEs) where conflation of the two references so you can't distinguish which of the two original DIEs it means might be entirely harmless. Consider:
a.c: void foo (void) { #include "b.c" #include "b.c" } b.c: { typedef int itype; itype x; bar (&x); }
This will generate two almost-identical lexical_block subtrees that differ only in their PC attributes. This means the child DIEs for "itype", "x", etc. have to be duplicated, just so that a reference to the first lexical_block's "itype" will be distinct from a reference to the second lexical_block's "itype". It almost certainly never matters to distinguish them for this case.
At a guess, cases like that don't seem real likely to appear in numbers. In the long run, we may want to address this with an extension form for two-pointer refs. That would allow the most aggressive DIE deduplication possible, consolidating all identical subtrees regardless of context. Then each ref itself gives not only the pointer to that DIE, but the pointer to the imported_unit DIE that is outermost among those in the logical walk you would do from the CU to arrive at the referenced DIE. That second ref disambiguates the direct DIE ref, so the two-pointer ref identifies which copy it would point to if the compressed form were splayed back out to the logical DIE view that inserts copies where DW_TAG_imported_unit appears.
As an implementation matter, determining this "context" to compare will entail a lot of tree walking. This requires some caching, something like a reference tracker class that parameterizes the dwarf classes. This may add some complication to the definition of those whole class trees.
I'd appreciate feedback on all this thinking. (I have thought a bit more than that about the implementation, too, though far from worked it all out.)
Thanks, Roland
elfutils-devel@lists.fedorahosted.org