C · Systems · Pwn
Step 64. First Encounter with Assembly — The Final Form of My Code
Level 1 — Programming and the Computer’s Inner Workings | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: Steps 56–63 complete. You’ve reproduced the four compilation stages by hand and know which stage’s artifact a
.sfile is.
- What you need: a WSL Ubuntu terminal, gcc, objdump. The verification environment is Ubuntu 24.04, gcc 13.3.0, objdump 2.42, x86-64.
- Caution: today’s exercises are 100% safe. We only extract and read the assembly of programs I wrote myself.
Last time we briefly opened a .s file. Strange words like mov and call. Today we read that file head-on. Assembly language is machine code transcribed into symbols humans can read — the lowest-floor language. Every program in the world — games, operating systems, Python — ultimately runs as a march of these small instructions. Today’s goal is not to code in assembly but to build the sense of matching "which instructions my C code becomes."
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain what assembly language is (the human-readable edition of machine code)
- Recognize six instructions:
mov,add,cmp,jmp/jle,call,ret - Compare C code (functions, if, for) side by side with its assembly
- Extract readable assembly with
gcc -S -masm=intel -O0 - Pull assembly out of an already-compiled executable with
objdump -d
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Assembly (x86-64, Intel syntax), C language, WSL Ubuntu bash (verified: gcc 13.3.0) |
| Today’s commands | gcc -S -masm=intel -O0 (extract assembly for reading), objdump -d -M intel (disassemble an executable), grep -A 15 "<function name>:" (see just one function) |
| Concepts needed | The machine-code/assembly correspondence, registers (rax, rdi, rsi, etc.), the six instructions, the calling convention, optimization (-O0 vs -O2) |
2-1. Machine Code and Assembly — The Translated Edition of 0s and 1s
What the CPU directly knows is only patterns of 0s and 1s (machine code). Since a human can’t read things like 10111000 ..., assembly is what you get by giving each pattern a name.
Machine code and assembly correspond almost one-to-one. So looking at assembly is looking at machine code.
2-2. Registers — Boxes on the CPU’s Palm
Inside the CPU are a few ultra-small, very fast boxes: registers. Calculations happen on top of these boxes, not in memory.
Today’s cast of faces in the x86-64 environment: rax (where results mainly land), rdx, rdi (a function’s first argument), rsi (second argument), rsp (marks the top of the stack), rbp (the current function’s reference point). Names starting with e, like eax and edi, are notations using only the lower 32 bits of the same box. For today, just know them as "named palm-sized boxes."
2-3. A Few Instructions Are Enough
There are hundreds of assembly instructions, but today you need six.
| Instruction | Meaning |
|---|---|
mov a, b |
Copy b into a (assignment) |
add a, b |
Add b to a |
cmp a, b |
Compare a and b (the result stays in markers called flags) |
jmp / jle / je |
Jump / jump if less or equal / jump if equal |
call name |
Call a function |
ret |
Return |
These six are the substance of all of if, for, and function calls.
2-4. Intel Syntax and AT&T Syntax — Two Notations
There are two ways of writing the same assembly: Intel syntax (mov rax, 1 — "1 into rax") and AT&T syntax (movq $1, %rax — order reversed, with % and $ marks).
gcc’s default output is AT&T syntax, but today we unify on the easier-to-read Intel syntax (-masm=intel). When you meet output showing % and $, just light your warning lamp: "ah, AT&T — order reversed."
2-5. A Function’s Prologue and the Calling Convention
The preparatory moves that appear every time a function starts, like push rbp and mov rbp, rsp, are called the prologue. You can skip over them wholesale as "the function’s warm-up exercises."
By contrast, there’s one promise you must remember: the calling convention — the promise that the first argument is handed over in rdi, the second in rsi, and the result is returned in rax. In today’s exercises you’ll see this promise actually kept.
3. Follow Along
3-1. One Function’s Assembly — The Smallest Comparison
Input (add.c)
int add(int a, int b) {
return a + b;
}
Compile (up to assembly only)
gcc -S -masm=intel -O0 add.c -o add.s
cat add.s
add:
endbr64
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], edi
mov DWORD PTR -8[rbp], esi
mov edx, DWORD PTR -4[rbp]
mov eax, DWORD PTR -8[rbp]
add eax, edx
pop rbp
ret
(Verified 2026-09-09. Assembly-directive lines starting with .cfi and the .LFB0 marker are omitted for readability.)
How to read the options: -masm=intel is "in Intel syntax"; -O0 is "optimization off — translate exactly as taught."
How to read the output: let’s follow the order. The first argument a arrived in the edi box, the second b in esi (the calling convention), and after setting both down in memory (-4[rbp], -8[rbp] — spots a few slots away from rbp), it reads them back and adds with add eax, edx. The result sits in eax (the lower part of rax) — the real form of the promise "a function returns its result in rax." The topmost endbr64 is a branch-protection instruction on modern CPUs; for now, read it as a "function start marker" and move on.
Predict: if you change it to
return a - b;, which one line changes? Predict, then re-extract and check.
Why: the sight of a one-line C function being translated into nine small instructions — this contrast is today’s starting point.
3-2. The Substance of if — Compare and Jump
Input (ifex.c)
int check(int score) {
if (score >= 60) {
return 1;
} else {
return 0;
}
}
Compile and check
gcc -S -masm=intel -O0 ifex.c -o ifex.s
cat ifex.s
check:
endbr64
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], edi
cmp DWORD PTR -4[rbp], 59
jle .L2
mov eax, 1
jmp .L3
.L2:
mov eax, 0
.L3:
pop rbp
ret
(Verified 2026-09-09. Assembly-directive lines omitted.)
How to read the output: the if vanished and became cmp + jle. "Compare score with 59, and if less or equal (jle), jump to the marker .L2." .L2 is the else lump; .L3 is the function’s end. The reason it compares with 59 is that flipping ">= 60" gives "<= 59."
A conditional was "one comparison + a jump between signposts."
Why: "if is cmp and a jump" — this one sentence is today’s most important harvest. The way to recognize an if in a program without source is exactly this.
3-3. The Substance of for — A Loop Made of Jumps
Input (forex.c)
int sum(int n) {
int total = 0;
for (int i = 1; i <= n; i++) {
total += i;
}
return total;
}
Compile and check
gcc -S -masm=intel -O0 forex.c -o forex.s
cat forex.s
sum:
endbr64
push rbp
mov rbp, rsp
mov DWORD PTR -20[rbp], edi
mov DWORD PTR -8[rbp], 0
mov DWORD PTR -4[rbp], 1
jmp .L2
.L3:
mov eax, DWORD PTR -4[rbp]
add DWORD PTR -8[rbp], eax
add DWORD PTR -4[rbp], 1
.L2:
mov eax, DWORD PTR -4[rbp]
cmp eax, DWORD PTR -20[rbp]
jle .L3
mov eax, DWORD PTR -8[rbp]
pop rbp
ret
(Verified 2026-09-09. Assembly-directive lines omitted.)
How to read the output: between .L2 (the condition check) and .L3 (the body), jle travels back and forth, forming a loop. Find where the for’s three parts went. Initial value 1 → mov DWORD PTR -4[rbp], 1. Increment i++ → add DWORD PTR -4[rbp], 1. Condition i <= n → cmp + jle.
A for was a jump loop of "check → body → increment → check again."
Predict: would the assembly of C code with the for rewritten as a while differ from this, or be the same? Experiment. To the compiler, the two are actually the same idea.
3-4. main and call — The Substance of Calling a Function
Input (callex.c)
#include <stdio.h>
int square(int n) {
return n * n;
}
int main(void) {
int r = square(7);
printf("Result: %dn", r);
return 0;
}
Compile and check
gcc -S -masm=intel -O0 callex.c -o callex.s
grep -A 16 "^main:" callex.s
main:
endbr64
push rbp
mov rbp, rsp
sub rsp, 16
mov edi, 7
call square
mov DWORD PTR -4[rbp], eax
mov eax, DWORD PTR -4[rbp]
mov esi, eax
lea rax, .LC0[rip]
mov rdi, rax
mov eax, 0
call printf@PLT
mov eax, 0
leave
ret
(Verified 2026-09-09. Assembly-directive lines omitted.)
How to read the output: just before calling square(7), it put 7 into edi — exactly per the promise "the first argument goes in rdi." It called with call square, and on return it took the result from eax and stored it in -4[rbp] (the variable r). Next, when calling printf, you can see it loads the result value into esi (second argument) and the string’s address into rdi (first argument).
The paths arguments and results travel — these promises are 2-5’s calling convention.
3-5. Even from an Executable — objdump
So far we’ve made .s files. You can also pull assembly out of an already-compiled executable.
gcc -O0 callex.c -o callex
objdump -d -M intel callex | grep -A 8 "<square>:"
0000000000001149 <square>:
1149: f3 0f 1e fa endbr64
114d: 55 push rbp
114e: 48 89 e5 mov rbp,rsp
1151: 89 7d fc mov DWORD PTR [rbp-0x4],edi
1154: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
1157: 0f af c0 imul eax,eax
115a: 5d pop rbp
115b: c3 ret
(Verified 2026-09-09. Addresses (1149, etc.) can vary by environment.)
How to read the output: objdump is a tool that turns an executable back into assembly for viewing (a disassembler). Read it in three strands. Far left (1149, 114d …) is each instruction’s memory address. The middle (55, 48 89 e5 …) is the real machine-code bytes; the right is those bytes translated into assembly. You can even see that push rbp is a single byte, 55, in machine code. The square we made lives inside the executable, and the multiplication was translated as imul eax, eax (eax × eax).
objdump’s Intel syntax writes subtraction inside brackets like [rbp-0x4], while gcc -S’s output writes -4[rbp]. Different handwriting for the same meaning.
Why: "even an executable without source can be read as assembly." Today it’s a program whose source we know, so we can check our answers. As this comparison training accumulates, the day comes when you read without an answer key.
3-6. Before and After Optimization — The Translator’s Two Faces
Let’s extract the same C with -O0 and -O2 and compare.
gcc -S -masm=intel -O0 add.c -o add_O0.s
gcc -S -masm=intel -O2 add.c -o add_O2.s
grep -v cfi add_O2.s
add:
endbr64
lea eax, [rdi+rsi]
ret
(Verified 2026-09-09.)
How to read the output: a function that was nine lines at -O0 became effectively two lines. The faithful procedure of setting down in memory and reading back was omitted by optimization with a "you can just add directly anyway." It even did the addition with the address-calculation instruction lea instead of add.
Think of there being two translators. -O0 is a faithful literal translator; -O2 is a smart paraphraser. For studying, the literal translator is good; most actually shipped programs are translated by the paraphraser.
Think about it: then if you open a real shipped program with objdump, will today’s "faithful structure" show up as-is? The answer is "close to no." The same C becomes different assembly depending on compile options — this is where you gain the humility of knowing that "this is not the one and only form of the original code" when reading assembly.
4. Missions & Exercises
Mission — Three C–Assembly Comparison Cards
Make three comparison cards. Each card holds "C code / assembly (key lines) / one line on the correspondence."
- Card 1 (function): extract the assembly of
int triple(int n) { return n * 3; }and mark the line where the multiplication happens - Card 2 (if): write a function yourself that returns 1 for even numbers and 0 for odd, and in its assembly find the
cmpand the jump, annotating what each means (hint: the remainder of division by 2 may be translated as anandoperation. Observing how it was translated is the assignment) - Card 3 (for): write a function yourself that multiplies from 1 to 10, and in its assembly find the loop (two signposts and a jump) and mark it with arrows
- On each card, write by hand the correspondence "one line of C ↔ how many lines of assembly"
- Open an executable containing card 1’s function with objdump and find the same function
Exercises
Problem 1. Explain the relationship between assembly and machine code, and state the positions where the two appear side by side in objdump output.
Problem 2. In 3-2, why was if (score >= 60) translated as cmp ..., 59 and jle?
Problem 3. The add extracted with -O0 was nine lines; with -O2, two. Which should you use when studying, and which side were most actually shipped programs translated with?
Problem 4. While reading assembly, you meet a line like movq $1, %rax. Which syntax is this, and should you read it as "put 1 into rax" or "put rax into 1"?
Answers & completion criteria · expand/collapse
5. Model Answers & Completion Criteria
Mission Model Answer
An example of card 1:
int triple(int n) { return n * 3; }
gcc -S -masm=intel -O0 triple.c -o triple.s
The line where the multiplication happens (verified 2026-09-09): imul eax, DWORD PTR -4[rbp], 3 — translated as one line meaning "multiply the n in memory by 3 into eax." On the card, write this: "C n * 3 ↔ one line imul ..., 3. The result goes to eax."
How to verify: ① check that all three cards were extracted with -O0 (omit -O0 and the computation can vanish wholesale). ② On card 2, find and note that even-number detection was translated with an instruction like and or test. ③ On card 3, connect with arrows the two signposts (two .L numbers) and the one jump traveling between them. ④ Compare whether the triple found with objdump has the same shape as the one in the .s.
Exercise Answers
Problem 1 answer. Machine code is the pattern of 0s and 1s the CPU directly knows; assembly is the human-readable edition that gives those patterns names. They correspond almost one-to-one. In objdump output, the middle column (e.g., 48 89 e5) is the machine-code bytes, and the right column (e.g., mov rbp,rsp) is the assembly translating them.
Problem 2 answer. Because the compiler flipped the condition and translated it as "the condition for jumping to else." The opposite of ">= 60" is "<= 59," so it becomes a form that compares with 59 and, if less or equal (jle), jumps to the else marker (.L2).
Problem 3 answer. For studying, -O0. Because the structure you learned (the prologue, setting down in memory and reading back) shows up as-is. Most actually shipped programs are translated at -O2 or above, so keep in mind that the shape changes considerably.
Problem 4 answer. AT&T syntax (the % and $ marks are the signal). AT&T is in "source → destination" order, the reverse of Intel syntax. So you should read it as "put 1 into rax."
Completion Criteria Checklist
- [ ] I can explain the assembly/machine-code relationship (one-to-one correspondence)
- [ ] I know what
mov,add,cmp,jmp/jle,call,retmean - [ ] I can find what C’s if and for look like in assembly (cmp+jump, the signpost loop)
- [ ] I can explain the paths arguments and results travel (rdi, rsi, rax — the calling convention)
- [ ] I can use
gcc -S -masm=intel -O0andobjdump -d -M intel - [ ] I can explain why
-O0and-O2output differ - [ ] Mission: I completed the three C–assembly comparison cards
6. Common Pitfalls & Fixes
Wall 1. Optimization Makes Code Vanish Wholesale
Symptom: you extract assembly and the function body is only a line or two, or the computation result is already baked in as a constant.
Cause: you left out -O0. The compiler pre-computes everything "that doesn’t need computing."
Fix: for study purposes, always -O0. It translates in exactly the structure you learned.
Wall 2. Mixing AT&T and Intel Syntax While Reading
Symptom: you read movq $1, %rax as "put rax into 1" and understand it backward.
Cause: AT&T syntax is in "source → destination" order, the reverse of Intel.
Fix: today we unified with -masm=intel. When viewing output from other documents or tools, if you see % and $, light your warning lamp: "AT&T — order reversed."
Wall 3. Wearing Yourself Out Trying to Understand Everything
Symptom: digging into every single line like push rbp and .cfi, you make no progress.
Cause: today’s goal is not full translation but comparison sense. The prologue and .cfi directive lines can be skipped wholesale as "warm-up exercises."
Fix: fix a reading order. (a) Find the function name, (b) find the operations I wrote (add, imul, cmp), (c) find call and ret. Find just those three and the function’s skeleton reads.
Wall 4. objdump Output Is Too Long
Symptom: hundreds of lines pour out and you can’t tell where your code is.
Cause: besides our code, the executable is stuffed with linked parts.
Fix: find the function name with grep. objdump -d -M intel file | grep -A 20 "<main>:". The standard library portions can be skipped for now.
Wall 5. gcc -S and objdump Notations Differ Subtly
Symptom: gcc output shows -4[rbp]; objdump shows [rbp-0x4] (verified 2026-09-09).
Cause: even within the same Intel syntax, bracket notation differs by tool.
Fix: both mean the same thing — "4 bytes below rbp." It’s only a notational difference; read the content (whose vicinity, how far away).
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Machine code | The pattern of 0s and 1s the CPU directly knows |
| Assembly | The human-readable edition of machine code — nearly one-to-one |
| Register | An ultra-small box inside the CPU — calculations happen here |
| Calling convention | Arguments in rdi, rsi; results in rax — a promise agreed to be kept |
| Prologue | The warm-up exercises at a function’s start (push rbp, etc.) |
| Disassembler | A tool that turns an executable back into assembly (objdump) |
Today’s Commands
| Command | What it does |
|---|---|
gcc -S -masm=intel -O0 file.c |
Extract readable assembly |
objdump -d -M intel executable |
Turn an executable back into assembly for viewing |
objdump ... | grep -A 15 "<main>:" |
See just my function’s portion |
A Sense More Important Than Commands
From today, assembly is a language "to read," not "to write." The compiler writes better than we do. The first move of people who must read executables without source — malware analysts, vulnerability researchers — was today’s objdump, and what their eyes need is today’s comparison sense.
Not every line will read yet. That’s fine. What’s needed today is not reading everything but the experience of recognizing one if and one for inside assembly. That experience accumulates into eyesight. Keep your three comparison cards safe — this practice of reading with the answer key (the source) beside you is the seed money for the day you read without one.
Once every box is checked, Step 64 is complete.
ONE STEP FURTHER
Finished this lesson?
Check the completion criteria, then mark your progress.