Development of a dynamic library (1/3)

Publié par cpb
Jan 28 2012

(Version originale en français)

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:

  • src/ containing the source code of the library,
  • lib/ where stand binary files and symbolic links described below,
  • include/ storing the header files of the library.
[~]$ mkdir factorial
[~]$ mkdir factorial/src
[~]$ mkdir factorial/include
[~]$ mkdir factorial/lib
[~]$ cd factorial
[factorial]$

Let’s create a file named src/fact.c containing our function.

And if you think you have found a bug in the code below, be kind and read the entire article before sending me a mocking mail 😉

#include <fact.h>

long long int factorial(long int n)
{
        long long int f = 1;
        do {
                f = f * n;
                n = n - 1;
        } while (n > 1);
        return f;
}

This file includes its own header, which ensures consistency of the prototype and implementation.

The file include/fact.h contains the following lines.

#ifndef LIB_FACT_H
#define LIB_FACT_H
    long long int factorial(long int n);
#endif

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).

Here is an example of compilation:

[factorial]$ ls src/
fact.c
[factorial]$ ls include/
fact.h
[factorial]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact.c
[factorial]$ ls src/
fact.c fact.o
[factorial]$

The generated fact.o object file will be used below.

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.

[factorial]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.0 src/fact.o
[factorial]$ ls lib/
libfact.so.1.0
[factorial]$

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.

[factorial]$ cd lib/
[lib]$ ln -sf libfact.so.1.0 libfact.so
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

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.

[lib]$ ldconfig -n .
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:05 libfact.so.1 -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

The links will allow us to compile an application that requires our library (through libfact.so) and then run it by making sure the major version is the right one (with libfact.so.1).

Using the library

Compiling an application

Let’s write a small program that uses our library. The factorial.c file will call our factorial() function for all the numbers found on the command line.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fact.h>

int main (int argc, char * argv[])
{
        long int n;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s value...n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1)
                        fprintf(stdout, "%ld! = %lldn", n, factorial(n));
        return EXIT_SUCCESS;
}

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.

[factorial]$ gcc -I ./include/ -L ./lib/ -o ./test/factorial ./test/factorial.c -lfact
[factorial]$ 
[factorial]$ ls -l test/
total 12
-rwxrwxr-x 1 cpb cpb 7359 2012-01-27 10:41 factorial
-rw-r--r-- 1 cpb cpb  382 2012-01-25 18:29 factorial.c
[factorielle]$

Running the application

If we run our program directly, the execution fails.

$ ./test/factorial 4 5 6
./test/factorial: error while loading shared libraries: libfact.so.1: cannot open shared object file: No such file or directory
$

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.

[factorial]$ export LD_LIBRARY_PATH=./lib/ 
[factorial]$ ./test/factorial 4 5 6
4! = 24
5! = 120
6! = 720
[factorial]$

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

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.

[factorial]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-2.c
[factorial]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.1 src/fact.o

Our library has been re-created with a new file name, so it’s necessary to rerun the ldconfig command.

[factorial]$ ldconfig -n lib/
[factorial]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
[factorial]$
[factorial]$ ./test/factorial 0 1 2
0! = 1
1! = 1
2! = 2
[factorial]$

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

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.

The new function in fact-3.c is implemented as follow:

int factorial(long int n, long long int * result)
{
        * result = 1;
        if (n < 0)
                return -1;
         do {
                 (*result) = (*result) * n;
                 n = n - 1;
         } while (n > 1);
        return 0;
}

Of course, we change the header file (fact-3.h):

#ifndef LIB_FACT_H
#define LIB_FACT_H
        int factorial(long int n, long long int * result);
#endif

We compile our library as we did before:

[factorial]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-3.c
[factorial]$ gcc -fPIC -shared -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 src/fact.o
[factorial]$ ldconfig -n lib/
[factorial]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[factorial]$ cd lib/
[lib]$ ln -sf libfact.so.2 libfact.so
[lib]$ ls -l
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:27 libfact.so -> libfact.so.2
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[lib]$ cd ..
[factorial]$

The links are in place to compile a new version of the test program (factorial-2.c).

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fact.h>

int main (int argc, char * argv[])
{
        long int n;
        long long int f;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s value...n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1) {
                        if (factoriel(n, & f) == 0)
                                fprintf(stdout, "%ld! = %lldn", n, f);
                        else
                                fprintf(stdout, "%ld! doesn't existn", n);
                }
        return EXIT_SUCCESS;
}

Let us compile and try it:

[factorial]$ gcc -I ./include/ -L ./lib/ -o ./test/factorial-2 ./test/factorial-2.c -lfact
[factorial]$ ./test/factorial-2 3 0 -3
3! = 6
0! = 0
-3! doesn't exist
[factorial]$

This time our application behaves correctly. We can remark that the presence of the former major release enables our previous executable to continue operating.

[factorial]$ ./test/factorial 3 0 -3
3! = 6
0! = 1
-3! = 1
[factorial]$
Dynamic library v. 2.0

Dynamic library v. 2.0

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.

URL de trackback pour cette page