Question
How can I generate the assembly code emitted by GCC for C or C++ source files? I want to inspect how a piece of source code is compiled and analyze the generated instructions.
Short Answer
You will learn how GCC transforms C or C++ source into an assembly file, how to use the -S option, and how optimization, target architecture, and syntax choices affect the output. You will also see practical commands for inspecting a single function and understanding what GCC produced.
Concept
GCC compiles source code through several stages. A simplified view is:
C/C++ source → preprocessing → compilation → assembly → machine-code object file → executable
The assembly stage is a human-readable, architecture-specific representation of the instructions GCC plans to emit. On an x86-64 computer, this may include instructions such as mov, add, cmp, and ret.
To stop GCC after creating assembly, use the -S option:
gcc -S program.c
For C++, use g++:
g++ -S program.cpp
GCC writes an assembly file next to the source file. For example, program.c becomes program.s.
Inspecting assembly is useful because compiler output depends on several inputs:
- Source code: Small source changes can produce different instructions.
- Optimization level:
-O0and-O2can produce dramatically different output. - Target CPU and architecture:
-m32,-m64,-march=..., and cross-compilers change the instruction set. - Compiler version: Different GCC versions may optimize code differently.
- Language mode and flags: Options such as
-std=c++20or-fno-exceptionscan affect output.
Assembly is not a stable representation of your source. It is the compiler's implementation for one specific build configuration.
Mental Model
Think of C or C++ as a recipe written for people, while assembly is the detailed set of motions given to a particular kitchen robot.
The same recipe can lead to different motion lists depending on the robot model and whether it is told to work carefully or quickly. Similarly:
- Your C/C++ source is the recipe.
- GCC is the translator.
- The CPU architecture is the robot model.
- Optimization flags tell GCC whether to prioritize easy-to-debug output or efficient output.
Using -S asks GCC to show you the motion list before it turns it into the final binary instructions.
Syntax and Examples
The core GCC option is -S:
gcc -S source.c
g++ -S source.cpp
Choose an output filename
Use -o to name the assembly file explicitly:
gcc -S source.c -o source.s
g++ -S source.cpp -o source.s
Example
Given square.c:
int square(int value) {
return value * value;
}
Generate assembly without optimization:
gcc -S -O0 square.c -o square-O0.s
Generate optimized assembly:
gcc -S -O2 square.c -o square-O2.s
The -O0 output usually preserves a more direct, step-by-step structure and may include stack-frame setup instructions. The -O2 output can be much shorter because GCC removes unnecessary work.
Use Intel syntax on x86
GCC normally emits AT&T-style x86 assembly. To request Intel-style syntax:
gcc -S -O2 -masm=intel square.c -o square.s
This option is relevant to x86-family targets. It changes the text syntax, not the program's behavior.
Add helpful compiler comments
gcc -S -O2 -fverbose-asm square.c -o square.s
-fverbose-asm asks GCC to add extra comments, which can make output easier to connect to the original source.
Preserve source lines in the output
gcc -S -O2 -g -fverbose-asm square.c -o square.s
The -g option includes debugging information. Together with -fverbose-asm, it can provide more useful source-related annotations in the assembly file.
Step by Step Execution
Consider this C function:
int add_and_double(int left, int right) {
int sum = left + right;
return sum * 2;
}
Compile it for a typical 64-bit x86 system:
gcc -S -O2 -masm=intel add.c -o add.s
A possible simplified assembly result is:
add_and_double:
add edi, esi
lea eax, [rdi + rdi]
ret
Read it step by step:
- On a common x86-64 calling convention, the first two
intarguments arrive in registers. Here they are represented byediandesi. add edi, esiaddsrighttoleft. The result is stored inedi.lea eax, [rdi + rdi]calculatesleft + left, which is equivalent to multiplying the sum by 2.leais often used for simple arithmetic even though its name means “load effective address.”retreturns to the caller. For anintreturn value, the result is placed ineax.
Exact register names and instructions can differ by platform, GCC version, and compiler options. The important idea is to connect each instruction sequence to the work required by the source code.
Real World Use Cases
Generating assembly is useful in several practical situations:
- Performance investigation: Check whether a hot loop was vectorized, inlined, or simplified.
- Learning calling conventions: See where function arguments and return values are placed.
- Validating assumptions: Confirm whether a
constexpression was computed at compile time. - Comparing optimization levels: Compare
-O0,-Og,-O2, and-O3for performance-sensitive code. - Debugging low-level integrations: Inspect code that interacts with hardware, system calls, atomics, or inline assembly.
- Checking portability: Compile for another target architecture and compare output using an appropriate GCC cross-compiler.
- Investigating code size: Determine whether abstractions, templates, or error-handling paths increase generated code size.
For example, a developer may inspect assembly to verify that a small helper function is inlined inside a tight loop rather than called repeatedly.
Real Codebase Usage
In real projects, assembly inspection is usually a focused diagnostic tool rather than part of the normal build.
Compile the same configuration you actually ship
Assembly is only meaningful when the flags match the build you want to study:
g++ -std=c++20 -O2 -DNDEBUG -S src/parser.cpp -o parser.s
If a release build uses -O2 but you inspect -O0, you are examining a different program shape.
Isolate a function
Developers often place a small function in a temporary file, compile it with production flags, and inspect only that output. This reduces noise from unrelated code.
Preserve functions while experimenting
A compiler can remove unused functions or inline them. For a small experiment, you may use a function in main or apply a GCC attribute when appropriate:
__attribute__((noinline))
int calculate_fee(int amount) {
return amount * 3 + 5;
}
noinline is useful for analysis, but it should not be added to production code unless there is a real reason.
Inspect an existing object file
If a build already produced an object file, disassemble it instead of recompiling:
objdump -d -C app.o
For Intel-style x86 syntax:
objdump -d -C -M intel app.o
-C demangles C++ names, making symbols such as template functions easier to read.
Compare before and after
When changing a performance-critical expression, save both assembly outputs and compare them. Focus on observable differences such as function calls, branches, loads, stores, and loop structure—not merely on instruction count.
Common Mistakes
Expecting -S to create an executable
-S stops after assembly generation. It does not create an executable.
gcc -S program.c
To build an executable, omit -S:
gcc program.c -o program
Inspecting unoptimized output when diagnosing release performance
This command is valid but often misleading for performance work:
gcc -S -O0 program.c
Use the same optimization level as the real build, often -O2 or -O3:
gcc -S -O2 program.c
Using gcc to link a C++ program
gcc can compile a .cpp file, but g++ is normally the correct C++ driver because it automatically links the C++ standard library when linking.
Prefer:
g++ -S source.cpp
For -S alone, linking does not happen, but using g++ still clearly communicates that the source is C++.
Assuming every source line has a matching instruction
Optimizers can combine expressions, remove unused calculations, inline calls, unroll loops, and replace operations with different but equivalent instructions. There is not always a one-line-to-one-instruction mapping.
Forgetting that assembly is architecture-specific
An x86-64 assembly file cannot be interpreted like ARM assembly. Make sure the target is the one you intend to inspect:
gcc -S -m64 source.c
Use target-specific options only when your compiler supports them.
Misreading C++ symbol names
C++ functions may have mangled names in assembly, such as names containing encoded parameter types. Use objdump -C for disassembly, or inspect the source-associated comments produced by -g -fverbose-asm.
Treating assembly output as guaranteed across compilers
Different GCC versions, flags, targets, and even minor source-context changes can produce different valid output. Rely on documented language behavior, not a particular instruction sequence, unless you deliberately control the toolchain and target.
Comparisons
| Task | Command or tool | Result |
|---|---|---|
| Generate an assembly source file | gcc -S file.c | Creates file.s and stops before assembling. |
| Generate an object file | gcc -c file.c | Creates file.o, containing machine code rather than readable assembly source. |
| Build an executable | gcc file.c -o app | Compiles, assembles, and links. |
| Disassemble an object or executable | objdump -d file.o | Converts existing machine code into assembly-like output. |
| View optimized assembly | gcc -S -O2 file.c | Shows output closer to a typical release build. |
| Use Intel x86 syntax | gcc -S -masm=intel file.c | Changes x86 assembly text from GCC's default AT&T syntax. |
-S versus objdump -d
Use -S when you are compiling source and want GCC's emitted assembly file. Use objdump -d when you already have an object file, library, or executable and need to inspect its machine code.
-O0 versus -O2
| Option | Typical purpose | Assembly characteristics |
|---|---|---|
-O0 | Straightforward debugging | More stack usage and less simplification; easier to relate loosely to source. |
-Og | Debugging with some optimization | Often a practical debug-oriented compromise. |
-O2 | General release builds | Removes redundant work and applies broadly safe optimizations. |
-O3 | More aggressive optimization | May unroll loops or make transformations that increase code size. |
The best option for inspection is usually the option used by the build you are investigating.
Cheat Sheet
# C source to assembly
gcc -S file.c
# C++ source to assembly
g++ -S file.cpp
# Choose output file
gcc -S file.c -o file.s
# Inspect release-style output
gcc -S -O2 file.c -o file.s
# x86 Intel syntax
gcc -S -O2 -masm=intel file.c -o file.s
# Add useful comments/source-related information
gcc -S -O2 -g -fverbose-asm file.c -o file.s
# Produce an object file instead
gcc -c file.c -o file.o
# Disassemble an existing object or executable
objdump -d file.o
objdump -d -C -M intel file.o
-Sstops after generating assembly.- Default assembly output uses the
.sextension. - Use
g++for C++ source files. - Match optimization and target flags to the build you are studying.
- Assembly syntax and instructions depend on the target architecture.
- Optimized assembly may not correspond one-for-one with source lines.
FAQ
How do I get GCC to output assembly code?
Use -S:
gcc -S program.c
For C++, use g++ -S program.cpp. GCC creates a .s assembly file.
What is the difference between gcc -S and gcc -c?
-S creates readable assembly text and stops before assembling it. -c creates a binary object file (.o) containing machine code and stops before linking.
How can I see optimized GCC assembly?
Add the optimization level used by the build, for example:
gcc -S -O2 program.c
How do I make GCC use Intel assembly syntax?
On supported x86 targets, add -masm=intel:
gcc -S -masm=intel program.c
Why does GCC assembly not look like my source code?
The compiler can remove, combine, reorder, inline, or replace operations as long as the program's observable behavior remains correct. Optimization makes this especially noticeable.
Can I get assembly from an already compiled executable?
Yes. Disassemble it with objdump:
objdump -d -C program
Add -M intel on x86 if you prefer Intel syntax.
Why are C++ function names unreadable in assembly?
C++ uses name mangling to encode overloads and type information. When using objdump, pass -C to demangle names.
Does generated assembly work on every CPU?
No. Assembly is specific to an architecture and often to a target configuration. x86-64, ARM64, and RISC-V use different instructions and calling conventions.
Mini Project
Description
Create a small C program and generate two assembly files: one with no optimization and one with release-style optimization. Comparing them demonstrates how GCC can simplify arithmetic before the program runs.
Goal
Generate and compare GCC assembly output for the same function at -O0 and -O2.
Requirements
Create a C file containing a function that performs simple arithmetic.
Generate an assembly file using -O0.
Generate a second assembly file using -O2.
Use -fverbose-asm to make the output easier to inspect.
Identify whether the optimized version removes intermediate work.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.