summaryrefslogtreecommitdiff
path: root/content/blog/intro-to-compiling-c/index.md
blob: 293382e3b8a949c6eed7b910ff6ed3d64ee1d76f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
+++
date = '2026-07-19T19:46:49-10:00'
title = 'Intro to Compiling C'
blog_tags = ['C', 'code']
+++

This guide explains to someone very new to C how to make compiling C code as
simple and extensible as possible. I wrote it a while ago on a whim, and I have
been using the resulting build system in personal projects for a while now. As
part of making this website, I decided to clean it up and make it my first blog
post.

I will assume you can read basic C code. Since so many languages take
inspiration from C, you can probably do this already if you know another
language. I will also assume you have some experience navigating a UNIX shell.

Now that you are ready, we can proceed with the guide.

## Compiling?

C is a compiled language, meaning that it must first be translated into
another language before it can be run. In the case of C, it is translated
into machine code that your computer's
processor can execute directly. This makes C quite fast and lightweight, since
it does not need help from another program in order to run. However, if
not handled correctly, this compilation process can become unwieldy and
complex, resulting in a lot of development overhead.

Many say C's compilation overhead is unavoidable and makes it a poor language
for data science, which requires rapid iteration on code, but I disagree.
Modern computers can compile C quite quickly and build tools like GNU `make`, which I will introduce later, allow
this process to be automated to a substantial degree. With a little scafolding,
iterative development with C can be just as fast as interpreted (i.e. non-compiled) languages
like Python.

## How is C compiled?

To compile C, you use a program called a compiler. There are many to
choose from, but I would recommend either `gcc` or `clang`.{{< marginnote >}}
These two compilers are very well-established, feature rich, and should be able to run on any system. Other compilers may not work for this guide due to missing features or incompatible command-line interfaces.
{{</marginnote>}} If you would like to follow along, you should install one of
these. For the purposes of this guide, either of them will work fine. I will
use `gcc` in my example commands, but you can simply replace it with `clang`
and it will still work, since they have nearly identical command-line
interfaces.

First, we create an empty directory containing, `cd` into it, and create a single C file called
`hello_world.c` with the following contents{{< marginnote >}}
Note that I prefix the contents with a comment with the file name. This is so
it is more clear later when there are multiple files.
{{< /marginnote >}}:

```c
// hello_world.c
#include <stdio.h>

int main(int argc, char ** argv) {
	printf("Hello, world!\n");
	return 0;
}
```

To compile this single C file into an executable, you can simply run the compiler
and pass the file path as a command line arguments like so:

```bash
$ gcc hello_world.c
```

The compiler reads the code in this file and compiles it.
The result is an executable file next to the code called `a.out`, which can be run.

```
$ ./a.out
Hello, world!
```

`a.out` is a weird file name. There are historical reasons it is called this by
default that I won't discuss here{{< marginnote >}}
[For those interested.](https://en.wikipedia.org/wiki/A.out)
{{< /marginnote >}}, but it can be changed using `gcc`'s
command line options like so:

```
$ gcc -o hello_world hello_world.c
$ ./hello_world
Hello, world!
```

If you pass "-o" to `gcc`, it will use the next argument as the file name it should
use for the output.

### C flags

The compilation process is quite complex, and there are many ways it can be
adjusted to meet your needs. To help with this, the C compiler accepts many
options called C flags, which alter the compiled code in various ways. While
the defaults are fairly sane, some of them can be changed to improve the
experience of developing C. The ones I typically use are
`-std=c99 -O2 -Wall -Wextra -Wpedantic -Werror`. Here is what they do:

- **-std=c99**: This tells the compiler which version of the C standard to use.
  This affects what syntax is considered valid. I personally like
  C99, but there are also more modern ones like C17 or C23 which you can also
  use.
- **-O2**: The -O flag (not to be confused with the -o flag) tells the compiler
  how aggressive it should be with optimization. The larger the number, the
  more aggressive. The number can range from 0 to 3. The compiler will accept
  larger numbers, but it will treat them the same as 3. Generally, 2 is
  recommended for most use cases, while 3 can sometimes offer improved
  performance, at the cost of larger compiled program sizes and sometimes less
  stability. There are also special flags like -Os and -Oz, which optimize for
  size instead of performance.
- **-Wall -Wextra -Wpedantic**: These flags enable many compiler warnings that
  will let you know if you are doing something the compiler thinks is wrong.
  Modern compilers are quite good at understanding C code, so you should take
  these warnings seriously.
- **-Werror**: This tells the compiler to treat all warnings as errors and
  refuse to compile code that produces warnings.

Another nice thing about C flags is that you can choose to enable or disable
them based on what you are trying to do. The above C flags are what I use when
trying to actually run code "for real". When I'm trying to fix bugs, I use a different set of C
flags: `-std=c99 -g -O0 -Wall -Wextra -Wpedantic -Werror -fsanitize=address`.
Some of these are the same as before, but others aren't:

- **-g**: This enables debug symbols being included in the compiled executable.
  These are short strings that a debugger like gdb can read to match up the
  instructions in the executable file with the source code. This lets
  it do things like step through the source code line-by-line, and even print
  out the values of variables mid-execution.
- **-O0**: This is the same flag as before and selects the optimization level.
  Here we are disabling optimization entirely. This helps the debugger work
  properly by making the compiled executable follow the source code much more
  closely. The optimization flag should not affect what the code actually does,
  so this won't affect the code's behavior while making it significantly easier
  to debug.
- **-fsanitize=address**: This enables the C address sanitizer, which is a
  runtime library that hooks into your code's memory management and
  prints out useful information regarding poorly managed memory. Manual memory
  management is well beyond the scope of this guide, but suffice it to say some
  of the bugs caused by doing it incorrectly can be nightmarishly difficult to
  fix without tools such as this.

### The Gritty Details

Before we can cover more advanced concepts relating to compiling C, such as
compiling programs split across multiple files or using libraries, we must
first get a better idea of how exactly the compilation process works.

Although so far it seems like compiling C code is an atomic process (put .c
file in, get executable out), it is actually a multi-stage process that can be
divided into 4 steps:

- **Preprocessing**: Here, the code is fed through a program called a preprocessor,
  which modifies the code according to directives located inside it.
  These directives are prefixed with a hashtag and can do things like copy
  contents from other files or define macros.
- **Compiling**: This is the main part of the compilation process, where the code,
  now with all the preprocessor directives resolved, is converted into
  assembly.
- **Assembling**{{< marginnote >}}
Some compilers skip assembly and go straight from C to machine code.
{{< /marginnote >}}: Once the assembly is created, it is then converted into machine
  code by a separate program called an assembler. The result of this is a
  relocatable binary file called an object file (typically with a suffix .o).
  While it is machine code, it is not executable yet since it may
  have unresolved calls to external functions.
- **Linking**: The last step, which is performed by another separate program called
  a linker, takes many object files and combines them into a single executable
  file. In the process, all the unresolved function calls are connected
  together. If the linker can't resolve any of the function calls, it fails and
  prints an error.

### Compiling Multi-file Programs

Suppose we have two C files that we want to compile together: `greet.c` and `main.c`.

In `greet.c`, we have a function that greets the user, and in `main.c`, we have our
program's entry point, in which we want to call the function in `greet.c`:

```c
// greet.c
#include <stdio.h>

void greet(const char *name) {
    printf("Hello, %s!\n", name);
}

// main.c
int main(int argc, char **argv) {
    greet(argv[1]);
    return 0;
}
```

To compile these two together, we could try passing both to the compiler:

```
$ gcc -o greet main.c greet.c
main.c: In function ‘main’:
main.c:4:9: error: implicit declaration of function ‘greet’ [-Wimplicit-function-declaration]
    4 |         greet(argv[1]);
      |         ^~~~~
```

When we try to do this, we get an error saying that the function "greet" is not
declared anywhere, even though we did so in `greet.c`. While this may seem
strange, it makes sense if we consider the steps of the compilation process we 
just discussed. This error is occurring in the second step, when the compiler
is trying to convert the code into assembly. Since this is done with each file
individually, before it has even tried to combine the files together, it
doesn't actually know what is in `greet.c` yet, or that `greet` is defined there.

How do we fix this? The key is to add something called a function definition. This is
similar to the declaration we already have, but without the actual code that
describes how the function should be executed. If we add one to the top of `main.c`, we get
this:

```c
// greet.c
#include <stdio.h>

void greet(const char *name) {
    printf("Hello, %s!\n", name);
}

// main.c
void greet(const char *name);

int main(int argc, char **argv) {
    greet(argv[1]);
    return 0;
}
```

This definition tells the compiler that this function is implemented elsewhere
in the project and should leave it for the linker to resolve. It is necessary
to put it there since the compiler needs to know what parameters the function
takes and what (if anything) it returns in order to compile the code that uses
it.

Now if we try to compile our code, it works as we would expect:

```
$ gcc -o greet main.c greet.c
$ ./greet John
Hello, John!
```

In a complex project, it would get quite unwieldy managing all of these
duplicated definitions of the same function. It would become challenging to
modify this function to, for example, add another parameter. To fix this, we
can use one of the other steps of compilation: the preprocessing step. One of
the things it can do is copy the contents of another file into the one we are
compiling. This way, we can put the definition of "greet" in a separate file
and use the preprocessor to copy it into any C file where we need it. If we
do this, the resulting code looks like this:

```c
// greet.c
#include <stdio.h>

#include "greet.h"

void greet(const char *name) {
    printf("Hello, %s!\n", name);
}

// greet.h
void greet(const char *name);

// main.c
#include "greet.h"

int main(int argc, char **argv) {
    greet(argv[1]);
    return 0;
}
```

A few things to note:

- These files containing function definitions are called "header files" and
  typically have the suffix ".h".
- Note that we use the `#include` directive to tell the preprocessor to copy
  the contents of greet.h into main.c, so the compiler knows the greet function
  is defined elsewhere while compiling main.c.
- Although it is not always required, it is standard practice to include a c
  file's own header in itself. Hence why we add `#include "greet.h"` to
  `greet.c`.

Now, if we need to add a parameter to our `greet` function, we only have to do
it in two places, regardless of how many places we use it.

And of course, it still compiles:

```
$ gcc -o greet main.c greet.c
$ ./greet John
Hello, John!
```

### Using Libraries

In the previous section, you may have noticed the `#include <stdio.h>` in
`greet.c`. This is an include directive just like the one for `greet.h`, but we
do not have a `stdio.h` file in our project. This is because this is a
system header file, provided by a library installed on our computer. In this
case, this file, the name of which is short for "standard input/output" is part
of the C standard library, `libc`, and is needed to use the `printf` function
to actually print the greeting in `greet.c`.

Generally, C libraries are distributed as several files: one or more header
files containing the definitions of functions the library provides, and a
single shared object file with the suffix `.so`, which, like an object file,
contains the compiled machine code that should be executed when these functions
are called.

In order to use a library, we must do two things. We must first include the
library's header file in our code so the compiler knows the functions exist.
Usually, libraries install headers in standard locations the compiler knows to
look in, so we can simply use an include directive like `#include <stdio.h>`.
Note the use of angle brackets instead of quotes. This tells the preprocessor
to look in the standard system header file locations instead of the current
directory. Sometimes you will need to include system header files in
non-standard locations. This can be done using the compiler command line option
`-I` followed by a path to a directory to search.

The second thing we must do is tell the linker to include the shared object
file when linking. This is done with a linker flag. These are passed to the
compiler in the same way as other flags, though they are typically placed at
the end of the command instead of the beginning. To tell the linker to include
a shared object
file, we pass the `-l` flag followed by the name of the library, which is the
name of the shared object file without the "lib" prefix or ".so" suffix. For
example, to include `libc.so`, we would pass the flag `-lc`{{< marginnote >}}
libc is usually included by default so `-lc` is rarely needed.
{{< /marginnote >}}. As with headers,
if a library installs its shared object file to a non-standard location,
we can tell the compiler to search additional directories with the `-L` flag in
the same way.

As an example, suppose we want to do trigonometry in our C code. There is a
common library called `libm` that includes code that does this. It has a
corresponding header called `math.h` which is installed in a standard location.

To use this library, our code would look like this, for example:

```c
// cosine.c
#include <stdio.h>

#include <math.h>

int main(int argc, char **argv) {
    float x = 2;
    float y = cosf(x); // <- cosf is defined in math.h

    printf("The cosine of %.3f is %.3f.\n", x, y);

    return 0;
}
```

To compile this, we would run the following:

```
$ gcc -o cosine cosine.c -lm
$ ./cosine
The cosine of 2.000 is -0.416.
```

### Splitting the Process

Often, the first three compilation steps are separated from the last one. This
is usually done to save time. If we have a program with many C files, it would
be nice if we only had to recompile the ones we changed. To do this, we must first separate the linking step from the other three. This can be done by
passing the `-c` flag to `gcc`. This tells it to stop just before the linking
step and instead of saving an executable file, to save the resulting object
file that would have been passed to the linker. The linking process can then be
done using the usual `gcc` command, but this time passing it the object files
instead of the source code.

For example, in order to compile our greeter program from before, instead of
using this single command.

```
$ gcc -o greet main.c greet.c
```

We could use the following sequence of commands:

```
$ gcc -c -o main.o main.c
$ gcc -c -o greet.o greet.c
$ gcc -o greet main.o greet.o
```

This allows us to recompile the program without having to recompile every
individual C file. For example, if we only modified `greet.c`, then we could
start at the second command and not bother recompiling `main.c`.

At this point, you know everything you need to start automating this process.
In practice, people rarely run these commands manually, but use tools to
automate them. I will now switch focus to one popular such tool: GNU `make`.

## What is GNU `make`?

GNU `make` is, at its heart, an automation tool. It allows you to specify
"recipes" for performing various tasks and lets you execute them via a simple
command: `make`. Although it was originally designed to automate complex
compilation processes (which is what we will be using it for), it can be used
in many other circumstances, and it is worth learning how to use well, even
beyond what I discuss here.

When you run make, it searches for a file named "Makefile" in your current
directory, and runs the specified recipe (called a "target"). You pick which
recipe to run by passing its name to `make` as a command line argument.

Targets are specified using the following syntax:

```make
<target name>: <prerequisites> ...
    command 1
    command 2
    ...
```

By default, make will assume that the target is the name of a file that should
be made using other files, called the prerequisites.
When it runs this recipe, it first checks if the target file doesn't exist or
if one of its prerequisites has changed since the target was last created. If either of
these is the case, then it will run the specified commands to recreate the target, otherwise it
assumes that recreating the target file is unnecessary, since it would give the
same result anyway.

The real power of make emerges once you chain many recipes together. Each
prerequisite file can itself be a target with its own recipe, and make will
check if it needs to be updated and do so before continuing with the current
recipe. Through this process, we can chain many recipes together to complete
complex multi-step tasks while also minimizing the steps taken.

### A Basic Makefile

If we go back to our `greet.c` and `main.c` files from before, we have the
following sequence of commands we used to compile the program:

```
$ gcc -c -o main.o main.c
$ gcc -c -o greet.o greet.c
$ gcc -o greet main.o greet.o
```

We can think of each command as taking several files and making a new one. Once
framed like this, we can convert this into a simple Makefile:

```make
greet: main.o greet.o
    gcc -o greet main.o greet.o

main.o: main.c
    gcc -c -o main.o main.c

greet.o: greet.c
    gcc -c -o greet.o greet.c
```

If we save this file and just run `make`, it will default to the first recipe
to make the `greet` executable. It will check that `main.o` and `greet.o` exist
and have been modified more recently than their respective prerequisites, and
if not, will run the commands to (re)create them before finally linking them
together.

Now if we modify `greet.c` and run `make` again, it will know it doesn't need
to recompile `main.c` and skip running that command.

However, now we have a new problem: maintaining this Makefile. Every time we add
a new C file we have to update our Makefile. Luckily, Make has a powerful system
of variables and macros we can use to make it self-update to handle this. I will
show you the updated Makefile and then explain all the differences:

```make
c_files := $(wildcard *.c)
o_files := $(patsubst %.c,%.o,$(c_files))

greet: $(o_files)
    gcc -o $@ $^

%.o: %.c
    gcc -c -o $@ $^
```

- First we use the `wildcard` macro to get a list of all the C files in our
  current directory and assign that list to the variable `c_files`.
- Next we use the `patsubst` macro to replace all the `.c` extensions in our
  `c_files` variable with `.o`, which gives us a list of object files.
- Now, when specifying our recipe for our final executable, we use the
  `o_files` variable to say it requires all the object files that result from
  compiling our C files. In the compilation command, we use the special variables `$@` and `$^`, which
  expand to the target file and the list of prerequisites, respectively.
- Lastly, instead of having a separate rule for each C file, we can use
  wildcards to create a generic recipe for any object file. Here we also use
  `$@` and `$^` to make the compiler command as generic as possible.

Now when we add a C file to our project, it will be included in the `c_files`
variable. Its corresponding object file will also appear in the `o_files`
variable, so it will be included in our final executable automatically without
us having to modify the Makefile.

### Adding C flags

We can use `make`'s variables to specify compiler and linker flags:

```make

CFLAGS ?= -std=c99 -O2 -Wall -Wextra -Wpedantic -Werror
LDFLAGS ?= -lc

c_files := $(wildcard *.c)
o_files := $(patsubst %.c,%.o,$(c_files))

greet: $(o_files)
    gcc $(CFLAGS) -o $@ $^ $(LDFLAGS)

%.o: %.c
    gcc $(CFLAGS) -c -o $@ $^
```

Note the use of `?=`. This allows the person running `make` to override these
values via the command line:

```
$ CFLAGS="-O0" make
```

We can also introduce two separate targets, `release` and `debug`, and
configure the C flags depending on which we are building:
```make
.DEFAULT_GOAL := release # You can manually specify the default target like this.

release: CFLAGS ?= -std=c99 -O2 -Wall -Wextra -Wpedantic
debug: CFLAGS ?= -std=c99 -g -O0 -Wall -Wextra -Wpedantic -fsanitize=address

LDFLAGS ?= -lc

c_files := $(wildcard *.c)
o_files := $(patsubst %.c,%.o,$(c_files))

release: greet
debug: greet

greet: $(o_files)
    gcc $(CFLAGS) -o $@ $^ $(LDFLAGS)

%.o: %.c
    gcc $(CFLAGS) -c -o $@ $^

.PHONY: release debug
```

Now we can specify which set of C flags to use by running `make release` or
`make debug`. It will default to the former. Note the last line, which is a
special "recipe" that tells make that release and debug are not intended to be
files and those recipes should always be run.

Also note the final recipe: `.PHONY`. This is a special target that tells
`make` that its prerequisite targets don't represent actual files (i.e. when
we run `make release`, it shouldn't check if a file named `release` exists).

### Handling Headers

You may have noticed an issue with this Makefile: the header files. These
files are also used to generate the object files, but we currently do not list
them as prerequisites. This means if we change a header, the object files made
with it won't be updated. This doesn't seem trivial to fix either, since now each
object file has a separate list of prerequisites that can change.

Luckily, C compilers have a nifty little feature that solves this problem. We
can have our C compiler output a list of headers in the C file being compiled
in the form of `make` recipes. We can then use `make`'s `include` macro to
include these recipes in our main Makefile, which will add the headers for each
C file to the prerequisites of the resulting object file.

To do this, we first add the needed flags to the compiler (see your compiler's
documentation for details on what each does):

```make
#...
%.o: %.c
    gcc $(CFLAGS) -MT $@ -MD -MP -MF $(patsubst %.o,%.d,$@) -c -o $@ $<
#...
```

Note that we have to change `$^`, which includes all prerequisites, to `$<`,
which only includes the first, since we will now have headers in our
prerequisites that we do not want to pass to the compiler.

Next, we add an include statement to include all these generated files:
```make
#...
-include $(wildcard *.d)
#...
```

Our Makefile now looks like this:

```make
.DEFAULT_GOAL := release

release: CFLAGS ?= -std=c99 -O2 -Wall -Wextra -Wpedantic
debug: CFLAGS ?= -std=c99 -g -O0 -Wall -Wextra -Wpedantic -fsanitize=address

LDFLAGS ?= -lc

c_files := $(wildcard *.c)
o_files := $(patsubst %.c,%.o,$(c_files))

release: greet
debug: greet

greet: $(o_files)
    gcc $(CFLAGS) -o $@ $^ $(LDFLAGS)

%.o: %.c
    gcc $(CFLAGS) -MT $@ -MD -MP -MF $(patsubst %.o,%.d,$@) -c -o $@ $<

-include $(wildcard *.d)

.PHONY: release debug
```

Now if we change a C file to include a new header, the compiler will rewrite
the corresponding `.d` file to add the new header to the
prerequisites of the corresponding object file. `make` will see this and know
to recompile the object if the header changes.

### Some Helper PHONY Targets

We can use the `.PHONY` target to also add some simple utility recipes. One I
always add is "clean" which deletes any intermediate files that can be
regenerated. In this case it should delete all the `.o` and `.d` files as well
as our executable:

```make
# ...

clean:
    -rm *.o *.d greet

.PHONY: release debug clean
```

You can define as many of these as you want. Other common ones are `install`
and `uninstall` to add or remove your executable to the system's installed
programs, or `dist` to create a distributable tarball of your source code

An implementation of these on a Linux system may be:

```make
# ...

clean:
    -rm *.o *.d greet

install:
    cp greet /usr/local/bin/greet
    chmod 755 /usr/local/bin/greet

uninstall:
    rm /usr/local/bin/greet

dist: clean
    tar czf ../greet.tar.gz --transform 's+^+greet/+' .

.PHONY: release debug clean install uninstall dist
```

### Separating Out Configuration

The last thing I like to do is to separate out some of the options into a
`config.mk` file that I can include in my main Makefile. Here I put some of the
build flags and other options so I do not have to modify my main Makefile. I
also separated them by purpose and added some other settings, like letting the
user specify which C compiler to use, or adding a C preprocessor macro to embed
the program name and version into the executable.

```make

# config.mk
PROGRAM := greet
VERSION := 0.1

BINPREFIX := /usr/local/bin

INCS := 
LIBS := -lc

CPPFLAGS := $(INCS) -DVERSION=$(VERSION)
release: CFLAGS := -std=c99 -O2 -Wall -Wextra -Wpedantic $(CPPFLAGS)
debug: CFLAGS := -std=c99 -g -O0 -Wall -Wextra -Wpedantic -fsanitize=address $(CPPFLAGS)
LDFLAGS := $(LIBS)

CC ?= gcc


# Makefile
.DEFAULT_GOAL := release

include config.mk

c_files := $(wildcard *.c)
o_files := $(patsubst %.c,%.o,$(c_files))

release: $(PROGRAM)
debug: $(PROGRAM)

$(PROGRAM): $(o_files)
    $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

%.o: %.c
    $(CC) $(CFLAGS) -MT $@ -MD -MP -MF $(patsubst %.o,%.d,$@) -c -o $@ $<

-include $(wildcard *.d)

clean:
    -rm *.o *.d $(PROGRAM)

install:
    mkdir -p $(DESTDIR)$(BINPREFIX)
    cp $(PROGRAM) $(DESTDIR)$(BINPREFIX)/$(PROGRAM)
    chmod 755 $(DESTDIR)$(BINPREFIX)/$(PROGRAM)

uninstall:
    rm $(DESTDIR)$(BINPREFIX)/$(PROGRAM)

dist: clean
    tar czf ../$(PROGRAM)-$(VERSION).tar.gz --transform 's+^+$(PROGRAM)-$(VERSION)/+' .

.PHONY: release debug clean install uninstall dist
```

## Wrapping up

At this point, you now have a simple build system that can easily compile C programs consisting of many files. You can use `make` to quickly iterate on portions of the program without having to recompile the whole thing. The build system can also handle complex interdependencies between these files without the Makefile needing to be changed, letting the program be extended with minimal friction to the developer.

As part of motivating this system, we have also explained how the C compilation process is structured, and touched on some of the reasons why it is the way it is. I hopee to have shown you that the process of compiling C code is not actually that complicated, and with a little bit of effort, it can be made extremely simple and automatic.

The remainder of this guide are some simple extensions to this Makefile that I occasionally use.

## Miscellaneous Tips and Tricks

### Git Versions

If you use git to distribute your source code, you can use it to automatically
generate version strings:

```make
VERSION := $(shell git describe --tags --dirty)
```

The above command will print out the currently checked-out tag if there is
one, otherwise it will append the current commit hash to the last tag in the
git log. Finally, if the work tree is dirty, it will append "-dirty" to the
version string.

Note that this does not work if you also distribute tarballs, in that case you
can use this more complex logic:

```make
VERSION := $(shell test -f ".version" && cat .version || git describe --tags --dirty)

# ...

dist: clean
    test -f ".version" && exit 1
    echo "$(VERSION)" > .version
    tar ...
    rm .version

```

This will check if a file called ".version" exists and use its contents as the
version before trying to use git. Then, when making a distribution tarball, it
will create the ".version" file before calling `tar`, so the tarballs have that
file in them. This way, an extracted tarball will still have the correct
version information (although it won't tell you if the code was modified like
git would).

### Embedding Build Settings Into the Executable

If you have a public issue tracker, it could be helpful to have people include
the build settings in their bug reports. To do this, first pass the relevant
info to the C preprocessor as macro definitions:

```make
program: ...
    gcc ... -DVERSION='"$(VERSION)"' -DCFLAGS='"$(CFLAGS)"' -DLDFLAGS='"$(LDFLAGS)"' 
...
```

Now you can use these in your program to print this information out:

```c
void print_build() {
    printf("program-" VERSION "\n");
    printf("CFLAGS: " CFLAGS "\n");
    printf("LDFLAGS: " LDFLAGS "\n");
}
```

Now simply add a command line argument, like "-V", that causes your program to
call this function and exit.