TinyCC provides multiple debugging capabilities including debug symbols, runtime backtraces, and integration with standard debuggers.
TinyCC supports two debug information formats:
STABS - Default debug format (traditional Unix format)
DWARF - Modern debug format (versions 2-5)
Generating debug symbols
# Compile with STABS debug information (default)
tcc -g program.c -o program
# Run with debug info
./program
DWARF debug information provides better compatibility with modern debuggers like GDB and LLDB.
Runtime backtraces
TinyCC can generate stack backtraces at runtime without external tools.
Enabling backtraces
Compile with backtrace support
Use the -bt option to enable runtime stack traces: # Enable backtraces with default depth (6 frames)
tcc -g -bt program.c -o program
# Enable backtraces with custom depth (N frames)
tcc -g -bt[N] program.c -o program
Example with custom depth: # Show up to 10 callers in stack traces
tcc -g -bt10 program.c -o program
Run the program
When a runtime error occurs, TCC automatically prints a stack trace: Output example: test.c:68: in function 'test5()': dereferencing invalid pointer
test.c:75: by main
Backtrace API
When compiled with -bt, the __TCC_BACKTRACE__ macro is defined and you can trigger backtraces programmatically:
#ifdef __TCC_BACKTRACE__
#include <stdio.h>
// Function prototype provided by TCC
int tcc_backtrace ( const char * fmt , ...);
int main () {
// Trigger a backtrace on demand
tcc_backtrace ( "Debug checkpoint reached \n " );
// Conditional backtrace
if (error_condition) {
tcc_backtrace ( "Error detected: %s \n " , error_msg);
}
return 0 ;
}
#endif
Using standard debuggers
GDB integration
TinyCC-generated executables can be debugged with GDB:
Compile with debug symbols
tcc -g -o program program.c
Debug commands
Common GDB commands: # Set breakpoint
(gdb) break main
(gdb) break program.c:42
# Run program
(gdb) run [args]
# Step through code
(gdb) next # Step over
(gdb) step # Step into
(gdb) continue # Continue execution
# Examine variables
(gdb) print variable_name
(gdb) info locals
# View backtrace
(gdb) backtrace
(gdb) bt
DWARF with GDB
For better GDB compatibility, use DWARF debug format:
# Compile with DWARF 4
tcc -gdwarf-4 program.c -o program
# Configure TinyCC to use DWARF by default
./configure --config-dwarf=4
make
DWARF 4 and 5 provide the best compatibility with modern versions of GDB (7.0+).
Debug-only code
Use preprocessor macros to include debug-only code:
#include <stdio.h>
#ifdef DEBUG
#define DEBUG_PRINT ( fmt , ...) \
fprintf (stderr, "DEBUG: " fmt, ## __VA_ARGS__ )
#else
#define DEBUG_PRINT ( fmt , ...) /* nothing */
#endif
int main () {
int value = 42 ;
DEBUG_PRINT ( "Value is: %d \n " , value);
return 0 ;
}
Compile with debug macros:
# Enable debug output
tcc -DDEBUG -g program.c -o program
# Production build (no debug output)
tcc program.c -o program
Bounds checking
TinyCC includes optional runtime bounds checking to detect memory errors.
Enabling bounds checking
# Compile with bounds checking
tcc -b program.c -o program
# Bounds checking implies -g
tcc -b program.c -o program
Bounds checking is available on i386, x86_64, ARM, ARM64, and RISC-V 64.
Types of errors detected
Bounds checking catches:
Array bounds
Invalid memory
Use after free
Double free
// Out of bounds array access
int tab [ 10 ];
for ( int i = 0 ; i < 11 ; i ++ ) {
sum += tab [i]; // Error on i=10
}
Bounds checking environment variables
Control bounds checking behavior:
# Print warning when pointer add creates illegal pointer
export TCC_BOUNDS_WARN_POINTER_ADD = 1
# Print bound checking calls (for debugging)
export TCC_BOUNDS_PRINT_CALLS = 1
# Print heap objects not freed at exit
export TCC_BOUNDS_PRINT_HEAP = 1
# Print statistics at exit
export TCC_BOUNDS_PRINT_STATISTIC = 1
# Continue execution after bound error (unsafe)
export TCC_BOUNDS_NEVER_FATAL = 1
# Run program with bounds checking
./program
Disabling bounds checking in code
Temporarily disable bounds checking for specific code sections:
#ifdef __TCC_BCHECK__
extern void __bounds_checking ( int x );
#define BOUNDS_CHECKING_OFF __bounds_checking ( 1 )
#define BOUNDS_CHECKING_ON __bounds_checking ( - 1 )
#else
#define BOUNDS_CHECKING_OFF
#define BOUNDS_CHECKING_ON
#endif
void performance_critical_function () {
BOUNDS_CHECKING_OFF;
// Fast code without bounds checks
for ( int i = 0 ; i < 1000000 ; i ++ ) {
// ... tight loop ...
}
BOUNDS_CHECKING_ON;
}
Disabling bounds checking should only be done in performance-critical sections where memory safety is guaranteed.
TinyCC preserves symbol information for debugging and dynamic linking.
Viewing symbols
# View symbols in executable
nm program
# View only defined symbols
nm -g program
# Use readelf for detailed symbol table
readelf -s program
# Use objdump for symbol information
objdump -t program
Exporting symbols
# Export all global symbols (useful for dlopen)
tcc -rdynamic program.c -o program
# Equivalent linker option
tcc -Wl,--export-dynamic program.c -o program
Compile shared libraries with debug symbols:
# Create shared library with debug info
tcc -shared -g -o libmylib.so mylib.c
# Use library with debug symbols
tcc -g program.c -L. -lmylib -o program
# Debug with GDB
gdb ./program
Bounds checking code is not included in shared libraries. The main executable must be compiled with -b for bounds checking.
Runtime debugging with -run
Debug code directly with -run:
# Run with backtraces enabled
tcc -g -bt -run program.c arg1 arg2
# Run with bounds checking
tcc -b -run program.c
# Combine options
tcc -g -bt -b -run program.c
Errors are reported with full context:
program.c:15: in function 'process_data': invalid memory access
program.c:42: by main
Profiling support
Create a profiling version of TCC:
# Build profiling version
make tcc_p
# Use for profiling
./tcc_p program.c -o program
Limitations
Debug limitations :
Signal handlers are not compatible with bounds checking
Fork in multi-threaded applications may cause issues with bounds checking
Generated code with -b is slower and larger
Some optimizations are disabled with debug symbols
Configuration
Build-time debug configuration
Configure debug features during TCC compilation:
# Disable backtrace support
./configure --config-backtrace=no
# Disable bounds checking
./configure --config-bcheck=no
# Set DWARF version (2-5)
./configure --config-dwarf=4
# Debug TCC itself
./configure --debug
See also