Title: Tips for workflows involving makefiles and gdb Date: 2022-11-22 Updated: 2022-11-25 (valgrind) Tags: c make gdb ctags tags vim valgrind vim: set tw=77 cc=77 : # Some tips for making C projects more convenient. This integrates: - make - ctags (hence vim, or other editors that support tags) - gdb - valgrind Create a config.mk. This can keep your workflow related stuff in a seperate file from the makefile (that you'll probably track in git). This should then be included in the makefile. Of course, many projects will ship a config.mk already: usually these contain different CFLAGS, LDFLAGS, etc. tailored for different operating systems and architectures. They don't change often though, so I would recommend leaving your changes to a config.mk untracked, and manually adding changes you want to propagate elswhere. git add -p is your friend. ## Now, what can you do in this file? 1. Debug flags. A lot of C programs will allow you to define a macro that makes them more verbose, amonst other things, making them ugly but easier to debug. A macro can be defined by passing the -D flag to a compiler. Usually flags used by the compiler are stored in $(CFLAGS) so this can be done with: CFLAGS += -DDEBUG Something I find useful is replacing calls to exit() or similar on errors with SIGTRAP: #ifdef DEBUG raise(SIGTRAP) #else exit(1); #endif SIGTRAP will cause the program to dump its core, which is useful as a debugger can inspect it to see what happened to the program. However, as I will show later in this file, it can also act as a breakpoint. 2. Tagging. You can create a target that regenerates a tag file whenever source is edited pretty quickly: all: tags tags: $(SRC) ctags -R . .PHONY: all tags Make will run the ctags command whenever a dependency (in this case, all the source files stored in $(SRC)) is changed. The ctags command will recurse through the directory the makefile is, outputting all the tag information to a file named, aptly, 'tags'. This file is then read by your editor (if it's any good). 3. Always running code via GDB. There are two different ways I might like to run some code. If I expect it to run fine, then I don't want to have to type 'run' in GDB, and then 'quit' once it finishes running - I want to being able to run it again quickly whenever any changes are made. In this case I use the following target: run: all # 'all' is usually a phony target that builds everything # You may want to put proper tests here as well. gdb ./$(BIN) -ex 'set confirm on' -ex run -ex bt -ex quit # -ex tells gdb to run a command, these are run in order .PHONY: test If everything goes smoothly, once the program exits, gdb should too. However, if something goes wrong, say a SIGSEGV, or perhaps a SIGTRAP that was raised when exiting due to an error (remember earlier on?), it will print a backtrace and allow you to start poking around. Invoke this with `make run` --- In other cases though, you know that something is wrong, know roughly where it happened (perhaps due to the backtrace triggered when you ran `make test`) but need to figure out why. Here you'll probably want to set a backtrace before the program is run, in which case this target may be handy: gdb: all gdb ./$(BIN) .PHONY: gdb Yeah, it's pretty simple, but better than typing it out or grabbing it from history. If you need to hand your program arguments, use --args. This can be placed in a macro for convenience: ARGS = whatever you want gdb: all gdb --args ./(BIN) $(ARGS) And since it's a macro, you could append $(ARGS) to the 'test' target, and now you only need to change it in one place. 4. Valgrind. This is a pretty memory-leak checker (amongst other things), though unfortunately it really slows down the program it's being run on. Hence it's probably a good idea to put it in a seperate target: VALFILE = valgrind.log VALSUPP = valgrind-suppress memcheck: all @echo Outputting to $(VALFILE) valgrind --tool=memcheck --leak-check=full \ --suppressions=$(VALSUPP) --log-file=$(VALFILE) \ ./$(BIN) $(ARGS) By default, valgrind outputs to stdout. This can get pretty messy if your program also outputs to stdout or otherwise uses the terminal. The --log-file flag can be used to specify a file for valgrind to write to instead. This is useful even if you're writing a GUI program as scrolling up terminals (if your terminal even has scrollback) can be a pain compared to opening up a file in less (or vim, which is pretty much always better in my opinion). Another thing this target does is provide valgrind with suppressions. Creating another file is in order. You can suppress anything you want, but one good usecase is suppressing anything other than your own program, i,e, code run by libraries: { ignore_versioned_libs Memcheck:Leak ... obj:*/lib*/lib*.so } { ignore_versioned_libs Memcheck:Leak ... obj:*/lib*/lib*.so.* } An unfortunate side-effect of this is that any callbacks that you provide to the library won't be checked.