MAKEPP_TUTORIAL_COMPILATION(1)

πŸ“¦ version 2.0.98.5, in debian bookworm, ubuntu jammy, ubuntu noble

πŸ’– MAKEPP_TUTORIAL_COMPILATION πŸ’–

okay bestie, this is the makepp_tutorial_compilation(1) page from the Makepp manual set, and it's literally just "how do Unix compilation commands work" explained for real humans. part 1 of 2, so buckle up πŸŽ€


✨ NAME ✨

makepp_tutorial_compilation β€” Unix compilation commands. that's it, that's the name, slay πŸ’…


🌸 DESCRIPTION 🌸

okay so first thing: if you already get what compilation commands do, you can skip this whole page, no cap. genuinely. go live your life πŸ¦‹

but here's the tea: distressingly few people ever get taught in their programming classes how to actually compile a program once they've written it. like, they teach you the language and then just... leave you there?? novices end up relying on one memorized command, or on the builtin rules in make, and that's it. and I've been shocked by how many extremely computer-literate people learned to compile without optimization because literally nobody ever told them it mattered. bestie. a rudimentary understanding of how compilation commands work can make your programs run twice as fast or more. that's worth five minutes of your time, fr fr. this page covers basically everything you need to know to compile C or C++ programs on just about any variant of Unix. πŸ’œ

the examples are mostly C, because C++ compilation is identical except the compiler has a different name. so suppose you're compiling source code in a file called xyz.c and you want to build a program called xyz. what even has to happen?? let's get into it.

you might already know you can build your program in one single step, with a command like this:

cc -g xyz.c -o xyz

and yes, this works. but it conceals a two-step process that you must understand if you're writing makefiles. (okay technically there are more than two steps, but you only have to understand two of them, bestie.) and for a program with more than one module, those two steps are usually explicitly separated. let's go. ✨


🧚 Compilation

step one is the translation of your C or C++ source code into a binary file called an object file. object files usually have an extension of .o. (for some more recent projects, .lo is also used for a slightly different kind of object file. iykyk.)

the command to produce an object file on Unix looks something like this:

cc -g -c xyz.c -o xyz.o

cc is the C compiler. sometimes alternate C compilers are used; a very common one is called gcc. a common C++ compiler is the GNU compiler, usually called g++. virtually all C and C++ compilers on Unix have the same syntax for the rest of the command (at least for basic operations), so the only difference would be the first word. πŸ‘‘

we'll explain what the -g option does later, don't worry about it yet.

the -c option tells the C compiler to produce a .o file as output. (if you don't specify -c, then it performs the second compilation step automatically. sneaky.)

the -o xyz.o option tells the compiler what the name of the object file is. you can omit this, as long as the name of the object file is the same as the name of the source file except for the .o extension.

for the most part, the order of the options and the file names does not matter. one important exception though: the output file must immediately follow -o. that one's non-negotiable πŸ’…


πŸ”— Linking

step two of building a program is called linking. an object file cannot be run directly; it's an intermediate form that must be linked to other components in order to produce a program. and what might those other components be? let's see:

  • Libraries. a library, roughly speaking, is a collection of object modules that are included as necessary. for example, if your program calls the printf function, then the definition of the printf function must be included from the system C library. some libraries are automatically linked into your program (e.g., the one containing printf) so you never need to worry about them. bless. πŸ™ˆ
  • Object files derived from other source files in your program. if you write your program so that it actually has several source files, normally you would compile each source file to a separate object file and then link them all together.

the linker is the program responsible for taking a collection of object files and libraries and linking them together to produce an executable file. the executable file is the program you actually run. 🌈

the command to link the program looks something like this:

cc -g xyz.o -o xyz

it may seem odd, but we usually run the same program (cc) to perform the linking. what happens under the surface is that the cc program immediately passes off control to a different program (the linker, sometimes called the loader, or ld) after adding a number of complex pieces of information to the command line. for example, cc tells ld where the system library is that includes the definition of functions like printf. until you start writing shared libraries, you usually do not need to deal directly with ld. 🫢

if you do not specify -o xyz, then the output file will be called a.out, which honestly seems like a completely useless and confusing convention. so always specify -o on the linking step. periodt.

if your program has more than one object file, you should specify all the object files on the link command.


πŸ’€ Why you need to separate the steps

okay so why not just use the simple, one-step command, like this:

cc -g xyz.c -o xyz

instead of the more complicated two-stage compilation:

cc -g -c xyz.c -o xyz.o
cc -g xyz.o -o xyz

when internally the first one is converted into the second?? the difference only matters if there's more than one module in your program. so suppose we have an additional module, abc.c. now our compilation looks like this:

# One-stage command.
cc -g xyz.c abc.c -o xyz

or

# Two-stage command.
cc -g -c xyz.c -o xyz.o
cc -g -c abc.c -o abc.o
cc -g xyz.o abc.o -o xyz

the first method, of course, is converted internally into the second method. which means both xyz.c and abc.c get recompiled every single time the command is run. but if you only changed xyz.c, there's no need to recompile abc.c, so the second line of the two-stage commands doesn't need to be done. and this can make a huge difference in compilation time, especially if you have many modules. for this reason, virtually all makefiles keep the two compilation steps separate. πŸ”₯

that's pretty much the basics, but there are a few more little details you really should know about. keep reading bestie.


πŸŽ€ Debugging vs. optimization

usually programmers compile a program either for debug or for speed. compilation for speed is called optimization; compiling with optimization can make your code run up to 5 times faster or more, depending on your code, your processor, and your compiler. iconic.

with such dramatic gains possible, why would you ever not want to optimize?? the most important answer is that optimization makes use of a debugger much more difficult (sometimes impossible). (if you don't know anything about a debugger, it's time to learn. the half hour or hour you'll spend learning the basics will be repaid many many times over in the time you'll save later when debugging. I'd recommend starting with a GUI debugger like kdbg, ddd, or gdb run from within emacs β€” see the info pages on gdb for instructions on how to do this.) optimization reorders and combines statements, removes unnecessary temporary variables, and generally rearranges your code so that it's very tough to follow inside a debugger. the usual procedure is to write your code, compile it without optimization, debug it, and then turn on optimization. πŸ’…

in order for the debugger to work, the compiler has to cooperate not only by not optimizing, but also by putting information about the names of the symbols into the object file so the debugger knows what things are called. this is what the -g compilation option does. there she is!! the girl we kept mentioning!! πŸ’–

if you're done debugging, and you want to optimize your code, simply replace -g with -O. for many compilers, you can specify increasing levels of optimization by appending a number after -O. you may also be able to specify other options that increase the speed under some circumstances (possibly trading off with increased memory usage). see your compiler's man page for details. for example, here is an optimizing compile command that I use frequently with the gcc compiler:

gcc -O6 -malign-double -c xyz.c -o xyz.o

you may have to experiment with different optimization options for the absolute best performance. you may need different options for different pieces of code. generally speaking, a simple optimization flag like -O6 works with many compilers and usually produces pretty good results. ✨

Warning: on rare occasions, your program doesn't actually do exactly the same thing when it is compiled with optimization. this may be due to (1) an invalid assumption you made in your code that was harmless without optimization, but causes problems because the compiler takes the liberty of rearranging things when you optimize; or (2) sadly, compilers have bugs too, including bugs in their optimizers. for a stable compiler like gcc on a common platform like an Pentium, optimization bugs are seldom a problem (as of the year 2000 β€” there were problems a few years ago). 😭

if you don't specify either -g or -O in your compilation command, the resulting object file is suitable neither for debugging nor for running fast. for some reason, this is the default. so always specify either -g or -O. no exceptions, bestie.

on some systems, you must supply -g on both the compilation and linking steps; on others (e.g. Linux), it needs to be supplied only on the compilation step. on some systems, -O actually does something different in the linking phase, while on others, it has no effect. in any case, it's always harmless to supply -g or -O for both commands. πŸ¦‹


🌟 Warnings

most compilers are capable of catching a number of common programming errors (e.g., forgetting to return a value from a function that's supposed to return a value). usually, you'll want to turn on warnings. how you do this depends on your compiler (see the man page), but with the gcc compiler, I usually use something like this:

gcc -g -Wall -c xyz.c -o xyz.o

(sometimes I also add -Wno-uninitialized after -Wall because of a warning that is usually wrong that crops up when optimizing.)

these warnings have saved me many many hours of debugging. πŸ‘‘


πŸ“ Other useful compilation options

often, necessary include files are stored in some directory other than the current directory or the system include directory (/usr/include). this frequently happens when you are using a library that comes with include files to define the functions or classes.

suppose, for example, you are writing an application that uses the Qt libraries. you've installed a local copy of the Qt library in /home/users/joe/qt, which means that the include files are stored in the directory /home/users/joe/qt/include. in your code, you want to be able to do things like this:

#include <qwidget.h>

instead of

#include "/home/users/joe/qt/include/qwidget.h"

okay so you can tell the compiler to go look for include files in a different directory by using the -I compilation option:

g++ -I/home/users/joe/qt/include -g -c mywidget.cpp -o mywidget.o

and bestie, note this: there is usually no space between the -I and the directory name. that's just how it is πŸ’…

so when the C++ compiler is out here looking for the file qwidget.h, it will look in /home/users/joe/qt/include before looking in the system include directory. and you can specify as many -I options as you want β€” stack 'em, go feral, no cap 🌈


🌸 Using libraries 🌸

okay so a lot of the time you'll have to tell the linker to link with specific external libraries, if you're calling any functions that aren't part of the standard C library. the -l (lowercase L, don't get it twisted) option says "link with this specific library":

cc -g xyz.o -o xyz -lm

-lm says to link with the system math library, which you will need if you're using functions like sqrt. it's giving "please give me my math bestie" ✨

⚠️ Beware: if you specify more than one -l option, the order can make a difference on some systems. so if you're getting undefined variables when you know you have included the library that defines them, you might try moving that library to the end of the command line, or even including it a second time at the end of the command line. iconic chaotic behavior, but that's the lore 🫠

sometimes the libraries you need are not stored in the default place for system libraries. -labc searches for a file called libabc.a or libabc.so or libabc.sa in the system library directories (/usr/lib and usually a few other places too, depending on what kind of Unix you're running). and the -L option specifies an additional directory to search for libraries.

to take the above example again, suppose you've installed the Qt libraries in /home/users/joe/qt, which means that the library files are in /home/users/joe/qt/lib. your link step for your program might look something like this:

g++ -g test_mywidget.o mywidget.o -o test_mywidget -L/home/users/joe/qt/lib -lqt

(on some systems, if you link in Qt you will need to add other libraries as well (e.g., -L/usr/X11R6/lib -lX11 -lXext). what you need to do will depend on your system.) lowkey every machine has its own personality πŸ™ˆ

note that there is no space between -L and the directory name. and the -L option usually goes before any -l options it's supposed to affect. order matters, bestie, i'm not kidding 😭

so how do you know which libraries you need? in general, this is a hard question, and varies depending on what kind of Unix you are running. the documentation for the functions or classes you are using should say what libraries you need to link with. if you are using functions or classes from an external package, there is usually a library you need to link with; the library will usually be a file called libabc.a or libabc.so or libabc.sa if you need to add a -labc option. πŸ“š


πŸ¦‹ Some other confusing things πŸ¦‹

you may have noticed that it is possible to specify options which normally apply to compilation on the linking step, and options which normally apply to linking on the compilation step. chaotic? yes. legal? also yes. for example, the following commands are valid:

cc -g -L/usr/X11R6/lib -c xyz.c -o xyz.o
cc -g -I/somewhere/include xyz.o -o xyz

the irrelevant options are ignored β€” the above commands are exactly equivalent to this:

cc -g -c xyz.c -o xyz.o
cc -g xyz.o -o xyz

so like… the compiler just shrugs and moves on. main character energy honestly πŸ‘‘


and that's a wrap on this page, bestie!! this little tutorial situation is straight from the perl v5.32.0 era, dated 2021-01-06, and it lives at MAKEPP_TUTORIAL_COMPILATION(1) β€” go forth and compile something iconic πŸ’–βœ¨πŸŽ€πŸ¦‹

πŸ“¦ where this page comes from