Maintaining Binary Compatibility

Maintaining a stable ABI is important. Users hate having to recompile things that used to work. A lot of downstream users keep systems for about 10 years and want to be able to keep running their stack on top of FreeBSD without worrying that an upgrade will break things.

This guide contains some rules of thumb for avoiding breaking the public ABI and KBI. Please add to it with examples and your own experience.

It's easy to forget that keeping a symbol does not mean that you have kept the binary interface stable. Users of that symbol should be able to keep using it in the same way before and after your change. Don't forget that keeping the semantics stable is as important as keeping the syntax the same.

Hide Everything

One general rule of thumb for making it easy to keep the ABI stable is to make it as simple as possible. Before exporting anything, consider whether it's actually required. Anything else should be hidden as private symbols and not published in headers.

For example, if you need to pass a structure outside of your code, consider whether the external code actually needs to know what the structure is.

Enumerated Types

In general, values in enumerated types should never be repurposed. When adding values, consider two things:

1. Will the user pass this value into your code? What happens if they omit it? 2. What happens if you pass this value out to code that wasn't expecting it?

In general, omitting the new value should always provide whatever the default behaviour was before. If this is not the case, then all public functions that accept the value should have their versions bumped and the old versions should retain the old behaviour.

The second is a bit more tricky, as you have no control over the external code. In general, a good approach is to define a ENUM_MAX or ENUM_LAST value in the enumeration. External code can then easily skip over values that it won't understand. This solves the easy part of the problem. The difficult part is what to do if that value is actually important? Fortunately, this is a relatively rare case. Most of the time, either old code will not cause the conditions where a new enum value can appear, or it can safely ignore the new value. It is sometimes possible to provide compatibility ABIs that will transform new values into old ones where applicable (e.g. if the new value is a special case of an old one).

Functions

Functions are the most obvious part of the public ABI. Functions and globals are the only things that appear in the symbol table of a binary. Fortunately, ELF provides support for multiple versions of the same symbol.

There are some detailed examples of how to use this in this tutorial. I will just give a quick overview of how to add a new symbol and how to modify the meaning of an old one.

Symbol versioning was introduced in FreeBSD 7, so the FBSD_1.0 version corresponds to FreeBSD 7, FBSD_1.1 to FreeBSD 8, FBSD_1.2 to FreeBSD 9 and so on. Each library contains a Symbol.map, which defines the versions of each public symbol.

Adding a new public symbol is easy: just add the function name to the correct section in the symbol map. For example, if you add a do_magic() function in FreeBSD 10, you'd add this to the Symbol.map file:

FBSD_1.3 {
        do_magic;
}

If there is already a FBSD_1.3 section, then you don't need to add a new one, just insert the symbol here.

Now imagine that in FreeBSD 11 you want to change the kinds of magic that are allowed. The do_magic(void) function now becomes do_magic(enum magic_kind). Existing code should continue to work, but new code should be able to use the new version.

The first thing to do is implement the legacy compatibility version, which will look something like this:

#if defined(COMPAT_FREEBSD10)
/* Legacy version of the do_magic() function, for FreeBSD 10 compatibility */
void do_magic_10(void)
{
        do_magic(magic_default);
}
__sym_compat(do_magic, do_magic_10, FBSD_1.3);
#endif

That's almost all there is to it. Just add do_magic to the FBSD_1.4 section of Symbol.map and you're done.

In general, the old versions of the symbols should be placed at the end of the file (or in a separate file) and wrapped in conditional compilation so that they are only compiled if the relevant COMPAT_FREEBSD macro is defined.

The __sym_compat() macro is in cdefs.h and expands to some magic assembler directives that tell the assembler to emit the symbol with the specified name and version.

Note that this even works if you compile against the FreeBSD 11 libraries and link against a shared library that links against the FreeBSD 10 version. Two different parts of the resulting program will call different versions of the symbol. You should ensure that your compatibility versions are safe to use in this case.

Structures

Structures are often problematic when creating a stable binary interface. One current problem, for example, is the proposed change of pthread_mutex_t from being a pointer to being an explicit structure. This will allow a mutex to be placed in shared memory. The obvious problem with this is what happens when two programs with different ideas of what a mutex looks like try to use it? This is a related problem to when a structure is placed in non-shared heap memory and used by shared libraries expecting different layouts.

The mutex problem is especially tricky. On potential solution is to store the mutex address with the low bit set in the first field of the structure. This means that anything expecting it to be a pointer - rather than a structure - will get a pointer to the real structure. The structure will work both as an opaque pointer or as a structure, depending on how it is used. This will, of course, require modifications to all of the pthread_mutex_* functions to disambiguate the two cases. For example:

typedef struct {
        uintptr_t magic;
        /* other stuff */
} pthread_mutex_t;
int pthread_mutex_lock(pthread_mutex_t *mutex)
{
        if (mutex->magic & 1) {
                assert((mutex->magic | ~1) == mutex);
        } else {
                mutex = *(pthread_mutex_t**)mutex;
        }
        /* Then the real implementation */
}

A more common case than changing a pointer into a structure is modifying an existing structure. To make this easier, each public structure should have a magic field. For example:

struct someStructv1 {
        int magic;
        int field;
};
struct someStruct {
        int magic;
        float field;
};
...
static inline float getField(struct someStruct *s)
{
        switch (s->magic) {
                case 0x12345678:
                        return ((float)((struct someStructv1*)s)->field;)
                case 0x87654321:
                        return (s->field);
                default:
                        assert(0 && "Invalid structure!");
        }
}

The value stored in the magic field allows you to disambiguate the two cases. It doesn't matter what the value stored here is, as long as it is unique for each version. Ideally, it should be a value that is unlikely to be accidentally written there (so low numbers are a bad idea) to make it more likely to fail early in the case of memory corruption.

This is not needed for structures that are not published externally. For example, if a structure is only ever passed outside by pointer (e.g. the locale structure form libc) then this versioning is not required because nothing outside can ever see the structure values.

For structure that may be allocated externally, you should provide a versioned init function that sets the magic field before performing any initialisation. It is often a good idea to add some padding to these structures as well.

BinaryCompatibility (last edited 2012-03-02 15:08:01 by theraven)