summaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
Diffstat (limited to 'content')
-rw-r--r--content/_index.md12
-rw-r--r--content/blog/_index.md6
-rw-r--r--content/blog/intro-to-compiling-c/index.md812
3 files changed, 830 insertions, 0 deletions
diff --git a/content/_index.md b/content/_index.md
new file mode 100644
index 0000000..26eb2ba
--- /dev/null
+++ b/content/_index.md
@@ -0,0 +1,12 @@
+Aloha!
+
+I am a PhD student and NSF Graduate Research Fellow at the University of Hawaiʻi at Manoa Institute for Astronomy. I'm interested in exoplanets, stars, and computer simulations.
+
+I'm also a hobby programmer and Linux enthusiast.
+
+Some relevant links:
+
+- [My bio on the IfA website](https://people.ifa.hawaii.edu/gradstudents/bio/joseph-hand/)
+- [My GitHub profile](https://github.com/josephhand) -- No longer active, I now self-host my code here.
+
+I do not, and never will, have a social media presence. If you would like to contact me, feel free to email me at {{< email >}}.
diff --git a/content/blog/_index.md b/content/blog/_index.md
new file mode 100644
index 0000000..61a9d43
--- /dev/null
+++ b/content/blog/_index.md
@@ -0,0 +1,6 @@
++++
+menus = 'nav'
+title = 'blog'
++++
+
+Recent blog posts:
diff --git a/content/blog/intro-to-compiling-c/index.md b/content/blog/intro-to-compiling-c/index.md
new file mode 100644
index 0000000..56f06b5
--- /dev/null
+++ b/content/blog/intro-to-compiling-c/index.md
@@ -0,0 +1,812 @@
++++
+date = '2026-07-19T19:46:49-10:00'
+title = 'Intro to Compiling C'
++++
+
+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. \ No newline at end of file