How to Do Versioning of Shared Library

How to do versioning of shared library?

The short version is that you do this via the soname of the library. Read chapter 3 at http://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html as well as chapter 3.3 ABI Versioning at http://www.akkadia.org/drepper/dsohowto.pdf

Best way to handle shared libraries versioning, deployment and usage

You can use Option 2, along with you can prefer the use of multimodule maven project

https://spring.io/guides/gs/multi-module/

You can keep it inside a framework repository assume it will be your parent repository and you can include it in your child projects, there way parent keeps on maintaining the version, like eg: how spring-boot-starter-parent works

https://www.springboottutorial.com/spring-boot-starter-parent

What does spring-boot-starter-parent exactly do in pom file?

Linux shared library versioning for backwards compatibility using libtool


However, binaries built against the new library will load and will then fail during runtime with an undefined symbol error if at runtime they enter a codepath that includes one of the new symbols not present in the old library.

TL;DR: I don't think there is a solution that will achieve desired result right now (unless you are already using versioned symbols), but you can make it a bit better now, and can fix it completely for the next time.


This is a problem that is that solved by the GNU symbol version extension.

It's probably best to have an example. Initial setup:

// foo_v1.c
int foo() { return 42; }

// main_v1.c
int main() { return foo(); }

gcc -fPIC -shared -o foo.so foo_v1.c
gcc -w main_v1.c ./foo.so -o main_v1

./main_v1; echo $?
42

Now let's modify foo.c so it's a drop-in replacement, but has new function:

mv foo.so foo.so.v1

// foo_v2.c
int foo() { return 42; }
int bar() { return 24; }

gcc -fPIC -shared -o foo.so foo_v2.c

./main_v1; echo $?
42

Everything still works (as expected). Now let's build main_v2 which requires new function.

// main_v2.c
int main() { return foo() - bar(); }

gcc -w main_v2.c ./foo.so -o main_v2

./main_v2; echo $?
18

Everything still works. And now we break things:

mv foo.so foo.so.v2
cp foo.so.v1 foo.so
./main_v1; echo $?
42
./main_v2
./main_v2: symbol lookup error: ./main_v2: undefined symbol: bar

Voila: we have a failure at runtime, instead of (desired) failure at load time. (This isn't actually visible in the above output, but can trivially verified by adding e.g. printf in main.)

Solution:
Let's give foo.so a version script:

// foo.lds
FOO_v2 {
global: bar;
};

gcc -fPIC -shared -o foo.so foo_v2.c -Wl,--version-script=foo.lds
gcc -w main_v2.c ./foo.so -o main_v2a

./main_v1; echo $?
42
./main_v2a; echo $?
18

As we can see, everything still works with the new version of the library.

But what happens when we run the new main_v2a against old foo.so?

mv foo.so foo.so.v2
cp foo.so.v1 foo.so
./main_v2

./main_v2a: ./foo.so: no version information available (required by ./main_v2a)
./main_v2a: symbol lookup error: ./main_v2a: undefined symbol: bar, version FOO_v2

This is little bit better: the failure is still happening at runtime, but the loader does mention FOO_v2, providing a hint that this is caused by some kind of "version too old" problem.

The reason the loader doesn't fail at load time is that the (old) foo.so has no version info whatsoever.

If you now repeat this process, creating FOO_v3 with a new function, and try to run main_v3 against the foo.so.v2 version of the library, you would get a failure at load time:

// foo_v3.c
int foo() { return 42; }
int bar() { return 24; }
int baz() { return 12; }

// foo_v3.lds
FOO_v2 {
global: bar;
};
FOO_v3 {
global: baz;
} FOO_v2;

// main_v3.c
int main() { return foo() - bar() - baz(); }

gcc -fPIC -shared -o foo.so foo_v3.c -Wl,--version-script=foo_v3.lds
gcc -w main_v3.c ./foo.so -o main_v3

./main_v3; echo $?
6

Now let's run main_v3 against foo.so.v2:

cp foo.so.v2 foo.so

./main_v3
./main_v3: ./foo.so: version `FOO_v3' not found (required by ./main_v3)

This time, the failure happens at load time. QED.

GCC: Specifying a Minimum Shared Library Version

You are describing external library versioning, where the app is built against libfoo.so.0, libfoo.so.1, etc. Documentation here.

Using external library versioning requires that exactly the same version of libfoo.so.x be present at runtime.

This is generally not the right technique on Linux, which, through the magic of symbol versioning, allows a single libfoo.so.Y to provide multiple incompatible definitions of the same symbol, and thus allows a single library serve both the old and the new applications simultaneously.

In addition, if you are simply always adding new symbols, and are not modifying existing symbols in incompatible way, then there is no reason to increment the external version. Keep libfoo.so at version 0, provide a int foo_version_X_Y; global variable in it (as well as all previous versions: foo_version_1_0, foo_version_1_1, etc.), and have an application binary read the variable that it requres. If an application requires a new symbol foo_version_1_2 and is run with an old library that only provides foo_version_1_1, then the application will fail to start with an obvious error.

How do applications resolve to different versions of shared libraries at run time?

Versioning of shared objects works as follows:

When you create a shared object, you give it both a real name and an soname. These are used to install the shared object (which creates both the object and a link to it).

So you can end up with the situation:

pax> ls -al xyz*
-rw-r--r-- 1 pax paxgroup 12345 Nov 18 2009 xyz.so.1.5
lrwxrwxrwx 1 pax paxgroup 0 Nov 18 2009 xyz.so.1 -> xyz.so.1.5
lrwxrwxrwx 1 pax paxgroup 0 Nov 18 2009 xyz.so -> xyz.so.1

with xyz.so.1.5 possessing the SONAME of xyz.so.1.

When the linker links in xyz.so, it follows the links all the way to xyz.so.1.5 and uses its SONAME of xyz.so.1 to store in the executable. Then, when you run the executable, it tries to load xyz.so.1 which will point to a specific xyz.so.1.N (not necessarily version 1.5).

So you could install xyz.so.1.6 and update the xyz.so.1 link to point to it instead and already-linked executables would use that instead.

One advantage of this multi-layer method is that you can have multiple potentially incompatible libraries of the same name (xyz.so.1.*, xyz.so.2.*) but, within each major version, you can freely upgrade them since they're supposed to be compatible.

When you link new executables:

  • Those linking with xyz.so will get the latest minor version of the latest major version.
  • Others linking with xyz.so.1 will get the latest minor version of a specific major version.
  • Still others linking with xyz.so.1.2 will get a specific minor version of a specific major version.

Now keep that last paragraph in mind as we examine your comment:

Now lets say I compile another version of the same library with the following real-name, libmy.so.2.0. The SONAME by guidelines would be libmy.so.2.0.

No, I don't believe so. The soname would be more likely to be libmy.so.2 so that you can make minor updates to the 2.x stream and get the latest behaviour.

Do Mach-O shared libraries (`.dylib`s) support symbol versioning?

Not really, but you can kinda roll your own.

To serve as an example, Apple has actually done some kind of symbol versioning to the stat() family of functions. Depending on macro definitions, the declaration of stat will expand to either this:

int stat(const char *, struct stat *) __asm("_stat");

Or this:

int stat(const char *, struct stat *) __asm("_stat$INODE64");

And this is what the library exports look like:

% nm -arch x86_64 /usr/lib//system/libsystem_kernel.dylib | fgrep ' _stat'
0000000000009650 T _stat
0000000000001fd8 T _stat$INODE64
0000000000001fd8 T _stat64
00000000000250dc T _statfs
0000000000002e4c T _statfs$INODE64
0000000000002e4c T _statfs64

(Note that you have to use the x86_64 version there, as the arm64 version never had the old implementation to begin with, and so _stat there is already the new version.)

But if you're just a user of a library, then unless you have broken headers with mismatched struct and function definitions, then this is not going to solve your segfault issue.



Related Topics



Leave a reply



Submit