Using gcc (the GNU C Compiler)


There's strictly no warranty for the correctness of this text. You use any of the information provided here at your own risk.


Contents:

  1. About gcc
  2. Giving the Executable another Filename
  3. Enabling Warnings
  4. Setting the C-Dialect
  5. Making the Executable Debuggable
  6. Optimization of the Executable
  7. Linking the Program against Libraries
  8. Compilation from more than one Source-File
  9. Using Object-Files for Compilation of Larger Applications
  10. Writing a Makefile and Using "make"
  11. Creating Shared Libraries for Linux Using "libtool"
  12. A Short Example of Building Executables with "autoconf" and "automake"
  13. Further Reading


1. About gcc

Preface (2021): This page was written many years ago.

"gcc" is the GNU project's open-source C-compiler.

It's freely available on Linux and also on Windows as part of the "MinGW"-project.

If you have some valid C-source-file called "hello.c" like:

#include <stdio.h>

int main(void)
{
    puts("Hello World !");
    return 0;
}

you can run

gcc hello.c

and gcc creates an executable called "a.out" in the current directory. On Linux, this can then be run doing

./a.out

So far, so good.


2. Giving the Executable another Filename

You can give the executable another filename than "a.out" with gcc's "-o"-option:

gcc hello.c -o hello


3. Enabling Warnings

If your code has severe mistakes, gcc aborts printing an error-message.

There may be other constructions in your code, gcc doesn't like very much, but don't make it abort. In this situation, gcc can print warnings, but this behaviour isn't enabled by default. You can use the "-Wall"-option to make gcc print warnings:

gcc -Wall hello.c -o hello

You should write C-code in a way, gcc doesn't show warnings, so you should use "-Wall". Remember, what Brian W. Kernighan, one of the two authors of "The C Programming Language" said:

Debugging is twice as hard as writing the code in the first place. Therefore, if you write
the code as cleverly as possible, you are, by definition, not smart enough to debug it.

So you should leave as few mistakes for debugging as possible. "-Wall" helps finding some of them at compilation-time.

Additionally, you can use the "-Wextra"-option (previously just called "-W") for even more warnings:

gcc -Wall -Wextra hello.c -o hello


4. Setting the C-Dialect

Programming languages develop throughout the times. For C, the ANSI-committee has defined standards, what valid C-syntax is.

An often-used standard is ISO C90.

There is also ISO C99, but some of its new features haven't been received well by a lot of programmers, so several major compilers like "Microsoft Visual C++" or "Borland C++BuilderX" still don't support it.

In fact, gcc does support C99 to a certain extend (you can use the "-std=c99"-option for this), but if you want your code to be able to be compiled by the other compilers mentioned above as well, you should still write in ISO C90.

To make gcc check, if your code is ISO C90-compatible, you can use the option "-ansi -pedantic":

gcc -Wall -Wextra -ansi -pedantic hello.c -o hello


5. Making the Executable Debuggable

If you want to debug your program lateron, for example using "gdb", the GNU debugger, you need to write certain information into the executable at compilation-time, so gdb knows the source-code of your program for example.

gcc writes this information, if the "-g"-option is given:

gcc -g -Wall -Wextra -ansi -pedantic hello.c -o hello

You can then run

gdb hello

and type "list" there to see the result: gdb can print the source-code written into the executable.


6. Optimization of the Executable

If your program runs fine and doesn't have any known bugs, you may want gcc to compile it to run even faster or to be smaller in size:
The gcc-options


7. Linking the Program against Libraries

When you use the "#include"-directive, the program is linked against some library.

You can check the libraries used by an executable with the "ldd"-command. For example shows

ldd ./hello

the libraries, an executable "hello" is linked against.

Some standard libraries like "libc.so.6" are recognized by "gcc" automatically. But the names of other libraries have to be passed to "gcc" manually.
If, for example, you want to use the math-library "libm.so.6" in a program "printsin.c", that prints the sinus of 45 degrees:

#include <stdio.h>
#include <math.h>

int main(void)
{
    printf("%f\n", sin(45));
    return 0;
}

compilation with

gcc -Wall -W -ansi -pedantic -o prsin printsin.c

doesn't work, because the math-library isn't found. You have to tell "gcc", that the program needs this library.

You can do that, using the "-l"-option:

Directly after the "-l", you have to pass the library-name: It's got to be just the part of the name between "lib" and ".so.6". So to link against "libm.so.6", it's just "-lm". Not more:

gcc -Wall -W -ansi -pedantic -o prsin printsin.c -lm

compiles the program above.

When "-l" is used, "gcc" searches its default-path for the library.
Several directories like "/lib" and "/usr/lib" are in this path. That's why the compilation above already worked.
But you could ask "gcc" to search for a library in a certain other directory using the "-L"-option. Here's an example, but with "/usr/lib" again:

gcc -Wall -W -ansi -pedantic -o prsin printsin.c -lm -L/usr/lib

To make "-L" work with any directory, you would have to do

/sbin/ldconfig /path/to/my/directory

as "root" first to add the directory to the "ld-library-cache". Usually, this isn't necessary, because most libraries install themselves into default-directories, because they want to be found by programs.

Notice, that the options "-l" and "-L" have to be given after the file-names to compile (because they refer to the linking-process).

If you want to find out more about libraries, you can just read on or go straight here.


8. Compilation from more than one Source-File

The code of larger C-programs is often separated into several source-files:

First, there is a ".c"-file containing the "main()"-function.

Then, there are one or more ".h"-files (header-file): In these file, there are the declarations of the program's functions and structs and also the "#define"-directives.

The ".c"-file with the "main()"-function contains "#include"-directives pointing to the ".h"-files, for example:

#include "mine.h"

Notice, that quotation-marks are used in the "#include"-directive, if the header-file is in the same directory as the main ".c"-file.
If gcc shall search for the header-file in the default-path, the header-file's name is given in angle brackets ("<>") like in

#include <stdio.h>

The functions declared in the ".h"-files are then written into ".c"-files with a name corresponding to that of the ".h"-files.
gcc has to be called with the names of the ".c"-files then.

Please take a look at this example with three files now (we are going to use this example for several purposes):

"hello.c" (1):

#include <stdio.h>
#include "mine.h"

int main(void)
{
    puts(printhello());
    return 0;
}

"mine.h" (2):

const char *printhello(void);

and "mine.c" (3):

const char *printhello(void)
{
    return "Hello World !";
}

These three files can be compiled together in a directory with

gcc -Wall -Wextra -ansi -pedantic hello.c mine.c -o hello


9. Using Object-Files for Compilation of Larger Applications

Compiling larger applications is rather time-consuming. So it would be rather painful, if the whole program had to be recompiled each time changes are made to only one or a few of many source-files.

Fortunately there's a solution for that: With the "-c"-option, gcc can produce ".o"-files (object-files) of ".c"-files.

".o"-files already contain machine-code, but they are not yet linked together to an executable.

So if you take the three files "hello.c", "mine.c" and "mine.h" from above and run

gcc -c hello.c mine.c

on them, the files "hello.o" and "mine.o" are created.

If for example "hello.c" is changed during further development, "mine.o" can be used for the next compilation-process instead of "mine.c".
If there are a lot of ".c"-files, using ".o"-files instead makes compilation significantly faster.


10. Writing a Makefile and Using "make"

04-19-2020

"make" is a GNU-tool for compiling larger applications. It reads in a file called "Makefile", that contains instructions in a certain format, and executes these instructions.

It would take too much time to recompile a whole project, when just small changes have been made to one or more source-files. Using "make" and a Makefile makes it possible, to just recompile the required files of a project. The rest of the project is relinked from already existing object-files (".o"), which contain previously compiled code.
gcc's option "-c" makes it create just the ".o"-object-files. Another "gcc source.o" then creates the executable.

After reading the Makefile, "make" recognizes automatically, which source files have been changed and need recompiling, simply by checking the time signature of the source files (command "touch").

In a Makefile, you first declare what file should be compiled, followed by a colon. After the colon you declare the object-files (".o") that are needed to compile the executable.
In the next line, you press one "tab" (it has to be a "tab", four space characters won't work), then specify the "gcc"-command that is needed for compiling (including "-o [filename]").

You don't have to mention the header-files (".h") in the Makefile, because they are included in the source-files themselves.
Header-files contain the declarations of the used structures and functions. The declarations should be inside an "#ifndef-statement ("ifndef" for "if not defined"), to make sure, that the declarations are only imported once into the source-files:

#ifndef NAME
#define NAME
...
...
...
#endif

Here's an example of a Makefile, two according ".c"-source-files and a ".h"-header-file.

example: example1.o example2.o
	gcc example1.o example2.o -o example

clean:
	rm ./*.o ./example

And the two according ".c"-files and the ".h"-file:

#include <stdio.h>
#include "example.h"

/* example1.c */

int main() {
    int size = 10; 
    int numbers[size];
    int i;
    for (i = 0; i < size; i++) {
        numbers[i] = i;
    }
    printIntArray(numbers, size);
    int sum = sumNumbers(numbers, size);
    printf("%d\n", sum);
    return 0;
}

---------------------------------------------------------

#include <stdio.h>
#include "example.h"

/* example2.c */

void printIntArray(int arr[], int size) {
    int i;
    printf("[");
    for (i=0; i < size; i++) {
        printf("%d", arr[i]);
        if (i < size - 1) {
            printf("%d", ", ");
        }
    }
    printf("]\n");
}

int sumNumbers(int arr[], int size) {
    int sum = 0;
    int i;
    for (i=0; i < size; i++) {
        sum += arr[i];
    }
    return sum;
}

---------------------------------------------------------

/* example.h */

#ifndef EXAMPLE
#define EXAMPLE

void printIntArray(int arr[], int size);
int sumNumbers(int arr[], int size);

#endif

---------------------------------------------------------

This also works for C++. Here's the same example, translated to C++11:

example: example1.o example2.o
	g++ -std=c++11 example1.o example2.o -o example

example1.o: example1.cpp
	g++ -std=c++11 -c example1.cpp

example2.o: example2.cpp
	g++ -std=c++11 -c example2.cpp

clean:
	rm ./*.o ./example

---------------------------------------------------------

#include <iostream>
#include <vector>
#include "example.h"

// example1.cpp

using namespace std;

int main() {
    
    vector<int> numbers;
    for (int i = 0; i < 10; i++) {
        numbers.push_back(i);
    }
    printIntVector(numbers);
    int sum = sumNumbers(numbers);
    cout << sum << endl;
    return 0;
}

---------------------------------------------------------

#include <iostream>
#include <vector>
#include "example.h"

// example2.cpp

void printIntVector(vector<int> vec) {
    int i;
    int vecsize = vec.size();
    cout << "[";
    for (i=0; i < vecsize; i++) {
        cout << vec[i];
        if (i < vecsize - 1) {
            cout << ", ";
        }
    }
    cout << "]" << endl;
}

int sumNumbers(vector<int> vec) {
    int sum = 0;
    for (auto i: vec) {
        sum += i;
    }
    return sum;
}

---------------------------------------------------------

// example.h
#ifndef EXAMPLE
#define EXAMPLE

using namespace std;

void printIntVector(vector<int> vec);
int sumNumbers(vector<int> vec);

#endif


11. Creating Shared Libraries for Linux Using "libtool"

With the just described usage of object-files, collections of functions can be created, that can be linked with several programs to executables.

One disadvantage of this way of reusing code is, that all functions of the object-file are linked into the executable at compilation-time.
If a program needs just one or a few of these functions, the size of the executable gets bigger than necessary.

To avoid this, programs can be linked against socalled libraries, that can be compiled from object-files (or directly from source-files).
There are:

Especially shared libraries can also be used as socalled "dynamically loaded libraries": That means, it is possible to implement into the main-program, when exactly the library shall be loaded into memory (see "man 3 dlopen" for details).

Note: Libraries contain already compiled code, so the library-developer can choose, if he wants to make the library's source-code available to the library-user (another programmer who wants to link his program against the library) or not.


So let's create a shared library for Linux from "mine.c", one of the three example-files "hello.c", "mine.c" and "mine.h" above, and then link "hello.c" against this library. What we're after is a library-file called "libmine.so.0".

It is possible to use "gcc" directly to create libraries, but it is quite difficult. Fortunately, there is a GNU shell-script (with over 7.000 lines of code) called

libtool

that takes care of the complicated "gcc"-calls needed (see "info libtool" for its documentation).

So, having "hello.c", "mine.c" and "mine.h" in a directory, we do

libtool --mode=compile gcc -c -Wall -W -ansi -pedantic mine.c

In the directory, the files "mine.o" and "mine.o" are created. Notice, doing

ls -A

that a hidden directory called

.libs

containing "mine.o" has also been created by "libtool".

Then we do

libtool --mode=link gcc -Wall -W -ansi -pedantic -o libmine.la mine.lo -rpath /usr/lib

Now move to the ".libs"-directory ("cd .libs"): There you find several files with "libmine.so..." !

There are some more files in this directory, but we want to get rid of them, so do in the ".lib"-directory:

rm libmine.a libmine.la libmine.lai mine.o

Ok, now only

libmine.so
libmine.so.0
libmine.so.0.0.0

should be left there. Become "root" and copy those to "/usr/lib" ("cp * /usr/lib").

Become a normal user again and move back to the parent directory with "hello.c" in it. We only need "hello.c" and "mine.h" here now. So delete the rest doing

rm -r .libs libmine.la mine.c mine.lo mine.o

Don't worry, that "mine.c" is deleted too, as you can always find its source-text above in this document.

Then we link "hello.c" agains our shared library "libmine.so.0" doing

gcc -Wall -W -ansi -pedantic -o hello hello.c -lmine

The last option "-lmine" is also quite important. It has already been explained above.

Then our working executable "./hello" is created, that is linked against our library "libmine.so.0", as you can test with the "ldd"-command:

ldd ./hello

After going through this example and testing the executable doing "./hello", you may want to delete the "mine"-library from your system again. To do that, do as "root":

rm /usr/lib/libmine.so /usr/lib/libmine.so.0 /usr/lib/libmine.so.0.0.0

You could also place the library somewhere else than in "/usr/lib". To do that you would have to

After that you could compile your executable with gcc's "-l" and "-L"-options.
But notice, that it wouldn't make much sense to install a shared library into a non-default-directory, because other programs on other systems may not find it then.


12. A Short Example of Building Executables with "autoconf" and "automake"

Here's a short example of compiling "hello.c", "mine.c" and "mine.h" with "autoconf" and "automake". If you have the three files in a directory, create the following two more files:

"Makefile.am" (1):

bin_PROGRAMS = hello
hello_SOURCES = hello.c mine.c

and "configure.in" (2):

AC_INIT(hello.c)
AM_INIT_AUTOMAKE(hello,0.1)
AC_PROG_CC
AC_PROG_INSTALL
AC_OUTPUT(Makefile)

Then, do:

aclocal
autoconf
touch NEWS README AUTHORS ChangeLog
automake -a

After that you can build the "hello"-executable with the commonly used commands:

./configure
make


13. Further Reading

With the information provided here you should be able to understand GNU's manual on development tools.
 

Back