During a recent training session, a conversation with a participant gave me the idea to check the options needed to perform debugging and coverage tests on a shared library.

A dynamic library (libXXXX.so file – “so” standing for Shared Object) is loaded into memory when the process starts. The executable file and the library file are independent before launching the application, and can be maintained separately.

I realized that some points were far from obvious, such as managing version numbers or activating coverage tests. Here is a list of the steps needed for the development of a dynamic library. The first article is devoted to compilation, version control and symbolic links. The second one will focus on debugging and step-by-step tracing of the library code. The third will describe how to perform coverage tests on the library.

Compiling and installing the library

Compiling library code

Let’s start by creating a small dynamic library, with a simple function: the implementation of the mathematical “factorial”.

I create a working directory named factorial including all the files. Then we make three sub-directories:

When compiling this file we will provide the following options on the gcc command line:

-c to tell gcc to stop his job after the compilation phase and thus providing an object file (not linking).

-I include/ telling gcc to look for .h header files in the include/ directory in addition to the usual directories (/usr/include…).

-fPIC to request the generation of a relocatable code (PIC stands for Position Independent Code). It is necessary for the creation of shared libraries even if this option has no effect on some architectures (x86 32-bit for example).

Note that during the development and testing phase, we do not use any optimization option, otherwise the compiler may change the executable code created and there won’t be an exact matching with the source file.

Generation of the library

The library itself is created by invoking gcc with the -shared option. We ask him to save the library in the libfact.so.1.0 file. The numbers 1 and 0 correspond respectively to the major and minor numbers of the library version.

It is customary to consider that a major number change represents a break in binary compatibility of the library and requires recompilation of applications, while a variation of the minor number represents only internal corrections or improvements that do not interfere with the programming interface.

We will tell gcc with the -Wl option to record in the heade rof the library its official name of the library including the major version number. It passes to the linker the string that follows the -Wl option after replacing commas by spaces. Thus the linker gets the -soname libfact.so.1 string.

It is recommanded to repeat the options passed in the previous compilation, such as -fPIC.

So we get the libfact.so.1.0 file, whose header contains the libfact.so.1 name.

Creating symbolic links

When we compile an application, we tell gcc to link it with the fact library. He looks for a file named libfact.so, not libfact.so.1.0. So we have to create a symbolic link named libfact.so pointing to the real library file. This link is created manually using the ln command.

During compilation gcc records the name of the library he used into the executable. This is the “official” name found in the SONAME section we filled previously with the -Wl,-soname option.

At runtime, the loader searches the library which major number matches those used during compilation. So he has to find a file named libfact.so.1, or rather a symbolic link named libfact.so.1 pointing to libfact.so.1.0.

The creation of the first symbolic link was needed to compile an application with the library, the second link is essential to run a program associated with it. This link is used much more frequently than the first one. To make the life of the administrator easier, a command named ldconfig will help to automatically create the links needed to allow users to run applications. It searches the directories containing system libraries (/lib, /usr/lib, /usr/local/lib, etc… and all given in /etc/ld.so.conf) and creates links on each library file with the name contained in the section. Let’s see an example where I force ldconfig through its -n option to explore only our lib/ directory.

This file is located in the factorial/test/ directory that we create now. It includes the <fact.h> header file. So the compiler has to find this header file. Two solutions:

Put the header file in /usr/include, /usr/local/include or any other directory where gcc searches. This should be reserved for critical files, needed by several applications and useful for the entire system.

Keep the file in an application specific directory and tell gcc where to find it.

I will obviously choose the second one.

In addition, we will put the -lfact option at the end of the command line, asking the linker to perform the linking with the libfact.so library. As for the header file, gcc must be told where he can find the libfact.so file we created previously as symbolic link. It is the role of the -L option.

Indeed, the dynamic linker which should start the process does not know where to find the library. We can see that it searches for the libfact.so.1 file (with the major number as extension). If our application is important enough to be used regularly by different users, it is legitimate to place library files in /usr/local/lib where the loader will find them. However if the application is currently under development or reserved for personnal use, it is preferable to leave the library in a sub-directory of our home directory. In this case, we must be fill (possibly in a startup script) the environment variable LD_LIBRARY_PATH to add the path to the library file.

Of course, the LD_LIBRARY_PATH variable can be given an absolute path rather than a relative one if you want to lauch the application from any location of the filesystem tree.

Dynamic library v. 1.0

Maintaining the library

Minor version update

Our library seems to works, let’s engage intensive testings:

[factorial]$ ./test/factorial 3
3! = 6
[factorial]$

Very good!

[factorial]$ ./test/factorial 2
2! = 2
[factorial]$

Perfect!

[factorial]$ ./test/factorial 1
1! = 1
[factorial]$

No problem.

[factorial]$ ./test/factorial 0
0! = 0
[factorial]$

Ouch!

By convention, it is hypothesized that 0! = 1 (you can check on Wikipedia if you wish). Our program is defective. The correction is fairly simple, just replace the loop

do {
f = f * n;
n = n - 1;
} while (n > 1);

by

while (n > 1) {
f = f * n;
n = n - 1;
}

That’s what I did in fact-2.c file. Theoretically I should keep the same source file and replace it in the new version of the library. I wanted to keep the previous version here for demonstration purposes so did I rename the file.

I will compile and generate a new library version incrementing the minor number. The interface of the factorial() function is not changed, executable files that depend on the library will continue to operate normally.

Our code works correctly for 0!. The previous version of the library is no more used, so we can delete it

[factorial]$ rm -f lib/libfact.so.1.0
[factorial]$

Dynamic library v. 1.1

Major version update

After a few tests, we are facing a new problem with our library.

[factorial]$ ./test/factorial -3
-3! = 1
[factorial]$

Our function returns a value when given a negative number. in mathematics, the factorial is only defined for natural numbers, not for negative integers. The function should report the argument error and not return a value (coherent but misleading).

We choose to modify the interface of our routine, which will take as argument a pointer to a long long integer where it will store the result and return a success (zero) or failure (-1) value. This change will involve an interface adaptation and recompilation of the applications using the library. So must we change the major version.

Conclusion

The management of major and minor numbers of dynamic library versions offers the following advantages:

The internal only modifications, represented by changes in the minor number, allow already compiled executables to run directly with the new version of the library and enjoy – without recompilation – the latest improvements.

Changes in the external interface of the library require a recompilation (possibly after adaptation) of the application.

Several major versions of the same library can coexist simultaneously for proper operation of different generations of an application. However, the compiler uses the new major version pointed to by the main symbolic link of the library (libfact.so)

We will see in the next article how to debug the code in the dynamic library, tracing it with step-by-step execution and examining the contents of its variables.