Question

Get Assembly Output from C/C++ with GCC

cppcassemblygccdisassembly

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: -O0 and -O2 can 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++20 or -fno-exceptions can 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.

Take Quiz

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:

  1. On a common x86-64 calling convention, the first two int arguments arrive in registers. Here they are represented by edi and esi.
  2. add edi, esi adds right to left. The result is stored in edi.
  3. lea eax, [rdi + rdi] calculates left + left, which is equivalent to multiplying the sum by 2. lea is often used for simple arithmetic even though its name means “load effective address.”
  4. ret returns to the caller. For an int return value, the result is placed in eax.

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 const expression was computed at compile time.
  • Comparing optimization levels: Compare -O0, -Og, -O2, and -O3 for 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.

Take Quiz

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

TaskCommand or toolResult
Generate an assembly source filegcc -S file.cCreates file.s and stops before assembling.
Generate an object filegcc -c file.cCreates file.o, containing machine code rather than readable assembly source.
Build an executablegcc file.c -o appCompiles, assembles, and links.
Disassemble an object or executableobjdump -d file.oConverts existing machine code into assembly-like output.
View optimized assemblygcc -S -O2 file.cShows output closer to a typical release build.
Use Intel x86 syntaxgcc -S -masm=intel file.cChanges 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

OptionTypical purposeAssembly characteristics
-O0Straightforward debuggingMore stack usage and less simplification; easier to relate loosely to source.
-OgDebugging with some optimizationOften a practical debug-oriented compromise.
-O2General release buildsRemoves redundant work and applies broadly safe optimizations.
-O3More aggressive optimizationMay 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.

Take Quiz

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
  • -S stops after generating assembly.
  • Default assembly output uses the .s extension.
  • 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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more