Object Files, Make, and Libraries

Introduction

This topic is not required reading for ECE 250. It is provided for interest's sake. If you find it useful, please let me know. If you have some feedback as to how this page can be improved, also let me know.

In this tutorial, we will introduce the following advanced topics:

  1. Compilation and linking,
  2. Creating intermediate object files,
  3. Make files,
  4. Header Files,
  5. Libraries, and
  6. Static library archives, and
  7. Dynamically-linked libraries.

A very good reference, which I wish I read before writing this page, is David A. Wheeler's HOWTO on this topic.

Compilation and Linking

In class, for ease of implementation and cross-platform development, we are both declaring and defining our classes within a single header file. For example,

demonstrates how the declaration and definitions of the Complex class may appear in the same header file. That header file is included in the main1.cpp file and successfully used.

{ecelinux:1} ls
Complex1.h  main1.cpp
{ecelinux:2} g++ main1.cpp
{ecelinux:3} ls
Complex1.h  a.out*  main1.cpp
{ecelinux:4} ./a.out
-13.1288 - 15.2008j
{ecelinux:5} 

In this case, the #include "Complex1.h" preprocessor statement at the top of the main1.cpp file combines all user code into a single file which is then compiled. The compiler, however, still links in the appropriate libraries associated with the iostream.h header file because it is part of the C++ Standard Library. To have the linker not link the C++ Standard Library, use the -nostdlib option to g++.

To view the manual page for g++, type man g++ or view the page here.

Creating Intermediate Object Files

One problem with this approach is that you could modify the main1.cpp file, however, then you would have to recompile both the definitions in Complex1.h and main1.cpp, even though the code in Complex1.h did not change. While this does not appear to be a serious problem at this level, it does, however pose serious problems for larger projects where compilation of the entire project from scratch may take hours.

If we split the Complex class into two files, the header file, Complex2.h, storing the class declaration and the source code, Complex2.cpp, storing the class definitions.

In this case, we cannot compile Complex2.cpp into an executable (a process involving both compilation and linking) because the file does not contain a int main (); function. Thus, we to restrict compilation simply to generating the object file:

{ecelinux:1} ls
Complex2.cpp  Complex2.h  main2.cpp
{ecelinux:2} g++ -c Complex2.cpp
{ecelinux:3} ls
Complex2.cpp  Complex2.h  Complex2.o  main2.cpp

To read more about object files, see http://en.wikipedia.org/wiki/Object_file.

Now that we have generated the object file Complex2.o, we can continue to compile the main2.cpp command, linking in the Complex2.o file:

{ecelinux:4} g++ main2.cpp Complex2.o
{ecelinux:5} ls
Complex2.cpp  Complex2.h  Complex2.o  a.out*  main2.cpp
{ecelinux:6} ./a.out
-13.1288 - 15.2008j
{ecelinux:7} 

This does not re-compile the Complex2.cpp file, however, you will observe that main2.cpp still includes the header file Complex2.h. This is because the compile uses this header file to ensure that the use of the class is correct and that it generates acceptable object code.

The process of compiling and linking main2.cpp could itself be broken into two steps:

{ecelinux:7} rm a.out
{ecelinux:8} g++ -c main2.cpp
{ecelinux:9} ls
Complex2.cpp  Complex2.h  Complex2.o  main2.cpp  main2.o
{ecelinux:10} g++ main2.o Complex2.o
{ecelinux:11} ls
Complex2.cpp  Complex2.h  Complex2.o  a.out*  main2.cpp  main2.o
{ecelinux:12} ./a.out
-13.1288 - 15.2008j
{ecelinux:13} 

Here, we generate object code from main2.cpp (generating main2.o) and then we link the two object files.

The iostream Library

Note that, by default, the iostream and cmath standard libraries are automatically linked by g++.

Make Files

References: Andrew Oram and Steve Talbott, Managing Projects with make 2nd ed., O'Reilly and Associates, Inc., 1993 and http://en.wikipedia.org/wiki/Makefile.

As you may suppose, it can get quite tedious generating object files each time you change a file. Suppose that you have the following pairs of files:

a.h, a.cpp
b.h, b.cpp
c.h, c.cpp

Suppose also that both b.h and c.h include a.h and that c.h includes b.h. These are called dependencies: if you change and recompile b.cpp, you must also recompile c.cpp, while if you change and recompile a.cpp, you must also recompile b.cpp and c.cpp, that is:

{ecelinux:1} g++ -c a.cpp
{ecelinux:2} g++ -c b.cpp a.o
{ecelinux:3} g++ -c c.cpp a.o b.o
{ecelinux:3} g++ main.cpp a.o b.o c.o

The make utility automates this process (to a point) by allowing the author to specify:

  • the dependencies, and
  • the command required to build the various files.

Once the dependencies are specified, the make untility will use the time stamps of the various files. For example, in the above example, b.o depends on a.o and b.cpp, and therefore, if either a.o or b.cpp has a more recent time stamp than it will recompile b.o.

The method for specifying these files is in a file called Makefile. To demonstrate, the following makefile would be appropriate for the above situation:

CPP = g++
OPTS =       # any options, e.g., -O for optimize
DEBUG =      # empty now, but assign -g for debugging

executable: main.cpp a.o b.o c.o c.h
	${CPP} ${OPTS} ${DEBUG} -o executable main.cpp a.o b.o c.o

c.o: c.cpp c.h
	${CPP} ${OPTS} ${DEBUG} -c c.cpp

b.o: b.cpp b.h
	${CPP} ${OPTS} ${DEBUG} -c b.cpp

a.o: a.cpp a.h
	${CPP} ${OPTS} ${DEBUG} -c a.cpp

clean:
	rm -f executable *.o core

You can access this Makefile (as well as other files) in here.

The components of this file include:

  • Variables which are of the form VAR = ... and which may be used throughout the file by referring to ${VAR}.
  • Targets which are the names of the files which are to be generated. In this case, we are calling the executable executable, simply to differentiate it from the other files.
  • Dependencies which are listed after the targets. If the time stamp on any of the dependencies is more recent than the time stamp of the target, the commands associated with the target are executed.
  • Commands which are a sequence of instructions (preceded by a tab(!!!) and not eight spaces)

If you simply type make at the command line, that is

{ecelinux:1} make

make will read Makefile and attempt to create the first target, in this case, executable. In this case, it will check the time stamp of the file executable (if it exists at all) and compare this to the time stamps of the dependencies main.cpp a.o b.o c.o c.h. For any dependency which is also a target, in this case, the three object files a.o b.o c.o, then it will ensure that these are up to date.

For example, the target a.o depends on a.cpp a.h. If either of these is more recent than a.o, make will execute g++ -c a.cpp. It will do the same for b.o and c.o.

Finally, having checked these three dependencies which are also targets, make will compare the time stamps of all the dependencies and if one of them is more recent than excecutable, make will execute g++ -o executable main.cpp a.o b.o c.o.

It is also possible to make other targets by specifying them at the command line, for example,

{ecelinux:2} make a.o

will ensure that a.o is up to date.

One common artificial target to include is clean. If you type

{ecelinux:3} make clean

make will search for the file clean, note that it is not present and consequently execute rm -f executable *.o core which will remove all object files, any core files, and the file executable. Because this does not actually create a file called clean, the command make clean can be run arbitrarily often.

Include Files

First, on ecelinux, the header files for the C++ Standard Library are located in the directory /usr/include/g++-3/. These header files (or .h files) store the declarations for the various classes and objects associated with the Standard Library.

You can read more about include files (header files) at http://en.wikipedia.org/wiki/Include_file.

Libraries

Now, the next question is "Where are the object files for the everything declared inside the corresponding header files?". You will not find a .o file in /usr/; instead you will find one of two possibilities:

  • Static libraries (or archives) (.a files), or
  • Dynamically-linked libraries (or archives) (.a files), or

We will look at thes two below, however, you can also read more about libraries at http://en.wikipedia.org/wiki/Library_(computer_science). This page includes information about both types of libraries listed below.

Static Library Archives

We will begin with a discussion on static archives. An archive is merely a collection of object files. For example, the archive for the math.h library archive is /usr/lib/libm.a. To inspect the object files stored in this archive, you can use the archive command:

{ecelinux:1} ar -t /usr/lib/libm.a | more
__libx_errno.o
acos.o
acosh.o
asin.o
asinh.o
atan2.o
atan.o
_TBL_atan.o
atanh.o
cbrt.o
ceil.o
copysign.o
cosh.o
erf.o
fabs.o
floor.o
gamma.o
--More--
{ecelinux:2} man ar
Reformatting page.  Please Wait... done

User Commands                                               ar(1)

NAME
     ar - maintain portable archive or library

SYNOPSIS
     /usr/ccs/bin/ar -d  [ -Vv ]  archive  file ...
                           .
                           .
                           .

Because the math library archive (cmath) are part of the C++ Standard Library, it is not necessary to explicitly specify that the math library archive should be linked. In C, however, it was necessary: if you used #include <math.h>, you were required to append a -lm to your compilation, e.g.,

{ecelinux:1} gcc main.c -lm
{ecelinux:2}

Similarly, in C++ you must specify, using the -l option any library archives which should be linked in. We will go through the process of generating a static archive containing the Complex2.o object file.

When you specify -llibname, the compiler looks for a library archive with the name liblibname.a. Thus, we will name our library libComplex2.a and (quickly) include the one object file Complex2.o:

{ecelinux:1} ls
Complex2.cpp  Complex2.h  Complex2.o  main2.cpp
{ecelinux:2} ar -cq libComplex2.a Complex2.o
{ecelinux:3} ls
Complex2.cpp  Complex2.h  Complex2.o  libComplex2.a  main2.cpp
{ecelinux:3}

Thus, to compile main2.cpp including this library, we must specify the library using -lComplex2 and we must specify any additional directories in which the compiler should search for library archives (by default, the compiler only searches the installed libraries).

{ecelinux:4} g++ main2.cpp -lComplex2 -L.
{ecelinux:5} ls
Complex2.cpp  Complex2.h  Complex2.o  a.out*  libComplex2.a  main2.cpp
{ecelinux:6} ./a.out 
-13.1288 - 15.2008j
{ecelinux:7} ls -al a.out 
-rwxr-xr-x    1 ece250   ece250      15560 Sep 16 04:58 a.out*
{ecelinux:8} 

Dynamically-Linked Library

A dynamically-linked, or shared, library is a library linked at run-time. This results in smaller executable files and greater efficiency as one dynamically-linked (shared) library is shared by many programs.

The first step is to generate a shared library (.so file) from the object files. This may be done through using the ld command:

{ecelinux:8} man ld
Reformatting page.  Please Wait... done

User Commands                                               ld(1)

NAME
     ld - link-editor for object files

SYNOPSIS

     /usr/ccs/bin/ld [ -64 ]  [ -a | -r ]  [ -b ]  [ -c name ]  [
     -C  ]   [ -G ]  [ -i ]  [ -m ]  [ -s ]  [ -t ]  [ -V ]  [ -B
                           .
                           .
                           .
{ecelinux:9} ld -shared Complex2.o -o Complex2.so
{ecelinux:10} ls
Complex2.cpp  Complex2.h  Complex2.o  Complex2.so  a.out*  libComplex2.a  main2.cpp
{ecelinux:11} 

We can now compile this in as if it were a standard object file:

{ecelinux:11} rm a.out
{ecelinux:12} g++ main2.cpp Complex2.so
{ecelinux:13} ls
Complex2.cpp  Complex2.h  Complex2.o  Complex2.so  a.out*  libComplex2.a  main2.cpp
{ecelinux:14} ls -al a.out
-rwxr-xr-x    1 ece250   ece250      11940 Sep 16 05:05 a.out*
{ecelinux:15}

Note that the executable is significantly smaller.

Previously, we indicated to the compiler as to where to look to find the library archive. In this case, however, it is up tot he operating system to find and allow the executable a.out to find the shared object. Consequently, we modify the LD_LIBRARY_PATH environment variable by adding the current working directory:

{ecelinux:15} setenv LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:.
{ecelinux:16} echo $LD_LIBRARY_PATH
/usr/local/lib:/usr/openwin/lib:/usr/openwin/lib/sparcv9:.
{ecelinux:17} ./a.out
-13.1288 - 15.2008j

Thus, we get the same result with a 23% smaller executable.