A debugger needs:
- to refer to variable and function names, not their addresses (e.g.
- to associate the address of machine code with a line in the source code.
To this purpose the compiler builds (
-g) a (augmented) symbol table (it may be embedded into the program or stored as a separate file). Sometimes other ancillary information are generated.
There are different formats to store these information (stabs, coff, dwarf...).
Contrarily to popular beliefs, many compilers (GCC for sure) write some symbols to an object file even in release mode (with the -O3 switch).
That's why even with a release binary, you can do:
$ gcc -O3 programName.c
$ gdb a.out
Reading symbols from a.out...(no debugging symbols found)...done.
Of course this isn't enough for a good debugging experience.
Symbols can be removed from the binary using the strip command:
$ strip -s a.out
$ nm a.out
nm: a.out: no symbols
$ gdb a.out
(gdb) b main
Function "main" not defined.
Also consider that there is an interaction between code optimization and debugging:
- some variables you declared may not exist at all;
- the flow of control may briefly move where you did not expect it;
- some statements may not be executed because they compute constant results or their values were already at hand
- some statements may execute in different places because they were moved out of loops
So, with many compilers, you cannot turn on both optimization and debugging (GCC has the -Og switch for debugging optimized code).