Reading Your First Assembly Listing
Walk through the assembly of a tiny function line by line and learn the anatomy of an instruction.
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 formsquare(int). - Instructions have the shape
mnemonic destination, source. In Intel syntax the destination comes first. -4[rbp]is a memory operand, andDWORD PTRspecifies a 4-byte (int-sized) access.- Unoptimized functions are mostly plumbing (prologue, stack stores/reloads, epilogue) wrapped around a little real work.
eaxholds the integer return value;imulperforms integer multiplication.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Reading Your First Assembly Listing - Quiz
Test your understanding of the lesson.
Practice Exercises
Complete the square Function
Finish the square function so it returns n multiplied by itself. The program prints square(5) and square(12). Once your output is correct, read the assembly panel and find the imul instruction that performs the multiplication.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!