A Function and Its Assembly

It is time to read real assembly. Here is a function that squares a number:

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

Compiled for x86-64 with optimizations turned off (-O0), the compiler produces this:

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

That might look like a lot for "multiply a number by itself", and it is: this is unoptimized output, written out in the most literal, step-by-step way. We start here precisely because it is literal. Every step is visible. Later you will see how -O2 crushes this down to almost nothing.

Let us take it apart.

The Label

square(int):

A line ending in a colon is a label: a name for the location of what follows. This label marks where the square function begins, so that a call elsewhere knows where to jump.

One detail worth knowing: in the raw .s file this label is not actually spelled square(int). C++ encodes the function's full signature into a single symbol through a process called name mangling, so the real label reads _Z6squarei. Tools (and this course) usually show you the demangled name because it is far easier to read. Just know that if you ever open a raw listing and see names like _Z6squarei, that is your square(int) in disguise.

The Anatomy of an Instruction

Every instruction in the body follows the same shape:

mnemonic   destination, source
  • The mnemonic says what to do (mov, add, imul, ...).
  • The operands say what to do it to.

This course uses Intel syntax, where the destination comes first. So mov rbp, rsp means "copy rsp into rbp", not the other way around. Reading destination-first is the single most important habit for reading these listings. (The next lesson shows the other syntax, AT&T, where the order is reversed, so you can recognise both.)

Line by Line

Now the body, one instruction at a time.

push rbp
mov  rbp, rsp

These two lines are the function prologue. Almost every unoptimized function starts this way. They set up a stack frame: a small region of memory this function uses for its local data. Chapter 4 is devoted to this, so for now just recognise the pattern: push rbp / mov rbp, rsp means "a function is setting itself up".

mov DWORD PTR -4[rbp], edi

This stores the function's argument into the stack frame. The argument n arrived in the register edi (more on why in Chapter 4). -4[rbp] is a memory operand: GCC writes the displacement before the brackets, so this means "the memory location 4 bytes below rbp" (you will also see this written [rbp-4] elsewhere; same thing). DWORD PTR says the value is a dword, 4 bytes, which is the size of an int. So the whole line reads: "copy the 4-byte argument in edi into the local slot at -4[rbp]."

mov eax, DWORD PTR -4[rbp]

Now it reads that value back out of the stack and into the register eax. (Unoptimized code is full of this store-then-immediately-reload pattern; the optimizer removes it.) The register eax is special: it is where a function's integer return value is expected to end up.

imul eax, eax

This is the actual work. imul is integer multiply. With eax as both destination and source, it computes eax = eax * eax: the value times itself. That is n * n, the entire point of the function.

pop rbp
ret

This is the epilogue. pop rbp tears down the stack frame the prologue set up, and ret returns to the caller. The return value is already sitting in eax, ready to be used.

What to Notice

Strip away the setup and teardown, and the function is really just one instruction, imul eax, eax, surrounded by bookkeeping. That is a recurring lesson in reading assembly: a lot of unoptimized output is plumbing (moving values to and from the stack, setting up frames), and only a little of it is the actual computation. Learning to skim past the plumbing and spot the real work is a skill you will build quickly.

The Same Function, Optimized

For contrast, here is the exact same C++ compiled with -O2:

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

No stack frame. No storing and reloading. The argument stays in a register, gets multiplied, and is returned. The optimizer recognised that all that plumbing was unnecessary. Seeing this side-by-side with the -O0 version is exactly the kind of comparison that makes reading assembly worthwhile, and it is what the final chapter of this course is all about.

Try It Yourself

In the exercise for this lesson you will complete a small function, run it to check its output, and then use the assembly panel in the editor to see the very instructions described above. Reading the listing of code you wrote yourself is the fastest way to make all of this click.

Key Takeaways

  • A line ending in : is a label marking a location, such as the start of a function.
  • C++ mangles names into symbols like _Z6squarei; tools usually show the demangled form square(int).
  • Instructions have the shape mnemonic destination, source. In Intel syntax the destination comes first.
  • -4[rbp] is a memory operand, and DWORD PTR specifies a 4-byte (int-sized) access.
  • Unoptimized functions are mostly plumbing (prologue, stack stores/reloads, epilogue) wrapped around a little real work.
  • eax holds the integer return value; imul performs integer multiplication.