Table of Contents
- Number Representation & Bit-Level Operations
- Integer Arithmetic & Overflow
- Floating Point
- x86-64 Assembly
- Control Flow in Assembly
- Procedures, Stack & Calling Convention
- Arrays, Structs & Unions
- Buffer Overflow & Security
- Linking
- Memory Hierarchy & Caches
- Exceptional Control Flow: Processes
- Exceptional Control Flow: Signals
- Virtual Memory & Address Translation
- Common Exam Patterns & Traps
1. Number Representation & Bit-Level Operations
Type Sizes (x86-64 Linux)
| Type | Bytes | Bits |
|---|---|---|
char |
1 | 8 |
short |
2 | 16 |
int, unsigned, float |
4 | 32 |
long, double, pointers |
8 | 64 |
long long |
8 | 64 |
Two's Complement (w bits)
- Range:
[-2^(w-1), 2^(w-1) - 1] - For 32-bit
int:[-2,147,483,648, 2,147,483,647]=[INT_MIN, INT_MAX] - For 64-bit
long:[-2^63, 2^63 - 1] - Negation:
-x = ~x + 1 - Therefore:
~x = -x - 1 - Asymmetric range:
|INT_MIN| > INT_MAX, so-INT_MINoverflows back toINT_MIN
Sign Extension vs Zero Extension
- Signed widening (e.g.,
int→long): replicate the sign bit. Instruction:movslq,movsbl, etc. - Unsigned widening: fill with zeros. Instruction:
movzbl,movzwl. Note:movlalready zero-extends to 64 bits. - Narrowing: truncate (drop high bits). Same bit pattern interpretation, no instruction needed beyond using a smaller register.
Bitwise vs Logical Operators
| Bitwise | Logical | Notes |
|---|---|---|
& |
&& |
Logical short-circuits, returns 0 or 1 |
| |
|| |
Logical short-circuits |
~ |
! |
!x = (x == 0) |
^ |
(none) | XOR — bit flipping |
Useful Bit Tricks
- Get bit n:
(x >> n) & 1 - Set bit n:
x \| (1 << n) - Clear bit n:
x & ~(1 << n) - Toggle bit n:
x ^ (1 << n) - Mask of low k bits:
(1 << k) - 1 - Multiply by 2^k:
x << k - Unsigned divide by 2^k:
x >> k(logical shift) - Signed divide by 2^k (rounded toward zero):
(x < 0 ? x + (1<<k) - 1 : x) >> k - XOR swap:
a^=b; b^=a; a^=b;(don't actually use this) - Check if power of 2:
x && !(x & (x-1))
Shift Operators
- Left shift
<<: fills with zeros (same for signed/unsigned) - Logical right shift
>>: fills with zeros (used for unsigned) - Arithmetic right shift
>>: fills with sign bit (used for signed) - C standard: signed
>>is implementation-defined, but on x86-64/GCC it's arithmetic - Undefined behavior: shifting by ≥ width or by negative amount
2. Integer Arithmetic & Overflow
Modular Arithmetic
All integer arithmetic in C is modulo 2^w (wraps around). This means:
- Addition, subtraction, multiplication: same bit pattern for signed and unsigned
- Equality
==compares bit patterns - The interpretation only matters for comparison (
<,>,<=,>=) and division/modulo
Mixed Signed/Unsigned Comparison
⚠️ Critical rule: When <, >, <=, >=, == involves one signed and one unsigned operand, the signed operand is converted to unsigned.
-1 < 0U // FALSE: -1 becomes 0xFFFFFFFF
-1 == 0xFFFFFFFFU // TRUE: same bits
2147483647 > -2147483648 - 1 // beware: RHS is INT_MIN, OK
Overflow Behavior
- Unsigned overflow: well-defined, wraps mod
2^w - Signed overflow: undefined behavior in C standard (compiler may assume it can't happen!)
- On x86-64 hardware, both wrap mod
2^w, but compiler optimizations may exploit UB
Common Identity Pitfalls
| Expression | True? | Reason |
|---|---|---|
(x < y) == (-x > -y) |
❌ | Fails when x = INT_MIN |
x * 2 / 2 == x |
❌ | Overflow can lose info |
(x + y) - x == y |
✅ | Mod arithmetic preserves this |
~x + ~y + 1 == ~(x + y) |
✅ | Both = -(x+y) - 1 |
ux - uy == -(y - x) |
✅ | Same bit pattern |
(x >= 0) || (x < (unsigned)x) |
❌ | When x < 0, x promotes to same unsigned value |
((x + y) << k) + y - x == (2^k+1)*y + (2^k-1)*x |
✅ | Algebra in mod arithmetic |
Integer Division Rules in C
- Truncates toward zero (since C99):
-7 / 2 == -3 %follows:(a/b)*b + a%b == a, so-7 % 2 == -1-INT_MINandINT_MIN / -1are undefined (would overflow)
3. Floating Point
IEEE 754 Format
| Type | Total bits | Sign | Exp | Frac | Bias |
|---|---|---|---|---|---|
float |
32 | 1 | 8 | 23 | 127 |
double |
64 | 1 | 11 | 52 | 1023 |
| s | exp | frac |
Three Categories of Encoding
- Normalized (exp ≠ 0 and exp ≠ all-1s):
- Value =
(-1)^s × 1.frac × 2^(exp - bias) - Implicit leading 1
- Value =
- Denormalized (exp = 0):
- Value =
(-1)^s × 0.frac × 2^(1 - bias) - Used for very small numbers and
±0
- Value =
- Special (exp = all-1s):
- frac = 0:
±∞ - frac ≠ 0:
NaN
- frac = 0:
Properties
+0.0 == -0.0(compare equal but different bits)NaN != NaN(never equal to anything, even itself)- Floating point math is not associative:
(a + b) + c ≠ a + (b + c)in general - Floating point math is commutative (except NaN)
Conversions
int→float: may lose precision (32-bit int has more precision than 23-bit frac)int→double: exact (32-bit int fits in 52-bit frac)float/double→int: truncates toward zero; out-of-range is undefined (typically returnsINT_MINon x86)
Rounding (Round-to-Even, default)
- Round to nearest representable
- On exact halfway: round to even (avoids statistical bias)
- Examples:
1.5 → 2,2.5 → 2,-0.5 → 0
4. x86-64 Assembly
Register File
| 64-bit | 32-bit | 16-bit | 8-bit | Convention |
|---|---|---|---|---|
%rax |
%eax |
%ax |
%al |
Return value, caller-saved |
%rbx |
%ebx |
%bx |
%bl |
Callee-saved |
%rcx |
%ecx |
%cx |
%cl |
Arg 4, caller-saved |
%rdx |
%edx |
%dx |
%dl |
Arg 3, caller-saved |
%rsi |
%esi |
%si |
%sil |
Arg 2, caller-saved |
%rdi |
%edi |
%di |
%dil |
Arg 1, caller-saved |
%rbp |
%ebp |
%bp |
%bpl |
Callee-saved (frame ptr) |
%rsp |
%esp |
%sp |
%spl |
Stack pointer |
%r8–%r9 |
%r8d–%r9d |
... | ... | Args 5, 6, caller-saved |
%r10–%r11 |
... | ... | ... | Caller-saved |
%r12–%r15 |
... | ... | ... | Callee-saved |
⚠️ Writing to a 32-bit register zeros the upper 32 bits. Writing to 8/16-bit does not.
Operand Forms
| Syntax | Meaning |
|---|---|
$Imm |
Immediate (constant) |
%reg |
Register |
Imm |
Memory at address Imm |
(%reg) |
Memory at *reg |
Imm(%reg) |
Memory at Imm + *reg |
(%base, %index) |
Memory at *base + *index |
(%base, %index, scale) |
Memory at *base + *index * scale (scale ∈ {1,2,4,8}) |
Imm(%base, %index, scale) |
Memory at Imm + *base + *index * scale |
Instruction Suffixes (Operand Size)
| Suffix | Size | Example |
|---|---|---|
b |
1 byte | movb |
w |
2 bytes | movw |
l |
4 bytes | movl |
q |
8 bytes | movq |
Common Data Movement
| Instruction | Effect |
|---|---|
mov S, D |
D = S |
movz** S, D |
Zero-extend |
movs** S, D |
Sign-extend |
movslq S, D |
Sign-extend 32-bit signed → 64-bit |
cltq |
Sign-extend %eax → %rax |
pushq S |
*--rsp = S (stack grows down) |
popq D |
D = *rsp++ |
lea S, D |
D = &S (compute address, no memory access) |
Arithmetic & Logic
| Instruction | Effect |
|---|---|
add S, D |
D = D + S |
sub S, D |
D = D - S |
imul S, D |
Signed multiply: D = D * S |
xor S, D |
D = D ^ S |
and S, D |
D = D & S |
or S, D |
D = D | S |
inc D / dec D |
D++ / D-- |
neg D |
D = -D |
not D |
D = ~D |
sal/shl k, D |
Left shift |
sar k, D |
Arithmetic right shift |
shr k, D |
Logical right shift |
lea is Magic
leaq doesn't access memory — it just computes the address. Compilers use it as a 3-operand add/multiply:
leaq 7(%rdi, %rsi, 4), %rax # rax = rdi + 4*rsi + 7
leaq (%rdi, %rdi, 2), %rax # rax = rdi * 3
leaq (,%rdi, 8), %rax # rax = rdi * 8
Assembly Idioms
| Idiom | Meaning |
|---|---|
xor %eax, %eax |
eax = 0 (smaller encoding than mov $0, %eax) |
test %rax, %rax |
Sets flags based on rax (used before conditional jump) |
cmp B, A |
Sets flags based on A - B |
lea k-1(%r), %s; test; cmovns; sar $k |
Signed division by 2^k |
(addr, idx, 4) |
Indexing into int array |
(addr, idx, 8) |
Indexing into pointer/long array |
5. Control Flow in Assembly
Condition Codes (Flags)
Set by cmp, test, and most arithmetic instructions:
- CF (Carry Flag): unsigned overflow
- ZF (Zero Flag): result was zero
- SF (Sign Flag): result was negative (high bit set)
- OF (Overflow Flag): signed overflow
Conditional Set/Move/Jump Suffixes
| Suffix | Meaning | Test |
|---|---|---|
e/z |
Equal / Zero | ZF |
ne/nz |
Not Equal | ~ZF |
s |
Negative | SF |
ns |
Non-negative | ~SF |
g |
Greater (signed) | ~(SF^OF) & ~ZF |
ge |
≥ (signed) | ~(SF^OF) |
l |
Less (signed) | SF^OF |
le |
≤ (signed) | (SF^OF) | ZF |
a |
Above (unsigned >) | ~CF & ~ZF |
ae |
≥ (unsigned) | ~CF |
b |
Below (unsigned <) | CF |
be |
≤ (unsigned) | CF | ZF |
Used as: je, jne, jg, setl, cmovne, etc.
cmp B, A reads as "compare A to B"
So cmp $5, %eax; jl L jumps if eax < 5. Operand order is reversed from intuition.
Loop Patterns
While loop (jump-to-middle):
jmp .Ltest
.Lloop:
<body>
.Ltest:
<condition>
jcc .Lloop
For loop (do-while transform):
<init>
jmp .Ltest # initial test (sometimes skipped if known true)
.Lloop:
<body>
<update>
.Ltest:
<condition>
jcc .Lloop
Translating for (i = 0; i < n; i++):
- Init:
mov $0, %ecx - Update:
add $1, %ecx - Test:
cmp %esi, %ecx; jl ...(ifnis in%esi)
Switch Statements
Compiled with jump tables when cases are dense:
cmpq $5, %rdi
ja .Ldefault
jmp *.Ltable(,%rdi,8)
.Ltable:
.quad .Lcase0
.quad .Lcase1
...
6. Procedures, Stack & Calling Convention
x86-64 Linux Calling Convention
First 6 integer/pointer arguments: %rdi, %rsi, %rdx, %rcx, %r8, %r9 (in order)
Return value: %rax (and %rdx for 128-bit returns)
Stack arguments: 7th and beyond go on the stack
Caller-saved (volatile): %rax, %rcx, %rdx, %rsi, %rdi, %r8–%r11 — caller must save before call if needed
Callee-saved (non-volatile): %rbx, %rbp, %r12–%r15 — callee must restore before returning
Stack pointer: %rsp — must be 16-byte aligned at function entry
Stack Frame Anatomy
High addresses
│ Caller's frame │
├──────────────────┤
│ Args 7+ │ <- pushed by caller
├──────────────────┤
│ Return address │ <- pushed by `call`
├──────────────────┤ <- %rsp on entry
│ Saved %rbp │ (optional, frame pointer)
├──────────────────┤
│ Saved callee- │
│ saved regs │
├──────────────────┤
│ Locals │
├──────────────────┤
│ Args for callees │ <- pushed/space allocated for inner calls
└──────────────────┘ <- %rsp now
Low addresses
Function Prologue/Epilogue
Prologue:
pushq %rbp # save old frame pointer
movq %rsp, %rbp # set new frame pointer (optional)
subq $N, %rsp # allocate locals
pushq %rbx # save callee-saved regs we'll use
Epilogue:
popq %rbx # restore callee-saved
addq $N, %rsp # deallocate locals (or `leave`)
popq %rbp
ret
call and ret
call addr: pushes return address (8 bytes), jumps toaddrret: pops return address into%rip
Recursion Patterns
- Save needed values in callee-saved registers (so they survive recursive calls)
- Common pattern:
pushq %rbx; movq %rdi, %rbx;— save arg in%rbxfor use after recursive call
7. Arrays, Structs & Unions
Array Basics
int A[10]; // 10 ints = 40 bytes contiguous
A[i] ≡ *(A + i)
&A[i] ≡ A + i ≡ (char*)A + i*sizeof(int)
Multidimensional arrays are row-major:
int M[R][C];
M[i][j] is at offset (i*C + j) * sizeof(int)
Pointer Arithmetic
p + i advances by i * sizeof(*p) bytes. Always.
Struct Alignment Rules (x86-64)
- Each field aligns to a multiple of its own size (or its alignment).
char→ align 1short→ align 2int,float→ align 4long,double, pointers → align 8
- Struct alignment = max alignment of any field
- Struct size is rounded up to a multiple of the struct's alignment (tail padding)
- Nested struct has alignment equal to its own max field alignment
Layout Procedure
struct S {
char a; // offset 0, size 1
// pad to align next field
long b; // offset 8, size 8
short c; // offset 16, size 2
// pad if needed for next field
int d; // align 4, so pad 2 bytes; offset 20, size 4
};
// Total: 24 bytes (already multiple of 8)
Worked Example
struct my_struct {
char a; // offset 0
// pad 7 bytes
long b; // offset 8
short c; // offset 16
// pad 6 bytes (next field aligns 8)
struct {
float *d[2]; // 2 × 8 = 16 bytes; align 8
} f; // offset 24, size 16
float e; // offset 40
// pad 4 bytes (struct must be multiple of 8)
};
// Total: 48 bytes
Reordering for Compactness
Group large fields first to minimize padding:
// 24 bytes (compact)
struct Good { long b; int d; short c; char a; };
// 32 bytes (wasteful)
struct Bad { char a; long b; short c; int d; };
Unions
All members share the same memory; size = max member size, alignment = max member alignment. Useful for:
- Reinterpreting bits (e.g.,
float↔int) - Tagged variants (with a separate tag field)
8. Buffer Overflow & Security
How Stack Smashing Works
void vulnerable() {
char buf[8];
gets(buf); // no bounds check!
}
If user inputs > 8 bytes, the overflow corrupts:
- Other locals
- Saved registers
- Saved
%rbp - Return address ← attacker target
Mitigations
| Mitigation | How it works |
|---|---|
| Stack canaries | Random value placed before return address; checked on return |
| ASLR (Address Space Layout Randomization) | Stack/heap base randomized |
| NX bit (No-eXecute) | Stack pages marked non-executable |
| PIE (Position-Independent Executable) | Code base also randomized |
Attack Types
- Code injection: put shellcode in buffer, redirect return address to it (defeated by NX)
- Return-oriented programming (ROP): chain existing code "gadgets" ending in
ret(defeats NX)
9. Linking
Compilation Pipeline
source.c → [preprocessor] → source.i → [compiler] → source.s
→ [assembler] → source.o → [linker + libs] → executable
Symbols
Every .o file has a symbol table. Each symbol is either:
- Defined in this file (function body, initialized global)
- Referenced but not defined (extern)
Symbol Categories
| Category | Storage | Examples |
|---|---|---|
| Global | Visible to other modules | int x = 5;, functions |
| Local | This module only | static variables, function locals (not in symbol table) |
| External | Defined elsewhere | extern int x; |
Strong vs Weak
- Strong: functions, initialized globals
- Weak: uninitialized globals (and
externdeclarations)
Linker Rules for Duplicate Symbols
- ❌ Two strong symbols with same name → error
- ✅ One strong + many weak → strong wins
- ⚠️ Multiple weak → linker picks one (often the largest); silent corruption risk
⚠️ Modern GCC (default -fno-common) treats this stricter — multiple weak symbols may also error. Older GCC with -fcommon allowed it.
Memory Segments
| Segment | Contents |
|---|---|
.text |
Code (read-only, executable) |
.rodata |
String literals, const globals |
.data |
Initialized non-zero globals/statics |
.bss |
Uninitialized or zero-initialized globals/statics (no actual bytes in file) |
| Heap | malloc'd memory; grows up |
| Stack | Function calls, locals; grows down |
Static vs Dynamic Libraries
- Static (
.a): archive of.ofiles; linker copies needed code into the executable at link time - Dynamic (
.so): loaded at runtime; smaller executables, can update libraries without recompiling
Library Link Order ⚠️
GCC processes the command line left to right, maintaining a list of unresolved symbols.
- A
.afile only contributes objects that resolve currently unresolved symbols - ⚠️ Files using a library must come BEFORE the library
gcc main.c libsum.a # ✅ works
gcc libsum.a main.c # ❌ symbol not found
Static vs Extern vs Default
int x; // global, weak (uninit)
int x = 0; // global, strong (init)
static int x; // file-local
extern int x; // declaration only, defined elsewhere
10. Memory Hierarchy & Caches
Hierarchy (top = fast/small, bottom = slow/big)
Registers (~1 cycle)
L1 cache (~4 cycles)
L2 cache (~10 cycles)
L3 cache (~30 cycles)
DRAM (~100 cycles)
SSD/disk (~100,000 cycles)
Locality
- Temporal locality: recently accessed → likely accessed again
- Spatial locality: addresses near recent accesses → likely accessed soon
Cache Anatomy
A cache has:
- S sets, each with E lines (a.k.a. ways)
- Each line has:
- Valid bit (1 if line holds data)
- Tag (identifies which block)
- Block of
Bbytes of data
Total cache capacity: C = S × E × B
Cache Types
| Type | E (lines/set) | Notes |
|---|---|---|
| Direct-mapped | 1 | Fast, simple, suffers from conflict misses |
| Set-associative | 2, 4, 8, ... | Most common (e.g., "8-way") |
| Fully-associative | All blocks in one set | Flexible but expensive hardware |
Address Decomposition
For a cache with B = 2^b byte blocks and S = 2^s sets:
| Tag (rest) | Set Index (s bits) | Block Offset (b bits) |
- Block offset: which byte within the block
- Set index: which set to look in
- Tag: identifies which block currently in the set
Cache Operation
- Extract set index from address; go to that set.
- Check all lines in that set for valid bit + matching tag.
- Hit: return data at block offset.
- Miss: fetch block from next level; place in set (evict using LRU if full).
Cache Misses
- Cold (compulsory): first access; unavoidable
- Conflict: would have hit but evicted due to limited associativity
- Capacity: working set > cache size
Write Policies
- Write-back + write-allocate: writes go to cache; only flushed on eviction; missed writes load the block first. (Common.)
- Write-through + no-write-allocate: writes go directly to memory; missed writes don't load the block.
Cache Performance Recipe (Exam)
For a sequence of accesses:
- Compute address breakdown.
- For each access:
- Compute set index =
(addr / B) mod S - Compute tag =
addr / (B × S)
- Compute set index =
- Track contents of each set (using LRU for eviction).
- Mark hit/miss.
Common Patterns to Recognize
Sequential access on int array, 16B blocks:
- 1 miss every 4 accesses → 25% miss rate
Two arrays at start addresses differing by exact cache size:
- They map to identical sets → conflict-miss disaster in direct-mapped
- 100% miss rate possible
- More associativity (≥ 2-way) often fixes this
Stride > block size:
- Every access misses → 100% miss rate (no spatial locality benefit)
Matrix Traversal
// Row-major access (good)
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
sum += A[i][j];
// Column-major access (bad)
for (j = 0; j < N; j++)
for (i = 0; i < N; i++)
sum += A[i][j]; // stride = N*sizeof(int)
Cache-Friendly Coding Tips
- Loops with stride 1 (row-major in C)
- Block/tile algorithms for matrix operations
- Avoid pointer-heavy structures with poor locality
- Pad arrays to avoid catastrophic conflict misses
11. Exceptional Control Flow: Processes
Process
A process is an instance of a running program with:
- Its own virtual address space
- Its own register state
- Open file descriptors
fork()
Creates a (nearly) identical child process.
pid_t pid = fork();
if (pid == 0) {
// CHILD code
} else if (pid > 0) {
// PARENT code; pid = child's PID
} else {
// error
}
⚠️ Both processes continue from the line after fork(). They have separate copies of all memory (copy-on-write).
Process Counting Recipe
To count outputs after n forks:
- After 1 fork: 2 processes
- After 2 forks: up to 4 processes
- After k forks: up to
2^kprocesses - Draw the process tree to track who's alive when
exec Family
Replaces current process's program with a new one. Same PID, but new code/data.
execve(path, argv, envp);
// returns only on error
wait / waitpid
Parent waits for child(ren) to terminate and reaps them (frees their PCB).
pid_t wait(int *status); // wait for any child
pid_t waitpid(pid_t pid, int *status, int opts);
// opts: WNOHANG = don't block; WUNTRACED, etc.
⚠️ Zombie: child has exited but not been reaped. Reap them or they leak resources.
Status Macros
WIFEXITED(status) // child exited normally
WEXITSTATUS(status) // exit code
WIFSIGNALED(status) // killed by signal
WTERMSIG(status) // signal number
exit() vs _exit() vs return
exit(n): clean exit, runs atexit handlers, flushes buffers_exit(n): immediate exit, no cleanup (use in signal handlers / after fork)return nfrom main: equivalent toexit(n)
12. Exceptional Control Flow: Signals
What is a Signal?
A small int message from kernel/process to a process. Examples:
| Signal | Number (typical) | Meaning |
|---|---|---|
SIGINT |
2 | Ctrl+C |
SIGKILL |
9 | Force kill (can't catch/block) |
SIGSEGV |
11 | Invalid memory access |
SIGCHLD |
17 | Child stopped/terminated |
SIGSTOP |
19 | Stop (can't catch/block) |
SIGCONT |
18 | Continue stopped process |
SIGUSR1/SIGUSR2 |
10/12 | User-defined |
Signal Lifecycle
- Sent (e.g.,
kill(pid, SIGINT)) - Pending at recipient
- Delivered (handler runs, or default action: terminate, ignore, stop, continue)
Per-Signal State
For each signal, the kernel tracks:
- Pending bit — only 0 or 1, no counter!
- Blocked bit — if set, the signal stays pending and is not delivered
⚠️ Signal Coalescing
Signals do not queue. If a signal is pending and another of the same type arrives, the second is lost (only one bit). Different signal types can each be pending simultaneously.
signal() and sigaction()
signal(SIGCHLD, handler);
// safer: sigaction() with explicit options
Sending Signals
kill(pid, SIGUSR1); // to a process
raise(SIGUSR1); // to self (= kill(getpid(),...))
alarm(seconds); // schedule SIGALRM
Blocking Signals
sigset_t mask, old;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, &old);
// critical section
sigprocmask(SIG_SETMASK, &old, NULL);
Signal Handler Rules
⚠️ In handlers:
- Only call async-signal-safe functions (
write,_exit,kill,signal...) — NOTprintf,malloc,free! - Save/restore
errnoif you call functions that might modify it - Use
volatile sig_atomic_tfor shared variables - Keep handlers short
SIGCHLD Handler Idiom
To handle multiple children dying together (avoid coalescing bug):
void handler(int sig) {
int saved_errno = errno;
while (waitpid(-1, NULL, WNOHANG) > 0)
; // reap all available
errno = saved_errno;
}
Important Facts (Exam Trivia)
- ✅ Process can send signal to itself
- ✅ Some signals (SIGKILL, SIGSTOP) cannot be blocked or caught
- ✅ During handler, same signal is automatically blocked, but other signals may interrupt
- ❌ Handlers cannot use
printf,mallocsafely - ❌ Signals do not queue per-type
Signals + fork() Interactions
- After
fork(), child inherits parent's signal handlers and signal mask - After
exec(), custom handlers reset to default; mask preserved
13. Virtual Memory & Address Translation
The Big Picture
Each process has its own virtual address space. The MMU translates virtual addresses (VA) to physical addresses (PA) using a page table maintained by the OS.
Pages
- Page: fixed-size chunk of virtual memory (e.g., 4 KB)
- Frame (or Physical Page): same-sized chunk in physical memory
- VPN (Virtual Page Number) → PPN (Physical Page Number) via page table
- Offset within page is preserved (VPO == PPO, same bits)
Address Decomposition
For page size P = 2^p:
Virtual Address (n bits):
| VPN (n - p bits) | VPO (p bits) |
Physical Address (m bits):
| PPN (m - p bits) | PPO (p bits) |
Page Table Entry (PTE)
Per VPN, the PTE typically holds:
- Valid bit: is the page in physical memory?
- PPN: where in physical memory
- Permission bits: read/write/execute, user/kernel
- Dirty bit, Reference bit, ...
Translation Steps
- Extract VPN from VA.
- Look up page table entry at index VPN.
- If valid = 1: extract PPN; PA = PPN ‖ VPO (concatenate).
- If valid = 0: page fault (kernel handles: load page from disk, or kill if invalid).
TLB (Translation Lookaside Buffer)
A small cache of recent PTEs to skip the page-table walk.
- Typically set-associative (e.g., 16 entries, 2-way → 8 sets)
- Indexed by TLB index (low bits of VPN)
- Tagged by TLB tag (high bits of VPN)
Address Decomposition for TLB
VPN = | TLBT (high bits) | TLBI (low bits) |
If TLB has 2^t sets:
- TLBI =
tbits (low bits of VPN) - TLBT =
(VPN bits) - tbits (high bits of VPN)
Translation Pipeline
VA → split into VPN + VPO
VPN → split into TLBT + TLBI
Look up TLB at set TLBI:
TLB hit? → use cached PPN
TLB miss? → walk page table:
Page valid? → cache in TLB, use PPN
Page invalid? → PAGE FAULT
PA = PPN ‖ VPO
Worked Example: 20-bit VA, 18-bit PA, 1024-byte pages, 16-entry 2-way TLB
- Page size 1024 = 2¹⁰ → VPO = PPO = 10 bits
- VPN = 20 - 10 = 10 bits
- PPN = 18 - 10 = 8 bits
- TLB sets = 16/2 = 8 = 2³ → TLBI = 3 bits
- TLBT = 10 - 3 = 7 bits
VA bit field: [19:13] = TLBT, [12:10] = TLBI, [9:0] = VPO
PA bit field: [17:10] = PPN, [9:0] = PPO
Translation Walkthrough Example
VA = 0x078E6 = 0000 0111 1000 1110 0110
- VPO = bits [9:0] =
11 1110 0110=0x0E6 - VPN = bits [19:10] =
0000 0111 10=0x01E - TLBI = low 3 bits of VPN =
110=0x6 - TLBT = high 7 bits of VPN =
0000 011=0x03
Then look up TLB set 6 with tag 03 → either hit or miss → consult PT if miss.
Multi-Level Page Tables
For very large address spaces, page tables themselves are huge. Solution: hierarchical page tables.
- e.g., x86-64 uses 4-level page table, each level indexed by 9 bits of VPN
- VA =
[L1 idx | L2 idx | L3 idx | L4 idx | offset]
Page Fault
Triggered when:
- Valid bit = 0 (page on disk or not allocated)
- Permission violation (write to read-only, etc.)
Kernel handler:
- Find a free physical frame (evict if needed)
- Load the page from disk (or zero-fill)
- Update PTE
- Restart faulting instruction
Why VM is Useful
- Isolation: processes can't see each other's memory
- Memory > physical RAM: disk-backed virtual memory
- Sharing: multiple processes can map same physical page (libraries, fork copy-on-write)
- Protection: read-only
.text, no-execute stack
14. Common Exam Patterns & Traps
Bit-Level Trick Catalog
| Expression | Reduces to |
|---|---|
~x + 1 |
-x |
~x |
-x - 1 |
x ^ x |
0 |
x | ~x |
-1 (all 1s) |
(x + (1<<k) - 1) >> k |
Signed divide by 2^k (for negative x) |
x & (x - 1) |
Clears lowest set bit |
x & -x |
Isolates lowest set bit |
Counterexamples for Common False Claims
| Claim | Counterexample |
|---|---|
x * 2 > x for all int x |
x = INT_MAX |
-x < 0 when x > 0 |
(true, but -INT_MIN = INT_MIN < 0) |
x < y ↔ -x > -y |
x = INT_MIN, y = 0 |
Signed x < unsigned u works as expected |
x = -1, u = 0 (false negative) |
| Float arithmetic is associative | 1e20 + 1 - 1e20 ≠ 1 |
Assembly Reading Cheat-Recipe
- Identify register usage:
%rdi/%rsi/...= args,%rax= return. - Find the loop:
jmpforward followed by labels and a back-edgejcc. - Recognize idioms:
lea= arithmeticcmov= branchless if-else(r1, r2, scale)= array indexinglea k-1; test; cmovns; sar k= signed/2^k
- Trace one iteration carefully; the rest follows.
Linking Trap Catalog
- Library order:
gcc main.c lib.a, notgcc lib.a main.c staticmakes things file-local- Multiple uninit globals of different types → silent corruption
- Initialized non-zero →
.data; uninit/zero →.bss; functions →.text; string literals →.rodata
Cache Trap Catalog
- Two arrays separated by exactly cache size → 100% miss in direct-mapped
- Stride > block size → no spatial locality benefit
- Write-allocate caches: writing to a missed line loads the entire block first
- Cold misses are unavoidable; conflict misses are reduceable with associativity
Process/Signal Trap Catalog
- Always count from a process tree diagram
- Variables after
fork()are separate copies printfbuffers may flush twice in child if not flushed before fork- Signals don't queue — handler counts ≠ signal-send counts
printfis not safe in handlers; usewriteinstead- Reap children in a
waitpid(-1, ..., WNOHANG)loop
VM Trap Catalog
- TLB index uses low bits of VPN, tag uses high bits
- VPO bits = PPO bits = log₂(page size); they're never translated
- PA size and VA size can differ
- TLB miss ≠ page fault; page fault is when PT entry says invalid
Quick Reference: Bit Field Decomposition Cheatsheet
For a Cache
Given B = 2^b byte blocks, S = 2^s sets:
- Block offset:
bbits (low) - Set index:
sbits (next) - Tag: rest (high)
For Virtual Memory
Given page size P = 2^p bytes, VA = n bits, PA = m bits:
- VPO =
pbits (low of VA) - VPN =
n - pbits (high of VA) - PPO =
pbits (low of PA, same as VPO) - PPN =
m - pbits (high of PA)
For TLB
Given TLB sets = 2^t, VPN bits = v:
- TLBI =
tbits (low of VPN) - TLBT =
v - tbits (high of VPN)
Combined VA Layout (when using both cache and TLB on physical addresses)
| TLBT | TLBI | VPO | ← virtual address split
| TAG | SET | OFF | ← cache split (on PA, but VPO is same as PPO)
If page size ≥ cache size, the cache index falls within VPO and you can index the cache before translation finishes (virtually-indexed, physically-tagged caches).
Top 30 Things to Memorize
~x = -x - 1-INT_MIN = INT_MIN(overflow)- Mixed signed/unsigned compare → signed promoted to unsigned
leais for arithmetic, doesn't access memorycmovis branchless if-else(base, idx, scale)is array indexing- Signed div by
2^k:(x + (x<0 ? 2^k - 1 : 0)) >> k - Calling convention args:
rdi, rsi, rdx, rcx, r8, r9 - Caller-saved:
rax, rcx, rdx, rsi, rdi, r8-r11; callee-saved:rbx, rbp, r12-r15 - Return value:
%rax - Stack grows toward lower addresses
pushq:*--rsp = X;popq:X = *rsp++cmp B, Asets flags as if computingA - B- Struct field aligns to its own size; total rounds up to max field alignment
.text(code),.data(init non-zero),.bss(uninit/zero),.rodata(const)- Library order: files using lib come BEFORE the lib
- Strong/weak: strong wins; multiple weak = picked one (often largest)
- Cache address =
[Tag | Set | Offset]; offset = log₂(block), set = log₂(num sets) - Direct-mapped cache + two arrays at cache-size offset = catastrophe
- Write-allocate + write-back means writes load the block first
fork()returns 0 in child, child PID in parent- After
fork(), separate memory spaces (copy-on-write) - To count prints with fork: draw the process tree
wait/waitpidreaps zombie children- Signals don't queue per-type; coalesce
- SIGKILL and SIGSTOP can't be blocked or caught
- In handlers: no
printf, nomalloc; usewrite,_exit - SIGCHLD handler:
while(waitpid(-1, NULL, WNOHANG) > 0); - VM: VA =
[VPN | VPO], PA =[PPN | PPO]; offsets identical - TLB: tag = high bits of VPN, index = low bits of VPN
Process Tree Quick Examples
// Example 1
fork(); fork();
// 4 processes total
// Example 2
for (i = 0; i < 2; i++) fork();
// 4 processes total (same as above)
// Example 3
if (fork() == 0) fork();
// Parent forks; child also forks → 3 processes
// Example 4: counting prints
for (i = 0; i < n; i++) {
fork();
printf("hi\n");
}
// Print count: 2 + 4 + 8 + ... + 2^n = 2^(n+1) - 2