Making Your Own Listings

So far, every assembly listing in this course has been handed to you. That was deliberate: you needed the vocabulary before the tools. But the real skill you are building is the ability to take any piece of C++ that interests you, generate its assembly, and read it. The examples in this course are a starting set, not the whole game. Once you can produce listings yourself, every function you ever write becomes something you can inspect, and every "I wonder what the compiler did with that" becomes a question you can answer in under a minute.

This lesson covers the four ways you will do that: asking the compiler directly with -S, disassembling a finished binary with objdump, using Compiler Explorer in the browser, and using the assembly view built into this site's editor.

Asking the Compiler: g++ -S

You already know the compilation pipeline: preprocess, compile, assemble, link. The compile stage is the one that produces assembly, and the -S flag tells the compiler to stop right there and write the assembly to a file instead of continuing on to machine code:

g++ -S square.cpp

This produces square.s, a plain text file you can open in any editor. No executable is created; the pipeline stops before the assembler runs.

Run exactly that command, though, and the output will fight you in two ways. First, GNU tools emit AT&T syntax by default, and you learned last lesson that this course reads Intel. Second, the file is cluttered with bookkeeping directives, lines starting with a dot, like .cfi_startproc and .cfi_def_cfa_offset, that exist for exception handling and debuggers, not for you. A better incantation is:

g++ -S -O0 -masm=intel -fno-asynchronous-unwind-tables square.cpp

Three flags do the cleanup:

  • -masm=intel switches the output to Intel syntax.
  • -fno-asynchronous-unwind-tables suppresses most of the .cfi_ directive noise, leaving mostly real instructions.
  • -O0 or -O2 chooses the optimization level. -O0 means "do not optimize", producing verbose but literal code that maps line-by-line onto your source. -O2 enables the full standard optimizer, producing the short, rearranged code that actually ships.

Get in the habit of always stating the optimization level explicitly. It changes the output more than any other flag, and comparing the -O0 and -O2 listings of the same function is one of the most instructive exercises there is.

If you use clang++ instead of g++, everything above works identically; Clang accepts the same flags and produces a .s file the same way.

Disassembling a Binary: objdump -d

-S asks the compiler for assembly on the way down. Sometimes you need to go the other direction: you have a compiled program or library and want to see what is inside it. That is disassembly, turning machine code back into assembly, and the standard tool is objdump:

g++ -O2 -o square square.cpp
objdump -d -M intel square

The -d flag disassembles the executable sections, and -M intel selects Intel syntax. The output shows each instruction alongside the actual machine-code bytes it was encoded as, plus the address where it lives.

When would you reach for objdump instead of -S?

  • You do not have the source. A third-party library or an already-built program can only be disassembled.
  • You want the final, linked result. The .s file from -S is pre-linking; objdump shows the program as it will truly execute, with real addresses and any link-time changes applied.
  • You are matching a crash address. Debuggers and crash reports give you addresses, and objdump output includes them.

The trade-off is that disassembly loses information. Variable names are gone, most labels are gone, and there is no color-coded connection back to your source. For studying your own code, -S or the tools below are friendlier.

Compiler Explorer

Compiler Explorer, at godbolt.org, is a free website with C++ source on the left and the generated assembly on the right, updating live as you type. It is, without much competition, the tool professionals reach for first when they want to see what a compiler does.

Three things make it exceptional:

  • Color-coded mapping. Each line of your C++ is highlighted in a color, and the assembly it produced is highlighted in the same color. Hover over a statement and you instantly see which instructions it became. This mapping is the single fastest way to learn to read assembly.
  • Every compiler, every version, every flag. A dropdown offers GCC, Clang, MSVC, and dozens of others, across many versions and architectures. A flags box takes anything you would pass on the command line, so typing -O2 there is all it takes to compare optimization levels. It shows Intel syntax by default and filters out directive noise for you.
  • Shareable links. Any snippet-plus-compiler-plus-flags setup can be turned into a short URL. This is why performance discussions online are full of godbolt links: they are reproducible evidence, not screenshots.

When you see an assembly listing in a blog post or a conference talk, odds are it came from Compiler Explorer.

The Assembly Panel in This Editor

You do not need to leave this site to practice any of this. Every exercise on the platform has a live assembly panel beside the code editor, which shows the x86-64 assembly for the code you have written, in Intel syntax, just like the listings in these lessons. It compiles when the page loads and again after every run, and the -O0 and -O2 buttons in its header switch optimization level. Clicking a line of assembly moves the editor cursor to the C++ line it came from, and moving the cursor in the editor highlights the instructions that line produced.

The workflow the course exercises are built around is simple:

  1. Write your C++ in the editor.
  2. Run it and confirm it behaves the way you expect.
  3. Read the assembly panel and work out what the compiler produced.

Step 3 is where the learning happens. Change something small, a type, an initializer, a loop bound, and watch what moves in the assembly. From this chapter onward, the exercises will ask you to do exactly that.

A Worked Example: -O0 vs -O2

Here is the whole round trip on one tiny function:

int square(int x)
{
    return x * x;
}

Compile it without optimization:

g++ -S -O0 -masm=intel -fno-asynchronous-unwind-tables square.cpp

The interesting part of square.s looks like this:

square(int):
    push rbp
    mov  rbp, rsp
    mov  DWORD PTR -4[rbp], edi
    mov  eax, DWORD PTR -4[rbp]
    imul eax, DWORD PTR -4[rbp]
    pop  rbp
    ret

Seven instructions. At -O0 the compiler is completely literal: it sets up bookkeeping, stores the parameter into memory on the stack, reads it back (twice) to multiply, then tears the bookkeeping down. Nothing is clever, which is exactly why -O0 output is the easiest to match against source code.

Now the same command with -O2:

square(int):
    imul edi, edi
    mov  eax, edi
    ret

Three instructions. The optimizer threw away the bookkeeping and the trips to memory, multiplied the argument register by itself, and moved the result into the return register. Same function, same behavior, less than half the code. Being able to produce this comparison yourself, for any function you care about, is the skill this lesson exists to give you.

Key Takeaways

  • Generating your own listings turns every piece of C++ you write into practice material; do not wait for a course to hand you assembly.
  • g++ -S stops the pipeline after the compile stage and writes a .s assembly file; add -masm=intel for Intel syntax and -fno-asynchronous-unwind-tables to cut directive noise. clang++ works the same way.
  • Always state the optimization level: -O0 gives literal, source-shaped code; -O2 gives the optimized code that really ships.
  • objdump -d -M intel disassembles a compiled binary; use it when you have no source or need the final linked program, at the cost of names and source mapping.
  • Compiler Explorer (godbolt.org) shows live, color-coded source-to-assembly mapping across compilers and flags, and is the tool most professionals use.
  • This site's editor has a live assembly panel giving an x86-64 view of your exercise code: write, run, then read the assembly.