This deck walks through x86-64 function-call assembly in depth, working from GCC -O0 output. It compares AT&T with Intel syntax and covers stack frames, RSP and RBP, addressing modes, arithmetic, and compare-and-branch control flow, then inspects the stack in GDB. Along the way it targets three traps that catch most people: reversing the operands, confusing an address with the value stored there, and treating stack allocation as though it were initialization.
Subject: Computer Architecture · 94 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Objectives
By the end of this deck you can:
1. Read a small GCC -O0 x86-64 assembly listing and split it into prologue, body, and epilogue.
2. Explain what RSP, RBP, RIP, argument registers, and EAX/RAX are doing during a function call.
3. Annotate mov, lea, add, sub, cmp, je, jne, and jmp with their effects on registers, memory, flags, and control flow.
4. Draw the stack frame for a real function and point to RBP-8, RBP+8, saved RBP, locals, and the return address.
5. Use basic GDB commands to step one instruction at a time and inspect the live stack.
Concept
Assembly is a line-by-line record of tiny machine actions: move this value, reserve these bytes, compare this number, jump there if the comparison says so.
The important move today is not memorizing every instruction. It is learning to translate each line into a state change: which register changed, which memory slot changed, or where execution goes next.
state — The current contents of registers, stack memory, flags, and the instruction pointer. Reading assembly means updating this state one instruction at a time.
Counterexample
Discussion prompt
Assembly is a line-by-line record of tiny machine actions: move this value, reserve these bytes, compare this number, jump there if the comparison says so.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Picture it
Figure (svg): Stack diagram showing higher addresses above lower addresses, with caller data, return address, saved RBP, and local variables.
Discussion prompt
Read the picture before the words. What is this showing, and what is the one thing it is built to make obvious? Commit to an answer, then read on.
Hint: Name the parts, then say what changes between them — and if nothing changes, say what is being held still.
Answer:
A function call borrows a piece of the stack for temporary work. RSP marks the moving bottom edge of the borrowed area; RBP often marks a stable reference point inside it.
Intuition
Figure (svg): Stack diagram showing higher addresses above lower addresses, with caller data, return address, saved RBP, and local variables.
A function call borrows a piece of the stack for temporary work. RSP marks the moving bottom edge of the borrowed area; RBP often marks a stable reference point inside it.
The stack grows downward: allocating space means subtracting from RSP, and giving space back means adding to RSP or restoring it from RBP.
Analogy
Discussion prompt
Explain The stack is a workbench with a moving edge by analogy to something with no Computer Architecture in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
A function call borrows a piece of the stack for temporary work. RSP marks the moving bottom edge of the borrowed area; RBP often marks a stable reference point inside it.
Concept
| register | plain meaning | why you care |
|---|---|---|
| RSP | stack pointer | top/edge of current stack space |
| RBP | base/frame pointer | stable anchor for this function's locals |
| RIP | instruction pointer | which instruction executes next |
| RAX/EAX | return/value register | where many integer results appear |
On x86-64, EAX is the low 32 bits of RAX. A function returning an int normally leaves that return value in EAX.
The ABI decides the calling convention. In the System V AMD64 ABI used by common Linux GCC examples, the first two int arguments arrive in EDI and ESI.
Comparison
Comparison matrix
From The four registers to watch first: refill the plain meaning column from what you know. The rest of the table is as it appeared.
| register | plain meaning | why you care |
|---|---|---|
| RSP | stack pointer | top/edge of current stack space |
| RBP | base/frame pointer | stable anchor for this function's locals |
| RIP | instruction pointer | which instruction executes next |
| RAX/EAX | return/value register | where many integer results appear |
Concept
gcc -O0 -S -mno-red-zone -fno-stack-protector -fno-asynchronous-unwind-tables score.c -o score.s| piece | job |
|---|---|
| -S | stop after producing assembly |
| -O0 | keep a literal, beginner-readable shape |
| -mno-red-zone | force visible stack allocation in this leaf function |
| -fno-stack-protector | avoid extra canary code |
| score.s | the assembly file to read |
Different compiler versions and flags can change the exact listing. For teaching, fix the flags first so everyone is annotating the same text.
Trade off
Comparison matrix
From The command that makes a .s file: every row here is a choice with a cost. Fill the job column, then say which row you would actually pick and what you give up for it.
| piece | job |
|---|---|
| -S | stop after producing assembly |
| -O0 | keep a literal, beginner-readable shape |
| -mno-red-zone | force visible stack allocation in this leaf function |
| -fno-stack-protector | avoid extra canary code |
| score.s | the assembly file to read |
Pattern
Predict first
The table runs: a | first int argument | 2 · b | second int argument | 3 · sum | local variable | 5 · doubled | local variable | 10
In The C function we will compile, given the rows so far: what is the next one — the row where source thing is return?
Correct: return | function result | 11
| source thing | role | value for score(2, 3) |
|---|---|---|
| a | first int argument | 2 |
| b | second int argument | 3 |
| sum | local variable | 5 |
| doubled | local variable | 10 |
| return | function result | 11 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. a and b are parameters. sum and doubled are local variables that the compiler will place somewhere in the frame at -O0.
Worked example
int score(int a, int b) {
int sum = a + b;
int doubled = sum * 2;
if (doubled == 10) {
return doubled + 1;
}
return doubled - 1;
}Name the source-level variables
Why: a and b are parameters. sum and doubled are local variables that the compiler will place somewhere in the frame at -O0.
| source thing | role | value for score(2, 3) |
|---|---|---|
| a | first int argument | 2 |
| b | second int argument | 3 |
| sum | local variable | 5 |
| doubled | local variable | 10 |
| return | function result | 11 |
Predict the high-level behavior first
Why: Assembly is easier when you already know what the C code is trying to do. For score(2,3), the branch is the true branch because doubled becomes 10.
Error analysis
Annotate
Walk the callouts on The C function we will compile. Each one is a place this is easy to get subtly wrong.
a and b are parameters. sum and doubled are local variables that the compiler will place somewhere in the frame at -O0.score(2,3), the branch is the true branch because doubled becomes 10.Concept
| region | what it does | typical clues |
|---|---|---|
| prologue | set up this function's frame | push rbp; mov rbp,rsp; sub rsp,N |
| body | implement C statements | loads, stores, arithmetic, comparisons, jumps |
| epilogue | tear down the frame and return | leave; ret or mov/pop/ret |
The prologue and epilogue are not the point of the C function. They are the bookkeeping that lets the function use stack memory and then return cleanly.
The body is where source-level variables and control flow appear: adding a+b, doubling, comparing with 10, and choosing a return value.
Explain it
Discussion prompt
Explain The compiler output has three jobs to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
The prologue and epilogue are not the point of the C function. They are the bookkeeping that lets the function use stack memory and then return cleanly.
Worked example
score:
pushq %rbp
movq %rsp, %rbp
subq $24, %rsp
movl %edi, -20(%rbp)
movl %esi, -24(%rbp)
movl -20(%rbp), %edx
movl -24(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
addl %eax, %eax
movl %eax, -8(%rbp)
cmpl $10, -8(%rbp)
jne .L2
movl -8(%rbp), %eax
addl $1, %eax
jmp .L3
.L2:
movl -8(%rbp), %eax
subl $1, %eax
.L3:
leave
ret| range | role | first question to ask |
|---|---|---|
| pushq through subq | prologue | how is the frame created? |
| argument stores | parameter spill | where did a and b land? |
| loads through stores | body arithmetic | which C expression is this? |
| cmp/jne/jmp labels | branch | which return path is chosen? |
| leave/ret | epilogue | how is the caller restored? |
Read it in chunks before reading individual lines
Why: Chunking keeps the listing from feeling like noise. Once the chunks are named, each instruction has a local purpose.
Concept
movl %edi, -20(%rbp)
addl %edx, %eax
cmpl $10, -8(%rbp)| AT&T clue | meaning |
|---|---|
| % | register name: %edi, %rbp, %eax |
| $ | immediate literal: $10 |
| source, destination | movl %edi, -20(%rbp) stores EDI into memory |
| suffix l/q | l is 32-bit, q is 64-bit |
The most common beginner bug is reading AT&T operands backward. In AT&T, the rightmost operand is usually what changes.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Tempting mistake: read movl %edi, -20(%rbp) as if the left side is the destination.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: That is Intel-style thinking applied to AT&T text.
Correct AT&T read: source first, destination second.
Why: That is Intel-style thinking applied to AT&T text. It reverses the data flow.
Trap
Tempting mistake: read movl %edi, -20(%rbp) as if the left side is the destination.
movl %edi, -20(%rbp)Wrong annotation: memory loads into EDI
Why: That is Intel-style thinking applied to AT&T text. It reverses the data flow.
| wrong read | actual problem |
|---|---|
| -20(%rbp) -> %edi | operands were reversed |
Correct AT&T read: source first, destination second.
movl %edi, -20(%rbp)Right annotation: store EDI into the stack slot at RBP-20
Why: %edi is the source. -20(%rbp) is the destination memory address.
| source | destination | effect |
|---|---|---|
| %edi | -20(%rbp) | memory[rbp-20] = edi |
Worked example
score:
push rbp
mov rbp, rsp
sub rsp, 24
mov DWORD PTR [rbp-20], edi
mov DWORD PTR [rbp-24], esi
mov edx, DWORD PTR [rbp-20]
mov eax, DWORD PTR [rbp-24]
add eax, edx
mov DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
add eax, eax
mov DWORD PTR [rbp-8], eax
cmp DWORD PTR [rbp-8], 10
jne .L2
mov eax, DWORD PTR [rbp-8]
add eax, 1
jmp .L3
.L2:
mov eax, DWORD PTR [rbp-8]
sub eax, 1
.L3:
leave
ret| same idea | AT&T | Intel |
|---|---|---|
| store first arg | movl %edi, -20(%rbp) | mov DWORD PTR [rbp-20], edi |
| add values | addl %edx, %eax | add eax, edx |
| compare | cmpl $10, -8(%rbp) | cmp DWORD PTR [rbp-8], 10 |
| unconditional jump | jmp .L3 | jmp .L3 |
Use one syntax consistently while learning
Why: Switching syntax mid-lesson creates fake confusion. GCC's default Unix assembly is AT&T; many textbooks and debuggers can also show Intel.
Concept
Figure (svg): Diagram showing -8(%rbp) as base register RBP plus a negative displacement, equivalent to Intel [rbp-8].
movl -8(%rbp), %eax
movl %eax, -4(%rbp)| operand | kind | read it as |
|---|---|---|
| -8(%rbp) | memory | bytes at address RBP - 8 |
| %eax | register | the low 32 bits of RAX |
| $10 | immediate | the literal integer 10 |
Memory operands are not variables by themselves. They are recipes for finding a location in memory.
Picture it
Figure (svg): Stack frame after prologue, showing saved caller RBP, return address, local slots at RBP minus offsets, and RSP at the bottom.
Discussion prompt
Read the picture before the words. What is this showing, and what is the one thing it is built to make obvious? Commit to an answer, then read on.
Hint: Name the parts, then say what changes between them — and if nothing changes, say what is being held still.
Answer:
During the body, RSP may move for calls or temporary pushes, so compilers often use RBP as a stable anchor in unoptimized teaching output.
Intuition
Figure (svg): Stack frame after prologue, showing saved caller RBP, return address, local slots at RBP minus offsets, and RSP at the bottom.
During the body, RSP may move for calls or temporary pushes, so compilers often use RBP as a stable anchor in unoptimized teaching output.
That is why locals appear as negative offsets like -4(%rbp) and -8(%rbp), while the return address sits above the anchor at 8(%rbp).
Worked example
subq $24, %rsp
movl %edi, -20(%rbp)
movl %esi, -24(%rbp)
movl %eax, -4(%rbp)
movl %eax, -8(%rbp)Reserve 24 bytes below RBP
Why: subq $24, %rsp moves the stack pointer down, making room for stack slots in this frame.
Assign a meaning to each offset only after seeing the stores
Why: -20(%rbp) gets a, -24(%rbp) gets b, -4(%rbp) later gets sum, and -8(%rbp) later gets doubled.
| offset | C-level meaning | size used here |
|---|---|---|
| -4(%rbp) | sum | 4 bytes |
| -8(%rbp) | doubled | 4 bytes |
| -20(%rbp) | a | 4 bytes |
| -24(%rbp) | b | 4 bytes |
Concept
| piece of contract | System V x86-64 teaching version |
|---|---|
| first int argument | EDI |
| second int argument | ESI |
| integer return value | EAX |
| return address | pushed by call |
| callee cleanup | restore RSP/RBP, then ret |
This contract is called a calling convention. It lets separately compiled functions agree on where arguments, return values, and saved control-flow information go.
Important boundary: Windows x64 uses a different first-argument register order. Today we are reading the Linux/System V style used by this GCC example.
Intuition
Figure (svg): Diagram showing call pushing the return address, then ret popping it back into RIP.
A call does two things: it saves the address of the next instruction on the stack, then jumps to the function.
A ret does the matching action: it removes that saved address from the stack and jumps back to it.
Ranking
Put in order
Put the moves of Prologue: line-by-line state changes into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. The CPU subtracts 8 from RSP, then writes the old RBP at the new top of stack.
Worked example
pushq %rbp
movq %rsp, %rbp
subq $24, %rsppushq %rbp saves the caller's frame pointer
Why: The CPU subtracts 8 from RSP, then writes the old RBP at the new top of stack.
movq %rsp, %rbp makes a stable anchor
Why: After the push, RSP points at the saved old RBP. Copying RSP into RBP makes that location the frame base.
subq $24, %rsp allocates local stack space
Why: Subtracting moves RSP downward. Those 24 bytes become the area where this function can spill values.
| instruction | RSP effect | RBP effect | memory effect |
|---|---|---|---|
| pushq %rbp | RSP = RSP - 8 | unchanged | memory[RSP] = old RBP |
| movq %rsp,%rbp | unchanged | RBP = RSP | none |
| subq $24,%rsp | RSP = RSP - 24 | unchanged | reserves 24 bytes |
Intuition
Figure (svg): Timeline showing RSP changing at entry, push rbp, sub 24, and leave.
Think of RBP as the label on the current function's stack frame. Think of RSP as the live edge of the stack.
When annotating, mark RSP changes loudly. Those are the lines that actually change the size or position of the current stack area.
Estimation
Predict first
At -O0, GCC often writes parameters to stack slots so the debugger has stable locations for source variables.
Commit before you compute: what does Spilling arguments into the frame come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Store the second parameter into its stack slot
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. ESI holds b. The store copies that 32-bit value into memory at RBP-24.
Worked example
movl %edi, -20(%rbp)
movl %esi, -24(%rbp)Store the first parameter into its stack slot
Why: EDI holds a. The store copies that 32-bit value into memory at RBP-20.
Store the second parameter into its stack slot
Why: ESI holds b. The store copies that 32-bit value into memory at RBP-24.
| instruction | before score(2,3) | after |
|---|---|---|
| movl %edi,-20(%rbp) | EDI = 2 | memory[RBP-20] = 2 |
| movl %esi,-24(%rbp) | ESI = 3 | memory[RBP-24] = 3 |
At -O0, GCC often writes parameters to stack slots so the debugger has stable locations for source variables.
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
Store the second parameter into its stack slot
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
At -O0, GCC often writes parameters to stack slots so the debugger has stable locations for source variables.
Step zero
Discussion prompt
Body: compute sum — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Load a into EDX
Answer:
Worked example
movl -20(%rbp), %edx
movl -24(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)Load a into EDX
Why: The first movl reads memory at RBP-20. For score(2,3), EDX becomes 2.
Load b into EAX
Why: The second movl reads memory at RBP-24. EAX becomes 3.
Add EDX into EAX
Why: AT&T order is source then destination, so addl %edx,%eax means EAX = EAX + EDX. Here 3 + 2 = 5.
Store the result as sum
Why: The final move writes EAX into RBP-4, which is the slot GCC uses for sum.
| line | EDX | EAX | memory written |
|---|---|---|---|
| 1 | 2 | - | - |
| 2 | 2 | 3 | - |
| 3 | 2 | 5 | - |
| 4 | 2 | 5 | sum = 5 at RBP-4 |
Blank canvas
Draw it
Draw what Body: compute sum just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
movl -20(%rbp), %edx
movl %edx, -4(%rbp)| instruction | direction | important detail |
|---|---|---|
| movl -20(%rbp), %edx | memory to register | EDX gets a copy |
| movl %edx, -4(%rbp) | register to memory | memory gets a copy |
| both | copy, not link | later changes do not automatically sync |
Assembly has no idea that a slot is called a or sum. Those names are source-level meanings we infer from the compiler's pattern.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Tempting mistake: after loading memory into EAX, assume EAX stays connected to that stack slot.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: addl $1,%eax changes EAX. It does not write back to memory unless a later store does that.
Correct: the first line copies the value; the second line changes only the register.
Why: addl $1,%eax changes EAX. It does not write back to memory unless a later store does that.
Trap
Tempting mistake: after loading memory into EAX, assume EAX stays connected to that stack slot.
movl -4(%rbp), %eax
addl $1, %eaxWrong annotation: memory at RBP-4 also changed
Why: addl $1,%eax changes EAX. It does not write back to memory unless a later store does that.
| mistake | why wrong |
|---|---|
| memory[RBP-4] becomes memory[RBP-4]+1 | the destination was EAX, not memory |
Correct: the first line copies the value; the second line changes only the register.
movl -4(%rbp), %eax
addl $1, %eaxRight annotation: EAX increments, memory is unchanged
Why: A register result returns through EAX just fine; memory only changes when the destination operand is memory.
| after line | EAX | memory[RBP-4] |
|---|---|---|
| mov | old sum | old sum |
| add | old sum + 1 | old sum |
Step zero
Discussion prompt
Body: compute doubled — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Load sum into EAX
Answer:
Worked example
movl -4(%rbp), %eax
addl %eax, %eax
movl %eax, -8(%rbp)Load sum into EAX
Why: The compiler reads sum from its stack slot at RBP-4.
Double EAX by adding it to itself
Why: addl %eax,%eax means EAX = EAX + EAX. If sum is 5, EAX becomes 10.
Store doubled
Why: The result is written to RBP-8, the stack slot for doubled.
| line | meaning | value for score(2,3) |
|---|---|---|
| 1 | EAX = sum | 5 |
| 2 | EAX = EAX + EAX | 10 |
| 3 | doubled = EAX | 10 |
Concept
addl %edx, %eax
subq $24, %rsp
subl $1, %eax| instruction | destination | effect |
|---|---|---|
| addl %edx,%eax | EAX | EAX = EAX + EDX |
| subq $24,%rsp | RSP | RSP = RSP - 24 |
| subl $1,%eax | EAX | EAX = EAX - 1 |
For annotation, always ask: which operand is the destination? That is the location that changes.
Concept
leaq -8(%rbp), %rax
movl -8(%rbp), %eax| instruction | what EAX/RAX receives | memory read? |
|---|---|---|
| leaq -8(%rbp), %rax | the address RBP - 8 | no |
| movl -8(%rbp), %eax | the 4-byte value stored there | yes |
effective address — The numeric address produced by an addressing expression such as RBP - 8. lea loads this address itself, not the value stored at that address.
Definition probe
Sort into buckets
Every line below is part of the definition of state or of effective address — one or the other, never both. Put each where it belongs.
lea loads this address itself, not the value stored at that address.lea loads this address itself, not the value stored at that address.Anomaly
Predict first
A student writes this, and it looks reasonable:
Tempting mistake: treat lea like a memory load because its operand looks like a memory operand.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: That annotation describes a mov load, not lea.
Correct: lea performs address arithmetic.
Why: That annotation describes a mov load, not lea.
Trap
Tempting mistake: treat lea like a memory load because its operand looks like a memory operand.
leaq -8(%rbp), %raxWrong annotation: RAX = memory[RBP-8]
Why: That annotation describes a mov load, not lea.
| wrong read | actual issue |
|---|---|
| RAX gets doubled | LEA never touched memory |
Correct: lea performs address arithmetic.
leaq -8(%rbp), %raxRight annotation: RAX = RBP - 8
Why: The result is a pointer/address. To read the int stored there, use a mov from that address.
| instruction | RAX after |
|---|---|
| leaq -8(%rbp), %rax | the address RBP - 8 |
Break the constraint
Discussion prompt
The rule this trap just fixed:
The result is a pointer/address. To read the int stored there, use a mov from that address.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
That annotation describes a mov load, not lea.
Concept
cmpl $10, -8(%rbp)
jne .L2| piece | meaning |
|---|---|
| cmpl $10,-8(%rbp) | compare doubled with 10 by setting flags |
| ZF | zero flag: 1 if the compared values are equal |
| jne .L2 | jump if ZF is 0 |
| fall through | continue to the next line if the jump is not taken |
A compare instruction usually does not store a normal result. Its result is the condition flags, and the next conditional jump reads those flags.
Ranking
Put in order
Put the moves of Branch trace: equal case into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. For score(2,3), sum is 5 and doubled is 10.
Worked example
cmpl $10, -8(%rbp)
jne .L2
movl -8(%rbp), %eax
addl $1, %eax
jmp .L3Assume doubled is 10
Why: For score(2,3), sum is 5 and doubled is 10.
cmp sees equality, so ZF becomes 1
Why: The compared values match: memory at RBP-8 is 10 and the immediate is 10.
jne is not taken
Why: jne means jump if not equal. Since ZF is 1, execution falls through to the true-return path.
EAX becomes doubled + 1
Why: The function prepares the return value 11 in EAX.
| moment | ZF | next instruction | EAX |
|---|---|---|---|
| after cmp | 1 | fall through | - |
| after mov | 1 | addl $1,%eax | 10 |
| after add | 1 | jmp .L3 | 11 |
Worked example
cmpl $10, -8(%rbp)
jne .L2
movl -8(%rbp), %eax
addl $1, %eax
jmp .L3
.L2:
movl -8(%rbp), %eax
subl $1, %eaxAssume doubled is 8
Why: For score(1,3), sum is 4 and doubled is 8.
cmp sees not equal, so ZF becomes 0
Why: The compared values do not match: 8 is not 10.
jne jumps to .L2
Why: jne reads ZF = 0, so execution skips the doubled+1 path.
EAX becomes doubled - 1
Why: At .L2, the function loads 8 and subtracts 1, so the return value is 7.
| moment | ZF | control flow | EAX |
|---|---|---|---|
| after cmp | 0 | jump to .L2 | - |
| after .L2 mov | 0 | next: subl | 8 |
| after sub | 0 | next: .L3 | 7 |
Intuition
Figure (svg): Control-flow graph showing cmp leading either to the fall-through path when ZF is 1 or to .L2 when ZF is 0, then joining at .L3.
Assembly is printed in one column, but jumps make it a graph. When teaching, draw the arrows as soon as you see labels and jumps.
A conditional jump chooses an edge based on flags. An unconditional jmp is just a direct edge to another label.
Concept
je .Lsame
jne .Ldifferent
jmp .Ldone| jump | condition | plain reading |
|---|---|---|
| je | ZF = 1 | jump if equal |
| jne | ZF = 0 | jump if not equal |
| jmp | always | jump no matter what |
The names je and jne only make sense relative to a previous compare or arithmetic instruction that set the flags.
Comparison
Comparison matrix
From je, jne, and jmp: refill the condition column from what you know. The rest of the table is as it appeared.
| jump | condition | plain reading |
|---|---|---|
| je | ZF = 1 | jump if equal |
| jne | ZF = 0 | jump if not equal |
| jmp | always | jump no matter what |
Worked example
.L3:
leave
retleave destroys this stack frame
Why: leave is shorthand for restoring RSP from RBP and then popping the saved caller RBP.
ret jumps back to the caller
Why: ret pops the return address into RIP. EAX already holds the function's return value.
| instruction | RSP effect | RBP effect | RIP/control effect |
|---|---|---|---|
| leave | RSP = old frame base, then RSP = RSP + 8 | RBP = saved caller RBP | none |
| ret | RSP = RSP + 8 | unchanged | RIP = return address |
Concept
leave
# equivalent idea:
movq %rbp, %rsp
popq %rbp| line | effect |
|---|---|
| movq %rbp,%rsp | throw away local stack space all at once |
| popq %rbp | restore caller's RBP and add 8 to RSP |
| ret | restore caller's RIP from the return address |
This is why a corrupted saved RBP or return address is serious: the epilogue trusts the stack frame layout.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Tempting mistake: read subq $24,%rsp as if it creates variables and fills them with zero.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Subtracting from RSP only moves the pointer.
Correct: stack space exists after the pointer moves; stores give slots meaningful values.
Why: Subtracting from RSP only moves the pointer. It does not clear the memory.
Trap
Tempting mistake: read subq $24,%rsp as if it creates variables and fills them with zero.
subq $24, %rspWrong annotation: 24 bytes are zeroed
Why: Subtracting from RSP only moves the pointer. It does not clear the memory.
| wrong assumption | actual |
|---|---|
| locals are zero | space is reserved but contents are unspecified |
Correct: stack space exists after the pointer moves; stores give slots meaningful values.
subq $24, %rsp
movl %edi, -20(%rbp)Right annotation: allocation first, meaningful writes later
Why: Only the movl store gives the RBP-20 slot the value of a.
| instruction | memory meaning |
|---|---|
| subq $24,%rsp | space reserved |
| movl %edi,-20(%rbp) | slot now holds a |
Concept
pushq %rbp
movq %rsp, %rbp
subq $24, %rsp
movl %edi, -20(%rbp)
leave
ret| instruction | does RSP change? | why |
|---|---|---|
| pushq %rbp | yes | push moves stack down by 8 |
| movq %rsp,%rbp | no | copies RSP into RBP |
| subq $24,%rsp | yes | RSP is the destination |
| movl %edi,-20(%rbp) | no | writes memory, not RSP |
| leave | yes | restores and pops |
| ret | yes | pops return address |
Shortcut: if RSP is the destination, or the instruction is stack/control-flow machinery like push, pop, call, leave, or ret, check the stack pointer.
Discrimination
Sort into buckets
Sort these by does RSP change?, from memory, without looking back at Which instructions modify RSP?. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.
Step zero
Discussion prompt
Annotate a 15-line listing — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: First pass: bracket prologue and epilogue
Answer:
Worked example
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl %edi, -4(%rbp)
movl -4(%rbp), %eax
addl $7, %eax
movl %eax, -8(%rbp)
cmpl $20, -8(%rbp)
jne .Lelse
movl $1, %eax
jmp .Ldone
.Lelse:
movl $0, %eax
.Ldone:
leave
retFirst pass: bracket prologue and epilogue
Why: Lines 1-3 build the frame. Lines 15-16 destroy it and return.
Second pass: name the data slots
Why: Line 4 stores the argument at RBP-4. Line 7 stores a computed local at RBP-8.
Third pass: draw branch arrows
Why: Line 8 sets flags; line 9 jumps to .Lelse if the stored value is not 20; line 11 skips over the else block.
| line group | annotation |
|---|---|
| 1-3 | prologue: save RBP, establish RBP, allocate 16 bytes |
| 4-7 | body: store arg, add 7, store computed value |
| 8-11 | if value equals 20, return 1 and jump to done |
| 12-14 | else path returns 0 |
| 15-16 | epilogue: leave frame and return |
Blank canvas
Draw it
Draw what Annotate a 15-line listing just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
gcc -O0 -g -mno-red-zone -fno-stack-protector score.c -o score
gdb ./score| piece | why use it |
|---|---|
| -g | include debug info so GDB knows source lines |
| -O0 | keep source and assembly easy to match |
| gdb ./score | start a debugger session on the executable |
When the student can see RSP and memory change one instruction at a time, the stack frame stops being a drawing and becomes a live object.
Explain it
Discussion prompt
Explain GDB lets you watch the state change live to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
When the student can see RSP and memory change one instruction at a time, the stack frame stops being a drawing and becomes a live object.
Pattern
Predict first
The table runs: score(2,3) | function under inspection | returns 11 · printf | visible confirmation | prints 11
In A full sandbox program, given the rows so far: what is the next one — the row where part is return 0?
Correct: return 0 | normal program exit | exit status 0
| part | purpose | expected behavior |
|---|---|---|
| score(2,3) | function under inspection | returns 11 |
| printf | visible confirmation | prints 11 |
| return 0 | normal program exit | exit status 0 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. The main function calls score(2,3), so the equal branch returns 11 and printf prints 11.
Worked example
#include <stdio.h>
int score(int a, int b) {
int sum = a + b;
int doubled = sum * 2;
if (doubled == 10) {
return doubled + 1;
}
return doubled - 1;
}
int main(void) {
int out = score(2, 3);
printf("%d\n", out);
return 0;
}Compile this exact file for GDB practice
Why: The main function calls score(2,3), so the equal branch returns 11 and printf prints 11.
| part | purpose | expected behavior |
|---|---|---|
| score(2,3) | function under inspection | returns 11 |
| printf | visible confirmation | prints 11 |
| return 0 | normal program exit | exit status 0 |
Step zero
Discussion prompt
GDB: break and start — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Set a breakpoint at score
Answer:
Worked example
break score
run
disassemble /r scoreSet a breakpoint at score
Why: break score tells GDB to stop when the function begins.
Run until the breakpoint
Why: run starts the program. Execution pauses at or near the beginning of score.
Show the assembly
Why: disassemble /r score prints the function's instructions, with raw bytes if GDB supports /r.
| command | question it answers |
|---|---|
| break score | where should execution stop? |
| run | can we reach this function? |
| disassemble /r score | what instructions will we step through? |
Error analysis
Annotate
Walk the callouts on GDB: break and start. Each one is a place this is easy to get subtly wrong.
break score tells GDB to stop when the function begins.run starts the program. Execution pauses at or near the beginning of score.disassemble /r score prints the function's instructions, with raw bytes if GDB supports /r.Ranking
Put in order
Put the moves of GDB: step one instruction into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Use it on the prologue so the student can see RSP drop after pushq %rbp.
Worked example
stepi
info registers rsp rbp rip eax edi esi
x/8gx $rspstepi executes exactly one machine instruction
Why: Use it on the prologue so the student can see RSP drop after pushq %rbp.
info registers shows the live register state
Why: Ask for the small set you care about first: stack pointer, frame pointer, instruction pointer, return register, and argument registers.
x/8gx $rsp displays stack memory
Why: x examines memory; 8g means eight giant words, each 8 bytes; x means print them in hexadecimal.
| command | state object |
|---|---|
| stepi | advances RIP by one instruction's effect |
| info registers ... | registers |
| x/8gx $rsp | memory starting at the current stack pointer |
Cost model
Annotate
In GDB: step one instruction, before reading the notes: mark where the time actually goes. Which line dominates?
RSP drop after pushq %rbp.x examines memory; 8g means eight giant words, each 8 bytes; x means print them in hexadecimal.Concept
x/8gx $rsp| piece | meaning |
|---|---|
| x | examine memory |
| 8 | show 8 units |
| g | giant word: 8 bytes per unit |
| x | format as hexadecimal |
| $rsp | start at the address currently stored in RSP |
GDB prints addresses on the left and the memory contents on the right. After pushq %rbp, the first 8-byte value at $rsp should be the saved old RBP.
Analogy
Discussion prompt
Explain How to read x/8gx $rsp by analogy to something with no Computer Architecture in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
GDB prints addresses on the left and the memory contents on the right. After pushq %rbp, the first 8-byte value at $rsp should be the saved old RBP.
Concept
display/i $rip
info registers rsp rbp rip
x/6gx $rsp
stepi| loop step | student says aloud |
|---|---|
| display/i $rip | which instruction is next? |
| info registers | what are RSP and RBP right now? |
| x/6gx $rsp | what is on the stack right now? |
| stepi | what changed after exactly one instruction? |
The key habit is prediction before stepping: ask the student what should change, then let GDB check the prediction.
Counterexample
Discussion prompt
The key habit is prediction before stepping: ask the student what should change, then let GDB check the prediction.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Tempting mistake: compile with aggressive optimization, then expect the beginner stack-frame diagram to match exactly.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: At higher optimization levels, the compiler may keep values in registers, fold arithmetic, omit RBP, or remove branches.
Correct for this lesson: compile for readability first.
Why: At higher optimization levels, the compiler may keep values in registers, fold arithmetic, omit RBP, or remove branches.
Trap
Tempting mistake: compile with aggressive optimization, then expect the beginner stack-frame diagram to match exactly.
gcc -O2 -S score.c -o score.sWrong expectation: every source variable gets a stack slot
Why: At higher optimization levels, the compiler may keep values in registers, fold arithmetic, omit RBP, or remove branches.
| expectation | why it fails |
|---|---|
| RBP frame appears | optimizer may omit frame pointer |
| sum slot exists | optimizer may never store sum |
Correct for this lesson: compile for readability first.
gcc -O0 -g -mno-red-zone -fno-stack-protector score.c -o scoreRight expectation: source structure remains visible
Why: -O0 keeps the listing closer to the C program, which is what you want while learning annotation.
| flag | teaching benefit |
|---|---|
| -O0 | literal structure |
| -g | source/debug info |
| -mno-red-zone | visible local allocation |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
RSP marks the moving bottom edge of the borrowed area; RBP often marks a stable reference point inside it.; On x86-64, EAX is the low 32 bits of RAX. A function returning an int normally leaves that return value in EAX.movl %edi, -20(%rbp) as if the left side is the destination.; Tempting mistake: after loading memory into EAX, assume EAX stays connected to that stack slot.Pattern
1. Identify the syntax
Why: AT&T reads source then destination and marks registers with %; Intel usually reads destination then source.
2. Split the listing into regions
Why: Mark prologue, body, branch labels, and epilogue before explaining every line.
3. Build the frame map
Why: Use stores to -offset(%rbp) to infer which stack slot holds which source-level value.
4. Annotate each instruction by state change
Why: For every line, write one of: register changed, memory changed, flags changed, or control flow changed.
5. Trace with one concrete call
Why: Pick values such as score(2,3). Write a table for registers, stack slots, flags, and next instruction.
6. Verify live in GDB
Why: Predict the effect, run stepi, inspect registers and stack memory, then correct the mental model.
Real world
Discussion prompt
Outside this lesson: where does x86 Function Calls in Assembly actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of The assembly annotation recipe is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
That deck walks through x86-64 function-call assembly in depth, working from GCC -O0 output. It compares AT&T with Intel syntax and covers stack frames, RSP and RBP, addressing modes, arithmetic, and compare-and-branch control flow, then inspects the stack in GDB. Along the way it targets three traps that catch most people: reversing the operands, confusing an address with the value stored there, and treating stack allocation as though it were initialization.
Elimination
Eliminate the wrong options
Which instruction directly subtracts 24 from RSP?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: C
Why: subq $24, %rsp has RSP as the destination, so it directly updates RSP to RSP - 24. pushq also changes RSP, but by 8 and for a different purpose: saving old RBP.
Check
Choose the instruction that directly decreases RSP in this prologue.
pushq %rbp
movq %rsp, %rbp
subq $24, %rsp
movl %edi, -20(%rbp)| line | RSP changes? |
|---|---|
| 1 | ? |
| 2 | ? |
| 3 | ? |
| 4 | ? |
Check your understanding
Which instruction directly subtracts 24 from RSP?
Answer: C
Why: subq $24, %rsp has RSP as the destination, so it directly updates RSP to RSP - 24. pushq also changes RSP, but by 8 and for a different purpose: saving old RBP.
pushq %rbp does change RSP, but it subtracts 8 and stores old RBP. The question asks which instruction subtracts 24.movq %rsp, %rbp copies the current RSP into RBP. RSP is the source, not the destination, so RSP does not change.movl %edi, -20(%rbp) writes to a stack memory slot. It uses RBP to form an address but does not change RSP.Invariant
Step through it
Step through Check: which line changes RSP? one row at a time. One of these columns never changes — find it, and say why it cannot.
Prediction
Predict first
In movl -8(%rbp), %eax, what does -8(%rbp) mean?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: The memory at address RBP - 8
Why: -8(%rbp) is an AT&T memory addressing expression. It means compute the address RBP - 8, then read the 4-byte value stored at that address into EAX.
Check
Read the memory operand in AT&T syntax.
movl -8(%rbp), %eax| operand | kind | meaning |
|---|---|---|
| -8(%rbp) | ? | ? |
| %eax | ? | ? |
Check your understanding
In movl -8(%rbp), %eax, what does -8(%rbp) mean?
Answer: B
Why: -8(%rbp) is an AT&T memory addressing expression. It means compute the address RBP - 8, then read the 4-byte value stored at that address into EAX.
$-8 in AT&T syntax. Parentheses around %rbp make this an address expression.8(%rbp) after the standard prologue, not below it at -8(%rbp).Trade off
Comparison matrix
From Check: what does this operand name?: every row here is a choice with a cost. Fill the meaning column, then say which row you would actually pick and what you give up for it.
| operand | kind | meaning |
|---|---|---|
| -8(%rbp) | ? | ? |
| %eax | ? | ? |
Prediction
Predict first
If -8(%rbp) holds 10, what happens at jne .L2?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: It does not jump because ZF is 1
Why: Comparing 10 with 10 sets the zero flag to 1 because the values are equal. jne means jump if not equal, which requires ZF = 0, so this jump is not taken.
Check
Assume memory at RBP-8 holds 10 just before these instructions.
cmpl $10, -8(%rbp)
jne .L2| value at RBP-8 | ZF after cmp | jump? |
|---|---|---|
| 10 | ? | ? |
Check your understanding
If -8(%rbp) holds 10, what happens at jne .L2?
Answer: B
Why: Comparing 10 with 10 sets the zero flag to 1 because the values are equal. jne means jump if not equal, which requires ZF = 0, so this jump is not taken.
je would jump when the values are equal. jne is the opposite condition.jmp always jumps. jne is conditional and reads the flags set by the previous compare.jne changes control flow by updating RIP if taken. It does not push, pop, or adjust RSP.Elimination
Eliminate the wrong options
Which instruction gives RAX the address RBP - 8 without reading the value stored there?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: leaq -8(%rbp), %rax computes the effective address RBP - 8 and stores that address in RAX. movl -8(%rbp), %eax would read the 4-byte value from that address instead.
Check
You want the address of the local slot, not the integer stored in it.
leaq -8(%rbp), %rax
movl -8(%rbp), %eax| goal | instruction |
|---|---|
| address RBP-8 | ? |
| value at RBP-8 | ? |
Check your understanding
Which instruction gives RAX the address RBP - 8 without reading the value stored there?
Answer: A
Why: leaq -8(%rbp), %rax computes the effective address RBP - 8 and stores that address in RAX. movl -8(%rbp), %eax would read the 4-byte value from that address instead.
movl -8(%rbp), %eax dereferences the memory operand and loads the stored value, not the address.cmpl $10, -8(%rbp) reads the value only to set flags for a later branch. It does not put the address in RAX.ret returns to the caller by popping a return address into RIP. It is not an address-arithmetic instruction.Comparison
Comparison matrix
From Check: lea or mov?: refill the instruction column from what you know. The rest of the table is as it appeared.
| goal | instruction |
|---|---|
| address RBP-8 | ? |
| value at RBP-8 | ? |
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — The assembly annotation recipe · Assembly is the compiler's diary · The stack is a workbench with a moving edge · The four registers to watch first · The command that makes a .s file. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
You can read a GCC -O0 function listing as a sequence of state changes: prologue builds the frame, body moves and computes values, compare/jump chooses a path, and epilogue restores the caller.
| thing you see | question to ask |
|---|---|
-8(%rbp) | which frame slot does this address? |
mov | which direction is the copy? |
add or sub | which destination changes? |
cmp | which flags will the next jump read? |
je/jne/jmp | where can RIP go next? |
push/sub/leave/ret | how did RSP change? |
The durable habit is prediction plus verification: annotate the line, predict the register/stack effect, then use GDB to check the live machine state.
Want this taught 1-on-1? Alexander tutors Computer Architecture — $55/session, free consultation.