What compiler-generated functions are and why you need to find them
When you compile C code, the compiler creates functions you did not write. These functions handle tasks like calling constructors and destructors, managing static initialization, and setting up the runtime environment. They appear in your compiled binary but not in your source files, which makes them invisible during code review and straightforward to miss when debugging.
You need to find these functions because they can hide performance problems, consume stack space, or cause unexpected behavior. A function that looks straightforward in your source code might call dozens of compiler-generated helpers behind the scenes. Understanding what the compiler added helps you see the real cost of your code and spot where optimization might help.
The tools and methods differ depending on whether you want to see generated functions in your source code before compilation, inspect them in the compiled binary, or trace them while your program runs.
Key Takeaways
- Compiler-generated functions appear in your binary but not your source code, so you must inspect the compiled output or use compiler flags to see them.
- The -fdump-tree-all flag in GCC and Clang writes intermediate compiler representations to disk, showing generated functions before final compilation.
- Tools like objdump, nm, and readelf read your compiled binary and list all functions, including those the compiler created.
- Debuggers like GDB let you step into and inspect generated functions at runtime, revealing what they do and how much time they consume.
- Static initialization, virtual method dispatch, and exception handling are the most common sources of hidden compiler-generated code.
Using compiler flags to dump intermediate representations
GCC and Clang can write out the intermediate code they generate during compilation. This intermediate form shows you exactly what the compiler created before it optimized and assembled the final binary. The flag -fdump-tree-all tells the compiler to write every stage of its internal representation to a series of files in your build directory.
Compile your code with the flag set:
gcc -fdump-tree-all -c myfile.c
The compiler creates files named myfile.c.001t.tu, myfile.c.002t.original, myfile.c.003t.gimple, and many others. Each file shows a different stage of compilation. The .gimple files are usually the most readable — they show the compiler's intermediate language, which is close enough to C to understand. Search these files for function names you did not write, or for calls to __static_initialization_and_destruction_0, __cxx_global_var_init, or similar patterns that signal compiler-generated code.
This method works best when you want to understand what the compiler did to a small piece of code before it was optimized away. The output is verbose and the files are large, so use it on individual source files rather than entire projects.
Reading function symbols from compiled binaries with nm and objdump
Once your code is compiled, the binary contains a symbol table listing every function and variable. Tools like nm and objdump read this table and show you what is inside.
nm lists all symbols in a compiled object file or executable:
nm myprogram | grep -i "\.text"
The output shows function names, their addresses, and their size. Compiler-generated functions often have names starting with underscores and containing patterns like __static_initialization, __cxx_, _ZN (C++ name mangling), or __do_global. The size column tells you how many bytes of code the compiler created — a large size means the compiler generated substantial code you did not see in your source.
objdump goes further and disassembles the binary, showing the actual machine instructions:
objdump -d myprogram | grep -A 20 "__static_initialization"
This shows you the assembly code for a specific generated function, which helps when you need to understand exactly what it does or why it is slow. The output is in assembly language, so this method works best when you already know what you are looking for.
Use readelf on Linux systems to inspect the symbol table in more detail:
readelf -s myprogram | grep FUNC
This lists all functions with their size and binding type, making it straightforward to spot generated code by name pattern.
Stepping through generated functions in GDB
The debugger GDB lets you run your program and step through every function call, including the ones the compiler created. This is the most direct way to see what generated code actually does and how long it takes to run.
Compile with debugging symbols so GDB can show you source code and function names:
gcc -g -O0 myprogram.c -o myprogram
The -g flag includes debugging information and -O0 disables optimization so the code matches your source as closely as possible. Start GDB and set a breakpoint at the function you want to inspect:
gdb ./myprogram
(gdb) break main
(gdb) run
Once the breakpoint is hit, use step to step into every function call, including generated ones. Use next to skip over function calls. When you step into a generated function, GDB shows you the assembly code if source code is not available. Use info locals to see what variables the generated function created, and print to inspect their values.
Set a breakpoint on a specific generated function by name if you know it:
(gdb) break __static_initialization_and_destruction_0
This method is slower than static analysis but shows you the real behavior at runtime, including what values are being passed and returned.
Common sources of compiler-generated functions
Static variable initialization is one of the largest sources of hidden code. When you declare a static variable with a non-trivial constructor or initializer, the compiler creates a function to run that initialization once, before main() is called. In C++, this function often has a name like __static_initialization_and_destruction_0.
Virtual method dispatch in C++ generates code to look up the correct function pointer in a virtual method table. This code is usually inlined, but in some cases the compiler creates a separate thunk function to handle the dispatch.
Exception handling creates code to unwind the stack, call destructors, and jump to catch blocks. Even if your code does not explicitly throw exceptions, the compiler may generate exception-handling code if you link against libraries that use exceptions.
Global object construction and destruction in C++ generates functions to call constructors on all global objects before main() and destructors after main() returns. These functions are often named __cxx_global_var_init or _GLOBAL__sub_I_.
Copy constructors and assignment operators that you do not explicitly define are generated automatically in C++. If you see _ZN followed by a class name and C1E or C2E in the symbol table, that is a generated constructor.
Reducing compiler-generated code
Once you find generated functions, you can reduce them. Mark constructors as explicit to prevent implicit conversions that trigger copy constructors. Use = delete to prevent the compiler from generating copy constructors and assignment operators you do not need. In C++, declare move constructors and move assignment operators to let the compiler optimize away temporary objects.
Avoid static variables with non-trivial initialization. If you must use them, initialize them with straightforward values that the compiler can handle at compile time rather than at runtime.
Use compiler optimization flags like -O2 or -O3 to let the compiler inline and eliminate generated functions that are no longer needed after optimization. Sometimes the generated code exists only because the compiler has not yet optimized it away.
Link with -fno-exceptions if your code does not use exceptions and you link against libraries that do not require them. This prevents the compiler from generating exception-handling code.
Frequently Asked Questions
Why does my binary have functions I did not write?
The compiler creates functions to handle initialization, cleanup, virtual method dispatch, and other runtime tasks that your source code implies but does not explicitly define. These functions are necessary for your program to run correctly, but they are invisible in your source code.
How do I know if a function name in nm output is compiler-generated?
Compiler-generated functions usually start with underscores and contain patterns like __static_initialization, __cxx_, __do_global, or _ZN (C++ name mangling). If you did not write a function with that name in your source code, the compiler created it.
Can I remove compiler-generated functions from my binary?
Not directly — they are necessary for your program to work. You can reduce them by avoiding the language features that trigger their generation, such as static variables with complex initialization or virtual methods in C++. Compiler optimization flags often eliminate generated functions that become unnecessary after optimization.
Does optimization remove compiler-generated functions?
Sometimes. When you compile with -O2 or -O3, the compiler inlines and eliminates generated functions that are no longer needed. However, some generated functions always remain because they are called at runtime or are required by the language specification.
How do I see what a compiler-generated function does?
Use objdump -d to disassemble it and read the assembly code, or use GDB to step through it at runtime. The assembly code shows you exactly what instructions the compiler generated and in what order they run.