Compiling and Debugging Tips for C


INDEX

Compiling C Programs gcc, using make
Handling Linking Errors
Debugging C gdb and valgrind


COMPILING

Use the GNU C compiler, gcc, to compile your C code. Compile your code with warnings turned on (-Wall turns on all warnings). And don't ignore warnings as you compile your code; warnings usually indicate a lurking problem that can lead to real problems later. As you develop your code, you should compile with -g too; this will add information to the executable file that is used by gdb and valgrind. It is a good idea to create a Makefile and use make to build your project code.
Here is some more information about make.

For more information about gcc make see the GNU manuals or run info (the man pages for gcc and make are not very complete).


LINKING

A common link-time error is forgetting to link library code into your program that uses it. If the compiler fails at link time with a list of undefined symbols in your program, it is due to your not linking in one or more .o files or library .a or .so files into your program (if you get an undefined reference error when a file is being compiled from .C to .o, then this means you forgot to include a header file). For example, if you are calling the sqrt function from the math library, you need to include the math.h header file in your .C file and you need to explicitly link the math library into your executable:

        gcc -g -Wall -o myprog myprog.o -lm
                                        ^^^
For standard C and C++ library functions, look at the man page for information on how to link in the library code as part of the gcc command line. For other library code, including libraries you have written, you need to tell the linker where the library code is located. To do this use the -L command line option followed by the path(s) to library code. For example, if I have two libraries in /home/newhall/mylibs/, one of which is a shared object file named libmymath.so and the other an archive file named libsimple.a, then I'd add the following to my makefile to link in my two libraries plus the standard math library (this is only part of the makefile):
# add the path to my library code; -L tells the linker where to find it
LFLAGS += -L /home/newhall/mylibs
# list of libraries to link into executable; -l tells the linker which 
# library to link in to the executable
LIBS = -lmymath	-lsimple -lm
OBJS = myprog.o

# path to any header files not in /usr/include or the current directory 
INCLUDES += -I/home/newhall/include -I../include

default: myprog

myprog:
	$(CC) $(CFLAGS) $(LFLAGS) -o myprog $(OBJS) $(LIBS)  

${OBJS}: %.o :  %.c
	${CC} -c ${CFLAGS} ${LFLAGS} ${INCLUDES} ${@:.o=.c}

To list symbols in .so .a .o or an executable files, you can use nm or objdump -t to list the contents of the symbol table. The output will include all symbols (e.g. functions, global variables), and will list information about them including if they are defined or not (defined in a .o means the code for this function or the declaration of this global variable is in this file, undefined means it is in some other .o, .a, or .so file).

Executable files that are built using static linking contain all the library code needed to run. Executable files that are built using dynamic linking do not contain library code from .so files. Instead, library code from .so files is dynamically loaded into the address space of the process at runtime. To list the shared object dependencies of an executable file (or of a .so file) use ldd. ldd will list the name of each shared object file and the full path to its location. If a program fails at runtime with a linking error, it is due to the runtime linker not being able to find one or more of the .so files needed to run the executable.

	% ldd a.out 
	linux-gate.so.1 =>  (0xffffe000) 
	libX11.so.6 => /usr/lib/libX11.so.6 (0xb7e3c000) 
	libm.so.6 => /lib/tls/libm.so.6 (0xb7e05000) 
	libc.so.6 => /lib/tls/libc.so.6 (0xb7cd3000) 
	libXau.so.6 => /usr/lib/libXau.so.6 (0xb7cd0000) 
	libXdmcp.so.6 => /usr/lib/libXdmcp.so.6 (0xb7ccb000) 
	libdl.so.2 => /lib/tls/libdl.so.2 (0xb7cc7000) 
	/lib/ld-linux.so.2 (0xb7f2b000)

DEBUGGING: gdb, ddd, valgrind

gdb and ddd

A debugger allows you to see what is going on inside your program as it runs and/or let you see what your program was doing when it crashed. gdb and ddd allow you to examine a program's state (variables, stack frame contents, etc.), allow you to set breakpoints to stop the program at a certain points to examine its state, and allow you to alter the value of your program's state (change a variable's value, call a function) as it runs. Learning to use a debugger can save you hours/days of time over trying to debug via printf statements.

GDB Guide:
"how to use gdb" information, including information on compiling C programs for use with gdb, running gdb, commonly used commands, example sessions, gdb and make, ddd, keyboard shortcuts, and links to gdb references.

Some sample programs that you can copy and try out with gdb are available here: /home/newhall/public/gdb_examples/

valgrind

valgrind is a tool for finding memory access errors in your code (memory leaks, reading uninitialized memory, accessing unallocated memory, array out-of-bounds errors, ...). In C and C++ programs, memory access errors are the most difficult bugs to find and to fix. valgrind can save you days worth of debugging effort by quickly pointing you to the source and type of these memory access bugs in your program code. valgrind is pretty easy to learn to use, and the effort you put in to learn how to use it will be more than made up for by the debugging time you save by using it.

Some sample programs that you can copy and try out with valgrind are available here: /home/newhall/public/purify_valgrind_examples/

Valgrind Guide: "how to use valgrind" information with a sample valgrind session and links to valgrind references.