In the first article of this series of two, we ran abidiff to compare the ABIs of the libstdc++.so shared libraries from RHEL 6.5 and RHEL 7.  In this article, we are going to analyze the resulting ABI change report that was emitted.

circles

Analyzing the results

The report starts with a header that summarizes the ABI differences:

Functions changes summary: 0 Removed, 10 Changed (1260 filtered out), 112 Added functions
Variables changes summary: 0 Removed, 3 Changed (72 filtered out), 97 Added variables

From the:

Functions changes summary: 0 Removed,

and:

Variables changes summary: 0 Removed,

parts of the header, we understand that all the global variables and functions exported in 6.5 are still present in 7. This an important characteristic to have from an ABI compatibility point of view. It implies that applications that assume the presence of certain exported functions or variables from the RHEL 6.5 version are assured to still find these functions and variables in the RHEL 7 version.

Of course, libstdc++.so in RHEL 7 adds many new global variables and functions. We can see that from these parts of the header:

Functions changes summary: [...] 112 Added functions

and

Variables changes summary: [...] 97 Added variables

This is expected from a new release that adds new features and will not cause any ABI incompatibility because applications that were dynamically linked against the RHEL 6.5 version will not be affected by the new global variables and functions added in RHEL 7.

An interesting part of this header is

Functions changes summary: [...] 10 Changed (1260 filtered out)

This is about functions whose sub-types may have changed in ways that don't alter the signature of the function. The sub-type of a function is either its return type or the type of its parameters.

To better understand the kind of changes abidiff is reporting about here, let's suppose we have a function named func that is exported by a shared library libf.so. The declaration of func would be:

void func(struct S*);

Where struct S is defined as:

struct S
{
  int i;
};

Now suppose that in a subsequent version of libf.so, one data member is added to struct S. It would thus become:

struct S
{
  char c;
  int m;
};

We can see that this change doesn't alter the signature of function func which still is:

void func(struct S*);

But the struct S that is pointed to by struct S* has changed, possibly in an ABI incompatible way. In other words, function func has a sub-type (struct S) that has changed. And it's this kind of sub-type changes that the abidiff report header refers to when it indicates:

Functions changes summary: [...] 10 Changed (1260 filtered out)

One interesting detail of this part of the header is the (1260 filtered out) item.  It indicates that 1260 changes were filtered out from the report, presumably because they are deemed "harmless" from an ABI compatibility point of view. These are typically changes like a data member that was declared public and now becomes private. Curious users can force abidiff to show these harmless changes by using the --harmless command-line option. It's worthwhile to notice that filtering out this huge number of harmless changes greatly helps to spot the potentially harmful ones that would otherwise be buried in the noise.

Further on in the report, we see 4 additional sections that provide details about the actual changes:

1. Details of the added functions:

112 Added functions:

[A] 'function __cxxabiv1::__cxa_dependent_exception* __cxxabiv1::__cxa_allocate_dependent_exception()' {__cxa_allocate_dependent_exception@@CXXABI_1.3.6}
[A] 'function void __cxxabiv1::__cxa_deleted_virtual()' {__cxa_deleted_virtual@@CXXABI_1.3.6}
[A] 'function void __cxxabiv1::__cxa_free_dependent_exception(__cxxabiv1::__cxa_dependent_exception*)' {__cxa_free_dependent_exception@@CXXABI_1.3.6}
[A] 'function int __cxxabiv1::__cxa_thread_atexit(void*, void*)' {__cxa_thread_atexit@@CXXABI_1.3.7}
[A] 'function void __cxxabiv1::__cxa_tm_cleanup(void*, void*, unsigned int)' {__cxa_tm_cleanup@@CXXABI_TM_1}
[A] 'method void __gnu_debug::_Safe_local_iterator_base::_M_attach(__gnu_debug::_Safe_sequence_base*, bool)' {_ZN11__gnu_debug25_Safe_local_iterator_base9_M_attachEPNS_19_Safe_sequence_baseEb@@GLIBCXX_3.4.17}
[A] 'method void __gnu_debug::_Safe_local_iterator_base::_M_detach()' {_ZN11__gnu_debug25_Safe_local_iterator_base9_M_detachEv@@GLIBCXX_3.4.17}
[A] 'method void __gnu_debug::_Safe_unordered_container_base::_M_detach_all()' {_ZN11__gnu_debug30_Safe_unordered_container_base13_M_detach_allEv@@GLIBCXX_3.4.17}
[A] 'method void __gnu_debug::_Safe_unordered_container_base::_M_swap(__gnu_debug::_Safe_unordered_container_base&)' {_ZN11__gnu_debug30_Safe_unordered_container_base7_M_swapERS0_@@GLIBCXX_3.4.17}
[A] 'function std::size_t std::_Fnv_hash_bytes(std::size_t, std::size_t)' {_ZSt15_Fnv_hash_bytesPKvmm@@CXXABI_1.3.5}
[A] 'function std::size_t std::_Hash_bytes(std::size_t, std::size_t)' {_ZSt11_Hash_bytesPKvmm@@CXXABI_1.3.5}
[A] 'method void std::_List_node_base::_M_hook(std::_List_node_base*)' {_ZNSt15_List_node_base7_M_hookEPS_@@GLIBCXX_3.4.14}
[A] 'method void std::_List_node_base::_M_reverse()' {_ZNSt15_List_node_base10_M_reverseEv@@GLIBCXX_3.4.14}
(... the rest of the 112 added functions are not shown ...)

In this section, abidiff shows the signatures of the functions that were added. For each added function it also shows the ELF symbol name of that function, inside curly brackets, including it's versioning information e.g, {__cxa_allocate_dependent_exception@@CXXABI_1.3.6}

2. Details of any function whose signature has not changed, but which contains changes in some sub-type. For the sake of conciseness, let's focus on some interesting bits exhibited in the report. For instance:

[C]'method void std::condition_variable::wait(std::unique_lock<std::mutex>&)' has some indirect sub-type changes:
  parameter 1 of type 'std::unique_lock&' has sub-type changes:
    in referenced type 'class std::unique_lock':
      1 data member change:
        type of 'std::mutex* std::unique_lock::_M_device' changed:
          in pointed to type 'class std::mutex':
            entity changed from class std::mutex to typedef std::unique_lock::mutex_type
              1 base class insertion:
                class std::__mutex_base

This excerpt is basically telling us that the type std::mutex that is now being accessed through the (typedef) naming std::unique_lock<std::mutex>::mutex_type has been changed; it now has one more base class named std::_mutex_base. That change however hasn't modified the size of the class.

It's also telling us that this class std::mutex related change impacts the data member std::unique_lock<std::mutex>::_M_device, which is of type std::mutex*.

The enclosing std::unique_lock<std::mutex> type is used by the exported method void std::condition_variable::wait(std::unique_lock<std::mutex>&).

As reviewers interested in ABI matters, we can now use our judgment and infer that this change is not harmful, mainly because it doesn't change the size of the impacted data types and also because it doesn't change any of the type layouts in an ABI-incompatible way. It's still quite interesting to have it pinpointed as a worthwhile ABI change.

It's worthwhile to notice how hierarchical the reported items about the type changes are. They mention the exported functions from which the changed types are used and they walk you all the way through how the changed type is used from that public entry point.

It's also interesting to note how difficult it would have been for reviewers to come up with this kind of ABI impact analysis by just looking at the original source change, which is, the addition of the new base class (std::_mutex_base) to std::unique_lock<std::mutex>::mutex_type. The remaining two sections are about similar topics, but for variables. That is, added variables and variables with sub-type changes. Analyzing them is left as an exercise for interested and coffee loving readers.

Caveats

Libabigail and its associated tools are still in their early days. There are ABI changes that are not yet caught. There are debug information constructs that are not yet supported. There can also be issues around the way it interprets those constructs that it does support.

So the results of running abidiff on libstdc++.so should be taken with rocks of salt.

That said, substantial work in under way to improve these areas and add features that we know are needed.

Conclusion

The Libabigail  based tool-set has already been useful to us internally at Red Hat, even in these early days. Having a hierarchical textual report about the ABI differences of two versions of a library, just by analyzing the content of the binaries, is a great proposition.

This can empower people responsible for software distributions to perform some deep ABI change impact analysis on the shared libraries they ship, just by looking at the binaries themselves. Libabigail being a re-usable library itself, these people can now envision building their own derivative tools to help themselves.

Or so we wish.


 

Hopefully, having read this article, you have a deeper understanding of the significant challenges in the area of ABI compatibility, as well as the significant investment Red Hat makes in developing solutions to some of the related problems. Naturally, these tools are all shared openly with the community.

We welcome feedback! If you have any questions or comments relating to this article, please feel free to add it below or contact us.

Last updated: April 5, 2018