Compiler-generated functions are invisible to your source code but show up in compiled binaries and debugging symbols

When you compile C code, the compiler creates functions you never wrote. These include constructors, destructors, copy functions, and internal helpers that support language features. They do not appear in your .c or .h files, but they exist in the compiled object files and executable. Finding them means looking in the right places: the symbol table, the debugger, or the assembly output.

The most direct way to see them is to examine the symbol table of your compiled binary using nm on Linux or macOS, or dumpbin on Windows. You can also step through them in a debugger like gdb or lldb, inspect the assembly output from your compiler, or use tools like objdump to disassemble the binary and read the generated code.

Key Takeaways

  • Use nm to list all symbols in a compiled object file or executable, including compiler-generated functions that do not appear in source code.
  • Compiler-generated functions often have mangled names (like _ZN3FooC1Ev in C++) or special prefixes (like __cxx_) that identify them as compiler output.
  • The -g flag during compilation includes debugging symbols, which makes compiler-generated functions visible in debuggers and symbol tables.
  • Disassembly tools like objdump and readelf show the actual machine code of compiler-generated functions so you can understand what they do.
  • Stepping through code in gdb or lldb lets you watch compiler-generated functions execute in real time and see where control flow goes.

Using nm to list all symbols in a binary

The nm command reads the symbol table of a compiled object file or executable and prints every function and variable name it finds. Compiler-generated functions appear in this list with their full names or mangled names.

Run nm your_program to see all symbols. Add the -C flag to demangle C++ names so they are readable: nm -C your_program. Add -D to see only dynamic symbols in a shared library. The output shows the memory address, symbol type (T for text/code, U for undefined), and the name. Compiler-generated functions often have prefixes like __ or names that do not match anything in your source files.

On Windows, use dumpbin /symbols your_program.exe to see the symbol table. On macOS, nm works the same way, but you may also use otool -L to see linked libraries and otool -t to see the text section.

Reading assembly output to find generated code

The compiler can output assembly code instead of a binary, which shows you exactly what code was generated for each function. This is the most direct way to see what the compiler created.

Compile with the -S flag to generate assembly: gcc -S -g your_file.c. This creates a .s file with the assembly code. Open it in a text editor and search for function names or patterns. Compiler-generated functions often have names starting with __ or _Z (C++ name mangling). You can also compile with -fverbose-asm to add comments that explain what each instruction does.

If you already have a binary, use objdump -d your_program to disassemble it and print the assembly. Pipe it to grep to search for specific function names: objdump -d your_program | grep -A 20 function_name. The -A 20 flag shows 20 lines after the match so you can see the full function body.

Using a debugger to step through generated functions

A debugger like gdb or lldb lets you run your program and step through every function call, including ones the compiler generated. This is useful when you want to see what a generated function actually does at runtime.

Compile with the -g flag to include debugging symbols: gcc -g your_file.c -o your_program. Start the debugger with gdb ./your_program. Set a breakpoint near where you expect a compiler-generated function to be called, then run the program. When the breakpoint hits, use step to step into the next function call, or next to step over it. Use backtrace to see the call stack and identify generated functions by their names.

In gdb, you can also use info functions to list all functions in the program, including generated ones. Use disassemble function_name to see the assembly code for a specific function. If you do not know the exact name, use info functions pattern to search for functions matching a pattern.

Identifying compiler-generated functions by their names

Compiler-generated functions have distinctive naming patterns that make them easy to spot in a symbol table or debugger output. Learning these patterns helps you recognize them without needing to decode every name.

In C, compiler-generated functions often start with __ (double underscore), such as __do_global_ctors or __cxa_finalize. In C++, the compiler mangles function names to encode the class, namespace, and parameter types. Mangled names start with _Z and contain numbers and letters that look like gibberish: _ZN3FooC1Ev is the constructor for class Foo. Use c++filt to decode mangled names: echo _ZN3FooC1Ev | c++filt outputs Foo::Foo().

Other common patterns include _GLOBAL__sub_I_ for global initializers, __static_initialization_and_destruction_ for static object setup, and __cxx_ for C++ runtime helpers. If a symbol name does not appear anywhere in your source code and has one of these prefixes, it was generated by the compiler.

Using readelf and objdump for detailed symbol information

The readelf and objdump tools provide more detailed information about symbols than nm alone. They show the size of functions, their section placement, and whether they are local or global.

Run readelf -s your_program to see a detailed symbol table with columns for address, size, type, binding, and name. This helps you identify which symbols are functions (type FUNC) and how large they are. Compiler-generated functions are often small and have names you do not recognize.

Use objdump -t your_program for a similar output in a different format. Add -C to demangle C++ names. You can also use objdump -h your_program to see the sections of the binary (like .text for code and .data for data), then use objdump -s -j .text your_program to dump the raw bytes of the code section.

Filtering symbol output to find only generated functions

When you run nm or readelf on a large program, the output can be hundreds of lines long. Filtering the output to show only compiler-generated functions makes it easier to find what you are looking for.

Use grep to search for common patterns. For example, nm your_program | grep "^__" shows all symbols starting with double underscore. Use nm your_program | grep "_Z" to find C++ mangled names. Combine multiple patterns with grep -E: nm your_program | grep -E "^__|_Z|__cxx" shows symbols matching any of those patterns.

You can also filter by symbol type. Run readelf -s your_program | grep FUNC to show only functions, then look for names that do not match your source code. Pipe to wc -l to count how many compiler-generated functions exist: readelf -s your_program | grep FUNC | wc -l.

Frequently Asked Questions

Why does my program have functions I never wrote?

The compiler generates functions to support language features like global initialization, exception handling, and runtime type information. In C++, it also generates constructors, destructors, and copy functions if you do not write them yourself. These functions are necessary for the program to run correctly, even though they do not appear in your source code.

Can I see compiler-generated functions without debugging symbols?

Yes, but it is harder. Without the -g flag, the symbol table still exists but contains less information. Use nm or readelf to see symbol names, and use objdump -d to disassemble the binary and see the code. You will not be able to step through them in a debugger or see line numbers, but you can still read the assembly.

What does a mangled C++ name mean?

C++ mangles function names to encode the class, namespace, and parameter types so the linker can distinguish between overloaded functions. For example, _ZN3FooC1Ev means the constructor (C1) for class Foo in the global namespace. Use c++filt to decode it: c++filt _ZN3FooC1Ev outputs Foo::Foo().

How do I know if a function is compiler-generated or from a library?

Compiler-generated functions have distinctive names starting with __ or _Z and do not appear in your source code. Library functions have normal names and come from linked libraries. Use nm -D your_program to see which symbols come from dynamic libraries, and use ldd your_program to see which libraries are linked.

Can I prevent the compiler from generating certain functions?

Some compiler-generated functions can be controlled with flags. For example, -fno-exceptions removes exception-handling code, and -fno-rtti removes runtime type information. However, most generated functions are necessary for the program to work, so removing them may cause crashes or undefined behavior. Check your compiler documentation for specific flags.