Application Binary Interfaces and Versioning
ELF objects processed by the link-editors provide many global symbols to which other objects can bind. These symbols describe the object's application binary interface (ABI). During the evolution of an object, this interface can change due to the addition or deletion of global symbols. In addition, the object's evolution can involve internal implementation changes.
Versioning refers to several techniques that can be applied to an object to indicate interface and implementation changes. These techniques provide for the object's controlled evolution while maintaining backward compatibility.
This chapter describes how an object's ABI can be defined and classifies how changes to this interface can affect backward compatibility. It also presents models by which interface and implementation changes can be incorporated into new releases of the object.
The focus of this chapter is on the runtime interfaces of dynamic executables and shared objects. The techniques used to describe and manage changes within these dynamic objects are presented in generic terms. A common set of naming conventions and versioning scenarios as applied to shared objects can be found in Appendix B, Versioning Quick Reference.
Developers of dynamic objects must be aware of the ramifications of an interface change and understand how such changes can be managed, especially in regards to maintaining backward compatibility with previously shipped objects.
The global symbols made available by any dynamic object represent the object's public interface. Frequently, the number of global symbols remaining in an object at the end of a link-edit are more than you would like to make public. These global symbols result from the relationship required between relocatable objects used to create the object. They represent private interfaces within the object itself.
Before defining an object's binary interface, you should first define only those global symbols you wish to make publicly available from the object being created. These public symbols can be established using the link-editor's -M option and an associated mapfile as part of the final link-edit. This technique is introduced in "Reducing Symbol Scope". This public interface establishes one or more version definitions within the object being created, and forms the foundation for the addition of new interfaces as the object evolves.
The following sections build upon this initial public interface. First though, you should understand how various changes to an interface can be categorized so that they can be managed appropriately.
Interface Compatibility
Many types of change can be made to an object. In their simplest terms, these changes can be categorized into one of two groups:
Compatible updates. These updates are additive, in that all previously available interfaces remain intact.
Incompatible updates. These updates have changed the existing interface in such a way that existing users of the interface can fail or behave incorrectly.
The following table categorizes some common object changes.
Table 5-1 Interface Compatibility Examples
Object Change | Update Type |
---|---|
The addition of a symbol | Compatible |
The removal of a symbol | Incompatible |
The addition of an argument to a non-varargs(3HEAD) function | Incompatible |
The removal of an argument from a function | Incompatible |
The change of size, or content, of a data item to a function or as an external definition | Incompatible |
A bug fix, or internal enhancement to a function, providing the semantic properties of the object remain unchanged | Compatible |
A bug fix, or internal enhancement to a function when the semantic properties of the object change | Incompatible |
Because of interposition, the addition of a symbol can constitute an incompatible update, such that the new symbol might conflict with an applications use of that symbol. However, this does seem rare in practice as source-level name space management is commonly used.
Compatible updates can be accommodated by maintaining version definitions internal to the object being generated. Incompatible updates can be accommodated by producing a new object with a new external versioned name. Both of these versioning techniques enable the selective binding of applications. They also enable verification of correct version binding at runtime. These two techniques are explored in more detail in the following sections.
Internal Versioning
A dynamic object can have one or more internal version definitions associated with it. Each version definition is commonly associated with one or more symbol names. A symbol name can only be associated with one version definition. However, a version definition can inherit the symbols from other version definitions. Thus, a structure exists to define one or more independent, or related, version definitions within the object being created. As new changes are made to the object, new version definitions can be added to express these changes.
There are two consequences of providing version definitions within a shared object:
Dynamic objects that are built against this shared object can record their dependency on the version definitions they bind to. These version dependencies will be verified at runtime to ensure that the appropriate interfaces, or functionality, are available for the correct execution of an application.
Dynamic objects can select only those version definitions of a shared object that they wish to bind to during their link-edit. This mechanism enables developers to control their dependency on a shared object to the interfaces, or functionality, that provide the most flexibility.
Creating a Version Definition
Version definitions commonly consist of an association of symbol names to a unique version name. These associations are established within a mapfile and supplied to the final link-edit of an object using the link-editor's -M option. This technique is introduced in the section "Reducing Symbol Scope".
A version definition is established whenever a version name is specified as part of the mapfile directive. In the following example, two source files are combined, together with mapfile directives, to produce an object with a defined public interface:
$ cat foo.c extern const char * _foo1; void foo1() { (void) printf(_foo1); } $ cat data.c const char * _foo1 = "string used by foo1()\n"; $ cat mapfile SUNW_1.1 { # Release X global: foo1; local: *; }; $ cc -o libfoo.so.1 -M mapfile -G foo.o data.o $ nm -x libfoo.so.1 | grep "foo.$" [33] |0x0001058c|0x00000004|OBJT |LOCL |0x0 |17 |_foo1 [35] |0x00000454|0x00000034|FUNC |GLOB |0x0 |9 |foo1 |
The symbol foo1 is the only global symbol defined to provide the shared object's public interface. The special auto-reduction directive "*" causes the reduction of all other global symbols to have local binding within the object being generated. This directive is introduced in "Defining Additional Symbols". The associated version name, SUNW_1.1, causes the generation of a version definition. Thus, the shared object's public interface consists of the internal version definition SUNW_1.1, associated with the global symbol foo1.
Whenever a version definition, or the auto-reduction directive, are used to generate an object, a base version definition is also created. This base version is defined using the name of the file itself, and is used to associate any reserved symbols generated by the link-editor. See "Generating the Output File" for a list of these reserved symbols.
The version definitions contained within an object can be displayed using pvs(1) with the -d option:
$ pvs -d libfoo.so.1 libfoo.so.1; SUNW_1.1; |
The object libfoo.so.1 has an internal version definition named SUNW_1.1, together with a base version definition libfoo.so.1.
Note - The link-editor's -z noversion option allows symbol reduction to be directed by a mapfile but suppresses the creation of version definitions.
Starting with this initial version definition, the object can evolve by adding new interfaces and updated functionality. For example, a new function, foo2, together with its supporting data structures, can be added to the object by updating the source files foo.c and data.c:
$ cat foo.c extern const char * _foo1; extern const char * _foo2; void foo1() { (void) printf(_foo1); } void foo2() { (void) printf(_foo2); } $ cat data.c const char * _foo1 = "string used by foo1()\n"; const char * _foo2 = "string used by foo2()\n"; |
A new version definition, SUNW_1.2, can be created to define a new interface representing the symbol foo2. In addition, this new interface can be defined to inherit the original version definition SUNW_1.1.
The creation of this new interface is important as it identifies the evolution of the object and enables users to verify and select the interfaces to which they bind. These concepts are covered in more detail in "Binding to a Version Definition" and in "Specifying a Version Binding".
The following example shows the mapfile directives that create these two interfaces.
$ cat mapfile SUNW_1.1 { # Release X global: foo1; local: *; }; SUNW_1.2 { # Release X+1 global: foo2; } SUNW_1.1; $ cc -o libfoo.so.1 -M mapfile -G foo.o data.o $ nm -x libfoo.so.1 | grep "foo.$" [33] |0x00010644|0x00000004|OBJT |LOCL |0x0 |17 |_foo1 [34] |0x00010648|0x00000004|OBJT |LOCL |0x0 |17 |_foo2 [36] |0x000004bc|0x00000034|FUNC |GLOB |0x0 |9 |foo1 [37] |0x000004f0|0x00000034|FUNC |GLOB |0x0 |9 |foo2 |