Skip to main content
p. 364

15.1 The C Compiler

Knowing how to use the C programming language compiler can give you a great deal of insight into the origin of the programs that you see on your Linux system. Most Linux utilities and many applications on Linux systems are written in C or C++. This chapter will primarily use examples in C, but you’ll be able to carry the information over to C++.

C programs follow a traditional development process: you write programs, you compile them, and they run. That is, when you write C program code and want to run it, you must compile human-readable code into a binary low-level form that the computer’s processor understands. The code that you write is called source code, and can encompass many files. You can compare this to the scripting languages that we’ll discuss later, where you don’t need to compile anything.

note

Most distributions do not include the tools necessary to compile C code by default. If you don’t see some of the tools described here, you can install the build-essential package for Debian/Ubuntu or the “Development Tools” yum groupinstall for Fedora/CentOS. Failing that, try a package search for “gcc” or “C compiler.”

The C compiler executable on most Unix systems is the GNU C compiler, gcc (often referred to by the traditional name cc), though the newer clang compiler from the LLVM project is gaining popularity. C source code files end with .c. Take a look at this single, self-contained C source code file, hello.c, from The C Programming Language, 2nd edition, by Brian W. Kernighan and Dennis M. Ritchie (Prentice Hall, 1988):

#include <stdio.h>

int main() {
printf("Hello, World.\n");
}

Save this source code in a file called hello.c and then run the compiler with this command:

$ cc hello.c
p. 365

The result is an executable named a.out, which you can run like any other executable on the system. However, you should probably give the executable another name (such as hello). To do this, use the compiler’s -o option:

$ cc -o hello hello.c

For small programs, there isn’t much more to compiling than that. You might need to add an extra library or include directory, but let’s look at slightly larger programs before getting into those topics.

15.1.1 Compiling Multiple Source Files​

Most C programs are too large to reasonably fit within a single source code file. Mammoth files become too disorganized for the programmer to manage, and compilers sometimes even have trouble processing large files. Therefore, developers typically separate the source code into component pieces, giving each piece its own file.

When compiling most .c files, you don’t create an executable right away. Instead, use the compiler’s -c option on each file to create object files containing binary object code that will eventually go into the final executable. To see how this works, let’s say you have two files, main.c (which gets the program started) and aux.c (which does the actual work), that appear as follows:

main.c:

void hello_call();

int main() {
hello_call();
}

aux.c:

#include <stdio.h>

void hello_call() {
printf("Hello, World.\n");
}

The following two compiler commands do most of the work of building the program: creating the object files.

$ cc -c main.c
$ cc -c aux.c

After these commands complete, you’ll have two object files: main.o and aux.o.

p. 366

An object file is a binary file that a processor can almost understand, except that there are still a few loose ends. First, the operating system doesn’t know how to start up an object file, and second, you likely need to combine several object files and some system libraries to make a complete program.

To build a fully functioning executable program from one or more object files, you must run the linker, the ld command in Unix. Programmers rarely use ld on the command line, however, because the C compiler knows how to run the linker program. To create an executable called myprog from these two object files, run this command to link them:

$ cc -o myprog main.o aux.o
note

Although you can compile each source file with a separate command, the preceding example already hints that it can be hard to keep track of them all during the compiling process; this challenge gets much worse as the number of source files multiplies. The make system described in Section 15.2 is the traditional Unix standard for managing and automating compiles. You’ll see how important having a system like make is as we deal with managing the files described in the next two sections.

Turn your attention back to the file aux.c. As mentioned, its code does the actual work of the program, and there can be many files like its resultant aux.o object file that are necessary to build the program. Now imagine that other programs might be able to make use of the routines we wrote. Could we reuse these object files? That’s where we’re headed next.

15.1.2 Linking with Libraries​

Running the compiler on source code usually doesn’t result in enough object code to create a useful executable program all by itself. You need libraries to build complete programs. A C library is a collection of common precompiled components that you can build into your program, and it’s really not much more than a bundle of object files (along with some header files, which we’ll talk about in Section 15.1.4). For example, there’s a standard math library that many executables draw from because it provides trigonometric functions and the like.

Libraries come into play primarily at link time, when the linker program (ld) creates an executable from object files. Linking using a library is often called linking against a library. This is where you’re most likely to have a problem. For example, if you have a program that uses the curses library but you forget to tell the compiler to link against that library, you’ll see linker errors like this:

badobject.o(.text+0x28): undefined reference to 'initscr'

The most important parts of these error messages are in bold. When the linker program examined the badobject.o object file, it couldn’t find the function that appears in bold, and thus it couldn’t create the executable. p. 367 In this particular case, you might suspect that you forgot the curses library because the missing function is initscr(); if you do a web search for the function name, you’ll nearly always find a manual page or some other reference to the library.

note

Undefined references don’t always mean that you’re missing a library. One of the program’s object files could be missing in the link command. It’s usually easy to differentiate between library functions and functions in your object files, because you’ll likely recognize the ones you wrote, or at least be able to search for them.

To fix this problem, you must first find the curses library and then use the compiler’s -l option to link against the library. Libraries are scattered throughout the system, though most libraries reside in a subdirectory named lib (/usr/lib is the system default location). For the preceding example, the basic curses library file is libcurses.a, so the library name is curses. Putting it all together, you would link the program like this:

$ cc -o badobject badobject.o -lcurses

You must tell the linker about nonstandard library locations; the parameter for this is -L. Let’s say that the badobject program requires libcrud.a in /usr/junk/lib. To compile and create the executable, use a command like this:

$ cc -o badobject badobject.o -lcurses -L/usr/junk/lib -lcrud
note

If you want to search a library for a particular function, use the nm command with the --defined-only symbol filter. Be prepared for a lot of output. For example, try this: nm --defined-only libcurses.a. On many distributions, you can also use the less command to view the contents of a library. (You might need to use the locate command to find libcurses.a; many distributions now put libraries in architecture-specific subdirectories in /usr/lib, such as /usr/lib/x86_64-linux-gnu/.)

There’s a library on your system called the C standard library, containing fundamental components considered a part of the C programming language. Its basic file is libc.a. When you compile a program, this library is always included unless you specifically exclude it. Most programs on your system use the shared version, so let’s talk about how that works.

15.1.3 Working with Shared Libraries​

A library file ending with .a (such as libcurses.a) is called a static library. When you link a program against a static library, the linker copies the necessary machine code from the library file into your executable. Once it does this, the final executable no longer needs the original library file when it runs, and because your executable has its own copy of the library code, the executable’s behavior is not subject to change if the .a file changes.

However, library sizes are always increasing, as is the number of libraries in use, and this makes static libraries wasteful in terms of disk space p. 368 and memory. In addition, if a static library is later found to be inadequate or insecure, there’s no way to change the executables that had been linked against it, short of finding and recompiling every executable.

Shared libraries counter these problems. Linking a program against a shared library doesn’t copy the code into the final executable; it just adds references to names in the code of the library file. When you run the program, the system loads the library’s code into the process memory space only when necessary. Many processes can share the same shared library code in memory. And if you need to slightly modify the library code, you can generally do so without recompiling any programs. When updating software on your Linux distribution, the packages that you’re updating can include shared libraries. When your update manager asks you to reboot your machine, sometimes it’s doing so to be sure that every part of your system is using new versions of shared libraries.

Shared libraries do have their own costs: difficult management and a somewhat complicated linking procedure. However, you can bring shared libraries under control if you know four things:

  • How to list the shared libraries that an executable needs
  • How an executable looks for shared libraries
  • How to link a program against a shared library
  • How to avoid the common shared library pitfalls

The following sections tell you how to use and maintain your system’s shared libraries. If you’re interested in how shared libraries work or if you want to know about linkers in general, you can check out Linkers and Loaders by John R. Levine (Morgan Kaufmann, 1999); “The Inside Story on Shared Libraries and Dynamic Loading” by David M. Beazley, Brian D. Ward, and Ian R. Cooke (Computing in Science & Engineering, September/October 2001); or online resources such as the Program Library HOWTO (https://bit.ly/3q3MbS6). The ld.so(8) manual page is also worth a read.

How to List Shared Library Dependencies​

Shared library files usually reside in the same places as static libraries. The two standard library directories on a Linux system are /lib and /usr/lib, though many more can be scattered throughout your system. The /lib directory should not contain static libraries.

A shared library has a suffix that contains .so (shared object), as in libc-2.15.so and libc.so.6. To see what shared libraries a program uses, run ldd prog, where prog is the executable name. Here’s an example for the shell:

$ ldd /bin/bash
linux-vdso.so.1 (0x00007ffff31cc000)
libgtk3-nocsd.so.0 => /usr/lib/x86_64-linux-gnu/libgtk3-nocsd.so.0
(0x00007f72bf3a4000)
libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f72bf17a000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f72bef76000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f72beb85000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0
(0x00007f72be966000)
/lib64/ld-linux-x86-64.so.2 (0x00007f72bf8c5000)
p. 369

In the interest of optimal performance and flexibility, executables alone don’t usually know the locations of their shared libraries; they know only the names of the libraries, and perhaps a little hint about where to find them. A small program named ld.so (the runtime dynamic linker/loader) finds and loads shared libraries for a program at runtime. The preceding ldd output shows the library names on the left side of the =>; that’s what the executable knows. The right side of the => shows where ld.so finds the library.

The final line of output shows the actual location of ld.so: /lib/ld-linux.so.2.

How ld.so Finds Shared Libraries​

One of the common trouble points for shared libraries is the dynamic linker being unable to find a library. The first place the dynamic linker should normally look for shared libraries is an executable’s preconfigured runtime library search path (rpath), if it exists. You’ll see how to create this path shortly.

Next, the dynamic linker looks in a system cache, /etc/ld.so.cache, to see if the library is in a standard location. This is a fast cache of the names of library files found in directories listed in the cache configuration file /etc/ld.so.conf.

note

As is typical of many of the Linux configuration files that you’ve seen, ld.so.conf may in turn include a number of files from a directory such as /etc/ld.so.conf.d.

Each line in ld.so.conf (or in the files it includes) is the name of a directory that you want to include in the cache. The list of directories is usually short, containing something like this:

/lib/i686-linux-gnu
/usr/lib/i686-linux-gnu

The standard library directories /lib and /usr/lib are implicit, which means that you don’t need to include them in /etc/ld.so.conf.

If you alter ld.so.conf or make a change to one of the shared library directories, you must rebuild the /etc/ld.so.cache file by hand with the following command:

# ldconfig -v

The -v option provides detailed information on libraries that ldconfig adds to the cache and any changes that it detects.

There is one more place that ld.so looks for shared libraries: the environment variable LD_LIBRARY_PATH. We’ll talk about this soon.

Don’t get into the habit of adding stuff to /etc/ld.so.conf. You should know what shared libraries are in the system cache, and if you put every bizarre little shared library directory into the cache, you risk conflicts p. 370 and an extremely disorganized system. When you compile software that needs an obscure library path, give your executable a built-in runtime library search path. Let’s see how to do that.

Say you have a shared library named libweird.so.1 in /opt/obscure/lib that you need to link myprog against. You shouldn’t have this strange path in /etc/ld.so .conf, so you need to convey that path to the linker. Link the program as follows:

$ cc -o myprog myprog.o -Wl,-rpath=/opt/obscure/lib -L/opt/obscure/lib -lweird

The -Wl,-rpath option tells the linker to include the specified directory into the executable’s runtime library search path. However, even if you use -Wl,-rpath, you still need the -L flag.

If you need to change the runtime library search path of an existing binary, you can use the patchelf program, but it’s generally better to do this at compile time. (ELF, the Executable and Linkable Format, is the standard format used for executables and libraries on Linux systems.)

How to Avoid Problems with Shared Libraries​

Shared libraries provide remarkable flexibility, not to mention some really incredible hacks, but you can abuse them to the point where your system becomes an utter and complete mess. Three particularly bad things can happen:

  • Missing libraries
  • Terrible performance
  • Mismatched libraries

The number one cause of all shared library problems is the environment variable LD_LIBRARY_PATH. Setting this variable to a colon-delimited set of directory names makes ld.so search the given directories before anything else when looking for a shared library. This is a cheap way to make programs work when you move a library around, if you don’t have the program’s source code and can’t use patchelf, or if you’re just too lazy to recompile the executables. Unfortunately, you get what you pay for.

Never set LD_LIBRARY_PATH in shell startup files or when compiling software. When the dynamic runtime linker encounters this variable, it often must search through the entire contents of each specified directory more times than you’d care to know. This causes a big performance hit, but more importantly, you can get conflicts and mismatched libraries because the runtime linker looks in these directories for every program.

If you must use LD_LIBRARY_PATH to run some crummy program for which you don’t have the source (or an application that you’d rather not p. 371 recompile, like Firefox or some other beast), use a wrapper script. Let’s say your executable is /opt/crummy/bin/crummy.bin and needs some shared libraries in /opt/crummy/lib. Write a wrapper script called crummy that looks like this:

#!/bin/sh
LD_LIBRARY_PATH=/opt/crummy/lib
export LD_LIBRARY_PATH
exec /opt/crummy/bin/crummy.bin $@

Avoiding LD_LIBRARY_PATH prevents most shared library problems. But one other significant problem that occasionally comes up with developers is that a library’s API may change slightly from one minor version to another, breaking installed software. The best solutions here are preventive: either use a consistent methodology to install shared libraries with -Wl,-rpath to create a runtime link path, or simply use the static versions of obscure libraries.

15.1.4 Working with Header (Include) Files and Directories​

C header files are additional source code files that usually contain type and library function declarations, often for the libraries that you just saw. For example, stdio.h is a header file (see the simple program in Section 15.1).

A great number of compilation problems are related to header files. Most such glitches occur when the compiler can’t find header files and libraries. There are even some cases where a programmer forgets to add the #include directive to code that includes a required header file, causing a failure to compile some of the source code.

Include File Problems​

Tracking down the correct header files isn’t always easy. Sometimes you’ll get lucky and find them with locate, but in other instances, there are several include files with the same names in different directories, and it’s not clear which is the correct one. When the compiler can’t find an include file, the error message looks like this:

badinclude.c:1:22: fatal error: notfound.h: No such file or directory

This message reports that the compiler can’t find the notfound.h header file that the badinclude.c file references. If we look at badinclude.c (on line 1, as the error indicates), we’ll find a line that looks like this:

#include <notfound.h>

Include directives like this do not specify where a header file should reside, only that it should be either in the default location or in one specified on the compiler command line. Most of these locations have include in their names. The default include directory in Unix is /usr/include; the p. 372 compiler always looks there unless you explicitly tell it not to. Of course, you’re unlikely to see the preceding error if the include file is in the default location, so let’s see how to make the compiler look in other include directories.

For example, say that you find notfound.h in /usr/junk/include. To tell the compiler to add this directory to its search path, use the -I option:

$ cc -c -I/usr/junk/include badinclude.c

Now the compiler should no longer stumble on the line of code in badinclude.c that references the header file.

note

You’ll learn more about how to find missing include files in Chapter 16.

You should also beware of includes that use double quotes (" ") instead of angle brackets (< >), like this:

#include "myheader.h"

Double quotes mean that the header file is not in a system include directory, and usually indicate that the include file is in the same directory as the source file. If you encounter a problem with double quotes, you’re probably trying to compile incomplete source code.

The C Preprocessor​

It turns out that the C compiler doesn’t actually do the work of looking for these include files. That task falls to the C preprocessor, a program that the compiler runs on your source code before parsing the actual program. The preprocessor rewrites source code into a form that the compiler understands; it’s a tool for making source code easier to read (and for providing shortcuts).

Preprocessor commands in the source code are called directives, and they start with the # character. There are three basic types of directives:

Include files An #include directive instructs the preprocessor to include an entire file. Note that the compiler’s -I flag is actually an option that causes the preprocessor to search a specified directory for include files, as you saw in the previous section.

Macro definitions A line such as #define BLAH something tells the preprocessor to substitute something for all occurrences of BLAH in the source code. Convention dictates that macros appear in all uppercase, but it should come as no shock that programmers sometimes use macros whose names look like functions and variables. (Every now and then, this causes a world of headaches. Many programmers make a sport out of abusing the preprocessor.)

note

Instead of defining macros within your source code, you can also define them by passing parameters to the compiler: -DBLAH=something works like the preceding #define directive.

p. 373

Conditionals You can mark out certain pieces of code with #ifdef, #if, and #endif. The #ifdef MACRO directive checks to see whether the preprocessor macro MACRO is defined, and #if condition tests to see whether con-dition is nonzero. For both directives, if the condition following the “if” statement is false, the preprocessor does not pass any of the program text between the #if and the next #endif to the compiler. If you plan to look at any C code, you’d better get used to this.

Let’s look at an example of a conditional directive. When the preprocessor sees the following code, it checks to see whether the macro DEBUG is defined and, if so, passes the line containing fprintf() on to the compiler. Otherwise, the preprocessor skips this line and continues to process the file after the #endif:

#ifdef DEBUG
fprintf(stderr, "This is a debugging message.\n");
#endif
note

The C preprocessor doesn’t know anything about C syntax, variables, functions, and other elements. It understands only its own macros and directives.

On Unix, the C preprocessor’s name is cpp, but you can also run it with gcc -E. However, you’ll rarely need to run the preprocessor by itself.