When I started reading Computer Systems: A Programmer’s Perspective (CS:APP), assembly was one of the hardest parts to understand.
At first, assembly looks much more complicated than C or C++. You suddenly see instructions like:
movq %rax, %rdx
subq $8, %rsp
cmpq %rsi, %rdi
jne .L3It can feel unreadable.
The good news is that assembly instructions themselves are usually quite simple.
The difficult part is understanding the ideas behind them:
- registers
- memory
- addresses
- pointers
- the stack
- function calls
- calling conventions
- comparisons
- jumps
- system calls
Once these concepts start making sense, assembly becomes much easier to read.
Before going into this article I advice you as the reader to know: hexadecimale, difference between bit & bytes and of course C/C++ programming as this really helps with the understanding of the assembly code it helped with the understanding an example: Assembly memory: movq %rax, (%rbx)
This guide focuses on the x86-64 assembly used throughout CS:APP. Its important to note that most of the assembly code is 64 bits. As for the different bits like 32, 16, 8, 1 there are specific assembly registers those are already mentioned in CS:APP. I highly recommend to read the CS:APP first then go back to this article, as CS:APP goes more in the detail.
Assembly Depends on the CPU Architecture
There is no single universal assembly language.
Different CPU architectures have different registers and different machine instructions.
For example:
x86-64
ARM64
RISC-VAssembly written for x86-64 cannot simply run on ARM64 because the processors understand different instruction sets.
CS mainly focuses on x86-64.
There is also another source of confusion: the same x86-64 instructions can be written using different assembly syntaxes.
Two common syntaxes are:
Intel syntax
AT&T syntaxCS mostly uses AT&T syntax.
For example, Intel syntax might write:
mov rax, rbxwhile AT&T syntax writes:
movq %rbx, %raxOne important difference is operand order.
AT&T syntax generally uses:
instruction SOURCE, DESTINATIONSo:
movq %rbx, %raxmeans:
copy the value in RBX into RAXRegisters
Registers are small storage locations directly inside the CPU.
Some common x86-64 registers are:
%rax
%rbx
%rcx
%rdx
%rsi
%rdi
%rsp
%rbp
%r8
%r9You can think of them as very small temporary storage locations the CPU can access quickly.
For example:
movq $10, %raxmeans:
put the value 10 into RAXThe $ means that 10 is an immediate value, meaning the literal number itself.
So:
movq $10, %raxmeans:
RAX = 10while:
movq %rbx, %raxmeans:
RAX = the value currently stored in RBXRegisters are not all used in exactly the same way.
For example:
%rsphas a special role as the stack pointer.
Other registers such as %rdi, %rsi, and %rax often have special roles because of calling conventions.
Register Sizes
You will often see names such as:
%rax
%eax
%ax
%alThese are not completely different registers.
They refer to different-sized portions of the same register.
%rax 64 bits
%eax 32 bits
%ax 16 bits
%al 8 bitsFor example:
movl $5, %eaxwrites 5 into the 32-bit %eax register.
On x86-64 there is an important rule:
Writing to a 32-bit register automatically clears the upper 32 bits of the corresponding 64-bit register.
So after:
movl $5, %eaxthe entire %rax register contains:
5The upper 32 bits become zero.
This is different from writing to %ax or %al.
For example:
movw $5, %axchanges only the lower 16 bits.
The rest of %rax remains unchanged.
Likewise:
movb $5, %alchanges only the lowest 8 bits.
This is one reason compilers frequently use 32-bit registers such as %eax.
Instruction Suffixes
In AT&T syntax, instructions often end with a letter describing the size of the value they operate on.
b = byte 8 bits
w = word 16 bits
l = long 32 bits
q = quad 64 bitsFor example:
movb
movw
movl
movqall perform a move, but on different-sized values.
Likewise:
addq
subq
cmpqoperate on 64-bit values.
Instructions
Assembly programs are mainly sequences of small operations.
For example:
movq $10, %rax
addq $5, %rax
subq $2, %raxConceptually:
rax = 10;
rax = rax + 5;
rax = rax - 2;After these instructions:
%rax = 13This is one of the main differences between reading C and reading assembly.
C might express something in one line:
x = 10 + 5 - 2;while assembly may perform several small operations one after another.
Memory
Registers are useful, but the CPU only has a limited number of them.
Most program data therefore lives in memory.
Memory can be imagined as a large sequence of bytes.
Each byte has an address.
For example:
Address Value
0x1000 72
0x1001 101
0x1002 108
0x1003 108
0x1004 111
0x1005 0These bytes could represent the C string:
HelloThe final 0 is important.
C strings end with a null byte:
'\0'So:
"Hello"actually occupies six bytes:
H e l l o \0The Three Main Operand Types
Before reading much assembly, it helps to understand the three operand types you will constantly encounter.
Immediate Values
An immediate is a literal value.
$10means:
the number 10For example:
movq $10, %raxplaces the number 10 into %rax.
Registers
A register operand refers to the value currently stored inside a register.
For example:
movq %rdi, %raxmeans:
copy the value in RDI into RAXMemory Operands
Parentheses usually mean that memory is being accessed through an address.
Consider:
movq (%rdi), %raxHere %rdi contains an address.
The parentheses mean:
go to the memory address stored in RDIand load the value stored there.
This difference is extremely important.
movq %rdi, %raxmeans:
copy the VALUE in RDI into RAXwhile:
movq (%rdi), %raxmeans:
treat RDI as an ADDRESS
go to that memory location
load 8 bytes
store them in RAXSuppose:
%rdi = 0x1000and memory contains:
0x1000 -> 42Then:
movq %rdi, %raxproduces:
%rax = 0x1000while:
movq (%rdi), %raxproduces:
%rax = 42One of the most useful questions you can ask while reading assembly is:
Does this register contain a value, or does it contain an address?
Memory Offsets
Assembly can also access memory using an offset.
For example:
movq 8(%rdi), %raxmeans:
load 8 bytes from memory at address RDI + 8If:
%rdi = 0x1000then:
8(%rdi)refers to:
0x1008You will see this constantly when assembly accesses:
- arrays
- structures
- local variables
- stack values
Understanding lea
You will frequently see:
leaqIts name means:
Load Effective AddressDespite the name, lea does not read memory.
Compare:
movq 8(%rdi), %raxwith:
leaq 8(%rdi), %raxThe first instruction means:
go to address RDI + 8
load the value stored there
place it into RAXThe second means:
calculate RDI + 8
place that result into RAXNo memory is accessed.
So:
leaq 8(%rdi), %raxis roughly:
rax = rdi + 8;Because lea can perform address calculations efficiently, compilers also use it for normal arithmetic.
For example:
leaq (%rdi,%rsi), %raxcomputes:
RDI + RSIwithout accessing memory.
More General Addressing
You may also see:
8(%rax,%rdx,4)This calculates an address as:
8 + RAX + RDX * 4The general form is:
displacement(base, index, scale)which calculates:
displacement + base + index * scaleThe scale is normally:
1
2
4
8This is especially useful for arrays.
If an array contains 4-byte integers:
array[i]the address may be calculated roughly as:
base + i * 4Strings in Assembly
Strings are simply bytes stored in memory.
Compiler-generated assembly might contain:
.section .rodata
.LC0:
.string "Hello".rodata means:
read-only dataThe directive:
.string "Hello"stores:
H e l l o \0The null terminator is added automatically.
By contrast:
.ascii "Hello"stores only:
H e l l oand does not automatically add a null byte.
.LC0 is a label representing the location of the string.
Labels beginning with .L are commonly compiler-generated local labels.
You may later see:
leaq .LC0(%rip), %rdiThis loads the address of the string into %rdi.
It does not copy the entire string into %rdi.
The %rip part uses RIP-relative addressing, meaning the address is calculated relative to the current instruction.
This is common in modern x86-64 compiler output.
Understanding the Stack
The stack is one of the most important concepts to understand when reading CS.
At first, instructions such as:
subq $24, %rsp
movq %rbx, 8(%rsp)
call foo
addq $24, %rspcan look completely arbitrary.
But once you understand what the stack is doing, they start to make sense.
The stack is simply an area of memory that programs use heavily when calling functions.
It is commonly used for things such as:
return addresses
local variables
saved registers
temporary values
extra function argumentsThe most important register for understanding the stack is:
%rsp%rsp is the stack pointer.
It contains the address of the current top of the stack.
The Stack Is Just Memory
The stack is not a completely separate piece of hardware.
It is part of the program’s memory.
Imagine memory like this:
Higher addresses
0x7fffffffe100
0x7fffffffe0f8
0x7fffffffe0f0
0x7fffffffe0e8
0x7fffffffe0e0
Lower addressesSome region of memory is being used as the stack.
If:
%rsp = 0x7fffffffe0f0then the current top of the stack is at:
0x7fffffffe0f0So %rsp does not contain the stack itself.
It contains a pointer into the stack.
The Stack Grows Downward
On x86-64, the stack normally grows toward lower memory addresses.
Suppose:
%rsp = 0x1000If we want to create 8 bytes of new stack space:
subq $8, %rspNow:
%rsp = 0x0ff8Conceptually:
Higher addresses
0x1000 old %rsp
0x0ff8 new %rsp
Lower addressesSo:
subq $8, %rspcan often be interpreted as:
reserve 8 bytes of stack spaceLater:
addq $8, %rspmoves %rsp back and releases that space.
You will often see:
subq $32, %rsp
# use stack space
addq $32, %rsp
retWhat Does “Top of the Stack” Mean?
The word “top” can be confusing because the stack grows toward lower addresses.
The top simply means:
the memory location currently pointed to by
%rsp
If:
%rsp = 0x1000then the top is 0x1000.
After:
subq $8, %rspthe new top is 0x0ff8.
The word “top” is about the logical stack structure, not the highest address.
push and pop
x86-64 provides instructions specifically for working with the stack.
For example:
pushq %raxConceptually, pushq does two things:
1. subtract 8 from %rsp
2. store the value at the new addressSo:
pushq %raxis roughly equivalent to:
subq $8, %rsp
movq %rax, (%rsp)Suppose:
%rsp = 0x1000
%rax = 42After:
pushq %raxwe get:
%rsp = 0x0ff8and memory contains:
0x0ff8 -> 42The opposite operation is:
popq %raxConceptually:
1. load the value at (%rsp)
2. add 8 to %rsproughly:
movq (%rsp), %rax
addq $8, %rspWhy Functions Need the Stack
Consider:
long add(long a, long b)
{
return a + b;
}This function is simple enough to use only registers.
A compiler could generate:
add:
leaq (%rdi,%rsi), %rax
retNo local stack space is needed.
But larger functions may:
- use many variables
- call other functions
- need to preserve registers
- need temporary storage
There are only a limited number of registers.
When more storage is needed, the stack becomes useful.
A Function’s Stack Frame
The area of stack memory used by one function is often called its stack frame.
Suppose a function executes:
subq $32, %rspYou can imagine:
Higher addresses
+----------------------+
| previous stack data |
+----------------------+
| |
| function's stack |
| 32 bytes |
| |
+----------------------+ <- %rsp
Lower addressesThose 32 bytes may be used for:
local variables
temporary values
saved registersBefore returning, the function may execute:
addq $32, %rspwhich releases the stack space.
The Stack Is Temporary
Stack storage normally exists only while the function is active.
Consider:
long foo()
{
long x = 10;
return x;
}If x is stored on the stack, its storage belongs to foo‘s stack frame.
Once foo returns, that storage may be reused.
This is why code like this is dangerous:
int *bad()
{
int x = 10;
return &x;
}x belongs to the current stack frame.
After the function returns, that memory is no longer safely owned by bad().
call Uses the Stack
Suppose we execute:
call fooThe CPU needs to know where execution should continue after foo finishes.
For example:
movq $5, %rdi
call foo
movq %rax, %rbxAfter foo finishes, execution should continue at:
movq %rax, %rbxThe call instruction stores the address of that next instruction on the stack.
This stored address is called the return address.
Conceptually:
call foodoes something like:
push address of next instruction
jump to fooret Uses the Stack
At the end of a function you will often see:
retret gets the saved return address from the stack and continues execution there.
Conceptually:
pop return address
jump to that addressSo:
call
|
| save return address
v
function executes
|
v
ret
|
| retrieve return address
v
caller continuesNested Function Calls
Suppose:
main()
{
foo();
}
foo()
{
bar();
}When main calls foo, a return address is pushed onto the stack.
Then foo calls bar, which pushes another return address.
The stack may look like:
Higher addresses
+-----------------------------+
| return address into main |
+-----------------------------+
| return address into foo | <- %rsp
+-----------------------------+
Lower addressesWhen bar executes:
retit returns to foo.
Then when foo executes:
retit returns to main.
This is why a stack works so well for function calls.
The most recently called function must return first.
This behavior is called:
LIFOwhich means:
Last In, First OutLocal Variables on the Stack
Consider:
long foo(long x)
{
long a = x;
long b = 10;
return a + b;
}An optimized compiler might keep everything in registers.
But imagine the variables are stored on the stack.
A simplified version could look like:
foo:
subq $16, %rsp
movq %rdi, (%rsp)
movq $10, 8(%rsp)
movq (%rsp), %rax
addq 8(%rsp), %rax
addq $16, %rsp
retAfter:
subq $16, %rspthe stack might look like:
Higher addresses
+------------------------+
| return address |
+------------------------+
| b = 10 | 8(%rsp)
+------------------------+
| a = x | (%rsp)
+------------------------+ <- %rsp
Lower addressesSo:
movq %rdi, (%rsp)stores a.
And:
movq $10, 8(%rsp)stores b.
Register Spilling
Registers are usually preferred because they are fast.
But there are only a limited number of them.
If the compiler needs more temporary storage, it may move some register values onto the stack.
This is called register spilling.
For example:
movq %rax, 8(%rsp)might temporarily save %rax.
Later:
movq 8(%rsp), %raxloads the value back.
Caller-Saved and Callee-Saved Registers
The calling convention also defines who is responsible for preserving certain registers.
There are two important groups:
caller-saved registers
callee-saved registersCaller-Saved Registers
A caller-saved register may be overwritten by the function being called.
If the caller needs that value later, the caller must preserve it.
Conceptually:
I need this value after the function call.
The called function may overwrite this register.
Therefore I must save it first.The value may be saved on the stack.
Callee-Saved Registers
A callee-saved register must be restored by the called function if that function changes it.
For example, %rbx is callee-saved under the x86-64 System V calling convention.
A function might do:
foo:
pushq %rbx
# use %rbx
popq %rbx
retAt the beginning:
pushq %rbxsaves the caller’s original value.
Before returning:
popq %rbxrestores it.
Function Prologue and Epilogue
You may hear these terms:
function prologue
function epilogueThe prologue is the setup done when entering a function.
The epilogue is the cleanup done before returning.
A traditional function might look like:
foo:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
# function body
addq $32, %rsp
popq %rbp
retThe beginning:
pushq %rbp
movq %rsp, %rbp
subq $32, %rspis the prologue.
The ending:
addq $32, %rsp
popq %rbp
retis the epilogue.
Modern compilers can generate different patterns, especially with optimization enabled.
What Is %rbp?
Traditionally, %rbp is used as a frame pointer.
The problem with %rsp is that it may move while a function executes.
For example:
pushq %rax
pushq %rbx
subq $32, %rspchanges %rsp several times.
A frame pointer gives the function a stable reference point.
A traditional setup is:
pushq %rbp
movq %rsp, %rbpThe stack might then look like:
Higher addresses
+--------------------------+
| function arguments |
+--------------------------+
| return address |
+--------------------------+
| old %rbp | <- %rbp
+--------------------------+
| local variable |
+--------------------------+
| local variable |
+--------------------------+ <- %rsp
Lower addressesThe function can then use offsets from %rbp.
For example:
movq -8(%rbp), %raxcould access a local variable.
Because %rbp remains stable, the offset remains easy to reason about.
%rbp Is Often Omitted
Modern compilers frequently omit the traditional frame pointer when optimization is enabled.
Instead, they directly use offsets from:
%rspThis frees %rbp for other uses.
So do not expect every function to contain:
pushq %rbp
movq %rsp, %rbpSome functions will.
Others will not.
Function Arguments and the Stack
On x86-64 Linux using the System V ABI, the first integer or pointer arguments are normally passed in:
1st -> %rdi
2nd -> %rsi
3rd -> %rdx
4th -> %rcx
5th -> %r8
6th -> %r9Additional arguments are usually passed using the stack.
For example:
foo(a, b, c, d, e, f, g, h);The first six integer or pointer arguments fit into registers.
The remaining arguments are passed using stack memory.
Stack Alignment
Sometimes you will see stack adjustments that do not appear to store any useful data.
For example:
subq $8, %rsp
call foo
addq $8, %rspOne possible reason is stack alignment.
The x86-64 System V ABI requires the stack to follow certain alignment rules around function calls.
The compiler may adjust %rsp simply to satisfy those rules.
So not every stack allocation corresponds to a C variable.
Recursion and the Stack
Consider:
long factorial(long n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}Each function call needs its own state.
Calling:
factorial(4)creates something like:
factorial(4)
factorial(3)
factorial(2)
factorial(1)Each active call may have its own stack frame.
Conceptually:
Higher addresses
+-------------------------+
| factorial(4) frame |
+-------------------------+
| factorial(3) frame |
+-------------------------+
| factorial(2) frame |
+-------------------------+
| factorial(1) frame | <- %rsp
+-------------------------+
Lower addressesWhen factorial(1) returns, its frame is removed first.
Then factorial(2) continues.
This again follows:
Last In, First OutStack Overflow
The stack has a limited size.
If a program keeps creating stack frames without returning, it can eventually run out of stack space.
For example:
void bad()
{
bad();
}produces:
bad
└── bad
└── bad
└── bad
└── ...Return addresses and stack frames keep accumulating.
Eventually the program runs out of valid stack memory.
This is called a stack overflow.
Large local variables can also consume significant stack space.
For example:
void foo()
{
char huge[100000000];
}How to Read Stack Code
Suppose CS shows:
foo:
pushq %rbx
subq $16, %rsp
movq %rdi, %rbx
movq %rsi, 8(%rsp)
call bar
movq 8(%rsp), %rdx
addq %rbx, %rax
addq $16, %rsp
popq %rbx
retRead it step by step.
First:
pushq %rbxThe function saves %rbx.
Then:
subq $16, %rspIt reserves 16 bytes of stack space.
Then:
movq %rdi, %rbxThe first argument is saved in %rbx.
Then:
movq %rsi, 8(%rsp)The second argument is saved on the stack.
Then:
call barAnother function is called.
After bar returns:
movq 8(%rsp), %rdxrestores the saved value.
Then:
addq %rbx, %raxuses the preserved value together with the result returned by bar.
Finally:
addq $16, %rspreleases the stack space.
Then:
popq %rbxrestores the original %rbx.
Finally:
retreturns to the caller.
What initially looked random now tells a clear story:
save
reserve
store
call
restore
calculate
cleanup
returnDrawing the Stack
One of the best techniques for CS exercises is to draw the stack.
Suppose:
pushq %rbx
subq $16, %rspYou can draw:
Higher addresses
+---------------------------+
| return address |
+---------------------------+
| saved %rbx |
+---------------------------+
| stack slot +8 |
+---------------------------+
| stack slot +0 | <- %rsp
+---------------------------+
Lower addressesThen if you see:
movq %rdi, 8(%rsp)update your drawing:
Higher addresses
+---------------------------+
| return address |
+---------------------------+
| saved %rbx |
+---------------------------+
| first argument | 8(%rsp)
+---------------------------+
| | (%rsp)
+---------------------------+ <- %rsp
Lower addressesThis is often much easier than keeping everything in your head.
Functions and Calling Conventions
Now that registers, memory, and the stack are clearer, function calls become easier to understand.
Consider:
long add(long a, long b)
{
return a + b;
}At the assembly level, the CPU does not know that the variables are called a and b.
Instead, programs follow an agreed set of rules called a calling convention.
On x86-64 Linux using the System V ABI:
1st argument -> %rdi
2nd argument -> %rsi
3rd argument -> %rdx
4th argument -> %rcx
5th argument -> %r8
6th argument -> %r9An integer return value is normally placed in:
%raxSo:
add:
leaq (%rdi,%rsi), %rax
retbecomes much easier to understand.
We know:
%rdi = a
%rsi = b
%rax = return valueSo:
leaq (%rdi,%rsi), %raxcomputes:
a + band stores the result in %rax.
Then:
retreturns to the caller.
The important idea is that %rdi does not inherently mean:
first argumentThe hardware does not care.
It has this meaning because of the calling convention.
Comparisons and Condition Codes
Suppose you see:
cmpq %rsi, %rdiIn AT&T syntax:
cmp SOURCE, DESTINATIONConceptually, cmp performs:
DESTINATION - SOURCESo:
cmpq %rsi, %rdicomputes:
%rdi - %rsiThe subtraction result is discarded.
Only special CPU flags are kept.
Some important flags are:
ZF = Zero Flag
SF = Sign Flag
CF = Carry Flag
OF = Overflow FlagFor example:
cmpq %rsi, %rdi
je .L1means roughly:
if (rdi == rsi)
goto L1;Operand order becomes especially important with greater-than and less-than comparisons.
For example:
cmpq %rsi, %rdi
jg .L1means:
if (rdi > rsi)
goto L1;It does not mean:
if (rsi > rdi)This is one of the easiest mistakes to make when reading AT&T syntax.
Signed and Unsigned Comparisons
Signed and unsigned values use different conditional jumps.
For signed comparisons:
jg jump if greater
jge jump if greater or equal
jl jump if less
jle jump if less or equalFor unsigned comparisons:
ja jump if above
jae jump if above or equal
jb jump if below
jbe jump if below or equalEquality uses:
je jump if equal
jne jump if not equalSo while reading assembly, always ask:
Is this value being treated as signed or unsigned?
Labels and Control Flow
Assembly uses labels to name locations.
For example:
.L1:
addq $1, %rax.L1 represents the location of that instruction.
A jump can then say:
jmp .L1which means:
continue execution at .L1This is how high-level constructs such as:
if
else
while
forbecome machine-level control flow.
For example:
if (x == 0) {
return 1;
}might involve:
cmpq $0, %rdi
jne .L1
movl $1, %eax
ret
.L1:Compiler-generated assembly does not always visually resemble the original C structure.
CS shows patterns such as guarded do-while and jump-to-middle transformations for this reason.
The important skill is following the jumps.
Where Does a Program Start?
Many assembly tutorials contain:
_start:and describe it as the beginning of a program.
That is useful when writing standalone assembly programs.
But CS mostly teaches you to understand assembly generated from normal C programs.
For such programs, main() is not literally the first instruction executed.
Conceptually:
Operating system
|
v
program entry point
|
v
runtime startup code
|
v
main()For CS, understanding normal functions is usually more important than manually writing _start.
System Calls
Now consider:
movq $60, %rax
movq $0, %rdi
syscallWhy 60?
Why %rax?
Why %rdi?
The answer is another convention.
A system call is how a normal program requests a service from the operating-system kernel.
Examples include:
reading
writing
creating processes
mapping memory
exitingOn x86-64 Linux, the system-call convention says that the system-call number goes in:
%raxFor x86-64 Linux:
60 = exitSo:
movq $60, %raxmeans:
perform the exit system callThe first system-call argument goes into:
%rdiThe exit system call needs an exit status.
So:
movq $0, %rdimeans:
exit status = 0Finally:
syscallenters the kernel.
So:
movq $60, %rax
movq $0, %rdi
syscallroughly means:
exit(0);System Calls Have Their Own Convention
System calls do not use exactly the same register convention as normal function calls.
Normal x86-64 System V function arguments use:
%rdi
%rsi
%rdx
%rcx
%r8
%r9Linux x86-64 system calls use:
1st argument -> %rdi
2nd argument -> %rsi
3rd argument -> %rdx
4th argument -> %r10
5th argument -> %r8
6th argument -> %r9Notice the fourth argument:
normal function -> %rcx
system call -> %r10One reason is that the syscall instruction itself overwrites %rcx.
System Call Numbers Are Architecture-Specific
The number:
60does not universally mean:
exitIt means exit specifically for the x86-64 Linux system-call ABI.
On Linux ARM64, for example:
93 = exitThis connects directly back to the beginning of this article:
Low-level behavior depends on both the architecture and the operating-system interface.
Function Calls vs. System Calls
Do not confuse:
callwith:
syscallA normal function call:
call foojumps to another function in your program or a library.
For example:
call printfcalls the printf library function.
A system call:
syscallcrosses into the operating-system kernel.
Conceptually:
Your program
|
+-- call foo
| normal function
|
+-- call printf
| library function
|
+-- syscall
operating-system kernelConnecting C to Assembly
The main skill CS wants you to develop is connecting C code to machine-level behavior.
Consider:
long load(long *p)
{
return *p;
}Using the calling convention:
%rdi = pSince p is a pointer, %rdi contains an address.
The assembly could look like:
load:
movq (%rdi), %rax
retNow reason through it.
movq (%rdi), %raxmeans:
treat RDI as an address
read 8 bytes from memory there
place the value into RAXAnd because %rax contains the return value:
return *p;This small function combines:
calling conventions
registers
pointers
memory operands
return valuesThis is exactly the kind of reasoning that becomes useful throughout CS.
How to Read Assembly
Do not try to read assembly like English.
Move through it one instruction at a time.
Ask:
What values are currently in the registers?
Which registers contain arguments?
Which register contains the return value?
Does this register contain a value or an address?
Are parentheses causing a memory access?
Is memory being read or written?
Did %rsp move?
What is stored on the stack?
What flags did cmp set?
Is the comparison signed or unsigned?
Where can the program jump next?
Did a function call happen?You can also keep a small table:
Register Meaning
%rdi first argument / pointer p
%rax return value
%rsp stack pointerThe meaning changes from function to function, but tracking it makes assembly much easier to understand.
What You Should Know Before Continuing CS:APP
You do not need to memorize the entire x86-64 instruction set.
The most important ideas are:
- Assembly is architecture-specific.x86-64, ARM64, and other architectures use different registers and instructions.
- CS mainly uses x86-64 AT&T syntax. AT&T usually writes:
source, destination - Registers are small storage locations inside the CPU.They can contain normal values or memory addresses.
- Register size matters.
%rax,%eax,%ax, and%alrefer to different portions of the same register. - Writing to
%eaxclears the upper half of%rax. - Assembly operands can be immediate values, registers, or memory locations.For example:
$10 %rax (%rax)all mean different things. - Parentheses usually mean memory is being accessed.
movq (%rdi), %raxreads from the address stored in%rdi. leaperforms address calculation without reading memory.- The stack is ordinary memory used heavily by functions.
%rsppoints to the top of the stack.- The x86-64 stack grows toward lower addresses.
pushstores a value on the stack.popremoves a value from the stack.callpushes a return address onto the stack.retuses that return address to return to the caller.- Functions may reserve stack frames for local variables and temporary values.
- Caller-saved and callee-saved registers explain why values are sometimes pushed onto the stack.
%rbpcan act as a frame pointer, but optimized code often omits it.- Calling conventions give registers meaning.
cmpperforms a subtraction and keeps only the flags.- Operand order matters in AT&T syntax.
- Signed and unsigned comparisons use different jumps.
- Labels represent locations in code or data.
- System calls follow their own convention.
- System-call numbers depend on the architecture and operating system.
The main goal is not to become an assembly programmer.
The goal is to understand what happens underneath the C code you already know.
Once you understand registers, memory operands, pointers, the stack, calling conventions, control flow, and system calls, the assembly in CS stops looking like random instructions and starts looking like another representation of the same program.

Leave a Reply