Return to lecture notes index
November 2, 2010 (Lecture 16)

Credits

What is make

"Make" is a utility that helps a software developer build software. Without some way of organizing a large build, software developers would be forced to waste time by rebuilding all components -- or to risk missing a modified component and building an inconsistent update. Make automates the process ensuring a fast, reliable build. Its configuration file, the "Makefile", serves roughly the same purpose as a CodeWarrior or Microsoft Project.

Why make?

Its easy and efficient to build small projects with a compile command. For example,

cc myapp.c -o myapp. 

However, this method becomes very inefficient for large projects such as the ones you will be building in this class. For example, running

cc foo.c bar.c baz.c -o myapp 

when baz.c changes means you are wasting time compiling the other files. Ideally, you would like to only compile baz.c and then link it with the other object files. You could do this yourself, but make lets you automate it. The goal of make is to build your project with the minimum amount of work.

Basic Idea

You supply make with a file (whose default nameis " Makefile") which describes the dependenciesbetween files in your project and a method for satisfying each dependency. These dependenciesform a DAG - for example:

Now, when make is run, for each dependency, if the target file is older than the file it depends on, it will execute the method to bring the target file up to date. For example, suppose we change foo.h. Make will recognize that foo.o and bar.o depend on it and will recompile them. Next, it will relink myapp.

Simple Makefile

A makefile consists of one or more rules which have the form:

target : source(s) 
[TAB]command
[TAB]command

The first character of each command line must be a TAB. The makefile corresponding to the above graph is

myapp: foo.o bar.o baz.o
       cc foo.o bar.o baz.o -o myapp
foo.o: foo.c foo.h
       cc -c foo.c
bar.o: bar.c bar.h foo.h  baz.h
       cc -c bar.c
baz.o: baz.c baz.h 
       cc -c baz.c

Comments Any line beginning with a `#' is ignored by make.

Makedepend

One headache with makefiles is making sure that you have specified dependencies to header files correctly. Makedepend is a tool that will do this for you automatically. You run makedepend on all your source files:

makedepend foo.c bar.c baz.c and it will add the correct dependencies to Makefile. You can use the -MM option of gcc to do the same thing. By default, makedepend adds the dependencies directly to a file named "Makefile" or "makefile". The -MM option of gcc/++ outputs the dependency rule to the console.

Macros

Makefiles can have macro definitions and uses. For example, with

CC    = gcc 
CCOPT = -g -DDEBUG -DPRINT 
#CCOPT = -O2

foo.o: foo.c foo.h
       $(CC) $(CCOPT) -c foo.c

foo.c will be compiled for debugging or with optimization depending on which CCOPT isuncommented. Macros definitions can also be modified when they are used. For example,

OBJECTS = foo.o bar.o baz.o 
dep:
      makedepend $(OBJECTS:.o=.c)

will cause makedepend to be called on foo.c, bar.c and baz.c when the dep target is made.

Suffix Rules

Often, a project has many rules that have common commands applied to files with the same suffixes. For example, each of the .o files in our example depend on its parent .c file and is compiled with the same command. We can replace this with a suffix rule:

.c.o :
       $(CC) $(CCOPT) -c $*.c -o $@

$* is a special macro for the prefix the two files share and $@ contains the target name.

Default Rules

Make has a lot of built in defaults that are used when a user-defined rule can't be found. For example, it can infer that foo.o depends on foo.c and use a generic C compilation rule to update foo.o. In general, AVOID USING THE DEFAULT RULES.

A Menagerie of Makes

There are many different Makes with widely varying features. Perhaps the most popular alternate make is GNU Make, which has the advantage of having a manual freely available. Some makes exploit the parallelism of the dependency graph to distribute the make across a number of workstations.

A Larger Example

CC    = gcc 
#CC   = cc 
CPP   = g++ 
INC   = -ILEDA/incl 
LIB   = -LLEDA 
CCOPT = -g -DDEBUG -DSPACEMONITOR\ 
            $(INC) $(LIB) 
#CCOPT  = -O2 $(INC) $(LIB) 
CPPOPT= $(CCOPT)

GENERALS  = cache.o disthandler.o \  
            internals.o dispatcher.o util.o \  
            builder.o group.o relation.o \  
            errprint.o diffmaprle.o aapair.o \  
            aablock.o stdrel_llb.o \  
            stdrel_sortseqpair.o

DISPLAY   = display.o 
INTERNALS = HPF.o 

DISPLIBS  = -lP -lG -lL -lWx -lX11 -lm 

all: fung  

fung: libdist.a libfxtimers.a fung.o \   
  $(DISPLAY)
      $(CPP) $(CPPOPT) fung.o \
      libdist.a libfxtimers.a \         
      $(DISPLAY) $(DISPLIBS) -o fung

libdist.a : $(GENERALS) $(INTERNALS)
      ar ruv libdist.a $(GENERALS) \           
          $(INTERNALS)

$(DISPLAY): display.C
      $(CPP) $(CPPOPT) -c display.C \          
      -o $(DISPLAY)

.c.o :
       $(CC) $(CCOPT) -c $*.c -o $@

dep: 
       makedepend $(INC) \         
       $(GENERALS:.o=.c) \           
       $(INTERNALS:.o=.c) \         
       $(DISPLAY:.o=.C)

Function-like Macros

Take a second. Try to call to mind the use we've made of the C preprocesssor. What have we done:

Now, let's think for a moment about functions. Functions are great. They give us a way to reuse code -- we can write a function once and use them over-and-over again. This gets us much bang for the buck -- and makes the code maintainble by ensuring that corrections or changes need only be made in once place. And, it helps to make code meaningful by identifying segments of code with meaningful and descriptive names.

But, these benefits come at a cost. When you take 15-213, you'll learn a little bit about the mechanics of a function call. For now, let's just recall something we've said before -- there is overhead associated with making a function call and in returning from a function. It takes time.

Normally, for most functions, this small amount of overhead is no big deal. But, for really small functions, it can take longer to make the call and to return than to actually do the work. And, in certain essoteric types of programming, it can, under certain circumstances, be diifuclt or impossible to make function calls, because function calls change the stack.

We can get around this by using #define to create a function-like macro. The basic idea is that we define something that looks like a function within a #define. And then, after that, we can basically use it like a function. But, there are three big differences:

Let's take a quick look at the following function-like macro and its use. We see that the macro si replaced and explanded by the preprocessor.

  #define multiply(x,y) (x * y)

  ...

  int c =  multiply(10,5); /* int c = (10 * 5);
  

Now, let's ocnsider the example below. It is broken. Do you see why? The extra space breaks the preprocessor -- the first space acts to separate the macro from its definition.

  #define multiply (x,y) (x * y)
  /*              ^            */
  /*              |            */
  /*          Evil space       */

  

Let's take a look at another interesting case. Here is an example that probably doesn't work as you'd expect:

  #define multiply(x,y) (x * y)

  ...

  int c =  multiply(10+5,5); /* int c = (10+5 * 5);

  

Take a second look at the code above. Notice that it seems that we are asking to evaluate "((10+5)*5) = 75". But, it is expanded without regard to the grouping, so the order of oeprations is controlling -- and multiply beats add: "Please excuse my dear aunt sally". So, we get "(10+5 * 5) = (10 + (5*5)) = 250"

To fix this, when defining a macro, we always ()-parenthesize each-and-every use of the macro's arguments as follows. Notice how the parentheses force the right grouping:

  #define multiply(x,y) ((x) * (y))

  ...

  int c =  multiply(10+5,5); /* int c = ((10+5) * (5));
  

Having said that, I'd like to reiterate and amplify what I've just said: Always parenthesize each and every use of a paramter within a macro -- whether you think you need it or not. I've shown you one alligator -- there are more in the swamp. Do go looking for them -- just stay safe in the boat. This is another example of defensive programming.

C's Bit-Wise Operators

C has several operators that are defined in terms of manipulations upon the individual bits that compose values, rather than upon the whole values. These operators include the following:

  << -- left shift
  >> -- right shift
  |  -- bit-wise OR
  &  -- bit-wise AND
  ~  -- bit-wise negation
  ^  -- bit-wise XOR
  !  -- NOT
  

Left Shift

The effect of the left shift operator is pretty easy to understand. At a high-level, it acts to multiply numbers by powers of two. For example:

  int x = 5;
  int y;

  y  = x << 1; /* Multiply x by 2^1 */
  printf ("%d\n", y); /* 10 */

  y  = x << 3; /* Multiply x by 2^3 */
  printf ("%d\n", y); /* 40 */
  

The reason that this works is that numbers are stored in memory in what is, in effect, a binary format. In decimal, each position represents a power of 10: 100=10^2, 10=10^1, 1=10^0. Each binary digit (bit) represents a power of 2.

So, each time we shift by one posititon to the left, back-filling the vacated positions with 0s, we are, making each of the shifted bits worth twice as much:

  5  =    0101
  10 =    1010  (after shifting left by one: 5 << 1)
  20 =   10100  (after shifting left by two: 5 << 2)
  

Right Shift

In theory a right-shift is the opposite of a left shift. It has the effect of dividing by two, as shown below:

  5  =    0101
  2  =    0010  (after shifting right by one: 5 >> 1)
  1  =    0001  (after shifting right by two: 5 >> 2)
  

And, what we've got above is exactly true for unsigned types and also for any non-negative value. But, the Right Thing to do gets a bit confused when it comes to signed types. In my experience, the effect will always be the same as dividing by two. But, this is absolutely not guaranteed by the standard -- the standard leaves the effect of any right-shift of a negative value completely up to the compiler.

The reason for this you'll study, in detail, in 15-213. It has to do with the fact that most hardware does not represent negative numbers in the same way as it does positive numbers. It uses an encoding known as two's compliment. This encoding makes, when a negative number is involved, simple math much easier to implement in hardware.

But, it also means that a so-called "arithmetic right shift" is harder to do. Instead of always shifting a 0 in on the right side, a 1 must be shifted in if the exisitng left-mst bit is a 1.

Although I think the standard should have been for the right shift \ to be "arithmetic", meaning have the same effect as dividing by a power of two, the standard leaves this to the compiler.

The Bit-Wise AND and OR Operators

The bit-wise AND and bit-wise OR operators act exactly as do their so-called logical version, but they operate at a per-bit, bit-wise level upon corresponding bits of the two operands, as shown below:

  /* 
   * Notice that only those bits that are 1 in both of the corresponding
   * input bits are 1 in the result 
   */
  x  = 9;         /* 1001 */
  y  = 5;         /* 0101 */
  z = x & y;      /* 0001 */

  /* 
   * Notice that those bits that are 1 in EITHER (or both) of the corresponding
   * input bits are 1 in the result
   */
  x  = 9;         /* 1001 */
  y  = 5;         /* 0101 */
  z = x | y;      /* 1101 */
  

Bit-wise Exclusive OR and Bit-wise Negation

Although there is no logical XOR in C, there is a bit-wise version. It does exactly as one might expect. Each result bit is true if, and only if, exactly one of the corresponding input bits is on. A result bit is false in the case that both correspnding bits are in the same state, whether true or false, as shown in the example below:

  /* 
   * Notice that, in order for a result bit to be one, EXACTLY one of the 
   * corresponding input bits must be one. 
   */
  x  = 9;         /* 1001 */
  y  = 5;         /* 0101 */
  z = x ^ y;      /* 1100 */
  

The negation operator simply flips each bit. Each one becomes a zero, and vice-versa:

  /* 
   * Notice that, in order for a result bit to be one, EXACTLY one of the 
   * corresponding input bits must be one. 
   */
  x  = 9;         /* 1001 */
  y = ~x;         /* 0110 */