In my most recent iteration of my Reverse-Reverse Engineering series, I decided to ditch the VisualStudio set up and go with a more traditional C/C++ development set up. This is both because I didn't want to become super dependent on the MSFT stack and because I wanted to learn new tooling. From the very introductory research I did, this pretty much necessitated learning Make.
For the relatively uninitiated, Make is a build system/pseudo-scripting language that allows a developer to set up a build pipeline for C/C++ projects (and probably others) by creating dependency chains that build your projects in the correct order. The whole thing is based on the idea of prerequisites: to build a .exe, I need a.o, b.o and c.o. To make a.o, I need to compile a.c, to make b.o, I need to compile b.c... etc.
The end result of make is cool: you can basically automate your entire build pipeline (and probably testing, too, though I haven't tried that out yet) and build your project with one single command:
make. You can also automate cleaning up those annoying build artifacts like stale executables and object files using a
make clean directive.
That said, the syntax is incredibly annoying and unintuitive. Check out my simple example below:
main : main.o helper.o gcc main.o helper.o -o main.exe main.o : main.c gcc -c main.c helper.o : helper.c gcc -c helper.c clean : @rm *.o *.exe
Let's walk through it line by line.
The first line is the default directive. It is seeking to build main(.exe) described on the left of the colon and looks for main.o and helper.o to do so. If it doesn't find main.o and helper.o, it will look further down the script to figure out how to build them. It finds a directive for main.o (the 4th line) and sees it depends on main.c. Since that's a source file, it should be in the directory, so it goes ahead and compiles it. It does the same for helper.o (builds it using helper.c) and finally returns to the initial directive with the prerequisites figured out and links the object files together to make main.exe.
This is an odd syntax, but it's only really as readable as it is because I skipped out on the syntax that's normally used. Here is a re-written version with the original directives commented out.
obj_files = main.o helper.o main : $(obj_files) gcc main.o helper.o -o main.exe %.o : %.c @echo '[-] Building $<' gcc -c $< -o $@ # main.o : main.c # gcc -c main.c # helper.o : helper.c # gcc -c helper.c clean : @rm *.o *.exe
The top line creates a list of object files necessary to build main.exe. The initial target directive to build main.exe checks for the existence of all of the object files in the (space-separated) list. The next directive (line 6) basically states "to build any .o file, start with a .c file of the same name." It will echo out
[-] Building <file name> because $< for whatever reason means
the prerequisite file or the .c file being used as input, and then runs gcc on the prerequisite .c file to get an outputted .o file, for whatever reason represented by $@.
Again, this syntax is... confusing. But to me, it's worth learning, if for no other reason than to get to move away from MSFT's tech stack (which has issues that I'll go into in another blog, probably) and to really learn the underlying tech in compiling and linking.