Having fun with gcc and make

Back to basics


I’ve been an IT professional for 6 year now and I’ve grown to love the scripting/programming language PHP. A lot of you use languages like these for easy and quick web development.

But we often forget that C/C++ is the foundation of these modern creations. Although I have had some C++ training in school but got alienated from it over the years. It was actually the love for PHP that got me back into C/C++. When compiling PHP and its set of extension I got into contact again with this fabulous language.

But this blog post isn’t entirely about C and C++. My goal is put gcc (the compiler par exelence) in the spotlight.

Gcc 101

Let’s start with the basic usage of gcc

The binaries

By default we use the gcc binary which is suited for compiling regular C programs. When programming in C++ it’s better to use the g++ binary.

A simple C++ script

It doesn’t get simpler than this: outputting Hello world!.

#include <iostream>
 
int main()
{
    std::cout << "Hello world!" << std::endl;
    return 0;
}

In PHP a simple echo would do, but in C++, you’ll need to initialize your tools:

  • Include you Input/output stream library called iostream.
  • Define a main function
  • Call the cout language construct that resides in the std namespace
  • Implement a proper return value for the main function

Compiling it into a binary

The easiest way to compile this script is by calling the g++ binary and by passing the name of the source file:

g++ test.cc

The g++ binary processes the test.cc source file and compiles it into machine code that is stored in a binary. Since we didn’t mention the name of the binary, g++ gives it the default name a.out

Running the binary

Calling the binary is simple, just execute it as follows:

./a.out

And you’ll get the expected output:

Hello world!

Gcc options

I’m just going to cover some basic gcc options. More information on gcc options can be found here.

Output

As mentioned, gcc defaults the name of its output binary to a.out. To control the name of the binary, use the -o option.

An example:

g++ test.cc -o test

This generates a binary based on test.cc called test.

Warnings

The -W option allows you to define the warning levels. Most developers set this value to -Wall. Compiling a program with these parameters goes as follows:

g++ -Wall test.cc

Binaries vs objects

By default gcc generates a single binary containing the entire program. When using a fair amount of source files, a minor modification can result in slow compile times. To avoid this, you can compile every C/C++ source file into an object in machine code. This happens via the -c option.

g++ test.cc -c

This command creates a file called test.o containing our simple program. This file is not executable, but can be linked into a binary when needed.

The upside is that you only have to recompile the source files in which modifications where made. Afterwards you can link the objects and create a binary.

g++ test.o -o test

This will start getting useful when you have a large number of source files.

Libraries

In most cases you won’t program all logic yourself unless you like to reinvent the wheel. You’ll probably include some third party code which is stored in the library folder of your operating system.

Let’s take the following example script I’ve found on the blog of Todd Papaioannou. It describes how to use the libcurl library to perform HTTP calls in a simple C++ program:

#include <string>  
#include <iostream>  
#include "curl/curl.h"  
 
using namespace std;  
 
// Write any errors in here  
static char errorBuffer[CURL_ERROR_SIZE];  
 
// Write all expected data in here  
static string buffer;  
 
// This is the writer call back function used by curl  
static int writer(char *data, size_t size, size_t nmemb,  
                  std::string *buffer)  
{  
  // What we will return  
  int result = 0;  
 
  // Is there anything in the buffer?  
  if (buffer != NULL)  
  {  
    // Append the data to the buffer  
    buffer->append(data, size * nmemb);  
 
    // How much did we write?  
    result = size * nmemb;  
  }  
 
  return result;  
}  
 
// You know what this does..  
void usage()  
{  
  cout << "curltest: \n" << endl;  
  cout << "  Usage:  curltest url\n" << endl;  
}   
 
/* 
 * The old favorite 
 */  
int main(int argc, char* argv[])  
{  
  if (argc > 1)  
  {  
    string url(argv[1]);  
 
    cout << "Retrieving " << url << endl;  
 
    // Our curl objects  
    CURL *curl;  
    CURLcode result;  
 
    // Create our curl handle  
    curl = curl_easy_init();  
 
    if (curl)  
    {  
      // Now set up all of the curl options  
      curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errorBuffer);  
      curl_easy_setopt(curl, CURLOPT_URL, argv[1]);  
      curl_easy_setopt(curl, CURLOPT_HEADER, 0);  
      curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);  
      curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writer);  
      curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer);  
 
      // Attempt to retrieve the remote page  
      result = curl_easy_perform(curl);  
 
      // Always cleanup  
      curl_easy_cleanup(curl);  
 
      // Did we succeed?  
      if (result == CURLE_OK)  
      {  
        cout << buffer << "\n";  
        return 0;  
      }  
      else  
      {  
        cout << "Error: [" << result << "] - " << errorBuffer;  
        return -1;  
      }  
    }  
  }  
}

As you might have noticed, Todd calls the curl.h header file located in the curl directory. This directory is located somewhere on your system. The exact directory depends on the version and distribution of your OS, but in my case the header file is located in /usr/include.

This is just the header, the actual libcurl library is stored in /usr/lib/ and is called libcurl.so. The upside about using this .so file is that the compiler doesn’t compile the entire library in our small program. A shared object (so) shares its content at runtime and can be reused by several other programs.

This architecture results in smaller binaries, faster compile times and more flexibility when having to modify a library. Compiling it requires the -l option. Libraries are named libname.so where name is replaced by the actual name of the library.

Gcc only needs to know the name of the library and the following command does so:

g++ -Wall test.cc -lcurl -o curltest

Notice the fact that we don’t mention libcurl.so, but just curl. The binary we generate is called curltest and takes a command line argument that represents the URL of the website to which the HTTP request is sent.

Static libraries

In some cases your library isn’t stored in an so file or you just want all logic included in you binary. In that case you’ll want to compile your libraries in a static way.

When there’s no shared object to link dynamically, your library is called libname.a. Compiling it via the -l option will statically compile it into your program.

When specifying the -static option, you can force static compilation.

Make makes it easy

Most of you have probably used the make command on Linux. We take this for granted and often forget the complexity behind the compilation tasks this command executes.

What does make do?

Make is actually a build tool that cooperates quite well with gcc. The make command consists of easy tasks, that as a whole can represent a complex build. All input is retrieved from a file called Makefile which includes all actions to be performed.

The build actions are grouped in targets. By default the target that is first defined will be called when executing make. All other targets can be invoked by adding the target name as an argument. The make install is a common example of this.

The Makefile

This is an example of a Makefile. Before any of the targets are called, a set of variables can be set. These variables are used to setup gcc as gcc cycles through all source files and compiles them.

In this simple example I’ve set 2 variables:

  • CC=g++: specify that g++ is our compiler binary
  • CFLAGS=-Wall: specify that the -Wall warning option should be passed to gcc

After the initial compilation phase when all source files are compiled into objects, the targets can be executed. Our Makefile contains 3 targets:

  • Main: the first target that is called by default. It links compiled objects into a single binary
  • Clean: deletes the main binary and all object files
  • Run: executes the binary
CC=g++
CFLAGS=-Wall
main: main.o hello.o
        $(CC) main.o hello.o  -o main
clean:
        rm -f main main.o hello.o
run: main
        ./main

Each of these targets has one or more dependencies. When the files main.o and hello.o don’t exist, the main target will not be executed and an error will occur. Similar patterns apply to the other targets.

We can now call:

  • make
  • make clean
  • make run

References

This is merely an illustration of what gcc and make can do. I didn’t even cover 1% of the features. For a complete reference, please visit the following pages:

No Comments

Leave a Reply

Your email is never shared.Required fields are marked *