Read a lecture top to bottom once, then come back and use the colored boxes as your review layer. Each box type always means the same thing:
Plain English, the technical version, and why it matters.
A sequence of steps you may need to reproduce.
Problems that get harder as you go.
Traps that cost points on exams.
Questions with the answer hidden until you open it.
Prerequisite knowledge the slides assume.
Your professor ended the L01a deck with a "Worth notice" list, which is the clearest signal of what gets tested. Those items are marked in each lecture's exam checklist. The course is cumulative, so earlier material can reappear on later tests.
Three interactive tools are built in: a Caesar cipher breaker, an RSA calculator, and a UNIX permission decoder. Use them to check your hand work, not to replace it: exams are closed-tool.
Course orientation
Source: L00-Course-Info-itec4235.pdf
This deck is logistics, but some of it is quietly testable (the ethics of authorized security work shows up again in L00B) and all of it affects your grade. The course is face-to-face, 4 credit hours, and mixes lecture, demonstrations, hands-on labs, and a term project. The theme of the whole course is on the closing slide: learn the concept, run the lab, and explain the evidence. Security "becomes real when you can observe, reproduce, and explain it."
Instructor: Dr. Peng Wang (wpeng2@gsu.edu), 25 Park Place, Room 714. Office hours are Tuesday/Thursday 1:30–2:00 pm in person and Friday 11:00 am–12:00 pm online over Zoom. Always email from your GSU account. Materials, deadlines, and submissions live in iCollege (D2L).
CSC 2720 (Data Structures) and CSC 3320 (System-Level Programming) with a C or better; students without them can be dropped at any time. Recommended: Java or Python, C, and Unix basics, plus comfort converting between decimal, binary, and hexadecimal. That last one matters a lot in this course, because keys, hashes, and ciphertexts are almost always shown in hex. A refresher follows below.
Recommended: Stallings & Brown, Computer Security: Principles and Practice, 5th ed. Many slides in L01a and L03a ("Table 1.1", "Figure 2.3") come straight from this book, so its chapters 1–2 are the best companion reading. Supplementary: Trappe (Introduction to Cryptography with Coding Theory, the source of the Alice/Bob/Eve slides) and Goodrich & Tamassia (Introduction to Computer Security, the source of the CIA/AAA and block cipher slides).
Foundations: explain ethical hacking and its legal boundaries; use controlled tools to investigate network and system behavior; identify common vulnerabilities, attacks, and malware. Practice: analyze how threats are discovered, validated, mitigated, and monitored; apply basic encryption, hashing, and secure protocols; document commands, evidence, and technical conclusions.
| Component | Weight | Notes |
|---|---|---|
| Assignments and labs | 30% | Five assignments in iCollege; practice labs may become graded |
| Midterm | 25% | In class; emphasizes material since the previous test |
| Final exam | 25% | In class; the course is cumulative so earlier material can return |
| Applied term project | 15% | Lab extension, technical analysis, report, demo, and/or presentation |
| Participation | 5% | Demonstrations and guided hands-on work |
Pop quizzes may count toward the exam grade or as bonus points, per what is announced in class.
Each letter is your percentage in that category (0–100). The weights add up to 1.00, so the result is also a percentage.
Worked example. Assignments 92, participation 100, midterm 78, project 88. What final exam score F gets you a 85 overall? 0.30(92) + 0.05(100) + 0.25(78) + 0.15(88) = 27.6 + 5 + 19.5 + 13.2 = 65.3. You need 0.25F = 85 − 65.3 = 19.7, so F = 78.8.
Common mistake: averaging the category scores without weights. The midterm and final together are half your grade; participation is only 5%.
One day late: 90% of total points. Two days late: 70%. More than two days: the submission closes. Last-minute technical problems do not automatically waive deadlines.
A 100-point lab earns a raw 86 but is one day late. The most common reading of "90% of total points" is that your score is multiplied by 0.9: 86 × 0.9 = 77.4. Another reading is that the maximum drops to 90 points. Because the wording allows both, confirm with Dr. Wang before you rely on either. The practical lesson is the same: two days late costs 30% and three days late costs everything.
Use only authorized course environments: local Docker containers, toy systems, packet captures, provided files, and simulated datasets. Never scan, attack, exploit, or test real third-party systems. This is not just a course rule. As L00B explains, accessing a computer without permission is a crime even if you "don't break anything."
Before Lab 0, install Docker Desktop, Git, and a terminal. STUDENT_SETUP.md and TROUBLESHOOTING.md are your first stop when stuck. The Lab 0 startup commands:
cd lab00-kali-docker-networking # move into the lab folder
docker compose up -d --build # build images and start containers in the background (-d = detached)
docker compose ps # list running containers and their status
docker compose exec kali bash # open a bash shell inside the "kali" container
A container is a lightweight, isolated environment that runs its own filesystem and processes on your machine's kernel. Docker Compose reads a docker-compose.yml file describing several containers (for example an attacker "kali" box and a victim box) and a private network between them. Because the whole lab lives inside that private network, you can practice scanning and attacking without touching any real system, which is exactly what the authorized-work policy requires.
Submit your own work and be able to explain it. Follow each assignment's rules on AI and collaboration; AI output is not a substitute for evidence or understanding. No AI or outside help on quizzes or exams unless explicitly allowed. Course materials are for enrolled students only and must not be posted publicly. Disagreement in class is fine when supported with evidence; harassment and personal attacks are not. Excused absences go through the Dean of Students.
The syllabus asks you to be comfortable with decimal, binary, and hexadecimal. Cryptography works on bits, but humans read those bits in hex. A 128-bit AES key is 32 hex digits; a SHA-256 digest is 64 hex digits. If you can move between these bases quickly, the rest of the course gets easier.
Decimal → binary: repeatedly divide by 2 and read the remainders bottom to top.
Binary → hex: group bits in fours from the right; each group is one hex digit. This works because 16 = 24.
Binary → octal: group bits in threes (8 = 23). This is how chmod 754 works (see L04a).
Any base → decimal: multiply each digit by its place value and add.
1. 13 in binary: 13÷2 = 6 r1, 6÷2 = 3 r0, 3÷2 = 1 r1, 1÷2 = 0 r1. Read upward: 1101. Check: 8+4+0+1 = 13.
2. 0x2F to decimal: 2·16 + 15 = 47.
3. 1011 0110 to hex: 1011 = B, 0110 = 6, so 0xB6 = 182.
4. How many bits in a 64-hex-digit hash? Each hex digit is 4 bits, so 64 × 4 = 256 bits (that is SHA-256).
5. One byte is 8 bits, which is exactly 2 hex digits (00–FF, 0–255). "Hello" is 5 bytes: 48 65 6C 6C 6F.
Grouping bits from the left instead of the right. 101101 is 0010 1101 = 0x2D, not 1011 01. Pad with leading zeros first.
This course is face-to-face, 4 credit hours, and cumulative: assignments and labs 30%, midterm 25%, final 25%, term project 15%, participation 5%, with late work at 90% after one day, 70% after two, and closed after that. All hands-on work happens only in authorized environments (the Docker labs), never on real third-party systems. The prerequisite skill you will use constantly is moving between decimal, binary, and hex: one hex digit is 4 bits, one byte is 2 hex digits, and a 64-hex-digit hash is 256 bits.
Ethical hacking
Source: L00B-EthicHacking.pdf (partial slides credited to samsclass.info)
This course teaches attack techniques. The only thing separating a security professional from a criminal who uses the same tools is authorization. This lecture draws that line so you understand why every lab stays inside Docker.
| Type | Behavior | Who benefits |
|---|---|---|
| Black hat (cracker) | Extraordinary skill used for malicious or destructive activity | Themselves, usually by hurting others |
| White hat | Hacker skills used for defense, with permission | Others and the common good |
| Grey hat | Works offensively and defensively at different times | Unpredictable; behavior cannot be predicted |
"Grey hat means mostly good." The slide's defining feature of grey hats is unpredictability, not good intentions. A grey hat who breaks in without permission to report a flaw has still accessed a system without authorization.
Attacks follow a repeatable lifecycle. Defenders (and ethical hackers) study the same lifecycle so they know where to detect and block an attacker.
flowchart TD
T([TRY]) --> S{Success?}
S -- No --> T
S -- Yes --> N([NEXT])
The slide's flowchart has three parts: a TRY box, a Success? decision diamond, and a NEXT box. A "No" sends you back to TRY; a "Yes" advances. The joke has a real point: hacking is mostly persistence. Attackers try many passwords, many ports, many exploits, and only need one to work. Keep this in mind for L01a's challenge #6: attackers need one weakness; defenders must close all of them.
nmap. Example: discovering port 22 (SSH) and 80 (HTTP) are open on a server.Why it exists: each phase feeds the next. You cannot exploit a service you have not found, and you cannot find it without knowing where to look.
Common exam questions: put the phases in order; identify the phase from a scenario ("the attacker ran nmap" = scanning); name a tool for a phase.
Common misconception: that scanning is the first step. Footprinting comes first, and much of it is legal public research.
The slide lists post-exploitation behavior. Each item has a purpose:
To keep exclusive control. If other attackers can use the same hole, they may disrupt the system or draw the administrator's attention. The attacker already has a backdoor, so they no longer need the original vulnerability.
Installing scanner programs (and possibly exploit programs). The victim is being used as a pivot so the scans do not trace back to the attacker.
The slides argue that "no damage" is not a defense. They build the case from moral philosophy (duty, Kant), property law (information as property), and specific statutes.
This is duty-based ethics (deontology, associated with Immanuel Kant): whether an act is right depends on the act and your obligations, not on what you want or on how things turn out. Applied here: breaking into a system to learn about it or to point out flaws is wrong even when nothing is damaged. The slide's analogy is breaking and entering. If you break into a house and take nothing, you have still committed a crime, because you took away the owner's sense of security.
The slides cite UK law. The main U.S. federal law is the Computer Fraud and Abuse Act (CFAA, 1986), which similarly criminalizes accessing a computer "without authorization" or "exceeding authorized access." The slide's mention of the Department of Justice and the CHIP units refers to U.S. enforcement.
Information deliberately published is accessible, but that does not give you the right to destroy or edit it. Information not deliberately published, like account numbers and personal data, should not be sought out regardless of your intentions.
The United Nations and Council of Europe draft law that applies across borders (the Council of Europe's 2001 Budapest Convention on Cybercrime is the best-known example). The slides define three roles a computer can play in a cybercrime:
| Role | Meaning | Example |
|---|---|---|
| Target | The computer itself is attacked | Spreading viruses |
| Tool | The computer is used to commit a traditional crime | Credit card fraud |
| Accessory | The computer stores evidence or illegal material | Storing stolen information |
Memory trick: TTA, "the computer is hit, used, or holds."
Hacktivism is hacking for a political or social cause (for example defacing a political group's website). The slide argues it violates the target's First Amendment free speech rights: you are silencing their speech to promote yours. The ethical alternative is to publish your own site or blog. The Kantian principle behind this: no one's rights should be taken at the expense of another's, because all people are equal.
Hacker communities develop norms to avoid getting caught: keep a low profile, do not brag to outsiders, do not "narc" on fellow hackers. The slide's argument is that these norms exist precisely because members know what they do is wrong.
Is port scanning legal? It depends. Some states consider it legal; the federal government does not treat it as a violation and leaves it to the states. Your ISP's Acceptable Use Policy may still forbid it and can cut off your service. IRC "bots" (programs that auto-respond to make it look like a person is present) may also be forbidden by the AUP.
Clearly illegal: accessing a computer without permission; installing worms or viruses; denial-of-service attacks; denying users access to network resources. Even a legitimate tester must make sure their actions do not prevent the customer's employees from doing their jobs.
These have no single "correct" answer, but a strong exam answer applies the lecture's framework: authorization, duty, and harm to others' rights.
Unethical under the lecture's framework. No permission was given, and "no damage" does not matter under duty-based ethics (the breaking-and-entering analogy). The ethical path is responsible disclosure through official channels or an authorized bug bounty.
Unethical for the same reason, and it also invades personal privacy. It takes away the owner's sense of security.
A real 2006 case (Christopher Soghoian). Arguments for: no system was broken into, and it exposed a real public-safety weakness. Arguments against: it could help actual attackers, and it was done without coordinating with authorities. Good answers weigh the public benefit against the risk and discuss disclosure to the TSA/airline first.
If the data was discovered in publicly released reports (no break-in), exposing a broken promise may be justified whistleblowing. If the hacker broke in to find it, the access itself is unethical and illegal. Publicizing the SSNs themselves would harm the very people being "protected."
Clearly unethical: this is the hacktivism case. It is unauthorized modification and silences the candidate's speech. Create your own site instead.
Organizations hire people to attack them on purpose, under contract, so they find weaknesses before criminals do. The one thing that makes this legal is the owner's permission.
| Model | What the tester knows | Who knows about the test | What it measures |
|---|---|---|---|
| White box | Everything: topology, technology; may interview IT staff and employees | Staff generally aware | Thorough internal weaknesses; easiest for tester |
| Black box | Nothing; must discover details | Staff not told | Whether security staff detect a real-looking attack |
| Gray box | Partial information | Varies | A hybrid balance of depth and realism |
Memory trick: the "box" is the target. A white box is transparent (you see inside), a black box is opaque, and gray is in between.
The slide shows an office floor plan with dimensions in feet. Along the top are a conference/break room and four office cubicles (15′, 15′, 12′, 12′, 12′ wide), each with a workstation. An 8′ closet labeled "Wire & phone equipment" holds the NetWare servers (NetWare was a Novell network operating system). Next to it is the Network administrator's office. The middle holds the Lobby and restrooms, with more workstations nearby. On the right are the Production (20′) and Shipping (30′) areas, each with two Data collection terminals. A note says some diagrams also show routers and firewalls.
What the instructor wants you to notice: this is the kind of document a white-box tester is given. It tells them where the servers physically are, how many hosts exist, and which areas (production floor terminals) might be less supervised. A black-box tester would have to reconstruct this through footprinting and scanning.
They change their behavior. Employees on alert will not click phishing links, will lock their screens, and will challenge strangers, so the test no longer reflects how the organization really behaves. Social engineering results become meaningless.
They may temporarily tighten defenses (patch, watch logs closely) just for the test, and you can no longer measure whether they would detect a real attack. The black-box model exists precisely to test detection and response.
The "nutshell" slide adds soft skills: knowledge of network and computer technology, the ability to communicate with management and IT staff, understanding of the law, and the ability to use the necessary tools.
Advantages: "to catch a thief you have to think like a thief"; helps close holes; protects banks and financial institutions; prevents website defacement; an evolving technique. Disadvantages: everything depends on the tester's trustworthiness (you are handing a stranger the keys), and professionals are expensive.
The textbook calls a hacker's toolkit a tiger box: a collection of operating systems and tools for hacking, such as network scanners, traffic monitors, keyloggers, and password crackers. Programming helps too: Perl (Practical Extraction and Report Language) for text processing and quick scripts, C for low-level exploits, and scripts in general (sets of instructions run in sequence).
| Tool | Purpose |
|---|---|
| Kali Linux | Security-focused Linux distribution; the "attacker's system." Your lab's kali container runs it. |
| Nmap | "King of scanners": finds live hosts, open ports, and services on a target. |
| Metasploit Framework | Runs exploits, generates payloads, and manages sessions on a compromised machine. |
| Command | What it does | Example |
|---|---|---|
pwd | Print working directory (where am I?) | pwd → /root |
man | Manual page for a command | man chmod |
ls | List directory contents (the slide shows "Is", a typo for lowercase ls) | ls -l shows permissions |
cd | Change directory | cd /etc |
mkdir / rmdir | Make / remove an empty directory | mkdir evidence |
cp / mv | Copy / move or rename | cp a.txt b.txt |
stat | Detailed file metadata: size, permissions, owner, timestamps | stat /etc/passwd |
adduser | Create a user account | adduser alice |
su | Switch user (defaults to root; needs that user's password) | su - |
sudo | Run one command as root (if you are authorized) | sudo apt update |
echo | Print text | echo $USER |
cat | Print a file's contents | cat /etc/passwd |
vi | Terminal text editor | vi notes.txt (i to insert, Esc :wq to save and quit) |
chmod | Change file permissions | chmod 750 script.sh (see L04a) |
ifconfig | Show network interfaces and IP addresses (newer: ip addr) | ifconfig eth0 |
ping | Test whether a host is reachable (ICMP echo) | ping -c 3 10.0.0.5 |
To protect against hacking: stay updated about the software you use through official, reliable sources, and educate employees and users about black-hat tactics.
| Term | Definition | Example | Memory trick | Similar | Opposite |
|---|---|---|---|---|---|
| Hacker | Accesses systems without authorization | Logging into a neighbor's router | "Hack" = unauthorized entry | Intruder | Authorized user |
| Cracker | Breaks in to steal or destroy data | Stealing a customer database | Cracks things open to break them | Black hat | White hat |
| Black hat | Malicious hacker for personal gain | Ransomware operator | Old westerns: villain wears black | Cracker | White hat |
| White hat | Defensive, authorized hacker | Contracted pen tester | Hero wears white | Ethical hacker | Black hat |
| Grey hat | Unpredictable mix of offense and defense | Breaks in uninvited, then reports the bug | Grey = blurred line | Hacktivist (sometimes) | — |
| Footprinting | Passive information gathering on a target | WHOIS lookups, job postings | Studying footprints before the hunt | Reconnaissance, OSINT | — |
| Scanning | Actively probing for hosts, ports, services | nmap on a lab subnet | Knocking on every door | Enumeration | — |
| Rootkit | Hidden software that keeps privileged access | Backdoor that hides from ps | Kit to stay root | Backdoor, Trojan | — |
| Hacktivism | Hacking for political/social causes | Defacing a candidate's site | Hack + activism | — | Lawful protest |
| Penetration test | Authorized break-in; report findings only | Testing a bank's web portal | Penetrate, then report | Ethical hacking | Unauthorized intrusion |
| Security test | Pen test plus policy review and solutions | Audit with remediation plan | "Security" is the bigger word, bigger scope | Security assessment | — |
| Security policy | Rules for behavior, access, and violations | "Passwords must not be written down" | The rulebook | Acceptable Use Policy | — |
| White/black/gray box | Pen test models by tester's knowledge | Black box: staff unaware | Can you see inside the box? | Full/zero/partial knowledge | — |
| Tiger box | Collection of OSs and hacking tools | A Kali laptop | A tiger's toolkit of claws | Toolkit | — |
| CHIP | DOJ Computer Hacking and Intellectual Property units | Federal cybercrime prosecution | CHIP = computer crimes | — | — |
| AUP | Acceptable Use Policy set by an ISP or organization | ISP bans port scanning | What use is acceptable | Terms of service | — |
| Nmap | Network/port scanner | nmap -sV target | Network mapper | Port scanner | — |
| Metasploit | Exploitation framework | Launching a known exploit in a lab | Meta-exploit toolbox | Exploit kit | — |
The only line between a security professional and a criminal is authorization. Hackers access systems without permission; crackers do it to steal or destroy; black, white, and grey hats differ by intent and predictability. Attacks follow footprinting → scanning → gaining access → maintaining access. Duty-based ethics says "no damage" is no defense, the law treats information as property, and a computer can be the target, tool, or accessory of a crime. Penetration tests report findings; security tests add policy review and fixes; white, black, and gray box describe how much the tester knows.
The CIA triad and an introduction to cryptography
Source: L01a-Ch01-CIA-Crypto1.pdf (Goodrich & Tamassia Ch. 1, Stallings & Brown Ch. 1, Trappe Ch. 1)
The deck's outline has four parts: fundamental concepts (the framework for everything else), access control models (who has access?), cryptographic concepts (protecting data), and implementation and usability (passwords, social engineering).
Every security question in this course can be phrased as "which of these three properties is at risk?" Confidentiality, Integrity, and Availability are the three goals security is trying to guarantee. NIST's official definition of computer security is built on them: measures and controls that ensure confidentiality, integrity, and availability of information processed and stored by a computer, including hardware, software, firmware, information data, and telecommunications (NISTIR 7298, 2019).
Encryption transforms information using a secret (the encryption key) so that it can only be read using another secret (the decryption key), which may or may not be the same key. The slide's diagram shows a sender encrypting plaintext with a shared secret key, sending ciphertext over a communication channel where an attacker is eavesdropping, and the recipient decrypting with the same shared key. The attacker sees only ciphertext.
Access control is the set of rules and policies limiting access to those with a "need to know." Need to know can be determined by identity (a person's name, a computer's serial number) or by role (manager, security specialist).
Authentication determines the identity or role someone has. It is based on some combination of three factors:
| Factor | Slide examples | Other examples | Weakness |
|---|---|---|---|
| Something you know | password=ucIb()w1V, mother=Jones, pet=Caesar | PIN, security questions | Can be guessed, phished, or shared |
| Something you have | Radio token with secret keys, smart card, key fob | Phone authenticator app, YubiKey | Can be lost or stolen |
| Something you are | A human with fingers and eyes | Fingerprint, face, iris | Cannot be changed if compromised |
Notice the slide's "mother=Jones, pet=Caesar": security questions are "something you know," and they are weak because the answers are often public.
Authorization determines whether a person or system is allowed to access a resource, based on an access control policy. It should prevent an attacker from tricking the system into granting access.
Authentication answers "who are you?" Authorization answers "what are you allowed to do?" Logging into iCollege is authentication; being able to see only your own grades is authorization. Authentication always comes first. Memory trick: authN = ideNtity, authZ = permiZion (sounds like it).
Physical security uses physical barriers: locked cabinets and doors, windowless rooms, sound-dampening materials, and even Faraday cages (rooms whose walls contain copper mesh so electromagnetic signals cannot enter or leave, blocking wireless eavesdropping). The OS file system (Unix, Windows) is also an access control tool; that is the subject of L04a.
1. A hospital's patient records are posted online. Confidentiality.
2. A student changes their grade in the registrar's database. Integrity.
3. A flood of traffic takes the GSU portal offline during registration. Availability.
4. Ransomware encrypts a company's files. Availability primarily (the owners cannot reach their data). If the attackers also copy the files first ("double extortion"), confidentiality is violated too.
5. An attacker intercepts an encrypted bank transfer and flips bits in the amount field without being able to read it. Integrity, even though confidentiality held. This is why encryption alone is not enough and why MACs exist (later in this lecture and L03a).
CIA covers the data. The "other security concepts" cover trust: can we trust the system's rules, can we trust who said something, and can people act without being tracked?
The slide's running example is an online music store. Policy: designers specify how users can access and copy songs. Permission: people who bought a song get limited access and copying rights. Protection: the store builds mechanisms (like DRM) that stop unauthorized copying. Memory trick: P-P-P, rules → rights → enforcement.
| Anonymity tool | How it works | Example |
|---|---|---|
| Aggregation | Combine many people's data so sums/averages cannot identify anyone | Census reports average income by county |
| Mixing | Intertwine transactions or messages so none can be traced to one person | Mix networks; cryptocurrency mixers |
| Proxies | Trusted agents act on your behalf so the action cannot be traced to you | A VPN or Tor relay making requests for you |
| Pseudonyms | Fictional identities known only to a trusted entity | A username only the site admin can link to your real name |
Security professionals use precise vocabulary so they can reason about risk. These terms (Stallings Table 1.1) are standard exam definitions, and the relationship diagram ties them together.
| Term | Plain English | Technical (Table 1.1) |
|---|---|---|
| Adversary (threat agent) | The bad actor | An individual, group, organization, or government that conducts or intends to conduct detrimental activities |
| Attack | The bad act | Any malicious activity that attempts to collect, disrupt, deny, degrade, or destroy information system resources or the information itself |
| Countermeasure | The defense | A device or technique that impairs adversarial activity or prevents espionage, sabotage, theft, or unauthorized access/use |
| Risk | How worried you should be | The extent to which an entity is threatened; a function of (1) the adverse impact if the event occurs and (2) its likelihood |
| Security policy | The rules | Criteria for providing security services; defines and constrains activities to keep systems and data secure |
| System resource (asset) | What you protect | Applications, support systems, programs, physical plant, mission-critical systems, personnel, equipment, or related groups of systems |
| Threat | What could go wrong | Any circumstance or event that could adversely impact operations, assets, individuals, other organizations, or the Nation via unauthorized access, destruction, disclosure, modification, or denial of service |
| Vulnerability | The weak spot | A weakness in a system, procedures, internal controls, or implementation that could be exploited by a threat source |
Variables: Impact is how bad the harm would be (dollars, lives, reputation). Likelihood is how probable the event is. The slide only says "a function of" both; multiplication is the common way to score it.
Worked example. On a 1–5 scale: a lost unencrypted laptop with customer data has impact 5 and likelihood 3 → risk 15. A defaced marketing page has impact 2 and likelihood 4 → risk 8. Prioritize the laptop (encrypt disks).
Common mistake: treating a high-likelihood, trivial event as the top priority. Both factors matter.
flowchart LR O[Owners] -- value --> A[Assets] O -- impose --> C[Countermeasures] O -- wish to minimize --> R[Risk] C -- to reduce --> R TA[Threat agents] -- wish to abuse and/or may damage --> A TA -- give rise to --> T[Threats] T -- that increase --> R R -- to --> A
Read the figure as two stories that meet at the assets in the center.
What to notice: owners never reduce threats directly; they can only impose countermeasures to reduce risk. An exam may give you a scenario and ask you to label the owner, asset, threat agent, threat, countermeasure, and risk.
The assets of a computer system fall into four categories: hardware, software, data, and communication facilities and networks.
Vulnerabilities map onto CIA. A resource can be corrupted (loss of integrity), leaky (loss of confidentiality), or unavailable or very slow (loss of availability).
Threats are capable of exploiting vulnerabilities and represent potential harm. Attacks are threats carried out, classified two ways:
| By effect | By origin |
|---|---|
| Active: tries to alter system resources or affect their operation | Insider: initiated by an entity inside the security perimeter (an authorized user misusing access) |
| Passive: tries to learn or use information without affecting resources | Outsider: initiated from outside the perimeter |
| Passive | Active | |
|---|---|---|
| Goal | Obtain transmitted information (eavesdropping, monitoring) | Modify the data stream or create a false stream |
| Types | Release of message contents (reading the message); traffic analysis (learning from patterns, such as who talks to whom, when, and how much, even when messages are encrypted) | Replay (capture and retransmit a valid message); masquerade (pretend to be someone else); modification of messages (alter, delay, or reorder); denial of service (prevent normal use) |
| Defense strategy | Prevent (encryption); hard to detect because nothing changes | Detect and recover; hard to prevent completely |
1. Sniffing Wi-Fi to read unencrypted emails → passive, release of message contents.
2. Noticing a CEO's encrypted messages to a rival company spike before a merger → passive, traffic analysis.
3. Recording an encrypted "unlock" signal from a car key fob and playing it back later → active, replay. Encryption did not help because the attacker never needed to read it.
4. Sending email with a forged "From: dean@gsu.edu" → active, masquerade.
Countermeasures prevent, detect, or recover from attacks. Two cautions from the slide: a countermeasure may itself introduce new vulnerabilities (a buggy antivirus runs with high privilege), and residual vulnerabilities may remain. The goal is not zero risk; it is to minimize the residual level of risk to the assets.
| Asset | Availability | Confidentiality | Integrity |
|---|---|---|---|
| Hardware | Equipment stolen or disabled, denying service | An unencrypted USB drive is stolen | A door sensor is replaced with one that always reports "closed" at certain times |
| Software | Programs deleted, denying access | An unauthorized copy of software is made | A working program is modified to fail or do something unintended |
| Data | Files deleted, denying access | Unauthorized read of data; statistical analysis reveals underlying data | Existing files modified or new files fabricated |
| Communication lines and networks | Messages destroyed or deleted; lines rendered unavailable | Messages read; traffic pattern observed | Messages modified, delayed, reordered, or duplicated; false messages fabricated |
Notice the hardware-integrity example: the attacker does not destroy the sensor, they replace it with one that lies. Also note "statistical analysis reveals underlying data," which is the failure mode aggregation (an anonymity tool) is supposed to prevent.
FIPS 200 is a U.S. federal standard listing the minimum security areas an organization must address. You do not need every word, but you should recognize each area and be able to say what it covers.
| Area | In one line |
|---|---|
| Access control | Limit system access to authorized users, their processes, and devices, and to permitted transactions |
| Awareness and training | Make staff aware of risks, laws, and policies; train them for security duties |
| Audit and accountability | Create and keep audit logs; trace actions to individual users |
| Certification, accreditation, and security assessments | Periodically assess controls, fix deficiencies, authorize operation, monitor continuously |
| Configuration management | Maintain baseline configurations and inventories; enforce secure settings |
| Contingency planning | Emergency response, backups, and post-disaster recovery to keep critical resources available |
| Identification and authentication | Identify and verify users, processes, and devices before granting access |
| Incident response | Prepare, detect, analyze, contain, recover; track and report incidents |
| Maintenance | Timely maintenance with controlled tools and personnel |
| Media protection | Protect paper and digital media; limit access; sanitize or destroy before disposal |
| Physical and environmental protection | Limit physical access; protect the facility, utilities, and environment |
| Planning | Write and update security plans and rules of behavior |
| Personnel security | Trustworthy staff; protect systems during terminations/transfers; sanctions for violations |
| Risk assessment | Periodically assess risk to operations, assets, and individuals |
| Systems and services acquisition | Fund security; secure development life cycle; install restrictions; secure third parties |
| System and communications protection | Monitor and protect communications at boundaries; secure architecture and engineering |
| System and information integrity | Fix flaws promptly; protect against malicious code; act on security alerts |
Personnel security (protecting systems during terminations and transfers). Access control is also involved.
Media protection (sanitize or destroy media before disposal).
Memory hook for #6, the most-quoted one: "The defender has to be right every time; the attacker only has to be right once."
An attack surface is how much of your system an attacker can touch. Shrinking it and layering defenses are the two main architectural strategies.
Slide examples: open ports on outward-facing web servers and the code listening on them; services inside a firewall; code that processes incoming data, email, XML, and office documents; interfaces, SQL, and web forms; and an employee with sensitive access who could be socially engineered.
| Category | Vulnerabilities in | Examples |
|---|---|---|
| Network attack surface | Enterprise networks, WANs, the Internet | Protocol flaws used for DoS, disrupting links, intruder attacks |
| Software attack surface | Application, utility, or OS code | Especially web server software |
| Human attack surface | Personnel or outsiders | Social engineering, human error, trusted insiders |
The x-axis is attack surface size (small → large). The y-axis is layering, meaning how many defensive layers protect the system (deep → shallow). Defense in depth means stacking multiple independent protections so one failure is not fatal. The quadrants: deep layering with a small surface is low risk; shallow layering with a large surface is high risk; the two mixed combinations are medium. The takeaway: you can compensate for one weakness with the other, but the best position combines both. L04a applies defense in depth to Chrome.
An attack tree puts the attacker's goal at the root ("bank account compromise") and breaks it into sub-goals, with leaves being concrete attacks. In the figure, the root branches into five paths: user credential compromise (theft of tokens and handwritten notes, malicious software installation, vulnerability exploits, brute-force attacks with PIN calculators, social engineering, web page obfuscation, user surveillance), injection of commands, user credential guessing (brute force), security policy violation, and use of known authenticated sessions (normal authentication with a pre-defined session ID, session hijacking). Leaves are coded by where the attack happens: UT/U for the user terminal and user, CC for the communications channel, IBS for the Internet banking server. Why it's useful: it lets defenders enumerate every path to the goal and see which countermeasure blocks which branches.
| Organization | What it is |
|---|---|
| NIST (National Institute of Standards and Technology) | U.S. federal agency for measurement science and standards; publishes FIPS (e.g., AES, FIPS 200) and SP 800 documents |
| ISOC (Internet Society) | Professional membership society; organizational home of the groups (like the IETF) that set Internet infrastructure standards |
| ITU-T (International Telecommunication Union) | United Nations agency where governments and industry coordinate global telecom (e.g., X.509 certificates) |
| ISO (International Organization for Standardization) | Nongovernmental body publishing International Standards (e.g., ISO 27001) |
Cryptography is the main technical tool for confidentiality, integrity, and authenticity. This section sets up the cast of characters, the notation, and the attack models used for the rest of the course.
The cast: Alice sends, Bob receives, Eve eavesdrops. Alice encrypts using a method prearranged with Bob. Bob receives the ciphertext and decrypts with the decryption key.
Variables: M = plaintext, C = ciphertext, E = encryption algorithm (with its key), D = decryption algorithm (with its key).
Why it works: E and D are chosen as inverses, so decryption undoes encryption. They are also chosen so that recovering M from C is infeasible for anyone without the key, which is what lets C travel over an insecure channel.
Example: with a Caesar cipher, E(HELLO) = KHOOR and D(KHOOR) = HELLO.
Common mistake: thinking D must use a different key from E. In symmetric systems the keys are usually the same.
Goals 1–2 are passive (the attacker is sometimes named Oscar); goals 3–4 are active (named Mallory, for "malicious"). The course generally uses Eve for both and assumes she is as bad as the situation allows. Goal 2 is the worst outcome because it compromises past and future messages.
These differ by how much information Eve has when trying to find the key. They are listed from weakest attacker to strongest.
| Attack | Eve has | Realistic example |
|---|---|---|
| Ciphertext only | Only ciphertext | Intercepted encrypted radio traffic |
| Known plaintext | Ciphertext plus the matching plaintext | WWII codebreakers knew messages began with a weather report |
| Chosen plaintext | Temporary access to the encryption machine; can encrypt plaintexts of her choosing (but cannot open the machine to read the key) | Getting a server to encrypt data you submitted |
| Chosen ciphertext | Temporary access to the decryption machine; can decrypt strings of her choosing | A server that reveals whether a submitted ciphertext decrypts correctly |
In a chosen-plaintext attack, Eve does not learn the key by reading it from the machine. She only observes input/output pairs and deduces the key. A good modern cipher should resist all four models.
A complete cryptosystem specifies seven things: (1) the set of possible plaintexts, (2) the set of possible ciphertexts, (3) the set of encryption keys, (4) the set of decryption keys, (5) the correspondence between encryption and decryption keys, (6) the encryption algorithm, and (7) the decryption algorithm. Memory trick: two message sets, two key sets, one link, two algorithms (2 + 2 + 1 + 2 = 7).
The simplest historical cipher. It is on the "worth notice" list: know how to apply it and how to break it.
Variables: P = plaintext letter number, C = ciphertext letter number, k = shift (the key), mod 26 wraps past Z back to A.
Why it works: adding k shifts; subtracting k shifts back. "mod 26" keeps the result a valid letter.
Example (k = 3): X = 23 → (23+3) mod 26 = 0 = A. So "XYZ" → "ABC".
Common mistake: forgetting to wrap (writing "[" after Z), or subtracting on encryption. For negative results, add 26: (1 − 3) mod 26 = −2 + 26 = 24 = Y.
Why it's breakable: there are only 26 keys (really 25 useful ones), and the cipher keeps English letter frequencies intact, just shifted.
Common exam questions: encrypt/decrypt a word with a given k; find k from a ciphertext; explain why the key space is too small.
Sdaj ukq dwra bejeodaz zaynulpekj kb pdeo patp, lhawoa mqeaphu ck pk pda ejopnqypkn wjz hawra.
Step 1. The three-letter word pda appears; guess it is "the."
Step 2. p → t: p = 15, t = 19. The encryption shift was 15 − 19 = −4, i.e., k = 22 (equivalently, shift back 4). To decrypt, add 4.
Step 3. Check: d+4 = h, a+4 = e. "the" confirmed.
When you have finished decryption of this text, please quietly go to the instructor and leave. (This was an in-class exercise; the payoff was the instruction itself.)
This comparison is explicitly on the "worth notice" list. Symmetric crypto is fast but has a key distribution problem. Public-key crypto solves distribution but is slow. Real systems use both.
Stream ciphers feed data in small pieces (bits or characters) and output corresponding small pieces. Block ciphers collect a block of bits and process it all at once, outputting a block. The course focuses on block ciphers: DES and Rijndael, which NIST selected in 2000 (standardized 2001) as AES, the replacement for DES. L03a goes deep on both.
Variables: xi = plaintext bit, yi = ciphertext bit, si = key stream bit, all in {0,1}. ⊕ is XOR, which is the same as addition mod 2 (the slide writes "xi + si mod 2").
Why it works: XOR is its own inverse: (x ⊕ s) ⊕ s = x ⊕ (s ⊕ s) = x ⊕ 0 = x. That is why encryption and decryption are the same function.
| a | b | a ⊕ b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Worked example: plaintext 1011 0010, key stream 0110 1100 → ciphertext 1101 1110. XOR the ciphertext with the same key stream: 1011 0010. Back to the plaintext.
Common mistakes: using AND/OR instead of XOR; reusing the key stream (see the repetition attack in L03a).
Variables: n = number of people who all want to talk privately with each other.
Why it works: symmetric crypto needs a separate secret for every pair. Each of n people pairs with n − 1 others, giving n(n−1), but that counts each pair twice (Alice–Bob and Bob–Alice), so divide by 2. With public keys, each person needs only their own pair; anyone can use your public key.
Worked examples: n = 5 → 5·4/2 = 10 symmetric keys vs. 5 pairs. n = 100 → 4,950 vs. 100 pairs (200 keys). n = 1,000 → 499,500 vs. 1,000 pairs.
Common mistake: forgetting to divide by 2, or answering "n keys" for public-key (it is n pairs; L03a counts that as 2n keys).
flowchart LR
subgraph SYM [Symmetric]
A1[Alice] -- "encrypt with K" --> C1((ciphertext)) -- "decrypt with K" --> B1[Bob]
end
subgraph PK [Public-key]
A2[Alice] -- "encrypt with Bob's PUBLIC key" --> C2((ciphertext)) -- "decrypt with Bob's PRIVATE key" --> B2[Bob]
end
E1[Eve] -. eavesdrops .-> C1
E2[Eve] -. eavesdrops .-> C2
In the symmetric diagram both ends hold the same "shared secret key." In the public-key diagram the sender holds the recipient's public key and the recipient holds the matching private key. The key distribution diagrams show a web of lines (one shared secret per pair, n(n−1)/2) versus each person holding one public/private pair.
A non-mathematical version: Bob mails Alice a box and an open padlock. Alice puts her message in, snaps Bob's lock shut, and mails it back. Only Bob has the key. The weakness is authentication: Eve could intercept the first mailing and substitute her own padlock, then intercept the locked box, open it with her key, read the message (and re-lock it with Bob's lock). This is a man-in-the-middle attack, and it is why public keys need to be tied to identities using digital certificates.
Public-key algorithms: RSA (hardness of factoring large integers, the most popular), ElGamal (discrete logarithm problem), NTRU (lattices), McEliece (error-correcting codes).
| Symmetric | Public key | |
|---|---|---|
| Keys | One shared secret key | Public/private key pair |
| Speed | Fast; good for large data | Slow; do not use for large quantities of data |
| Keys for n users | n(n−1)/2 | n pairs |
| Key distribution | Hard: key must be shared secretly in advance | Easy: publish the public key (but must authenticate it) |
| Signatures / nonrepudiation | No (both parties know the key) | Yes |
| Examples | Caesar, DES, 3DES, AES, RC4, ChaCha20 | RSA, ElGamal, NTRU, McEliece |
B) Symmetric.
C. A is false (key count explodes as n grows). B is backwards (symmetric is faster). D describes public-key crypto.
How strong is a cipher? Mostly: how long would it take to find the key? Key length sets the upper bound, but a badly designed algorithm can fall far below it.
Variables: k = key length in bits; rate = keys tested per second.
Why it works: each bit doubles the number of possible keys. On average you find the key halfway through.
Worked examples.
Units: use seconds, then divide by 3.15 × 107 (seconds per year).
Common mistake: thinking a 57-bit key is "slightly" stronger than 56-bit. It is twice as strong. And 128-bit is 272 times stronger than 56-bit, not about twice.
Key length is not everything. The simple substitution cipher (any permutation of the alphabet) has 26! ≈ 4 × 1026 keys, far more than DES's 7.2 × 1016, yet it is trivial to break with frequency analysis. Some algorithms can be attacked by methods other than brute force, and some use their key bits inefficiently: "not all 128-bit algorithms are created equal." Faster-than-brute-force attacks you will meet: frequency analysis (substitution and Vigenère ciphers) and birthday attacks.
Nothing lasts forever. DES survived 20 years of scrutiny before falling to a purpose-built parallel computer (the EFF's "Deep Crack," 1998). Quantum computing could change the landscape again.
Encryption gives confidentiality. These four tools give integrity and authenticity. L03a covers each in depth; here is the conceptual introduction.
Encrypt for secrecy: use the recipient's public key. Sign: use the sender's private key. Signing with a private key does not provide confidentiality, because anyone can "decrypt" with the public key. Memory trick: "Public locks, private signs."
Example: SHA-256("hello") begins 2cf24dba5fb0a30e…, while SHA-256("Hello") begins 185f8db32271fe25…. One capital letter changed everything.
MAC diagram explained. The sender runs message M and the shared key through hash h, producing MAC 4C66809, and sends both. An attacker in the channel modifies the message to M′. The recipient computes h on M′ with the shared key, gets 87F9024, compares with the received 4C66809, finds a mismatch ("=?"), and flags the attack.
flowchart LR
M[message M] --> ENC[Encrypt with Alice's private key] --> C[Ciphertext C] --> CH((channel)) --> C2["C' received"] --> DEC[Decrypt with Alice's public key] --> M2["M'"] --> H2[Hash] --> X2["MAC''"]
M --> H1[Hash] --> X1[MAC] --> CH2((channel)) --> X1b["MAC' received"]
X1b --> CMP{Match?}
X2 --> CMP
CMP -- Yes --> OK[Bob accepts]
CMP -- No --> BAD[Reject]
Top path: Alice encrypts M with her private key (a signature), producing C. It travels to Bob as C′ (possibly altered). Bob decrypts with Alice's public key to get M′, then hashes M′ to get MAC″. Bottom path: Alice also hashes the original M and sends that value (MAC), which arrives as MAC′. Decision: Bob compares MAC′ with MAC″. If they match, the message is authentic and unmodified; if not, reject. The slide also shows a key icon beside the hash steps, meaning the hash may be keyed (a true MAC). What to notice: two independent paths must agree, and only Alice could have produced a C that decrypts under her public key.
Passwords are the most common authentication factor and one of the weakest. The "worth notice" list asks you to create a strong password with reasonable analysis, which means doing the math below.
The slide's diagram: the user enters Dog124; it passes through a hash function; the result is stored in the password file next to the username, e.g., Butch:ASDSA21QW3R50EERWWER323…. At login, the system hashes what you typed and compares hashes. The plaintext password is never stored, so a stolen password file does not directly reveal passwords; attackers must guess and hash candidates (a brute-force or dictionary attack). Modern systems also add a random salt per user so identical passwords get different hashes. On Linux the hashes live in /etc/shadow (see L04a).
Variables: C = size of the character set; L = password length; N = number of possible passwords.
Why it works: each of the L positions independently has C choices, so multiply C by itself L times.
Character sets: digits 10; one case of letters 26; both cases 52; special characters 32; practical keyboard total 52 + 10 + 32 = 94; full 7-bit ASCII 27 = 128.
| 6-character password using… | Combinations |
|---|---|
| Digits only (106) | 1,000,000 |
| One letter case (266) | 308,915,776 |
| 32 special characters (326) | 1,073,741,824 |
| Upper and lower case (526) | 19,770,609,664 |
| All 94 practical symbols (946) | 689,869,781,056 |
| 7-bit ASCII (1286) | 4,398,046,511,104 |
| Length (94 symbols) | 94L | Guesses/sec to exhaust in 60 days |
|---|---|---|
| 5 | 7,339,040,224 | 1,415 |
| 6 | 689,869,781,056 | 133,076 |
| 7 | 64,847,759,419,264 | 12,509,214 |
| 8 | 6,095,689,385,410,816 | 1,175,866,008 |
| 9 | 572,994,802,228,616,704 | 110,531,404,750 |
Variables: T = how long the password stays valid, in seconds. The slide uses a 60-day password change policy: T = 60 × 24 × 3600 = 5,184,000 s.
Interpretation: if an attacker can test more than this many passwords per second, they can try every possible password before you are forced to change it.
Worked example (6 chars): 689,869,781,056 ÷ 5,184,000 ≈ 133,076 guesses/s.
The slide gives two attackers: a computer with an RTX 4090 GPU at 5,000,000 guesses/s, and the Frontier supercomputer at 440,000,000,000,000 (4.4 × 1014) guesses/s.
1. 6 characters vs. the GPU. Needed rate 133,076/s. The GPU does 5,000,000/s, far more. Broken well within 60 days (694 billion ÷ 5 million ≈ 138,000 s ≈ 1.6 days).
2. 7 characters vs. the GPU. Needed 12,509,214/s, more than 5,000,000. The GPU needs 64.8 × 1012 ÷ 5 × 106 ≈ 1.3 × 107 s ≈ 150 days to exhaust, longer than 60. Safe against this attacker under the change policy (though on average the key is found halfway, around 75 days, still over 60).
3. 8 characters vs. the GPU. 6.1 × 1015 ÷ 5 × 106 ≈ 1.22 × 109 s ≈ 38.6 years. Safe.
4. 8 characters vs. Frontier. 6.1 × 1015 ÷ 4.4 × 1014 ≈ 14 seconds. Even 9 characters falls in about 22 minutes. The lesson: length and character variety matter, and so does slow, salted hashing that limits guesses per second.
5. Which slide password is strongest? Seattle1 (dictionary word + digit), M1ke03 (short, name + leetspeak), P@$$w0rd (famous leetspeak of "password", in every cracking list), TD2k5secV (9 characters, mixed case, digits, no dictionary word). TD2k5secV is strongest. The math above assumes random passwords; real attackers try dictionary words and common substitutions first, so the first three fall far faster than their raw CL.
Rules for strong passwords: use characters from at least three of these groups: uppercase, lowercase, digits, special characters. Prefer pass phrases like I re@lly want to buy 11 Dogs!: long, easy to remember, and they include all four groups. Length grows the exponent, which beats any single substitution trick.
Attacks on the human attack surface rather than the technology:
| Technique | How it works | Example |
|---|---|---|
| Pretexting | Invent a story that convinces an administrator or operator to reveal secrets | "Hi, I'm from IT, I need your password to fix your account" |
| Baiting | Offer a "gift" to get the victim to do something insecure | A USB drive labeled "Salaries 2026" left in the parking lot |
| Quid pro quo | Offer a service and expect something in return | "Free tech support" in exchange for remote access |
Memory trick: pretexting = a pretext (a false story); baiting = a lure; quid pro quo = Latin for "something for something."
The outline lists "access control models," and the worth-notice list asks: how do you draw an access control matrix when roles and access rights are given? The deck does not include a slide for it, so here is the full method. L04a builds on it with ACLs and RBAC.
Given: Instructors can read and write the gradebook and the exams. TAs can read and write the gradebook and read the exams. Students can read the syllabus. Everyone can read the syllabus; only instructors can write it.
| Subject \ Object | gradebook | exams | syllabus |
|---|---|---|---|
| Instructor | r, w | r, w | r, w |
| TA | r, w | r | r |
| Student | — | — | r |
ACL for exams (a column): Instructor: rw; TA: r. Capability list for TA (a row): gradebook: rw; exams: r; syllabus: r.
Harder follow-up: in a real system, the matrix is mostly empty (thousands of users × millions of files), so systems store only the non-empty entries, either by column (ACLs) or by row (capabilities). That trade-off is the subject of L04a's "Capabilities vs. ACLs."
| Term | Definition | Example | Memory trick | Similar | Opposite |
|---|---|---|---|---|---|
| Confidentiality | No unauthorized disclosure | Encrypted medical records | Confidential = keep quiet | Secrecy, privacy | Disclosure |
| Integrity | No unauthorized modification | Checksum on a download | Integer = whole, untouched | Correctness | Tampering, corruption |
| Availability | Timely access for authorized users | Redundant servers | Available = there when needed | Uptime | Denial of service |
| Authentication | Determining identity/role | Password + fingerprint | AuthN: who are you? | Identification | Impersonation |
| Authorization | Deciding what an identity may access | Only admins edit settings | AuthZ: what can you do? | Access control | Privilege escalation |
| Faraday cage | Enclosure blocking electromagnetic signals | Copper-mesh server room | Cage for radio waves | Shielding, TEMPEST | — |
| Checksum | Value computed from all of a file's contents | CRC on a packet | Check the sum | Hash, digest | — |
| Assurance | How trust is provided and managed (policies, permissions, protections) | Music store DRM | P-P-P | Trust management | — |
| Authenticity | Statements are genuine | Signed software update | Authentic = real | Origin integrity | Forgery |
| Nonrepudiation | Signer cannot deny a statement | Signed contract | Can't "repudiate" (reject) it | Accountability | Deniability |
| Anonymity | Records not attributable to anyone | Tor browsing | A-nonym = no name | Pseudonymity | Attribution |
| Threat | Potential for harm | Ransomware gangs targeting hospitals | What could happen | Danger | Safeguard |
| Vulnerability | Exploitable weakness | Unpatched server | Where you're vulnerable | Flaw, bug | Control |
| Attack | A threat carried out | Running an exploit | The threat in action | Intrusion | Defense |
| Countermeasure | Means to prevent/detect/recover | Firewall | Counters the attack | Control, safeguard | Attack |
| Risk | Impact × likelihood | High-value, likely breach | How much to worry | Exposure | Assurance |
| Passive attack | Observes without changing | Traffic analysis | Passive = just watching | Eavesdropping | Active attack |
| Active attack | Alters data or operation | Replay, masquerade | Active = doing something | Tampering | Passive attack |
| Replay | Resending captured valid messages | Re-sent car unlock signal | Press "replay" | — | Fresh message |
| Masquerade | Pretending to be another entity | Spoofed email sender | Wearing a mask | Spoofing, impersonation | Authentication |
| Traffic analysis | Learning from message patterns | Who emails whom, when | Watching the traffic, not the cars' cargo | Metadata analysis | — |
| Attack surface | Reachable, exploitable vulnerabilities | Open ports | Surface an attacker can touch | Exposure | Hardened core |
| Defense in depth | Multiple independent layers of protection | Firewall + OS isolation + sandbox | Castle: moat, wall, keep | Layered security | Single point of failure |
| Attack tree | Goal broken into sub-goals and concrete attacks | Figure 1.5 banking tree | Root = goal, leaves = attacks | Threat model | — |
| Plaintext / ciphertext | Readable / encrypted message | HELLO / KHOOR | Plain = readable | Cleartext / cryptogram | Each other |
| Key | Secret controlling encryption | AES-128 key | Key opens the lock | Secret | — |
| Kerckhoffs's principle | Security should rely only on the key | AES is public | No security by obscurity | Shannon's maxim | Security through obscurity |
| Ciphertext-only attack | Eve has only ciphertext | Intercepted traffic | Weakest attacker | — | Chosen-ciphertext |
| Known-plaintext attack | Eve has plaintext/ciphertext pairs | Known message headers | She knows some answers | Crib | — |
| Chosen-plaintext attack | Eve encrypts inputs of her choice | Using an encryption oracle | She picks the questions | — | — |
| Chosen-ciphertext attack | Eve decrypts inputs of her choice | Padding oracle | Strongest attacker | — | Ciphertext-only |
| Symmetric cryptosystem | Same key encrypts and decrypts | AES | Symmetric = same both sides | Secret-key crypto | Public-key crypto |
| Public-key cryptosystem | Public key encrypts, private key decrypts | RSA | Public lock, private key | Asymmetric crypto | Symmetric crypto |
| Stream cipher | Encrypts bit/byte at a time with a key stream | ChaCha20 | Water flowing | — | Block cipher |
| Block cipher | Encrypts fixed-size blocks | AES (128-bit blocks) | Building blocks | — | Stream cipher |
| Caesar cipher | Shift each letter by k | k=3: A→D | Caesar used 3 | Shift cipher, ROT13 | — |
| Frequency analysis | Using letter frequencies to break substitution | Most common letter → e | ETAOIN | Cryptanalysis | Brute force |
| Brute-force attack | Trying every key | 256 DES keys | Brute = no brains, all muscle | Exhaustive search | Cryptanalytic attack |
| One-time pad | Random key as long as message, used once | Cold War hotline | One time only | Vernam cipher | Reused key stream |
| Man-in-the-middle | Attacker secretly relays/alters communication | Padlock swap | Sits in the middle | Interception | End-to-end authentication |
| Digital signature | Private-key transform anyone can verify | Signed PDF | Private signs | — | Forgery |
| Hash function | One-way, collision-resistant fingerprint | SHA-256 | Hash browns can't become potatoes | Digest | Encryption (reversible) |
| MAC | Keyed hash for integrity | HMAC-SHA256 | Message Authentication Code | Keyed hash | Unkeyed checksum |
| Certificate authority | Trusted signer of identity-key bindings | Let's Encrypt | The notary | Trusted third party | Self-signed |
| Pretexting | Invented story to extract secrets | Fake IT call | Pretext = false story | Vishing | — |
| Baiting | A "gift" that triggers insecure action | Infected USB drive | Bait on a hook | Lure | — |
| Quid pro quo | Service offered for something in return | Fake tech support | Something for something | — | — |
| Access control matrix | Subjects × objects table of rights | Gradebook example | Rows who, columns what | ACL (column), capability list (row) | — |
Security protects confidentiality, integrity, and availability, plus assurance, authenticity, and anonymity. Threat agents create threats that raise risk (impact × likelihood) to assets; owners impose countermeasures. Attacks are passive (release of contents, traffic analysis: prevent them) or active (replay, masquerade, modification, DoS: detect and recover). Cryptography rests on the key, not a secret algorithm: the Caesar cipher falls to brute force or frequency analysis, symmetric crypto needs n(n−1)/2 keys while public-key needs n pairs, each key bit doubles brute-force work, and passwords are judged by CL against the attacker's guessing rate. Hashes, MACs, signatures, and certificates supply integrity and authenticity; an access control matrix records who may do what.
Cryptographic tools: ciphers, RSA, and hashing
Source: L03a-Cryptographic.pdf (Goodrich & Tamassia Ch. 8, Stallings & Brown Ch. 2)
This is the most math-heavy lecture so far. It deepens every idea from L01a: symmetric ciphers get real algorithms (DES, AES) and modes of operation, public-key crypto gets its math (RSA), and hashing gets its security properties and applications. If you only have time for three things, master RSA key setup with the extended Euclidean algorithm, CBC mode, and the three hash properties with the birthday bound.
Alice wants to send plaintext P to Bob over an eavesdropped channel. If they have already agreed on a scheme and a secret key K, she sends ciphertext C instead. The questions this section answers: what makes a scheme good, how expensive is it, and how big is the ciphertext compared to the plaintext?
Notation used throughout: K = secret key, EK(P) = encryption, DK(C) = decryption. Plaintext and ciphertext are typically the same length. Both functions must have efficient algorithms.
| Brute-force attack | Cryptanalytic attack | |
|---|---|---|
| Idea | Try every key on some ciphertext until the output is intelligible | Exploit the algorithm's characteristics to deduce the plaintext or key |
| Relies on | Enough computing power; ability to recognize correct plaintext | The nature of the algorithm, general knowledge about the plaintext, sample plaintext–ciphertext pairs |
| Cost | On average, half of all keys must be tried | Can be far less than brute force |
| If successful | That key is found | All past and future messages under that key are compromised |
Diagram explained: the four attack models (a–d). Each panel shows an encryption algorithm box with a key, and Eve. In (a) ciphertext only, Eve sees just the ciphertext. In (b) known plaintext, she sees both the plaintext ("Hi, Bob. Don't invite Eve to the party! Love, Alice") and its ciphertext. In (c) chosen plaintext, Eve supplies the plaintext herself (the alphabet "ABCDEFG…") and sees what comes out. In (d) chosen ciphertext, Eve supplies ciphertext (bit strings like 001101) and sees the resulting plaintext (gibberish like "IJCGA, CAN DO HIFFA GOT TIME."). The progression shows Eve gaining more control at each step.
Brute force needs recognizable plaintext. Trying key K means computing DK(C) and asking "is this a likely plaintext?" That requires knowing something about the plaintext's structure (a PDF header, an email format). The defense is a key that is a sufficiently long random value, making exhaustive search infeasible.
Why can a brute-force attacker tell the right key from the wrong ones? Because real language is highly redundant. Out of all possible bit strings, very few are English. This section quantifies how much ciphertext an attacker needs before only one key produces sensible output.
ASCII encodes each English character as 8 bits (one byte), so a t-character message is an n-bit array with n = 8t. Entropy (Shannon) measures information content: how many bits of genuinely unpredictable information each symbol carries. If every one of 256 byte values were equally likely, each byte would carry 8 bits. English carries only about 1.25 bits per character, because letters are predictable ("th", "ing", "q" is followed by "u"). The rest is redundancy.
The slide shows two large ovals labeled "Plaintexts: n-bit strings" and "Ciphertexts: n-bit strings." Inside the plaintext oval is a small region labeled "English text." Arrows show encryption scattering that small region across the whole ciphertext oval. The point: English plaintexts are a very small subset of all n-bit arrays, so a random wrong key almost always decrypts to something outside that subset, which is garbage an attacker can reject.
Variables: t = characters, n = 8t bits, β = the language's constant (β < 1; for English β ≈ 1.25/8 ≈ 0.16).
Why it works: 1.25 bits of entropy per character means roughly 21.25 ≈ 2.4 "effective choices" per character, so t characters give 21.25t meaningful strings. Substitute t = n/8.
Fraction of n-bit strings that are valid: 2βn / 2n = 1 / 2(1−β)n. For a 64-bit block that is 1/254, a vanishingly small probability.
Variables: k = key length in bits; β = language constant; n = ciphertext length in bits.
Derivation. A brute-force attacker tries all 2k keys, producing 2k candidate plaintexts. Each is valid with probability 1/2(1−β)n, so the expected number of valid-looking plaintexts is 2k / 2(1−β)n. When this equals 1, only the true plaintext looks valid (no "spurious keys"). Set 2k = 2(1−β)n, so k = (1−β)n and n = k/(1−β).
Interpretation: the unicity distance is the minimum ciphertext length at which a brute-force attacker can, in principle, identify the unique correct key. It says nothing about how long the search takes.
Worked examples (1 − β ≈ 0.844):
Common mistakes: dividing by β instead of (1 − β); forgetting the answer is in bits (divide by 8 for characters); concluding a short message is "safe" (it only means brute force cannot distinguish keys, and real attackers usually have more ciphertext).
Letters, pairs ("th"), and triples ("the") are not uniformly distributed, and that knowledge breaks substitution ciphers (as with the Caesar practice in L01a). Substitution also works on binary numbers: a substitution box (S-box) is a lookup table mapping each small input bit pattern to an output pattern (for example, a 4-bit S-box maps each of 16 inputs to a 4-bit output). S-boxes are the nonlinear core of DES and AES.
Block ciphers are the workhorses of symmetric encryption. You need to know their block sizes, key sizes, why DES was replaced, and how AES is structured.
Block ciphers need the plaintext length to be a multiple of b. Padding must be unambiguous: you cannot just add zeros, because the receiver could not tell padding zeros from real trailing zeros. PKCS5 (PKCS7 for general block sizes) appends a sequence of identical bytes, each equal to the number of padding bytes. You always pad, even when the plaintext already fills the last block; in that case you add an entire block of padding.
1. Slide example. "Roberto" is 7 bytes. Need 16 − 7 = 9 bytes of padding, each with value 9: Roberto + 09 09 09 09 09 09 09 09 09. (The slide writes "Roberto999999999" where 9 means the byte value 9, not the character "9".)
2. A 14-byte message gets 02 02.
3. A 15-byte message gets 01.
4. Tricky. A 16-byte message gets a whole new block of sixteen 10 bytes (0x10 = 16). Total 32 bytes. Why? If you added nothing and the message happened to end in byte 01, the receiver would wrongly strip it.
5. Unpadding. Read the last byte, say it is 5; remove the last 5 bytes (and check they all equal 5).
6. Block count. A 40-byte message with 16-byte blocks: 40 = 2 × 16 + 8, pad 8 bytes → 48 bytes → 3 blocks.
| DES | Triple DES | AES | |
|---|---|---|---|
| Plaintext block (bits) | 64 | 64 | 128 |
| Ciphertext block (bits) | 64 | 64 | 128 |
| Key size (bits) | 56 | 112 or 168 | 128, 192, or 256 |
For many years the most widely used scheme (FIPS PUB 46; the algorithm is called DEA). It uses a 64-bit plaintext block and a 56-bit key to produce a 64-bit ciphertext block. Two strength concerns: (1) the algorithm itself, though DES is the most studied cipher in existence and no fatal structural flaw was found; and (2) the 56-bit key, which is woefully inadequate for modern processor speeds. Concern (2) is what killed it.
| Key size | Cipher | Keys | At 109 decryptions/s | At 1013 decryptions/s |
|---|---|---|---|---|
| 56 | DES | 256 ≈ 7.2 × 1016 | 255 ns ≈ 1.125 years | about 1 hour |
| 128 | AES | 2128 ≈ 3.4 × 1038 | 5.3 × 1021 years | 5.3 × 1017 years |
| 168 | Triple DES | 2168 ≈ 3.7 × 1050 | 5.8 × 1033 years | 5.8 × 1029 years |
| 192 | AES | 2192 ≈ 6.3 × 1057 | 9.8 × 1040 years | 9.8 × 1036 years |
| 256 | AES | 2256 ≈ 1.2 × 1077 | 1.8 × 1060 years | 1.8 × 1056 years |
Why 255 for a 56-bit key: on average you search half the key space, and half of 256 is 255. At 109 per second, each try takes 1 nanosecond, so the time is 255 ns ≈ 3.6 × 1016 ns = 3.6 × 107 s ≈ 1.1 years.
At 1013/s: 3.6 × 1016 / 1013 = 3,600 s ≈ one hour. That is why DES is dead.
Check AES-128: 2127 ≈ 1.7 × 1038 ns = 1.7 × 1029 s ÷ 3.15 × 107 ≈ 5.4 × 1021 years. The universe is about 1.4 × 1010 years old.
Common mistake: mixing units. The slide's footnotes mention microseconds, but the numbers only work out with nanoseconds (109 per second).
Runs DES three times with two or three distinct keys (first standardized for finance in ANSI X9.17, 1985). The usual pattern is Encrypt–Decrypt–Encrypt: C = EK3(DK2(EK1(P))). Attractions: the 168-bit key defeats brute force, and the underlying algorithm is the well-studied DES. Drawbacks: sluggish in software (three times the work of an already slow cipher) and still a 64-bit block, which is small by modern standards.
3DES was not reasonable long-term, so in 1997 NIST publicly called for proposals. Requirements: security equal to or better than 3DES, much better efficiency, a symmetric block cipher with 128-bit blocks and 128/192/256-bit keys. After narrowing to five finalists, NIST selected Rijndael (by Joan Daemen and Vincent Rijmen) in November 2001, published as FIPS 197. The versions are AES-128, AES-192, and AES-256.
flowchart TD P[Plaintext P] --> X((XOR)) K[Key K] --> X X --> X0[X0 = P XOR K] X0 --> R1[Round 1] --> R2[Round 2] --> D[... Rounds 3 to 9 ...] --> R10[Round 10] --> C[Ciphertext C = X10]
The 128-bit working value is called the state. First, the plaintext is XORed with the key: X0 = P ⊕ K. Then rounds 1 through 10 each take Xi−1 and produce Xi through an invertible transformation. The ciphertext is the output of round 10: C = X10. The slide's figure also shows K feeding into every round, because each round mixes in a round key derived from K. (AES-192 uses 12 rounds and AES-256 uses 14.)
The 128-bit state is arranged as a 4 × 4 grid of bytes.
Why it exists: after a few rounds, every output bit depends on every input bit and key bit in a complicated way. Exam note: the final round skips MixColumns. Common misconception: "AES encrypts the plaintext ten times with the same key." It uses a different round key each round.
Memory trick: "Some Shy Monkeys Add" → Sub, Shift, Mix, Add.
Real data is longer than one 64- or 128-bit block. A mode of operation describes how a block cipher encrypts and decrypts a sequence of blocks. The simplest mode leaks patterns; the more common one hides them.
ECB diagram: four independent columns. Each P[i] enters its own EK box and exits as C[i]. No arrows connect the columns.
The ECB penguin: image (a) is Tux, the Linux penguin. Image (b) is the same bitmap encrypted with ECB. You can still clearly see the penguin's outline, because every block of solid black pixels encrypted to the same ciphertext block, and every block of solid white to another. This is the single most famous illustration of why ECB is unsuitable for documents and images.
flowchart LR V[IV V] --> X0((XOR)) P0[P0] --> X0 --> E0[E_K] --> C0[C0] C0 --> X1((XOR)) P1[P1] --> X1 --> E1[E_K] --> C1[C1] C1 --> X2((XOR)) P2[P2] --> X2 --> E2[E_K] --> C2[C2]
CBC encryption diagram: the IV V is XORed with P[0], the result is encrypted to get C[0], and C[0] travels diagonally to be XORed with P[1], and so on. CBC decryption diagram (the slide's right half): each C[i] goes into DK, and the output is XORed with the previous ciphertext block (or V for the first) to produce P[i]. Notice that decryption of block i only needs C[i] and C[i−1], so decryption can be done in parallel, but encryption cannot.
To see the chaining, use a toy (insecure) cipher EK(x) = x ⊕ K on 4-bit blocks, with K = 0110, IV V = 0011, and two identical plaintext blocks P[0] = P[1] = 1010.
ECB: C[0] = 1010 ⊕ 0110 = 1100; C[1] = 1100. Identical blocks leak.
CBC: C[0] = E(P[0] ⊕ V) = E(1010 ⊕ 0011) = E(1001) = 1001 ⊕ 0110 = 1111. C[1] = E(P[1] ⊕ C[0]) = E(1010 ⊕ 1111) = E(0101) = 0101 ⊕ 0110 = 0011. Different, even though the plaintext blocks were identical.
CBC decryption check: P[1] = C[0] ⊕ D(C[1]) = 1111 ⊕ (0011 ⊕ 0110) = 1111 ⊕ 0101 = 1010 ✓. P[0] = V ⊕ D(C[0]) = 0011 ⊕ (1111 ⊕ 0110) = 0011 ⊕ 1001 = 1010 ✓.
In CBC, one bit of C[1] is flipped in transit. Which plaintext blocks are affected on decryption? P[1] = C[0] ⊕ D(C[1]) is completely garbled (D scrambles it). P[2] = C[1] ⊕ D(C[2]) has exactly the same single bit flipped (because C[1] is XORed in directly). P[3] onward is fine. In ECB, only P[1] would be garbled. This is why the slides say ECB "can tolerate the loss or damage of a block" and CBC needs reliable delivery of all blocks in sequence.
| ECB | CBC | |
|---|---|---|
| Strengths | Very simple; blocks can be encrypted in parallel; tolerates loss or damage of a block | Hides plaintext patterns; most common mode; fast and relatively simple |
| Weaknesses | Repeated plaintext patterns appear in ciphertext; unsuitable for documents and images | Requires reliable, sequential transmission of all blocks; unsuitable when packets may be lost (music, video streaming) |
| Needs IV? | No | Yes (random, unpredictable) |
"The IV must be secret." In most real protocols the IV is sent in the clear; it must be random and unpredictable, not secret. (The slide says it is transmitted encrypted, which is also acceptable.) Also: "ECB is fine if the key is strong." No: ECB leaks structure regardless of key strength.
Both examples generate a random AES key, create an AES cipher in ECB mode with PKCS5 padding, encrypt "My secret message" (17 bytes, so it pads to 32 bytes / 2 blocks), then decrypt.
// Java (javax.crypto)
KeyGenerator keygen = KeyGenerator.getInstance("AES");
SecretKey aesKey = keygen.generateKey(); // random AES key
Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // algorithm/mode/padding
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey);
byte[] ciphertext = aesCipher.doFinal("My secret message".getBytes());
aesCipher.init(Cipher.DECRYPT_MODE, aesKey);
byte[] plaintext1 = aesCipher.doFinal(ciphertext);
# Python (pycryptodome)
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
key = get_random_bytes(16) # 16 bytes = AES-128
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(b"My secret message", AES.block_size)) # block_size = 16
decipher = AES.new(key, AES.MODE_ECB)
plaintext = unpad(decipher.decrypt(ciphertext), AES.block_size)
Note the difference: Java's PKCS5Padding pads automatically inside doFinal, while Python requires explicit pad/unpad calls. Also note these examples use ECB only because it is simplest; in real code you would use CBC (with an IV) or an authenticated mode like GCM.
When data arrives continuously and at unpredictable length (voice, video), stream ciphers encrypt it on the fly without padding.
| Synchronous stream cipher | Self-synchronizing stream cipher | |
|---|---|---|
| Key stream from | Only the secret key K | The secret key and the q previous ciphertext symbols |
| Lost data | Works on unreliable channels if packets carry sequence numbers (so the receiver knows where in the key stream to be) | A loss causes a delay of q steps, then decryption resumes automatically |
Why it works: the block cipher acts as a pseudo-random generator; XOR is self-inverse, so decryption is the same operation. Only EK is ever used, never DK.
Common mistake: reusing the same (K, t) for two messages, which creates the repetition attack below.
If the same key stream S encrypts two messages: C1 = P1 ⊕ S and C2 = P2 ⊕ S. The attacker XORs the ciphertexts: C1 ⊕ C2 = P1 ⊕ S ⊕ P2 ⊕ S = P1 ⊕ P2. The key cancels out. With the redundancy of English, two XORed plaintexts can often be separated, and if either plaintext is known, the other falls immediately: P2 = (C1 ⊕ C2) ⊕ P1.
Numeric check: P1 = 1100, P2 = 1010, S = 0111. C1 = 1011, C2 = 1101. C1 ⊕ C2 = 0110 = P1 ⊕ P2 ✓. This is why the one-time pad must be used once.
Setup: a message is retransmitted with one attacker-chosen byte X inserted, using the same key stream (for example an email re-sent with a new message number).
Diagram explained: the original row shows P[i], P[i+1], P[i+2], P[i+3] lined up with S[i], S[i+1], … and producing C[i], C[i+1], …. The retransmission row shows P[i], X, P[i+1], P[i+2] lined up with the same S[i], S[i+1], S[i+2], S[i+3]. Everything after the insertion is shifted one position relative to the key stream.
The cascade:
One inserted byte unravels everything after it. Defense: never reuse a key stream.
RSA is built entirely from a handful of number facts: primes, GCDs, modular arithmetic, multiplicative inverses, and Euler's theorem. Learn these well and RSA becomes mechanical.
29 mod 13 = 3, because 29 = 3 + 2·13. 13 mod 13 = 0, because 13 = 0 + 1·13. −1 mod 13 = 12, because −1 = 12 + (−1)·13 (slide writes it as 12 = −1 + 1·13).
Trap: many calculators and programming languages give −1 for "−1 % 13". In math (and on the exam) the answer is always in 0…n−1, so add n: −1 + 13 = 12.
Why it works: a mod b = a − qb. Any number dividing both a and b also divides a − qb, and vice versa, so the pairs (a, b) and (b, a mod b) have exactly the same common divisors. The numbers shrink every step until one is 0.
When it's used: checking gcd(e, φ(n)) = 1 in RSA setup, and (extended version) computing d.
Algorithm EuclidGCD(a, b)
if b = 0: return a
else: return EuclidGCD(b, a mod b)
| a | b | a mod b |
|---|---|---|
| 412 | 260 | 152 |
| 260 | 152 | 108 |
| 152 | 108 | 44 |
| 108 | 44 | 20 |
| 44 | 20 | 4 |
| 20 | 4 | 0 |
| 4 | 0 | return 4 |
Also: gcd(21, 12) = gcd(12, 9) = gcd(9, 3) = gcd(3, 0) = 3.
Running time (the "Analysis" slide). Let ai, bi be the arguments of the i-th call. Then ai+2 = bi+1 = ai mod ai+1. Claim: ai+2 ≤ ½ai. Case 1: if ai+1 ≤ ½ai, then ai+2 < ai+1 ≤ ½ai. Case 2: if ai+1 > ½ai, then ai mod ai+1 = ai − ai+1 < ½ai. So the first argument at least halves every two calls, giving at most 1 + 2 log max(a, b) calls: O(log max(a, b)) arithmetic operations (also expressible as O(log min(a, b))). For a 2048-bit number that is only a few thousand steps.
| x (mod 11) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| x−1 | — | 1 | 6 | 4 | 3 | 9 | 2 | 8 | 7 | 5 | 10 |
Check a few: 2·6 = 12 = 11 + 1 ✓; 5·9 = 45 = 44 + 1 ✓; 10·10 = 100 = 99 + 1 ✓.
Theorem: x has an inverse in Zn if and only if gcd(x, n) = 1. In Z10, only 1, 3, 7, 9 have inverses (1·1, 3·7 = 21, 7·3, 9·9 = 81, all ≡ 1 mod 10). Corollary: if p is prime, every nonzero residue mod p has an inverse. More examples: the inverse of 5 mod 14 is 3 (15 = 14 + 1); 2 has no inverse mod 14 (gcd = 2); 4x ≡ 1 (mod 7) means finding x, k with 4x = 7k + 1, so x = 2.
Powers repeat. For prime p, the successive powers of each element of Zp cycle, and the cycle lengths divide p − 1. The slide's table for p = 7:
| x | x2 | x3 | x4 | x5 | x6 |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | 4 | 1 | 2 | 4 | 1 |
| 3 | 2 | 6 | 4 | 5 | 1 |
| 4 | 2 | 1 | 4 | 2 | 1 |
| 5 | 4 | 6 | 2 | 3 | 1 |
| 6 | 1 | 6 | 1 | 6 | 1 |
What to notice: the last column is all 1s (Fermat's little theorem). Cycle lengths are 1, 3, 6, 3, 6, 2, all divisors of 6. Elements with cycle length 6 (here 3 and 5) generate every nonzero residue; they are called generators.
Example (p = 5): 14 = 1; 24 = 16 = 15 + 1; 34 = 81 = 80 + 1; 44 = 256 = 255 + 1. All ≡ 1.
Corollary: the inverse of x mod p is xp−2 mod p. Proof: x · xp−2 = xp−1 ≡ 1. Example: inverse of 3 mod 7 is 35 mod 7 = 243 mod 7 = 5, and 3·5 = 15 ≡ 1 ✓ (matches the table above: 35 = 5).
Uses: primality testing, computing inverses mod a prime.
Variables: Zn* = the elements of Zn relatively prime to n (the multiplicative group). φ(n) = the totient = |Zn*|, the count of those elements.
Key values: Z10* = {1, 3, 7, 9}, so φ(10) = 4. For prime p, φ(p) = p − 1 (so Euler's theorem generalizes Fermat's). For n = pq with distinct primes, φ(n) = (p − 1)(q − 1), the formula RSA uses.
Example (n = 10): 34 = 81 ≡ 1; 74 = 2401 ≡ 1; 94 = 6561 ≡ 1 (mod 10).
Why φ(pq) = (p−1)(q−1): of the pq numbers 0…pq−1, the ones sharing a factor with pq are the q multiples of p and the p multiples of q, with 0 counted twice: pq − p − q + 1 = (p−1)(q−1).
Common mistake: using φ(n) = n − 1 for composite n. That only holds for primes.
RSA (Rivest, Shamir, Adleman, MIT, 1977) is the best-known and most widely used public-key scheme. Its idea: multiplying primes is easy, but factoring their product seems infeasible. Modular exponentiation takes O((log n)3) bit operations (easy), while the best known factoring algorithms grow far faster than any polynomial in the number of digits (hard). Keys are large, e.g., 1024–2048 bits.
Variables: p, q = secret primes; n = modulus (public); φ(n) = totient (secret); e = public (encryption) exponent; d = private (decryption) exponent; M = plaintext as a number with 0 ≤ M < n; C = ciphertext.
Why it works: see the correctness proof below. In one line: Med = Mkφ(n)+1 = (Mφ(n))k·M ≡ 1k·M = M.
When it's used: encrypting small secrets (like a session key) and digital signatures. Not bulk data.
Common mistakes: computing d mod n instead of mod φ(n); choosing e that shares a factor with φ(n); encrypting M ≥ n (the message must be smaller than the modulus, so split it into blocks if needed).
Common exam questions: given p, q, e, find n, φ(n), d; encrypt a small M; decrypt a small C; explain why e must be coprime to φ(n); explain what an attacker needs to break it.
Common misconceptions: "e must be prime." It only has to be coprime to φ(n) (the slide says pick a prime because that makes coprimality easy to check). "n must be kept secret." n is public; p, q, and φ(n) are secret.
The smallest positive integer expressible as ia + jb is exactly gcd(a, b). Slide example: a = 21, b = 15, d = 3, with i = 3, j = −4: 3·21 + (−4)·15 = 63 − 60 = 3.
How it gives inverses: run the extended algorithm on (x, n) to get (d, i, j) with d = ix + jn. Case 1: d = 1. Then ix = 1 − jn ≡ 1 (mod n), so i is the inverse of x (reduce i mod n if negative). Case 2: d ≠ 1. Then x has no inverse.
Find d with 7d ≡ 1 (mod 40).
Forward (Euclid):
40 = 5·7 + 5
7 = 1·5 + 2
5 = 2·2 + 1 ← remainder 1, so gcd = 1 and an inverse exists
Backward: write 1 using the last line, then substitute remainders upward.
1 = 5 − 2·2
= 5 − 2·(7 − 1·5) = 3·5 − 2·7
= 3·(40 − 5·7) − 2·7 = 3·40 − 17·7
So 7·(−17) ≡ 1 (mod 40), and d = −17 mod 40 = 23. Check: 7 × 23 = 161 = 4 × 40 + 1 ✓.
Keep rows of the form (left, right) where left ≡ right × e (mod φ) is always true. Start with (φ, φ) and (e, 1). Repeatedly:
| Left | Right | q used | Check: right × 7 mod 40 |
|---|---|---|---|
| 40 | 40 | — | 0 ≡ 40 ✓ |
| 7 | 1 | — | 7 ✓ |
| 40 − 5·7 = 5 | 40 − 5·1 = 35 | 5 | 245 mod 40 = 5 ✓ |
| 7 − 1·5 = 2 | 1 − 1·35 = −34 → 6 | 1 | 42 mod 40 = 2 ✓ |
| 5 − 2·2 = 1 | 35 − 2·6 = 23 | 2 | 161 mod 40 = 1 ✓ |
d = 23. The slide's pseudocode names the columns α (left) and β (right): r = α ÷ e; d = β − q·r; if r == 1 output d; otherwise shift the rows down and repeat. The "check" column shows why the method works: the invariant holds on every row, so the row with left = 1 says 1 ≡ d × e.
p = 3, q = 11 → n = 33, φ(n) = 2 × 10 = 20. e = 3 (gcd(3, 20) = 1). d = 7, since 3 × 7 = 21 = 20 + 1.
Public key (33, 3); private key 7.
Encrypt M = 2: C = 23 mod 33 = 8.
Decrypt: 87 = 2,097,152; 2,097,152 mod 33 = 2 ✓.
p = 7, q = 17 → n = 119, φ = 6 × 16 = 96. e = 5. d = 77, since 5 × 77 = 385 = 4 × 96 + 1.
Encrypt M = 19: C = 195 mod 119 = 66. Decrypt: 6677 mod 119 = 19 ✓. (You would not do 6677 by hand without repeated squaring; see the appendix.)
p = 5, q = 11 → n = 55, φ = 4 × 10 = 40. e = 3, d = 27 (3 × 27 = 81 = 2 × 40 + 1). Encryption is C = M3 mod 55; decryption is M = C27 mod 55.
The slide tabulates every M from 1 to 54. Sample rows: M = 2 → C = 8; M = 3 → 27; M = 4 → 64 − 55 = 9; M = 19 → 6859 mod 55 = 39; M = 10 → 10; M = 11 → 11; M = 54 → 54. Notice some messages encrypt to themselves (1, 10, 11, 21, 34, 44, 45, 54). These "fixed points" are a known quirk of textbook RSA with tiny numbers.
p = 5, q = 11 → n = 55, φ = 40. Choose e = 7 (gcd(7, 40) = 1). Find d: 7d mod 40 = 1 → d = 23 (both methods above). Public key (55, 7); private key 23.
Encrypt M = 8: C = 87 mod 55. By repeated squaring: 82 = 64 ≡ 9; 84 ≡ 81 ≡ 26; 87 = 84·82·8 ≡ 26·9·8 = 1872 ≡ 1872 − 34·55 = 2.
Decrypt: 223 mod 55. 210 = 1024 ≡ 34; 220 ≡ 342 = 1156 ≡ 1; 223 = 220·23 ≡ 1·8 = 8 ✓.
Encrypt M = 88 (88 < 187): C = 887 mod 187 = 11. Decrypt: 1123 mod 187 = 88 ✓.
Enter small primes to check your hand work. The d computation is shown as the professor's two-column table.
Step 1: generate primes. Get a pseudo-random number and test it. The slide's test uses Fermat: for prime n and any a, an mod n = a. If an mod n ≠ a, n is definitely composite. But if an mod n = a, n could still be composite (a pseudoprime), so the test is probabilistic. (The slide's second bullet, "for composite n and any a, an mod n ≠ a," is not always true; that is exactly the "BUT." Example: 2341 mod 341 = 2 even though 341 = 11 × 31.)
Step 2: choose e. Any prime not dividing φ(n); 3, 17, 65537 are common. 65537 has only two 1-bits (10000000000000001), which makes encryption fast.
Step 3: compute d with the extended Euclidean algorithm (the slide's version carries an auxiliary number ui = ui−2 − ui−1qi−2 mod n, starting from u0 = 0, u1 = 1; this is Method B above in different notation).
The proof extends to the rare case where M shares a factor with n (using the Chinese Remainder Theorem). This proof is also what the "RSA trapdoor 1-to-1 function" slide shows: d is the trapdoor, the secret that makes an otherwise one-way function easy to invert.
The three hard problems (the "C = Me mod (n = pq)" slide): from n, it is hard to find p and q; from (n, e), it is hard to find d; from (n, e) and C, it is hard to find M with C = Me.
RSA's security rests on the widely believed difficulty of factoring; the best known algorithms take super-polynomial time. Practical guidance: keys of 256 bits or fewer can be cracked in hours on a personal computer; 1024-bit keys may be breakable soon; the current recommendation is 2048 bits.
| Key length (bits) | PCs to factor in one year | Memory |
|---|---|---|
| 430 | 1 | 128 MB |
| 760 | 215,000 | 4 GB |
| 1,020 | 342 × 106 | 170 GB |
| 1,620 | 1.6 × 1015 | 120 TB |
What to notice: each few hundred extra bits multiplies the required resources enormously.
Implementing RSA requires: arbitrary-size integers and their arithmetic (overall); random number generation with a given bit length, primality testing, GCD, and multiplicative inverse (setup); modular power (encryption and decryption).
Advantage of public key: private keys never need to be transmitted or revealed, and strangers can communicate spontaneously. Public-key crypto is meant to supplement symmetric crypto, not replace it. In software DES is about 100× faster than RSA (about 1000× in hardware). With n users: n(n−1)/2 secret keys versus 2n keys for public-key.
Why: you get RSA's easy key distribution and the symmetric cipher's speed. Today's TLS follows the same idea (with AES instead of DES).
RSA signatures: the public exponent is usually much smaller than the private one, so verification is faster than signing. An RSA signature (with a secure hash) is superior to a handwritten one: it attests to both the signer's identity and the message contents, cannot be moved to another document, and fails if the message changes in the slightest way. That is what lets important documents exist in digital form only.
Scenario (diagram): a web browser sends CLIENT HELLO; the server replies SERVER HELLO with (e, N); the browser picks a random 64-bit session key K and sends C = RSA(K) = Ke mod N. The server decrypts with d. Eve sees C.
Attack (meet in the middle): about 20% of the time, K factors as K = K1·K2 with both parts below 234. Then C / K1e ≡ K2e (mod N).
Total ≈ 240 ≪ 264. The lesson: "textbook" RSA (raw Me with no padding) is deterministic and multiplicative, which leaks structure. Real systems pad the message with randomness first (e.g., RSA-OAEP).
Hashes give every piece of data a short, fixed-size fingerprint. They underpin integrity checks, password storage, signatures, and blockchains. The exam-critical content: the three security properties, the birthday attack, and which algorithms are broken.
A hash table stores items at locations determined by their hash value. Collisions are handled by chaining (a list at each slot) or open addressing (probe for another slot). The number of slots is proportional to the expected number of items, and a good hash spreads items uniformly so searches take constant expected time. Hash tables only need speed and spread; cryptographic hashes add the security properties below.
| Property | Also called | Attacker is given… | …and must find |
|---|---|---|---|
| Preimage resistance | One-way | A hash value x | Any P with h(P) = x |
| Second preimage resistance | Weak collision resistance | A specific plaintext P | A different Q with h(Q) = h(P) |
| Collision resistance | Strong collision resistance | Nothing | Any pair P ≠ Q with h(P) = h(Q) |
Collision resistance implies second preimage resistance: if you could find a second preimage for a given P, you would have found a collision. The reverse is not true, because in a collision attack the attacker gets to choose both inputs, which is easier. Memory trick: "one given, one given-and-match, none given."
1. A stolen password-hash file: the attacker has hash values and wants passwords. → Preimage resistance.
2. A software download page lists the SHA-256 of the official installer; an attacker wants a malicious installer with the same hash. → Second preimage resistance (the target file is fixed).
3. An attacker prepares two contracts, one fair and one unfair, with the same hash, gets you to sign the fair one, then swaps. → Collision resistance (the attacker controls both). This is exactly the MD5 chosen-prefix attack below.
Hash values should be at least 256 bits to resist brute force, for the reason the birthday attack explains.
In a room of just 23 people, there is a better than 50% chance two share a birthday. It feels wrong because you are not looking for someone matching your birthday; you are looking for any matching pair, and 23 people form 253 pairs.
Variables: m = number of possible hash values (m = 2b for a b-bit hash); k = number of plaintexts tried; Fk = probability of no collision after k tries.
Derivation. The i-th plaintext avoids all i−1 earlier hashes with probability 1 − (i−1)/m. Multiply for all k: Fk = (1 − 1/m)(1 − 2/m)…(1 − (k−1)/m). Using 1 − x ≈ e−x for small x, the product becomes e−(1+2+…+(k−1))/m = e−k(k−1)/2m. Setting Fk = ½ gives k(k−1)/2m = ln 2, so k2 ≈ 2m ln 2 and k ≈ √(2 ln 2)·√m ≈ 1.17√m.
Interpretation: you need only about the square root of the output space to find a collision. √(2b) = 2b/2, so a b-bit hash gives only b/2 bits of collision resistance.
Worked examples:
Common mistakes: saying a 128-bit hash takes 2127 tries to collide (that is the preimage/brute-force figure, not the collision figure); forgetting the square root.
| Algorithm | Origin | Output | Status |
|---|---|---|---|
| MD5 | Ron Rivest, 1991 | 128 bits | Insecure; still found in legacy systems |
| SHA-0 / SHA-1 | NSA, NIST standard, 1993 | 160 bits | Insecure; still in legacy apps; flaws less severe than MD5's |
| SHA-2 (SHA-256, SHA-512) | NSA/NIST, 2002 | 256 or 512 bits | Still considered secure |
| SHA-3 | Public competition announced 2007 (Keccak won in 2012) | 224–512 bits | Secure; different internal design |
MD5's chosen-prefix collision (Marc Stevens, Arjen Lenstra, Benne de Weger): start with any two plaintexts P and Q, and compute suffixes S1 and S2 so that P‖S1 and Q‖S2 have the same MD5 hash, using about 250 hash evaluations. With this, attackers have built pairs of different executables or PDF documents with identical MD5 hashes.
flowchart LR IV[IV] --> F1[compress] P1[P1] --> F1 --> F2[compress] P2[P2] --> F2 --> F3[compress] P3[P3] --> F3 --> F4[compress] P4[P4] --> F4 --> D[digest]
A compression function works on fixed-length input. An iterated hash extends it to any length: pad the message, split it into blocks P1…P4, and feed each block together with the previous output (starting from an initialization vector) into the compression function. The last output is the digest. It inherits the compression function's collision resistance. MD5 and SHA-1/SHA-2 are built this way.
The accompanying chart plots hashing time (0–0.06 ms) against input size (0–1000 bytes) for SHA-1 and MD5. Both lines rise roughly linearly, confirming that time is proportional to input length, with MD5 slightly faster than SHA-1.
A plain hash detects accidental changes, but an attacker can simply recompute it. Adding a secret key makes the hash impossible to forge.
Why an attacker fails: without K they cannot compute the correct MAC for a forged M′.
Variables: h = underlying hash (e.g., SHA-256); K = secret key (padded to the hash's block size); A and B = fixed constants (in the standard, A is "opad" = byte 0x5C repeated and B is "ipad" = byte 0x36 repeated); ‖ = concatenation; ⊕ = XOR.
Why the simple versions fail. Because standard hashes are iterated, these are all insecure:
Why HMAC works: two nested hash passes with differently masked keys block length extension. HMAC is as secure as its underlying hash, and it is an Internet standard (used in IPsec, TLS).
Common mistake: writing HMAC as h(K ⊕ M) or forgetting the nesting.
Figure 2.3, message authentication using a MAC. On the left, the message and key K enter a "MAC algorithm" box; the resulting MAC (checkered block) is appended to the message and transmitted. On the right, the receiver runs the received message through the same MAC algorithm with K and compares the result to the received MAC.
Figure 2.5, message authentication using a one-way hash, shows three variants between Source A and Destination B:
Chaining hashes together lets one expensive signature (or one trusted value) authenticate a long sequence of cheap hashes.
Start from a random seed r and hash backward. The sequence x1, x2, …, xn is pseudo-random. Values are revealed in order x1, x2, …, and anyone can check a newly revealed xi+1 by testing h(xi+1) = xi.
Security rests on preimage resistance: seeing xi does not let anyone compute the next value xi+1.
Applications: one-time passwords (Lamport/S-Key: the server stores the last accepted value; each login reveals the next one) and incremental micropayments (PayWord: each revealed value is worth one unit).
With n = 4: x4 = r, x3 = h(r), x2 = h(h(r)), x1 = h(h(h(r))). The server stores x1. Login 1: user sends x2; server checks h(x2) = x1, then stores x2. Login 2: user sends x3; server checks h(x3) = x2. An eavesdropper who captured x2 cannot produce x3 because that would require inverting h.
Transmit: the signed x1, then packets (p1, x2), (p2, x3), …, (pn, xn+1). Each packet carries the hash of the next packet.
Verify: check h(p1 ‖ x2) = x1 (trusted, since it was signed); now x2 is trusted, so check h(p2 ‖ x3) = x2; and so on.
Properties: integrity of the first hash implies integrity of the rest; any prefix of the stream is signed and cannot be repudiated; constant overhead (one hash per packet); one slow signature plus n fast hashes. Limits: offline (the sender must have the whole stream in advance, since hashes are computed backward) and requires reliable transmission (one lost packet breaks every later check).
flowchart TD g[g = root] --> c[c] g --> d[d] c --> a[a = h x1,x2] c --> b[b = h x3,x4] d --> e[e] d --> f[f] a --> x1[x1] a --> x2[x2] b --> x3[x3] b --> x4[x4] e --> x5[x5] e --> x6[x6] f --> x7[x7] f --> x8[x8]
Each leaf is an item; each internal node is the hash of its two children: a = h(x1, x2), b = h(x3, x4), c = h(a, b), and so on up to the root g. The root hash is a digest of the entire set.
Assumptions: the hash is collision resistant, and the verifier already trusts the root hash.
Slide example. Proof for x4 is [(x3, L), (a, L), (d, R)]. Verifier computes b = h(x3, x4) (x3 goes on the left), then c = h(a, b) (a on the left), then g′ = h(c, d) (d on the right), and checks g′ = g. In one line: g = h(h(a, h(x3, x4)), d).
Why it exists: the proof has logarithmic size (one hash per level; for a million items, about 20 hashes) and logarithmic verification time.
Common exam question: write the proof for a different leaf. Try x6: [(x5, L), (f, R), (c, L)].
Misconception: the L/R tags are not optional. h(a, b) ≠ h(b, a).
To authenticate a stream p1…pn when packets may be lost: build a hash tree over the items (i, pi), transmit the signed root hash, and send each packet as (i, pi, proof(i, pi)). Each packet verifies independently, so lost packets do not block later ones. Cost: logarithmic space overhead and verification time per packet. Like the validation chain, it is an off-line scheme.
| Validation chain | Hash tree scheme | |
|---|---|---|
| Overhead per packet | Constant (one hash) | Logarithmic (a proof) |
| Tolerates loss? | No, needs reliable delivery | Yes |
| Signatures | One | One (on the root) |
| Online or offline | Offline | Offline |
Variables: the exponent p is written in binary as pb−1…p1p0 (b bits). Each apj is either a (bit 1) or 1 (bit 0).
Why it works: reading bits left to right, squaring doubles the exponent built so far and multiplying by a adds 1. That is exactly how you build a binary number digit by digit. Reducing mod n each step keeps numbers small.
Cost: O(log p) multiplications instead of p − 1. For a 2048-bit exponent: about 4,000 operations instead of 22048.
18 in binary is 10010.
| i | bit | multiply by | Qi |
|---|---|---|---|
| 1 | 1 | 3 | 3 |
| 2 | 0 | 1 | 32 = 9 |
| 3 | 0 | 1 | 92 = 81 mod 19 = 5 |
| 4 | 1 | 3 | 52 = 25 mod 19 = 6; 6 × 3 = 18 |
| 5 | 0 | 1 | 182 = 324 = 17·19 + 1 → 1 |
Answer: 1. Sanity check with Fermat: 19 is prime, so 318 mod 19 must be 1 ✓.
Another: 87 mod 55, with 7 = 111: Q1 = 8; Q2 = 64 mod 55 = 9, × 8 = 72 mod 55 = 17; Q3 = 289 mod 55 = 14, × 8 = 112 mod 55 = 2. Matches RSA Example 4.
How many primes are there? About n / ln n primes are ≤ n. So among random b-bit numbers, roughly 1 in (b ln 2) is prime, and you expect to find a prime after O(b) tries. For 512-bit candidates that is about 1 in 355 (1 in 178 if you only try odd numbers).
Base-x pseudoprime: an integer n ≥ 2 with xn−1 mod n = 1 (it passes the Fermat test for base x). Every prime passes. Composite base-2 pseudoprimes are rare: a random 100-bit integer is one with probability below 10−13, and the smallest is 341 (= 11 × 31). The test runs fast with repeated squaring.
Variables: witness(x, n) is a compositeness test. If n is prime it always returns false. If n is composite it returns true except with small probability q. k = desired confidence; t = number of random trials.
Algorithm RandPrimeTest(n, k): repeat t times: pick random x; if witness(x, n) is true, return "composite." After all t trials, return "prime." A "composite" answer is always correct; a "prime" answer is wrong with probability at most 2−k.
Worked example: the Rabin–Miller witness has q ≤ 1/4, so log2(4) = 2. For k = 80, t = 40 trials gives error ≤ 2−80.
Common mistake: thinking a "composite" result could be wrong. Only "prime" can be wrong.
| Term | Definition | Example | Memory trick | Similar | Opposite |
|---|---|---|---|---|---|
| Bijection | One-to-one and onto mapping | EK on n-bit blocks | Every input has a unique partner | Permutation | Many-to-one (hash) |
| Cryptanalytic attack | Exploits algorithm structure | Frequency analysis | Analysis, not muscle | Cryptanalysis | Brute force |
| Entropy | Information content per symbol | English ≈ 1.25 bits/char | How surprising each letter is | Randomness | Redundancy |
| Redundancy | Predictable, non-informative structure | "q" is followed by "u" | Redundant = repeated | Pattern | Entropy |
| Unicity distance | Ciphertext length where only one key gives valid plaintext | 304 bits for AES-256 English | Unique + distance | — | Spurious keys |
| S-box | Lookup table substitution on bits | AES SubBytes | S = substitution | Substitution table | P-box (permutation) |
| Padding (PKCS5) | Bytes each equal to pad length | "Roberto" + 9×0x09 | The pad says its own size | PKCS7 | Zero padding |
| DES | 64-bit block, 56-bit key cipher | Legacy banking | Dead Encryption Standard | DEA | AES |
| 3DES | DES three times, 112/168-bit key | ANSI X9.17 | Triple the work | TDEA | — |
| AES / Rijndael | 128-bit block; 128/192/256-bit keys; FIPS 197 | Wi-Fi WPA2, disk encryption | Advanced = current | — | DES |
| State | AES's 128-bit working value | X0 … X10 | The state of the scramble | — | — |
| Round key | Per-round key derived from K | AddRoundKey | A key for each round | Subkey | — |
| Mode of operation | How blocks are combined | ECB, CBC, CTR | The "mode" for many blocks | — | — |
| ECB | Each block encrypted independently | ECB penguin | Electronic Code Book: a lookup book | — | CBC |
| CBC | Each block XORed with previous ciphertext | C[i] = E(C[i−1] ⊕ P[i]) | Blocks linked in a chain | — | ECB |
| Initialization vector (IV) | Random starting block | C[−1] = V | Initial value for the chain | Nonce | Fixed/zero IV |
| Key stream | Pseudo-random bits XORed with data | S[0], S[1], … | A stream of key | Pad | — |
| Synchronous stream cipher | Key stream from key only | CTR mode | Needs sync (sequence numbers) | — | Self-synchronizing |
| Self-synchronizing stream cipher | Key stream from key + q previous ciphertexts | CFB mode | Heals itself after q steps | — | Synchronous |
| RC4 | Rivest's 1987 stream cipher | Old WEP/TLS | Rivest Cipher 4 | — | — |
| CTR mode | Key stream = EK(t), EK(t+1), … | AES-CTR | Counter | — | — |
| ChaCha20 | Bernstein's stream cipher; 256-bit key, 96-bit nonce | TLS 1.3 on phones | Dance steps: fast | Salsa20 | — |
| Nonce | Number used once | ChaCha20's 96-bit value | N-once | IV | Reused value |
| Repetition attack | Reused key stream → C1⊕C2 = P1⊕P2 | Two-time pad | Key cancels itself | — | — |
| Insertion attack | Inserted known byte reveals key stream cascade | Re-sent email | One insert unravels all | — | — |
| Relatively prime | gcd = 1 | 15 and 28 | No shared factors | Coprime | Common factor |
| Multiplicative inverse | xy ≡ 1 (mod n) | 7 · 23 ≡ 1 (mod 40) | Undo multiplication | Reciprocal | — |
| Euler's totient φ(n) | Count of 1…n coprime to n | φ(10) = 4 | φ = "fee" for membership | — | — |
| Fermat's little theorem | xp−1 ≡ 1 mod prime p | 24 mod 5 = 1 | Little theorem, big use | Euler's theorem | — |
| Extended Euclidean algorithm | Finds i, j with ia + jb = gcd | d = 23 | Euclid plus bookkeeping | Bézout | — |
| RSA | Public-key scheme based on factoring | C = Me mod n | Rivest-Shamir-Adleman | — | Symmetric cipher |
| Trapdoor function | Easy one way, easy back only with a secret | RSA with d | Trapdoor opens only from one side | One-way function | — |
| Digital envelope | Symmetric-encrypted message + RSA-encrypted key | TLS key exchange | Lock the key in an envelope | Hybrid encryption | — |
| Textbook RSA | Raw RSA with no padding | Meet-in-the-middle attack | Only safe in textbooks | — | RSA-OAEP |
| Preimage resistance | Given x, hard to find P with h(P) = x | Password hashes | One-way street | One-wayness | — |
| Second preimage resistance | Given P, hard to find Q ≠ P with same hash | Download checksums | Match this one | Weak collision resistance | — |
| Collision resistance | Hard to find any P ≠ Q with same hash | Signing contracts | Match any two | Strong collision resistance | — |
| Random oracle | Ideal random hash model | Security proofs | An oracle answers randomly but consistently | — | — |
| Birthday attack | Collision search in ~1.17√m tries | 264 for MD5 | 23 people, shared birthday | — | Preimage search |
| MD5 | 128-bit hash, broken | Colliding PDFs | Message Digest 5 | — | SHA-256 |
| SHA-1 / SHA-2 | 160-bit (broken) / 256–512-bit (secure) | SHA-256 in Git, Bitcoin | Secure Hash Algorithm | SHA-3 | — |
| Compression function | Fixed-input-size building block of a hash | MD5's inner function | Squeezes two inputs to one | — | — |
| Iterated hash | Chains a compression function over blocks | Merkle–Damgård | Iterate block by block | — | Sponge (SHA-3) |
| HMAC | h((K⊕A) ‖ h((K⊕B) ‖ M)) | IPsec | Hash-based MAC, hashed twice | Keyed hash | h(K‖M) |
| Length-extension attack | Extending h(K‖M) without K | Forged API requests | Keep hashing from the end | — | — |
| Hash chain | xi = h(xi+1) | S/Key one-time passwords | Links revealed one by one | Lamport chain | — |
| Validation chain | xi = h(pi ‖ xi+1) | Signed video stream | Each packet vouches for the next | — | — |
| Merkle (hash) tree | Binary tree of hashes with a root digest | Git, Bitcoin, Certificate Transparency | Leaves → root | — | — |
| Membership proof | Sibling hashes along the path to root | [(x3,L),(a,L),(d,R)] | Siblings vouch for you | Audit path | — |
| Repeated squaring | Fast modular exponentiation | 318 mod 19 | Square every bit, multiply on 1s | Square-and-multiply | Naive repeated multiplication |
| Pseudoprime | Composite that passes a Fermat test | 341 (base 2) | Pseudo = fake prime | Carmichael number | Prime |
| Rabin–Miller | Randomized primality test, q ≤ 1/4 | 40 rounds for 2−80 | More rounds, more confidence | Miller–Rabin | Deterministic test |
A good cipher is consistent (DK(EK(P)) = P) and resists brute force and cryptanalysis; English redundancy sets the unicity distance n = k/(1−β). DES (64-bit block, 56-bit key) fell to brute force; 3DES patched it; AES (128-bit blocks, 128/192/256-bit keys, SubBytes–ShiftRows–MixColumns–AddRoundKey) replaced it. ECB leaks patterns; CBC chains blocks with an IV; stream ciphers must never reuse a key stream. RSA: n = pq, φ = (p−1)(q−1), ed ≡ 1 mod φ via extended Euclid, C = Me mod n, and it is secure because factoring is hard. Hashes need preimage, second-preimage, and collision resistance, and the birthday bound cuts a b-bit hash to b/2 bits; HMAC fixes h(K‖M); hash chains and Merkle trees authenticate long sequences cheaply.
Operating system security
Source: L04a-os_sec.pdf (UNIX and Windows security models, sandboxing, Chrome architecture)
L01a said the OS file system is an access control tool. This lecture shows how: who the OS thinks you are (users, UIDs, tokens), what it protects (files, processes), how it decides (permission bits, ACLs), and how modern software shrinks the damage when something goes wrong (privilege dropping, capabilities, sandboxes).
The lecture opens with a blunt claim: vulnerabilities are inevitable. So systems must be designed to stay safe even when some component is buggy or some user is malicious.
Any single memory bug might let an attacker run their own code:
A CVE (Common Vulnerabilities and Exposures) ID is the public catalog number for a known vulnerability, formatted CVE-year-number.
sudo that let users run programs with another user's privileges (including root). It had been in the code since 2011 and affected Linux, macOS, and BSD. Ten years unnoticed in one of the most security-critical programs.get_data in zipimport.c allowed a heap buffer overflow by specifying a negative data size, and it could be triggered from inside an ordinary Python script.Takeaway slide: systems must be designed to be resilient in the face of both software vulnerabilities and malicious users.
flowchart TD B[Bug in Chrome's JavaScript interpreter] --> L1[Browser layer: malicious site cannot reach other tabs] L1 --> L2[OS layer: cannot reach other processes such as a password manager] L2 --> L3[Hardware layer: cannot install permanent malware in firmware] L3 --> L4[Network layer: cannot infect nearby computers]
If an attacker exploits the JavaScript engine, Chrome should still stop the malicious site from touching other tabs. If that fails, the OS should stop the renderer from reading other processes' memory. If that fails, hardware should prevent persistent firmware malware. And the network should stop the infection from spreading. Compare to L01a's Figure 1.4: deep layering lowers risk.
Subjects are not just people. Least privilege applies everywhere:
"A Domain should not be able to read another domain's cookies." Subject = domain; operation = read; object = cookies. Try: "An App should only be able to edit its own data." Subject = app; operation = edit (write); object = its data.
UNIX (and Linux, macOS, BSD) uses one simple model everywhere, and nearly everything is represented as a file, so file permissions control almost everything.
| Element | In UNIX |
|---|---|
| Subjects (who?) | Users and processes |
| Objects (what?) | Files and directories. "Files" also include sockets, pipes, hardware devices, kernel objects, and process data |
| Access operations (how?) | Read, write, execute |
A UNIX system has many accounts. Service accounts run background processes (e.g., www-data for a web server). User accounts are typically tied to a human. Every user has a unique integer User ID (UID). UID 0 is root, which has access to everything; many system operations can only run as root.
root:x:0:0:root:/root:/bin/bash
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
systemd-resolve:x:101:103:,,,:/run/systemd/resolve:/usr/sbin/nologin
zakir:x:1001:1001:Zakir Durumeric,,,:/home/zakir:/bin/bash
dabo:x:1009:1009:Dan Boneh,,,:/home/dabo:/usr/sbin/nologin
| Field | Example (zakir) | Meaning |
|---|---|---|
| 1 username | zakir | Login name |
| 2 password | x | Placeholder: the real password hash is in /etc/shadow, readable only by root |
| 3 UID | 1001 | User ID |
| 4 GID | 1001 | Primary group ID |
| 5 GECOS | Zakir Durumeric,,, | Full name and contact info |
| 6 home | /home/zakir | Home directory |
| 7 shell | /bin/bash | Program started at login. /usr/sbin/nologin blocks interactive login |
What to notice: service accounts (www-data, backup) have low UIDs and nologin, so nobody can log in as them. Human accounts start around 1000. Even the human account dabo has nologin, so that person cannot get a shell on this machine. And /etc/passwd is world-readable, which is exactly why the hashes were moved to /etc/shadow (recall L01a: stolen hashes can be brute-forced).
A group is a named collection of users who can share files and resources. Every group has a group ID (GID) and a name. A user can belong to several groups.
Reading an ls -l listing and deciding who can do what is a near-certain exam question. Master this section.
Every file and directory has exactly one user owner and one group owner. Three classes of subject get permissions: the user (owner), the group, and other (everyone else). Each class gets three operations: read, write, execute. The owner can change the file's permissions and group; only root can change the user owner.
zakir@scratch-01:~$ ls -l
total 8
drwxrwx--- 5 zakir csc1302-tas 4096 Apr 2 15:56 homework
drwxrwx--- 5 zakir csc1302-instr 4096 Apr 2 15:56 grades
drwxrwxr-x 11 zakir csc1302-tas 4096 Dec 28 21:09 lectures
-rwxr-xr-- 1 zakir dabo 0 Apr 11 04:15 test.py
d | rwx | rwx | r-x | 11 | zakir | csc1302-tas | 4096 | Dec 28 21:09 | lectures |
|---|---|---|---|---|---|---|---|---|---|
Type: d directory, - regular file | User (owner) permissions | Group permissions | Other permissions | Link count | User owner | Group owner | Size (bytes) | Last modified | Name |
| Letter | On a file | On a directory |
|---|---|---|
| r | Read the contents | List the names inside (ls) |
| w | Modify the contents | Create, delete, or rename entries (together with x) |
| x | Run it as a program | Enter it (cd) and access items inside by name |
| - | Permission not granted | |
x on every directory in its path.Common misconception: "permissions add up." They do not. If a file is ----rwx---, its owner has no access even if the owner is in the group, because step 2 stops at the owner triplet.
homework is drwxrwx--- with group csc1302-tas. Drew is not the owner (zakir) but is in the group, so the group triplet rwx applies: Drew can list the directory (r), create/delete/rename files in it (w), and enter it (x). Whether Drew can read a particular file inside depends on that file's own permissions.
The student falls under "other." homework (---): nothing. grades (---): nothing. lectures (r-x): list and enter, but not create or delete files. test.py (r--): read it, but not modify or run it. (All of this assumes the student can traverse zakir's home directory, i.e., it grants x to others.)
-rwxr-xr--, group dabo: zakir (rwx) and members of group dabo (r-x) can execute it. Others cannot.
Why it works: each triplet is 3 bits (r = 100₂ = 4, w = 010₂ = 2, x = 001₂ = 1), so each triplet is one octal digit (the grouping-by-three rule from the number systems refresher).
Worked examples: rwxr-xr-- = 7 5 4 → chmod 754 test.py. rwxrwx--- = 770. rw-r--r-- = 644 (typical file). rwx------ = 700 (private). A fourth leading digit sets special bits: 4 = setuid, 2 = setgid, so -rwsr-xr-x = 4755.
Common mistake: putting the digits in the wrong order. It is always user, group, other.
Slide example ACL for hw/: Dan read/write; Zakir read/write; Amelie read.
| Role | hw/ | exams/ | grades/ | lectures/ |
|---|---|---|---|---|
| csc1302-instr | r/w | r/w | r/w | r/w |
| csc1302-tas | r/w | read | — | r/w |
| csc1302-students | read | — | — | read |
| cs-students | — | — | — | read |
This is an access control matrix whose rows are roles. UNIX implements it with groups. Reading it: TAs can grade homework and see exams but not final grades; any CS student can read lectures.
Users do not touch files directly; processes do, on their behalf. So the real security question is: which user does each process act as, and can that change?
Rules for changing them: root can set EUID, RUID, and SUID to anything. An unprivileged process can only change its EUID to its current RUID or SUID.
| Call | EUID | RUID | SUID | Use |
|---|---|---|---|---|
setuid(x) (as root) | x | x | x | Permanently drop privilege |
seteuid(x) | x | unchanged | unchanged | Temporarily drop privilege |
if (fork() == 0) {
int sock = socket(":80"); // needs root: port 80 is privileged
setuid(getuid("www-data")); // now EUID = RUID = SUID = www-data
}
Only root can listen on ports below 1024, so Apache must start as root. Without dropping privilege, any Apache bug would hand the attacker unrestricted control. After opening the socket (which stays open), the child calls setuid and becomes www-data for good. A bug now only gives the attacker www-data's limited rights. This is least privilege and privilege separation in action.
# EUID = RUID = SUID = 0
seteuid(100); # EUID = 100; RUID = SUID = 0
<perform dangerous operation>
setuid(0); # allowed: 0 equals RUID/SUID → back to root
The dangerous operation runs with user 100's limited rights. Because RUID and SUID still say 0, the process can return to root afterward.
if (authenticate(uid, pwd) == S_SUCCESS) {
if (fork() == 0) {
seteuid(uid); // EUID := uid; RUID and SUID stay 0
exec("/bin/bash"); // user gets a shell
}
}
The bug: seteuid only changes EUID. The user's shell still has SUID = 0, so the user can simply call setuid(0) (allowed, since 0 equals the saved UID) and become root.
The slide's fix: also call setuid(uid), so EUID = RUID = SUID = uid and the user cannot switch back.
Real-world footnote: on actual Linux, call setuid(uid) while still root, and do it before anything else. In the slide's order, after seteuid(uid) the process is no longer privileged, and a real unprivileged setuid changes only the EUID, leaving SUID = 0. Answer exam questions using the slide's simplified model, but know the principle: drop privilege completely, while you still have the power to.
systemd─┬─accounts-daemon───2*[{accounts-daemon}]
├─lighttpd(www-data)
├─rsyslogd(syslog)───3*[{rsyslogd}]
├─screen(zakir)───bash─┬─zdns───82*[{zdns}]
│ └─ziterate
├─sshd─┬─sshd───sshd(zakir)───bash───pstree
│ └─sshd───sshd(dabo)───bash
├─systemd-resolve(systemd-resolve)
This is pstree -u. The main system process systemd starts as root and forks everything else. A name in parentheses means the process's UID changed from its parent's. So lighttpd(www-data) is a web server that dropped to www-data; rsyslogd(syslog) runs as syslog. The sshd branch shows privilege separation: the root sshd forks a per-connection sshd, which after authentication forks sshd(zakir) running as the logged-in user, whose bash then runs pstree itself. {…} entries are threads (e.g., 82 zdns threads).
Sometimes an ordinary user legitimately needs to do something only root can. UNIX solves this by letting certain programs run as their owner, which is powerful and dangerous.
passwd, but any bug in a setuid-root program gives an attacker root.The passwd example. Changing your password means updating /etc/shadow, which only root can read or write. Normally your process (running as you) could not. So passwd is setuid root:
zakir@scratch-03:~$ ls -ali /usr/bin/passwd
2235 -rwsr-xr-x 1 root root 59640 Mar 22 2019 /usr/bin/passwd
The s in the owner's execute position (arrow on the slide) is the setuid bit. The owner is root, so anyone who runs passwd gets EUID = 0. (The leading 2235 is the inode number, from the -i flag.)
It looks at the real UID. The EUID is root (from the setuid bit), but the RUID is still the person who ran the program. passwd lets an ordinary RUID change only its own password; only a real root may change anyone's.
setuid() system call (in code) | setuid bit (on an executable file) |
|---|---|
| Lets a running process change its own user IDs (EUID, RUID, SUID ← x) | Makes the program run as the file's owner and group rather than the calling user |
| Used to drop privilege | Used to gain privilege |
Same name, opposite directions. The call usually lowers privilege; the bit raises it.
sudo: run a single command as root. Requires you to be "blessed" (listed in the sudoers configuration); asks for your password.su: become root (or another user) by knowing that account's password.sudo su: become root without knowing root's password (sudo runs su as root, and root needs no password to su).Traditional UNIX has only two levels: privileged processes (EUID = 0) bypass all kernel permission checks, and unprivileged ones get full checking. Many utilities, like ping (which needs raw network sockets), were made setuid root just to get one small power, and got all of root with it. That is exceptionally dangerous: a bug in any of them can compromise the whole system.
ping uses CAP_NET_RAW instead of setuid root.| Capability | Grants |
|---|---|
| CAP_KILL | Bypass permission checks for sending signals (e.g., kill other users' processes) |
| CAP_NET_BIND_SERVICE | Bind a socket to privileged ports (< 1024). Apache could use this instead of starting as root |
| CAP_SYS_PTRACE | Trace arbitrary processes with ptrace (debuggers) |
| Pros | Cons |
|---|---|
| Simple model protects most situations | ACLs are coarse-grained; do not capture enterprise complexity |
| Flexible enough for most simple systems in practice | Cannot separate different applications within one user account |
| Nearly all system operations require root, and people get sloppy |
Windows uses the same subject/object idea but with much richer, finer-grained ACLs and a token-based identity for each process.
Flexible ACLs: objects have full ACLs (any number of entries), allowing fine-grained permissions. Users can belong to multiple groups, groups can be nested, and ACL entries can allow or deny.
(Background: Windows orders explicit deny entries ahead of allow entries, so a deny normally wins.)
| Capability | ACL | |
|---|---|---|
| How access is checked | Subject presents an unforgeable ticket granting access; the system does not care who the subject is | System checks whether the subject is on the object's list |
| Matrix view | A row (per subject) | A column (per object) |
| Analogy | A movie ticket: whoever holds it gets in | A guest list at the door |
| Easy to answer | "What can this subject access?" | "Who can access this object?" |
"Linux capabilities" (CAP_KILL, etc.) and "capabilities" in the capability-vs-ACL sense are different concepts that share a name. Read the question carefully.
User-based permissions protect users from each other, but not from the programs they run. Sandboxing isolates applications from each other even within one user account.
Weak protection on desktops: a malicious application running as you has access to all your files. The slide's example: Adobe Acrobat (running as you) could edit, delete, or encrypt and ransom all your data. This is the "cannot separate applications within a user account" con from UNIX.
Diagram explained: Pre-2006 vs. modern. On the left, a single "Browser Process" box contains everything: tabs, plugins, and the browser UI all share one process, so a bug anywhere compromises everything. On the right, the modern design splits work into separate processes: Network, Browser, UI, Storage, GPU, Device, Renderer, and Plugin, with dashed lines showing that they communicate through defined channels.
| Process | Responsibility |
|---|---|
| Browser process | The "chrome" of the app (address bar, bookmarks) plus invisible privileged work such as network requests |
| Renderer process | Everything inside the tab where a website is displayed (HTML, CSS, JavaScript) |
| Plugin process | Plugins used by the site (e.g., Flash) |
| GPU process | GPU tasks, isolated because the GPU handles requests from many apps and draws them on the same surface |
Diagram explained: process-based site isolation. A browser window shows a page from a.com containing two iframes, b.com and c.com. Each iframe is drawn connected to its own renderer process, and a.com has a separate renderer too. Content from different sites never shares a process, so a compromised renderer for one site cannot read another site's data (such as your bank's cookies).
CreateRestrictedToken to make a token with a subset of the user's privileges, and assigns user and group S-1-0-0 (Nobody, the null SID). This removes access to nearly every system resource: as long as disk root directories have non-null security, no files (even ones with null ACLs) are reachable, and there is no network access (Vista and later).Why it exists: defense in depth. Each layer blocks a different escape route. Common exam question: "Name the mechanisms Chrome uses to sandbox renderers and what each blocks."
| Term | Definition | Example | Memory trick | Similar | Opposite |
|---|---|---|---|---|---|
| CVE | Public ID for a known vulnerability | CVE-2021-3156 | Catalog of Vulnerabilities | Advisory | Zero-day (not yet public) |
| Heap overflow | Writing past a heap buffer | sudo Baron Samedit | Pile overflows | Buffer overflow | Bounds checking |
| Use-after-free | Using memory after freeing it | Browser exploits | Using a returned rental | Dangling pointer | — |
| Defense in depth | Multiple independent protection layers | Chrome → OS → HW → network | Castle walls | Layered security | Single point of failure |
| Least privilege | Only the access needed for the task | Faculty edit only their classes' grades | Need-to-know | Minimal privilege | Over-privilege |
| Privilege separation | Split system into separately limited parts | Apache child as www-data | Separate locks on a safe | Compartmentalization | Monolith |
| Subject | Acting principal | User, process, app | Who | Principal | Object |
| Object | Protected resource | File, memory, device | What | Resource | Subject |
| UID | Integer user ID | zakir = 1001 | User ID | SID (Windows) | — |
| root | UID 0, bypasses checks | sudo target | Root of all power | Superuser, Administrator | Unprivileged user |
| Service account | Account for background processes | www-data | Serves, doesn't log in | System account | User account |
| /etc/passwd | World-readable user list | zakir:x:1001:… | Name misleads: no passwords now | — | /etc/shadow |
| /etc/shadow | Root-only password hash file | Updated by passwd | Hashes hide in the shadows | SAM (Windows) | /etc/passwd |
| GID / group | Named set of users sharing access | csc1302-tas | Group ID | Role | — |
| User/group/other | The three UNIX permission classes | rwx r-x r-- | U-G-O, left to right | — | — |
| chmod / octal mode | Set permissions as r=4, w=2, x=1 digits | chmod 754 | 4-2-1 | — | — |
| ACL | Per-object list of who can do what | hw/: Dan rw, Amelie r | Guest list | DACL | Capability list |
| RBAC | Permissions assigned to roles | csc1302-tas role | Roles change slower than people | Group-based access | Per-user ACLs |
| fork | Create a child copy of a process | Shell running a command | A fork in the road | spawn | — |
| EUID | UID used for permission checks | 0 while passwd runs | Effective = in effect now | — | RUID |
| RUID | UID of whoever started the process | You, while passwd runs | Real = the real person | — | EUID |
| SUID (saved UID) | EUID before the last change | 0 after seteuid(uid) | Saved for later | — | — |
| setuid() call | Set EUID, RUID, SUID (as root) | Apache dropping to www-data | Set all three, permanently | — | seteuid() |
| seteuid() call | Set only EUID | Temporary drop | e = effective only | — | setuid() |
| setuid bit | Executable runs as its owner | -rwsr-xr-x passwd | s = "switch to owner" | setgid bit | Capabilities |
| sudo | Run one command as root if permitted | sudo apt update | "Superuser do" | runas | — |
| su | Switch user with that user's password | su - | Substitute user | — | — |
| Linux capabilities | Root powers split into pieces | CAP_NET_BIND_SERVICE | Root, sliced | Privileges (Windows) | All-or-nothing root |
| SID | Windows security identifier | S-1-0-0 (Nobody) | Security ID | UID | — |
| Security descriptor | Owner/group SIDs + DACL + SACL | Any Windows file | The object's label | — | — |
| DACL | Allow/deny access entries | Deny "Guests" write | Decides | ACL | SACL |
| SACL | Which access attempts are audited | Log failed opens | Surveils | Audit policy | DACL |
| Access token | A Windows process's security context | User + groups + privileges | ID badge | Credentials | — |
| Impersonation token | Temporarily adopted security context | File server acting as client | Wearing someone's badge | seteuid | — |
| Capability (ticket) | Unforgeable token granting access | File descriptor, movie ticket | Holder gets in | Capability list | ACL |
| Sandbox | Restricted execution environment | macOS App Sandbox | Kids play only in the box | Jail, container | Full-trust app |
| Reference monitor | Component checking every access | Android permission checks | The referee | Security kernel | — |
| Renderer process | Chrome process that runs web content | One per site | Renders the page | — | Browser process |
| Site isolation | Different sites in different processes | b.com iframe in its own renderer | Separate houses for separate sites | — | Shared process |
| Broker | Privileged supervisor for sandboxed workers | Chrome browser process | Middleman with the keys | — | — |
| Restricted token | Token with a subset of privileges | CreateRestrictedToken | Badge with most doors removed | — | Full token |
| Job object | Windows container limiting a process group | No clipboard, no subprocesses | A job with a strict contract | cgroup | Interactive process |
| Integrity level | Windows trust tier: untrusted → system | Low-integrity renderer | Can't write up | — | — |
Bugs are inevitable, so systems use defense in depth, least privilege, and privilege separation. UNIX subjects are users and processes, objects are files, and ls -l permissions are checked owner → group → other, stopping at the first match (octal digit = 4r + 2w + 1x). Processes carry EUID, RUID, and SUID; setuid() drops privilege, the setuid bit gains it, and the SSH seteuid bug shows why you must drop completely. Linux capabilities split root; Windows uses SIDs, DACLs, SACLs, and tokens; capabilities are tickets while ACLs are guest lists. Sandboxes isolate apps within one user, and Chrome layers process isolation, site isolation, a broker, and four Windows sandbox mechanisms.
ls -l line and decide what a given user can do (owner → group → other).Operating system security, part 2: Windows
Source: L04b-os_sec2.pdf (Windows figures and tables from Jones & Bartlett Learning, 2025)
L04a built the general toolkit (least privilege, UNIX permissions, setuid) and sketched Windows in one section. This deck goes all the way into Windows: how the OS is layered, the three classic access control models, how a logon turns into a security access token, how Active Directory shares identities across machines, how Windows decides allow or deny on an object, and how you audit it all. It closes by revisiting Chrome's sandbox, which is built from exactly these Windows pieces.
The deck opens with two review questions on L04a's setuid material. Expect this style on quizzes: one command-recognition MCQ and one "explain with a scenario" short answer.
B) chmod u+s admin_tool. u+s adds the "s" (set user ID) bit to the owner's execute position.
Why the others are wrong: A sets ordinary rwxr-xr-x bits with no special bit. C changes the owner and group but no mode bits. D is not a real command; setuid is a system call, not a shell utility. The octal version would be chmod 4755 admin_tool (the leading 4 is setuid, from L04a's octal notation), so D mixes up the command name and argument order.
A running process calls setuid(uid) to change its user IDs. As root it sets EUID, RUID, and SUID all at once, so it is the standard way to permanently drop privilege after finishing the one task that needed root.
Scenario: Apache must start as root to bind port 80 (ports below 1024 are privileged). Right after opening the socket, the worker calls setuid(getuid("www-data")). From then on a bug in the web server only gives an attacker www-data's rights, not root's. See L04a: UNIX processes and user IDs. Don't confuse it with the setuid bit, which raises privilege.
Slides 3–5 repeat L04a's opening: any single buffer overflow, use-after-free, or null pointer dereference can let an attacker run code; we are getting better at finding bugs, but there will always be bugs (the slide underlines "always"). The example is again the sudo heap overflow CVE-2021-3156, found by Qualys in January 2021, present since 2011, affecting Linux, macOS, and BSD. The takeaway slide: systems must be designed to be resilient against both software vulnerabilities and malicious users, which means defense in depth (Chrome, then the OS, then hardware, then the network each stopping a JavaScript-engine exploit at its layer).
The full treatment, including the second CVE and the layer diagram, is in L04a: design principles. Everything below is how Windows implements those principles.
Before you can reason about Windows security you need a map of what runs where. The key split is the same one from CSC 4320's dual-mode operation: code in kernel mode can touch everything, code in user mode has to ask.
According to the slides, current versions of Windows:
A pure microkernel (like Mach, from CSC 4320 Chapter 2) pushes almost everything into user space and talks by message passing. Windows keeps a small microkernel core but runs the executive, drivers, and the HAL in kernel mode too, for speed. That is why it is often called a hybrid kernel. The security consequence: more code runs with full privilege, so a buggy kernel-mode driver can compromise the whole machine.
flowchart TD
subgraph UM [User mode]
IS[Integral subsystem]
ES[Environment subsystem]
end
subgraph KM [Kernel mode]
EX[Executive]
KD[Kernel mode drivers]
MK[Microkernel]
HAL["Hardware abstraction layer (HAL)"]
end
HW[Hardware]
IS <--> EX
ES <--> EX
EX <--> KD
EX <--> MK
KD <--> HAL
MK <--> HAL
HAL <--> HW
Read it top to bottom. A box labeled User mode holds the integral subsystem and the environment subsystem. Double arrows cross the user/kernel boundary into the Executive, the only kernel component user mode talks to. Below the executive sit kernel mode drivers and the microkernel, and both connect down to the hardware abstraction layer (HAL), which is the only thing that touches the Hardware box outside the diagram. What to notice: there is exactly one door from user mode (the executive) and exactly one door to hardware (the HAL).
| Component | What it does | Why it exists |
|---|---|---|
| Hardware Abstraction Layer (HAL) | Provides the actual access to physical hardware. All other kernel-mode programs reach hardware through it. | Microsoft supports a new hardware platform by writing a new HAL module instead of rewriting the whole OS. |
| Kernel mode drivers | Give user programs and other kernel programs access to individual devices, through the HAL, and translate devices into file objects. | Programs can treat a device like a file (the abstraction idea from CSC 4320). |
| Microkernel | The memory-resident core: CPU synchronization, process/thread and interrupt scheduling, and exception handling. | The minimum that must always be in memory and always privileged. |
| Executive | The "highest level" of kernel-mode programs: manages objects, I/O, security, and processes. | User-mode programs interact with the OS only through the executive. |
| Kernel (supervisor) mode | User mode | |
|---|---|---|
| Runs | HAL, drivers, microkernel, executive | Application programs and the Windows user-mode layer |
| Access | Complete access to hardware and system services | Must request services through the executive |
| Security meaning | An attractive target: compromising it means owning the machine | A compromise is contained to what that user's token allows |
The Windows user-mode layer handles user interaction and processing requests and passes I/O requests to the right kernel-mode drivers via the executive. It has two components:
Screenshots explained. Two slides titled "Windows operating system components" show familiar tools rather than new concepts. Task Manager lists one app (Task Manager itself) and 22 background processes (AggregatorHost, COM Surrogate, Runtime Broker, Search, Spooler SubSystem App, OneDrive, and others) with CPU, memory, disk, and network columns: these are user-mode processes. Computer Management is open to System Tools → Local Users and Groups → Groups, listing built-in groups such as Access Control Assistance Operators, Administrators, Backup Operators, Cryptographic Operators, Event Log Readers, Guests, Hyper-V Administrators, IIS_IUSRS, Remote Desktop Users, and Users. That second screen is where local accounts and groups are managed (see below).
The HAL. Every other kernel-mode program reaches hardware only through the HAL, so a new HAL module hides the hardware differences from everything above it.
The executive. User mode never calls drivers, the microkernel, or the HAL directly; the executive passes the I/O request to the correct kernel-mode driver.
Every OS has to answer "who decides who gets access?" There are three classic answers: the owner decides (DAC), a central policy of labels decides (MAC), or your job role decides (RBAC).
| DAC | MAC | RBAC | |
|---|---|---|---|
| Who decides | The object's owner | Central policy (labels and clearances) | Administrators, by defining roles |
| Decision based on | User identity and owner's settings | Clearance ≥ classification, plus need to know | Roles assigned to the user |
| Typical setting | General-purpose OSes | Military, government | Organizations; combined with DAC in Windows |
| Example | You share a document with a classmate | A Secret-cleared analyst cannot open a Top Secret file | Everyone in "TAs" can edit the gradebook |
1. A Google Drive owner clicks Share and adds a friend. DAC: the owner's discretion.
2. An officer with Secret clearance and a need to know opens a Confidential report. MAC, allowed because Secret ≥ Confidential.
3. The same officer tries to open a Top Secret report about their own project. MAC, denied: need to know is not enough if clearance is below the classification.
4. A new hire is added to the "Payroll" group and can suddenly open payroll files. RBAC, implemented with a group.
This deck uses the acronym DAC for two unrelated things: Discretionary Access Control (the model above) and Windows Server's Dynamic Access Control (a feature set covered below). Read the full name in a question before answering.
Windows never makes access decisions based on your username. It uses numbers (SIDs), bundled into a token that rides along with every process you start.
Each local system defines local users and groups during installation, and you can add more with Computer Management → Local Users and Groups.
| Well-known SID | String value | Identifies |
|---|---|---|
| Null SID | S-1-0-0 | A group with no members, often used when a SID is not known |
| World | S-1-1-0 | A group that includes all users ("Everyone") |
| Local | S-1-2-0 | Users who log on at local terminals |
| Creator Owner ID | S-1-3-0 | Placeholder replaced by the SID of the user who creates a new object |
| Creator Group ID | S-1-3-1 | Placeholder replaced by the primary group SID of the user who creates a new object |
Memory hook: the second number counts up, 0 = nobody, 1 = world, 2 = local, 3 = creator (owner is .0, group is .1). The null SID S-1-0-0 returns at the end of the lecture: it is the identity Chrome gives its sandboxed renderer.
Diagram explained: Windows SAT. A tall box labeled "User's security access token (SAT)" contains four stacked gray blocks, top to bottom: User SID, Group SIDs, List of privileges, and Other access information. What to notice: identity (the SIDs) and rights (the privileges) travel together in one object.
Why it exists: access checks later are fast, because everything needed is already in the token; Windows doesn't re-query the account database on every file open.
Common exam question: put the steps in order, or map them to identification → authentication → authorization (authorization happens later, when the token is compared against an object's DACL).
Common misconception: that adding a user to a group takes effect immediately in running programs. The group list was copied into the token at logon, so the user usually has to log off and on again.
Computing is spread across many machines that still need to share users and resources. Windows offers two ways to organize that: peer-to-peer workgroups, or a central directory.
| Workgroup | Active Directory (domain) | |
|---|---|---|
| Structure | Peer-to-peer network of Windows computers sharing resources; decentralized | True hierarchical directory (tree model) using LDAP and the X.500 structure |
| Where users and groups live | Defined separately on each computer | Defined once at a central location and shared among computers |
| Changing a permission | Must be applied to every computer | Done centrally; shared within one or more domains |
| Cost | Easy and cheap to set up | Needs more administration time and resources up front |
| Scale | Unmanageable past about half a dozen computers; highly discouraged | Reduces redundant administrative effort at any size |
X.500 is an ITU directory standard that names entries in a tree (country → organization → unit → person). LDAP (Lightweight Directory Access Protocol) is the simpler protocol for querying and updating such a tree over TCP/IP. Active Directory names look like CN=Alice,OU=CS,DC=gsu,DC=edu, which is X.500-style naming accessed over LDAP. ITU also defined X.509 certificates, from L01a's standards table.
Diagram explained: distributed SAT. Three boxes form an equation. The first, Domain controller, contains Domain user SID and Domain group SIDs. A plus sign leads to Computer where resource resides, containing Local group SIDs. An equals sign leads to Complete SAT, containing all three: domain user SID, domain group SIDs, local group SIDs. What to notice: the domain contributes who you are everywhere; the local machine adds groups that only mean something on that machine; the full token is assembled at the destination rather than sent whole.
Services (databases, web servers) need accounts too, and their passwords traditionally never got changed. Managed service accounts (Windows Server 2012 and newer) are managed domain accounts that can be shared across systems; domain controllers change their passwords automatically at the domain level. That removes a classic weakness: stale, shared, never-rotated service passwords.
sequenceDiagram participant C as Client participant K as KDC (domain controller) participant S as Server with object C->>K: Request K-->>C: Access ticket C->>S: Access ticket
The slide compresses Kerberos to one request and one ticket. Underneath, the AS step happens once at logon: the client proves its identity and receives a ticket-granting ticket (TGT). Then, for each service, the client shows the TGT to the TGS and receives a service ticket for that server. Tickets are encrypted with keys the KDC shares with each party and expire after a few hours, which limits replay. That is the "first AS, then TGS" order on the slide.
Windows applies L04a's least-privilege principle at the account level, and uses groups (roles) to hand out exactly the access each job needs.
| Group | Description | Least-privilege note |
|---|---|---|
| Account operators | Create and modify most types of accounts; can log in locally to domain controllers | Powerful; keep membership small |
| Administrators | Unrestricted access to the computer, or to the entire domain if the computer is a domain controller | The opposite of least privilege; use only for admin tasks |
| IIS_IUSRS | Built-in group used by Internet Information Services since version 7.0; the IUSR account belongs to it and provides consistency for web users | A service identity, like UNIX's www-data |
| Users | Run applications, access local resources, shut down or lock the computer, install per-user applications | The default for normal work |
| Guests | One-time users with the basic privileges of a regular user; the entire profile is deleted at logout | Nothing persists |
| Backup operators | Back up and restore all files on a computer, regardless of the files' permissions | Bypasses DACLs by design, so a stolen account reads everything |
| Remote desktop users | Establish remote connections to a Remote Desktop Session Host server | Grants remote login, a big attack surface |
(Groups as listed for Windows Server 2016, 2019, and 2022.)
"Backup operators just copy files, so it's a low-risk group." Its whole purpose is to read and restore every file regardless of permissions. Anyone in it can read payroll, password databases, and private mail. Treat it almost like Administrators.
| User rights | Permissions | |
|---|---|---|
| Apply to | What a user can do on the system (log on locally, shut down, back up files) | A specific object (this file, that printer) |
| Maintained through | Group security policy objects | Each object's own security settings |
Hierarchy to memorize: object → security descriptor → DACL → ACEs, where each ACE says allow or deny a permission to one SID. (L04a adds the other half of the descriptor: the SACL for auditing.)
Screenshots explained. The privilege escalation request dialog asks "Do you want to allow this app to make changes to your device?" for UserAccountControlSettings, showing Verified publisher: Microsoft Windows and the program location C:\Windows\system32\UserAccountControlSettings.exe, with Yes and No buttons. Check the publisher line: an unknown publisher on a prompt you didn't expect is a red flag. The UAC settings window has a four-position slider from Always notify (top) to Never notify (bottom). At the top, you are notified when apps try to install software or change the computer and when you change Windows settings yourself, which the dialog recommends if you routinely install new software and visit unfamiliar websites. Sliding to Never notify turns off the safety prompt entirely.
Compare to L04a's sudo. On Linux an admin account runs normally unprivileged and types sudo (plus a password) to run one command as root. On Windows an administrator's processes run with the limited half of the split SAT and a UAC prompt switches to the full half for that one program. Same idea: privilege on request, not by default. Difference: UAC for an admin is usually just a Yes click (a consent prompt), while a standard user gets a credential prompt asking for an admin's password.
Dynamic Access Control is a collection of features that describe user and data attributes, which Windows uses in policies for finer control over who can access data. Its four features:
Memory hook: Classify, Control, Audit, Encrypt. It moves Windows from "who is on this file's list" toward "what kind of data is this and what attributes does the user have," which looks more like MAC's labels.
Anything Windows can protect has a DACL. This section covers what the permissions are, how to see them, and the exact rule Windows uses to turn several ACEs into one allow-or-deny answer.
Every resource whose access can be controlled is a securable object. Common ones: NTFS files and folders; named and unnamed pipes; processes and threads; registry keys; Windows services; local and remote printers; network shares; and job objects (the same job objects Chrome uses to fence in renderers). A securable object needs a DACL for Windows to control access, and the DACL (a collection of ACEs) can be edited in the object's Properties → Security page.
Screenshot explained: Windows object DACL. The test Properties dialog, Security tab, for C:\Users\rshimonski\Desktop\Bookwork\test.txt. The Group or user names list shows SYSTEM, rshimonski, and Administrators. With SYSTEM selected, the Permissions for SYSTEM grid has Allow checks for Full control, Modify, Read & execute, Read, and Write, and nothing under Deny. Each row of names plus its Allow/Deny column is an ACE. The Advanced button opens the special permissions.
| Permission | Description (from the slide) |
|---|---|
| Full Control | No restrictions on access to the object |
| Modify | All modifications to files and folders; cannot delete files or folders, change permissions, or take ownership |
| Read and Execute | Traverse folders; execute files; list folders; read data, basic and extended attributes, and permissions |
| Read | List folders; read data, basic and extended attributes, and permissions |
| Write | Create files and folders; write data and basic and extended attributes; read permissions |
| Special Permissions | The ACE for this user or group is defined on the Advanced page |
The difference between Read and Read and Execute is exactly traverse/execute. The difference between Modify and Full Control is permission changes and ownership. On the slide, Modify also excludes deletion; see the note below.
Each basic permission is really a bundle of these finer ones. Several have two names because the same bit means one thing on a folder and another on a file.
| Special permission | On a folder | On a file (nonfolder) |
|---|---|---|
| Traverse Folder / Execute File | Navigate through the folder | Run the file |
| List Folder / Read Data | List contents | Read data |
| Create Files / Write Data | Create files in it | Write data |
| Create Folders / Append Data | Create subfolders | Append to existing data |
| Read Attributes / Read Extended Attributes | Read basic / extended object attributes | |
| Write Attributes / Write Extended Attributes | Write basic / extended object attributes | |
| Delete | Delete nonfolder objects | |
| Read Permissions | Read the object's permissions | |
| Change Permissions | Change the object's permissions | |
| Take Ownership | Become the new owner of the object | |
UNIX's r on a directory is List Folder, x is Traverse Folder, and w is roughly Create Files + Delete. Windows splits these further and adds Change Permissions and Take Ownership as separately grantable rights; in UNIX only the owner (or root) can chmod, and only root can chown.
Screenshots explained. DACL Advanced Security Settings for HelloWorld.txt shows the owner (rshimonski, with a Change link) and three tabs: Permissions, Auditing, Effective Access. The Permissions tab lists entries with columns Type, Principal, Access, and Inherited from: Allow SYSTEM Full control, Allow Administrators Full control, Allow rshimonski Full control, each inherited from C:\Users\rshimonski\. "Inherited from" means the ACE was copied down from the parent folder; the Disable inheritance button stops that. The Effective Access tab lets you pick a user (rshimonski) and click View effective access to see the final computed result for each special permission (Full control, Traverse folder / execute file, List folder / read data, and so on) along with an "Access limited by" column. Use it to check the algorithm below on a real file.
| Identifier | Identifies | Notes |
|---|---|---|
| SID (security identifier) | A security subject or group of subjects | Used in tokens and ACEs |
| GUID (globally unique identifier), also called UUID | Objects that may originate from different computers: computers, web browsers, database records, files, application components | 128-bit value, unique without a central registry |
| CLSID (class identifier) | Objects in the Windows Registry | The Registry uses GUIDs to identify objects and record attributes; those GUIDs are stored as CLSIDs |
Common CLSIDs from the slide (type shell:::{…} into Run or Explorer to open them):
| CLSID | Opens |
|---|---|
::{20d04fe0-3aea-1069-a2d8-08002b30309d} | My Computer |
::{450d8fba-ad25-11d0-98a8-0800361b1103} | My Documents |
::{208d2c60-3aea-1069-a2d7-08002b30309d} | My Network Places |
::{1f4de370-d627-11d1-ba4f-00a0c91eedba} | Network Computers |
::{7007acc7-3202-11d1-aad2-00805fc1270e} | Network Connections |
::{2227a280-3aea-1069-a2de-08002b30309d} | Printers and Faxes |
::{645ff040-5081-101b-9f08-00aa002f954e} | Recycle Bin |
::{d6277990-4c6a-11cf-8d87-00aa0060f5bf} | Scheduled Tasks |
::{450d8fba-ad25-11d0-98a8-0800361b1103}\My Folder | "My Folder" under Documents (assumes it exists) |
You don't need to memorize the hex. Know that the format is 8-4-4-4-12 hex digits (32 hex digits = 128 bits, from the number-systems refresher) and that the Recycle Bin is the slide's featured example.
Common exam question: given a DACL and a user's group memberships, decide allow or deny.
File budget.xlsx has this DACL: Allow Staff: Read; Allow Finance: Read, Write; Deny Interns: Write.
1. Dana (Staff) wants to Read. One matching ACE (Allow Staff Read). Allowed.
2. Dana (Staff) wants to Write. Staff has no Write ACE. Under the slide's rule 2a (no ACE for the requested access) it is allowed. In real Windows it is denied: see the note below. Answer the way the question frames it, and if a short answer allows it, mention both.
3. Farid (Finance) wants to Write. One matching ACE (Allow Finance Write). Allowed.
4. Ivy is in Finance and Interns and wants to Write. Two matching ACEs: Allow (Finance) and Deny (Interns). Any deny wins, so denied, even though she is in Finance.
5. Ivy wants to Read. Only Allow Finance Read matches; the Interns ACE is about Write, not Read. Allowed. Deny only affects the permission it names.
Lesson: use a Deny ACE to carve an exception out of a broad allow ("all of Finance except interns can edit").
Missing ACEs. Real Windows distinguishes a null DACL (no DACL at all), which grants everyone full access, from a DACL that simply has no ACE matching you, which denies access implicitly. The slide's rule 2a lumps both into "allow." That is dangerous if applied to a real system, but use the slide's rule on the course exam unless the question says otherwise.
Deny ordering. Real Windows checks explicit deny ACEs before allow ACEs, which is why "any deny wins" works in practice (L04a mentioned this). An inherited deny, however, can be overridden by an explicit allow on the object itself.
Modify and Delete. The slide says Modify cannot delete. In current Windows the Modify permission does include Delete; what it lacks is Change Permissions and Take Ownership. Know the slide's wording for the exam and the real behavior for the lab.
Access control decides; auditing remembers. Without logs you can't tell who read the salary spreadsheet or whether someone is guessing passwords.
Screenshot explained: Local Audit Policy. The Local Security Policy console is open to Security Settings → Local Policies → Audit Policy. The nine policies and their settings are: Audit account logon events (No auditing), Audit account management (No auditing), Audit directory service access (No auditing), Audit logon events (Failure), Audit object access (Success, Failure), Audit policy change (No auditing), Audit privilege use (No auditing), Audit process tracking (No auditing), Audit system events (No auditing). Recording logon failures catches password guessing; recording object access success and failure shows both who got in and who was blocked.
Audit logon events, Failure. Each wrong password produces a failed-logon record, and a burst of them is the brute-force signature (L01a's password-cracking math is what the attacker is running).
Failures show attempts that access control blocked. Successes show who actually read or changed sensitive data, which is what matters after an insider abuses legitimate access or a stolen account is used.
Expression-based security audit policy (Windows Server 2012 and newer): Dynamic Access Control lets administrators write targeted audit policies as expressions over user, computer, and resource claims (attributes). Slide example: audit everyone without a high security clearance who attempts to access highly sensitive documents. That records exactly the risky events instead of flooding the log with every access.
| Tool | Stands for | What it does |
|---|---|---|
cacls.exe | Control Access Control List | Legacy command-line tool to display or modify DACLs on files and folders. Still shipped for people used to it and for old batch files. |
icacls.exe | Integrity Control Access Control List | Replaces and extends cacls (and the older xcacls). Lists, updates, and backs up ACLs; finds files owned by a user; changes ownership; replaces one user's permissions with another's. |
robocopy.exe | Robust File Copy | Copies files and folders with or without their ACLs. |
:: list the DACL of a folder
icacls C:\Reports
:: give group Finance Modify, inherited by files (OI) and subfolders (CI)
icacls C:\Reports /grant Finance:(OI)(CI)M
:: explicit deny Write for Interns
icacls C:\Reports /deny Interns:W
:: back up the ACLs of the whole tree to a file
icacls C:\Reports /save acl.txt /T
:: copy everything including security (the S in DATS)
robocopy C:\Reports D:\Backup /E /COPY:DATS
(Command examples are standard usage for illustration, not from the slides.) Exam point: plain copying to a new location usually makes files inherit the destination folder's permissions; robocopy with /COPY:…S or /SEC preserves the original ACLs.
A: accounts alice and bob (Atlanta accountants), carol (Savannah accountant).
G: alice and bob go in global group ATL-Accountants; carol goes in SAV-Accountants. Global groups gather users from one domain.
U: both global groups go in universal group All-Accountants, which can span domains.
L: All-Accountants goes in local group Reports-Modify on the file server.
P: the C:\Reports DACL gets one ACE: Allow Reports-Modify: Modify.
When a new accountant joins in Savannah, you add one account to SAV-Accountants and touch no ACLs. When the Reports folder moves to a new server, you recreate one local group and one ACE. Microsoft documentation often writes this as AGDLP or AGUDLP ("DL" = domain local); it's the same idea.
The deck ends with the same Chrome slides as L04a: the pre-2006 single browser process vs. the modern multi-process design, the browser/renderer/plugin/GPU process roles, process-based site isolation, the broker, and the restricted token. The full notes are in L04a: sandboxing. What is new here is that you now know the Windows parts Chrome is built from:
CreateRestrictedToken.S-1-0-0 ("Nobody") from the well-known SIDs table, so no DACL grants it anything. As long as disk root directories have non-null security, no files can be opened (even ones with null ACLs), and there's no network access on Vista and later.Diagram explained: Chrome broker architecture. A Broker box (the main browser process) contains an Interception Manager, an IPC Service, a Policy Engine, and a Policy block. A dotted IPC line runs down to two Target boxes, each inside a dotted Sandbox border. Each target contains an IPC Client, a Policy Engine Client, and Interceptions. How it works: when sandboxed code calls a Windows API it isn't allowed to use (say, opening a file), the interception catches the call and sends it over IPC to the broker; the broker's policy engine checks it against the policy and, if allowed, performs the action with the broker's own privileges and returns the result. The renderer never holds the power itself. This is privilege separation plus a reference monitor.
| Term | Definition | Example | Memory trick | Similar | Opposite |
|---|---|---|---|---|---|
| Windows NT | Code base all current Windows versions build on | Windows 11, Server 2022 | New Technology | — | Windows 9x line |
| Kernel (supervisor) mode | Privileged mode with full hardware access | Drivers, HAL | The supervisor sees all | Ring 0 | User mode |
| HAL | Only layer that touches physical hardware | Porting Windows to ARM | Hides hardware | Device abstraction | — |
| Executive | Top kernel layer; user mode's only door into the kernel | Object and security manager | The executive takes the meetings | System call layer | — |
| Microkernel (Windows) | Memory-resident core: sync, scheduling, exceptions | Thread scheduling | Micro = the core | Kernel core | Executive |
| Environment subsystem | Runs programs written for other OSes | WSL, old Windows apps | Other environments | Compatibility layer | — |
| Integral subsystem | User-mode logon, access control, networking | Logon screen | Integral to security | — | — |
| DAC (discretionary) | Owner decides who gets access | Sharing a file | At the owner's discretion | ACLs | MAC |
| MAC | Labels and clearances decide access | Secret vs Top Secret | Mandatory = no choice | Multilevel security | DAC |
| RBAC | Access by role; users assigned roles | Payroll group | Role → rights | Groups | Per-user ACEs |
| SID | Permanent ID for a user or group | S-1-1-0 World | Security ID | UID | Username (changeable) |
| Null SID | S-1-0-0, group with no members | Chrome renderer token | 0 = nobody | nobody user | World SID |
| SAT | User SID, group SIDs, privileges attached to each process | Token built at logon | Badge on every process | Access token | — |
| Workgroup | Peer-to-peer; accounts on each PC | Home network | Work in a small group | P2P | Domain |
| Active Directory | Central hierarchical directory (LDAP, X.500) | University domain | The directory of everyone | LDAP directory | Workgroup |
| Domain | Computers sharing one user database | gsu.edu | One domain, one rulebook | Realm | Workgroup |
| Distributed SAT | Domain SIDs + local group SIDs assembled at the resource | File server logon | Parts, not the whole | — | Shipping full tokens |
| Managed service account | Domain service account with automatic password changes | SQL Server service | Managed = auto passwords | gMSA | Static service password |
| Kerberos | Ticket-based default Windows authentication | Domain logon | Three-headed dog: client, KDC, server | SSO | Password per server |
| KDC / AS / TGS | Domain controller role; authenticates, then grants tickets | TGT then service ticket | AS first, TGS second | Trusted third party | — |
| LUA | Least privilege user account | Standard user for daily work | Least User Access | Least privilege | Daily admin account |
| User right | System-wide ability, set by group policy | Log on locally | Rights = system | Privilege | Permission |
| Permission | Access to a specific object | Read on report.docx | Permission = object | ACE | User right |
| ACE | One allow/deny entry for a SID | Deny Interns Write | Entry | ACL row | — |
| DACL | Discretionary list of ACEs on an object | Security tab | DACL Decides | ACL | SACL |
| UAC | Split admin token with escalation prompt | "Allow this app to make changes?" | Admins ask permission too | sudo | Always-admin |
| Dynamic Access Control | Attribute-based classify, control, audit, encrypt | Auto-tag SSNs | Dynamic, not discretionary | ABAC | Static ACLs |
| RMS | Rights Management Services; auto-encrypts tagged files | Encrypted HR docs | Rights managed | IRM | — |
| Securable object | Any resource with a DACL | Registry key, printer | Can be secured | Object | — |
| Special permissions | Fine-grained rights inside basic ones | Take Ownership | The fine print | Advanced permissions | Basic permissions |
| Effective access | Final computed rights for a user | Effective Access tab | What actually takes effect | Effective permissions | — |
| GUID / UUID | 128-bit globally unique object ID | Database record key | Globally unique | UUID | — |
| CLSID | GUID identifying a Registry class/object | Recycle Bin 645ff040… | Class ID | ProgID | — |
| Auditing | Recording access attempts for later analysis | Failed logon events | The audit trail | Logging | — |
| Expression-based audit | Audit rules over user/resource claims | Low-clearance users on sensitive docs | Audit by expression | Targeted auditing | Audit everything |
| cacls / icacls | Command-line ACL viewers/editors (legacy / current) | icacls C:\Reports | i = integrity, improved | chmod, setfacl | — |
| robocopy | Robust copy, optionally with ACLs | /COPY:DATS | Robust copy | rsync | Plain copy |
| AGULP | Accounts → Global → Universal → Local → Permissions | Reports-Modify group | "A Good User Loves Permissions" | AGDLP | Per-user ACEs |
| Broker | Privileged process acting for sandboxed targets | Chrome browser process | Middleman with the keys | Reference monitor | — |
Windows is an NT-based, modular, modified-microkernel OS: user-mode subsystems talk only to the executive, and only the HAL touches hardware. Access control comes in three models (DAC, MAC, RBAC), and Windows mixes DAC with RBAC. Identity is a permanent SID; logon builds a security access token of user SID, group SIDs, and privileges attached to every process. Active Directory centralizes accounts in domains, shares only token parts across the network, and authenticates with Kerberos (KDC as AS, then TGS). UAC gives admins a split token; DACLs of allow/deny ACEs decide access, where any deny wins; auditing records the attempts; AGULP keeps permissions on groups, never people.
chmod u+s and what the setuid() call is for, with the Apache scenario.S-1-0-0 and S-1-1-0.Buffer overflow
Source: L05-Buffer overflow.pdf (Stallings & Brown Chapter 10, figures and tables 10.1–10.12)
L04a and L04b were about keeping a process inside its box. This lecture is about the oldest way to break out of the box from the inside: hand a program more input than it reserved room for and overwrite whatever sits next in memory. It is the concrete version of the "vulnerabilities are inevitable" slide you have now seen three times, and it ties directly to CSC 3320's C pointers and arrays and CSC 3210's stack frames.
From L04a: any buffer overflow, use-after-free, or null pointer dereference may let an attacker run code, so systems need defense in depth. From CSC 3320: C arrays have no bounds checking and strcpy/gets never check sizes. From CSC 3210: a stack frame holds the saved return address and old frame pointer above the local variables, and the stack grows downward while a string copy writes upward. Those two directions pointing at each other is the whole attack.
Buffer overflows are a very common attack mechanism, first used at scale by the Morris Worm in 1988. Prevention techniques are well known, yet they remain a major concern for two reasons the slide states plainly: a legacy of buggy code in widely deployed operating systems and applications, and continued careless programming practices.
| Year | Event |
|---|---|
| 1988 | The Morris Internet Worm uses a buffer overflow in fingerd as one of its attack mechanisms. |
| 1995 | A buffer overflow in NCSA httpd 1.3 is discovered and published on the Bugtraq mailing list by Thomas Lopatic. |
| 1996 | Aleph One publishes "Smashing the Stack for Fun and Profit" in Phrack, a step-by-step introduction to exploiting stack-based overflows. |
| 2001 | The Code Red worm exploits a buffer overflow in Microsoft IIS 5.0. |
| 2003 | The Slammer worm exploits a buffer overflow in Microsoft SQL Server 2000. |
| 2004 | The Sasser worm exploits a buffer overflow in the Windows 2000/XP Local Security Authority Subsystem Service (LSASS). |
Memory hook for the dates: Morris 1988 started it, Aleph One 1996 documented it, and the worm years 2001, 2003, 2004 (Code Red, Slammer, Sasser) industrialized it.
| The error | The consequences |
|---|---|
| A process attempts to store data beyond the limits of a fixed-size buffer, overwriting adjacent memory locations | Corruption of program data |
| Those locations can hold other program variables, parameters, or program control flow data | Unexpected transfer of control |
| The buffer can live on the stack, in the heap, or in the data section of the process | Memory access violations |
| Execution of code chosen by the attacker |
Read the consequences as a ladder of severity: corrupt data is a bug, an unexpected transfer of control is a vulnerability, and attacker-chosen code is a compromise.
To exploit an overflow, an attacker must:
Vulnerable programs are identified by: inspection of program source; tracing the execution of programs as they process oversized input; and tools such as fuzzing, which automatically feed large or malformed inputs to find crashes.
The program declares int valid = FALSE; and two 8-byte buffers, char str1[8] and char str2[8]. It fills str1 with a known tag, reads user input into str2 with gets(), and sets valid = TRUE only when the first 8 characters match:
int main(int argc, char *argv[]) {
int valid = FALSE;
char str1[8];
char str2[8];
next_tag(str1);
gets(str2);
if (strncmp(str1, str2, 8) == 0)
valid = TRUE;
printf("buffer1: str1(%s), str2(%s), valid(%d)\n", str1, str2, valid);
}
The slide shows three runs:
| Input typed | Output | What happened |
|---|---|---|
START | str1(START), str2(START), valid(1) | Normal, matching run |
EVILINPUTVALUE | str1(TVALUE), str2(EVILINPUTVALUE), valid(0) | Input overflowed str2 and overwrote str1 with the tail of the input |
BADINPUTBADINPUT | str1(BADINPUT), str2(BADINPUTBADINPUT), valid(1) | The overflow was crafted so the spilled half equals the first half, so the comparison passes: authentication bypassed without knowing the tag |
The figure is a three-column table of memory: addresses on the left (bfffbd0 through bfffbf4, rising), the 4-byte words before gets(str2), the same words after, and a label saying what each word holds. Reading from the bottom (low addresses) up: str2[0-3], str2[4-7], str1[0-3], str1[4-7], then a gap, then valid, the old base pointer, the return address, argc, and argv.
What to notice, and the reason this figure exists:
str2 sits at the lowest addresses, and str1 sits immediately above it, so writing past the end of str2 walks straight into str1.str1 and str2 read BADINPUT (hex 42414449 4e505554), and valid has flipped from 00000000 to 01000000."An overflow just corrupts the next variable, so the program prints garbage." It can, but the dangerous case is that the next thing in memory is not data at all: it is the saved return address, a frame pointer, or a function pointer. Overwriting data changes what the program computes; overwriting control data changes what the program does.
At the machine level, data is held in processor registers or in memory, and an assembly language programmer is entirely responsible for interpreting any saved value correctly. From there the slide draws a three-way split:
| Language family | Type safety | Buffer overflows? | Cost |
|---|---|---|---|
| Assembly | None: the programmer interprets every value | Wide open | Maximum control |
| Modern high-level languages (Java, Python, C#) | Strong notion of type and valid operations | Not vulnerable | Run-time overhead and some limits on use |
| C and related languages | High-level control structures, but direct access to memory | Vulnerable, with a large legacy of widely used unsafe code | Speed and closeness to the machine |
| Routine | What it does | Why it is unsafe |
|---|---|---|
gets(char *str) | Read a line from standard input into str | No way at all to limit the length: the input decides |
sprintf(char *str, char *format, ...) | Create str according to the supplied format and variables | Output length depends on the values formatted |
strcat(char *dest, char *src) | Append src to dest | No check that dest has room for both |
strcpy(char *dest, char *src) | Copy src into dest | No check that dest is as long as src |
vsprintf(char *str, char *fmt, va_list ap) | Create str from a format and an argument list | Same problem as sprintf |
The safer counterparts take a size: fgets, snprintf, strncat, strncpy, vsnprintf. CSC 3320 flagged the same pair (strcpy vs strncpy) and the same catch: strncpy may leave the result without a null terminator.
A stack buffer overflow happens when the overflowed buffer is a local variable, so the spill lands in the function's own stack frame. It is also called stack smashing, it is what the Morris Worm used, and it is still widely exploited.
flowchart TD A["P's frame: return addr"] --> B["P: old frame pointer"] B --> C["param 2"] C --> D["param 1"] D --> E["Q: return addr in P"] E --> F["Q: old frame pointer ← frame pointer"] F --> G["local 1"] G --> H["local 2 ← stack pointer"] H --> I["free space (stack grows down)"]
Function P calls function Q. Going down the figure: P's return address and old frame pointer, then the two parameters P pushed for Q, then Q's return address (an address inside P), then the old frame pointer that the frame pointer register points at, then Q's local variables, and finally the free space the stack grows into. Why the frame exists at all, in the slide's words: when one function calls another it needs somewhere to save the return address, and locations to save the parameters passed in and possibly register values.
The critical adjacency: the locals (where buffers live) sit below the saved frame pointer and return address, and a string copy writes upward. Overflow a local buffer far enough and the return address is what you hit.
On the left is the program file, split into global data and program machine code. Dashed arrows map those into the process image on the right, which from the bottom (low addresses) up holds: the process control block, program machine code, global data, the heap (arrow pointing up), spare memory, the stack (arrow pointing down), and kernel code and data at the top of memory. What to notice: the heap and stack grow toward each other through the spare region, and there are three places a vulnerable buffer can live, which is exactly the list at the end of this chapter (stack, heap, global data).
void hello(char *tag)
{
char inp[16];
printf("Enter value for %s: ", tag);
gets(inp);
printf("Hello your %s is %s\n", tag, inp);
}
The runs on the slide show the progression:
Bill and Lawrie) prints the greeting and the program finishes.X characters produces Segmentation fault (core dumped): the return address was overwritten with XXXX, which is not a valid code address, so the CPU faults when hello returns.tag pointer and the return address so that the function returns back into hello a second time with a different tag, and only then crashes. That second greeting is the proof that control flow itself was redirected.Figure 10.6 is the before-and-after stack table for this program. Before gets(inp), the words from low to high are inp[0-3] through inp[12-15], then two words of other data, then the old base pointer at bffffbd8, the return address at bffffbdc, and the tag pointer at bffffbe0. After typing a 24-character input, the inp words read ABCD EFGH QRST UVWX and the overflow continues into abcd efgh, changing the saved base pointer, the return address, and the tag pointer. Compare the "after" column with the labels on the right and you can see exactly which bytes of your input land on the return address.
With char inp[16] on a 32-bit system, the frame looks like this from the start of inp: bytes 0–15 are the buffer, bytes 16–19 are other saved data, bytes 20–23 are the saved frame pointer, and bytes 24–27 are the return address.
So an attacker who wants to control the return address writes 24 filler bytes and then a 4-byte address. Typing 20 Xs only corrupts the frame pointer; typing 28 Xs reliably lands on the return address. This arithmetic, offset from the buffer to the saved return address, is what "understand how that buffer is stored in memory" means in the attacker's checklist.
The second example has three functions: getinp(char *inp, int siz), which safely uses fgets(inp, siz, stdin); display(char *val), which copies into a local char tmp[16] using sprintf(tmp, "read val: %s\n", val); and main, which calls getinp(buf, sizeof(buf)) on its own char buf[16] and then display(buf). The runs show SAFE working normally, and a long run of Xs printing "buffer3 done" and then dying with a segmentation fault.
getinp does everything right; the bug is in display. sprintf writes the 11-character prefix "read val: " plus the value into a 16-byte buffer, so even a 16-byte input overflows tmp. Also notice when the crash appears: main finished printing "buffer3 done" first, because the corrupted return address only matters when the damaged frame returns. A crash can happen far away in time and place from the overflow.
The Metasploit Project provides useful information to people who perform penetration testing, IDS signature development, and exploit research; the same automation is why attack quality no longer depends on the attacker's assembly skill.
The figure has three panels showing the same thing at three levels:
sh = "/bin/sh", build an argument array args[0] = sh; args[1] = NULL; and call execve(sh, args, NULL). In other words: replace this process with a shell (the same execve from CSC 4320's process chapter).nop instructions (the end of a NOP sled), then uses a jmp/call pair so that call pushes the address of the "/bin/sh" string onto the stack and pop %esi retrieves it. That trick is what makes the code position independent: it never hard-codes where it was loaded. It then zeroes a register with xor, writes a null terminator onto the end of the string, fills in the argument array, loads the syscall number for execve, and fires int $0x80 to enter the kernel.Two details worth remembering for exam questions: shellcode must avoid null bytes, because it travels inside a C string that would otherwise be cut short, and it usually begins with a NOP sled so the guessed return address only has to land somewhere in the sled rather than exactly on the first instruction.
| Instruction | Meaning |
|---|---|
MOV src, dest | Copy (move) the value from src into dest |
LEA src, dest | Load effective address: copy the address of src into dest |
ADD / SUB src, dest | Add or subtract, leaving the result in dest |
AND / OR / XOR src, dest | Bitwise operation, result in dest |
CMP val1, val2 | Compare, setting CPU flags |
JMP / JZ / JNZ addr | Jump, jump if zero, jump if not zero |
PUSH src / POP dest | Push onto / pop off the stack |
CALL addr / RET | Call a function / return from it |
LEAVE | Clean up the stack frame before leaving a function |
INT num | Software interrupt used to access an operating system function |
NOP | No operation: do nothing (the sled) |
| 32-bit | 16 / 8-bit forms | Use |
|---|---|---|
%eax | %ax, %ah, %al | Accumulator: arithmetic, I/O, and executing interrupt calls |
%ebx | %bx, %bh, %bl | Base register: access memory, pass system call arguments, return values |
%ecx | %cx, %ch, %cl | Counter register |
%edx | %dx, %dh, %dl | Data register: arithmetic, interrupt calls, I/O |
%ebp | — | Base pointer: address of the current stack frame |
%eip | — | Instruction pointer (program counter): address of the next instruction |
%esi | — | Source index: pointer for string or array operations |
%esp | — | Stack pointer: address of the top of the stack |
The three that matter for this chapter are %esp (top of stack), %ebp (current frame), and %eip (what executes next). An overflow attack is, in one line, a way to write into %eip using nothing but data. If you took CSC 3210, these are the x86 cousins of sp, fp, and the program counter.
The figure is a terminal transcript, and the order of the commands is the lesson:
dir -l buffer4 shows -rwsr-xr-x 1 root knoppix: the vulnerable program is setuid root (the s bit from L04a).whoami prints knoppix, and cat /etc/shadow is refused with "Permission denied": the ordinary user cannot read the password hashes.whoami and cat /etc/shadow for the spawned shell to read.buffer4 prints the greeting with garbage, then root, then the full contents of /etc/shadow.Why this is the punchline of the lecture: the overflow alone gave code execution, but it is the setuid root bit that turned code execution into privilege escalation. Least privilege (L04a) and buffer overflows are the same story told from two sides.
| Target program can be | Shellcode can |
|---|---|
| A trusted system utility (often setuid) | Launch a remote shell when an attacker connects to it |
| A network service daemon | Create a reverse shell that connects back to the attacker |
| Commonly used library code | Use local exploits that establish a shell |
| Flush firewall rules that currently block other attacks | |
Break out of a chroot (restricted execution) environment, giving full access to the system |
Note the reverse shell entry: it exists because firewalls usually block inbound connections but allow outbound ones, so the victim dials out to the attacker.
Defenses split into two broad approaches. Compile-time defenses aim to harden programs so new code resists attack; run-time defenses aim to detect and abort attacks in programs that already exist. You need both, because you cannot recompile the world.
Modern high-level languages are not vulnerable to buffer overflow attacks, because the compiler enforces range checks and permissible operations on variables. The slide is equally clear about the price:
C's designers emphasized space efficiency and performance over type safety, assuming programmers would exercise due care. So programmers must inspect code and rewrite anything unsafe. The slide's example is the OpenBSD project, whose developers audited the entire code base, including the operating system, standard libraries, and common utilities, producing what is widely regarded as one of the safest operating systems in widespread use.
Two functions that contain no obviously dangerous library call, which is the point:
/* (a) unsafe byte copy */
int copy_buf(char *to, int pos, char *from, int len)
{
int i;
for (i = 0; i < len; i++) {
to[pos] = from[i];
pos++;
}
return pos;
}
/* (b) unsafe byte input */
short read_chunk(FILE fil, char *to)
{
short len;
fread(&len, 2, 1, fil); /* read length of binary data */
fread(to, 1, len, fil); /* read len bytes of binary data */
return len;
}
len bytes into to starting at pos and never checks the size of to. The caller's numbers decide whether this is safe, so the bug lives in a different function from the crash.short, so a negative or huge value from an attacker-supplied file turns the second fread into an overflow. Rule: never trust a length field that came from the same untrusted source as the data.Handling dynamically allocated memory is harder, because the size is not known at compile time, so checking it requires language extensions and library routines; programs and libraries must be recompiled, which creates problems with third-party applications. A second approach replaces the unsafe standard routines with safer variants. Libsafe is the slide's example: it is implemented as a dynamic library arranged to load before the standard libraries, so calls such as strcpy reach the checked version first.
Stackshield and Return Address Defender (RAD) are GCC extensions that take a different route to the same goal: function entry code writes a copy of the return address to a safe region of memory, function exit code compares the return address in the stack frame against that saved copy, and if it has changed, the program aborts.
Memory hook: a canary detects collateral damage next to the return address; Stackshield/RAD detect a changed return address directly.
Use virtual memory support to mark some regions of memory non-executable, so shellcode sitting in a data buffer cannot run. This requires support from the memory management unit (MMU); it has long existed on SPARC/Solaris systems and is more recent on x86 Linux, UNIX, and Windows. The complication the slide notes: some legitimate programs need executable stack code (certain language runtimes and nested functions), so special provisions are needed.
Manipulate the location of key data structures, the stack, heap, and global data, using a random shift for each process, and also randomize the location of heap buffers and of standard library functions. Because modern address spaces are large, wasting some of the range has negligible impact. The effect on the attacker is direct: the guessed return address in Figure 10.9 stops being guessable, and a wrong guess crashes the program instead of running shellcode.
Place guard pages between critical regions of memory, flagged in the MMU as illegal addresses, so any attempted access aborts the process. An extension places guard pages between stack frames and between heap buffers; the cost is execution time to support the large number of page mappings needed.
Line them up and each one removes a step the attack needs: safe languages and coding remove the overflow, canaries catch the overwritten return address, NX stops injected code from executing, ASLR stops the attacker from knowing where to jump, and guard pages stop the walk between regions. This is defense in depth from L04a applied to one bug class, and it is why real exploits today must chain several tricks together.
A variant overwrites the buffer and the saved frame pointer rather than the return address. The saved frame pointer is changed to refer to a dummy stack frame the attacker built; the current function returns into that replacement frame, and control transfers to the shellcode in the overwritten buffer. This matters because of off-by-one attacks, a coding error that allows exactly one more byte to be copied than there is space for, which is enough to change the least significant byte of the saved frame pointer.
Defenses: stack protection mechanisms that detect modification of the stack frame or return address at function exit, non-executable stacks, and randomization of the stack and of system libraries.
Defenses are the same three: stack protection that detects the modified return address, non-executable stacks (which this partly bypasses), and randomization of the stack and of system library locations, which is the one that really bites, because the attack needs the library function's address.
The attacked buffer lives in the heap, typically located above the program code, in memory requested by the program for dynamic data structures such as linked lists of records. The heap has no return address, so there is no easy transfer of control; instead an attacker looks for a function pointer to overwrite, or manipulates the heap's own management data structures.
The figure's vulnerable program allocates a record that contains exactly that pairing:
typedef struct chunk {
char inp[64]; /* vulnerable input buffer */
void (*process)(char *); /* pointer to function to process inp */
} chunk_t;
int main(int argc, char *argv[]) {
chunk_t *next;
setbuf(stdin, NULL);
next = malloc(sizeof(chunk_t));
next->process = showlen;
printf("Enter value: ");
gets(next->inp);
next->process(next->inp); /* calls whatever the pointer now holds */
printf("buffer5 done\n");
}
Overflowing inp by more than 64 bytes overwrites process, and the very next line calls it. The attack transcript in part (b) does the same thing as the stack attack: a NOP sled plus shellcode inside the 64-byte buffer, then an address that points back into that buffer, and the program hands the attacker a root shell reading /etc/shadow.
Defenses: make the heap non-executable, and randomize the allocation of memory on the heap.
The same structure, moved to the global data section: a global struct chunk holding char inp[64] and a process function pointer, filled by gets(chunk.inp) and then called as chunk.process(chunk.inp). The buffer may be located above the program code, and an attacker aims to overwrite a function pointer that is later called, or adjacent process management tables. The attack script differs from the heap one only in the address it writes, because global data sits at a fixed, predictable location.
Defenses: a non-executable or randomized global data region, moving function pointers away from data buffers, and guard pages between the global data region and its neighbors.
A stack frame already contains control data (the saved return address and frame pointer) that the CPU will use automatically at function exit. The heap and the global data region contain only data, so an attacker needs something that will later be treated as an address to execute: a function pointer, or the allocator's own management structures.
Address space randomization of system libraries. The attack requires the exact address of a library function to put in the return address slot; non-executable memory does not help, because the library code is legitimately executable.
Canaries protect the saved return address in a stack frame. A heap overflow overwrites a function pointer in heap memory, so no canary is anywhere near the data being corrupted. Each defense covers one specific mechanism, which is exactly why layering is required.
| Term | Definition | Example | Memory trick | Similar | Opposite |
|---|---|---|---|---|---|
| Buffer overflow | Writing more data into a buffer than it can hold, overwriting neighbors | gets into char inp[16] | The cup overflows onto the table | Buffer overrun | Bounds-checked write |
| Stack smashing | Buffer overflow where the buffer is a local variable | Morris Worm on fingerd | Smash the frame | Stack overflow | Heap overflow |
| Return address | Saved address a function returns to | bffffbdc in Figure 10.6 | The way home | Saved %eip | — |
| Saved frame pointer | Caller's frame base, saved on entry | Target of off-by-one attacks | Old base ptr | Old %ebp | — |
| Shellcode | Attacker's machine code, usually spawning a shell | execve("/bin/sh") | Code that gets a shell | Payload | Legitimate code |
| NOP sled | Run of no-op instructions before shellcode | 90 90 90 … | Slide into the payload | Landing zone | Exact-address jump |
| Position-independent code | Code that works wherever it is loaded | jmp/call trick in Figure 10.8 | No fixed address | PIC | Absolute addressing |
| Fuzzing | Automatically feeding malformed input to find crashes | Finding vulnerable programs | Fuzzy random input | Dynamic testing | Source inspection |
| Metasploit | Framework providing exploit and payload tooling | Shellcode generation | Meta-exploit toolbox | Exploit kit | — |
| Unsafe library routine | C function with no length limit | gets, strcpy, sprintf | No size, no safety | Table 10.2 | fgets, strncpy, snprintf |
| Stack canary | Random guard value checked at function exit | StackGuard-style protection | Canary in the coal mine | Stack protector | Unprotected frame |
| Stackshield / RAD | GCC extensions that save and re-check the return address | Abort on mismatch | Shield the return | Canary | — |
| Libsafe | Dynamic library loaded before the standard one, with checked routines | Safer strcpy | Library that is safe | Safe libraries | libc as shipped |
| NX / executable space protection | Marking memory non-executable via the MMU | Non-executable stack | No eXecute | DEP | Executable stack |
| ASLR | Randomizing stack, heap, global data, and library locations | Random shift per process | Move the furniture | Randomization | Fixed addresses |
| Guard page | Page marked illegal between memory regions | Between stack frames | A fence with an alarm | Canary page | Contiguous regions |
| Off-by-one | Error allowing exactly one byte too many | Changes low byte of saved frame pointer | One too far | Fencepost error | — |
| Replacement stack frame | Overwriting the saved frame pointer to use a dummy frame | Off-by-one exploitation | Swap the frame | Frame pointer overwrite | Return address overwrite |
| Return to system call | Returning into existing library code instead of injected code | return-to-libc | Use their code, not yours | ROP ancestor | Code injection |
| Heap overflow | Overflow of a dynamically allocated buffer | Overwrite process pointer | Pile spills over | Global data overflow | Stack overflow |
| Function pointer | Variable holding a code address that is later called | next->process(...) | Data that becomes control | Callback | Plain data |
A buffer overflow puts more data into a fixed-size buffer than it holds, overwriting neighboring memory: at best corrupt data, at worst execution of attacker-chosen code. C is exposed because it has no bounds checking and a legacy of unsafe routines (gets, strcpy, sprintf, strcat, vsprintf). On the stack, locals sit below the saved frame pointer and return address, so a long input can overwrite the return address and redirect control into shellcode (often a NOP sled plus position-independent code that calls execve("/bin/sh")), and against a setuid root program that means privilege escalation. Defenses are compile-time (safe languages, audited code, safe libraries such as Libsafe, canaries and Stackshield/RAD) and run-time (non-executable memory, ASLR, guard pages). The variants exist to get around those: replacement stack frames and off-by-one errors target the saved frame pointer, return-to-libc defeats non-executable memory by reusing existing code, and heap and global data overflows have no return address, so they overwrite a function pointer that is called later.
BADINPUTBADINPUT makes valid true without knowing the tag.%esp, %ebp, and %eip and their roles.Malware
Source: L06a-Malware.pdf (Stallings & Brown Chapter 6; Table 6.1 and Figures 6.1–6.3). The deck's title slide reads "Buffer Overflow & Malware," because it continues straight on from L05.
L05 showed you one mechanism: a bug that lets an attacker run code. This lecture is the catalogue of what gets run. Malware is organised around two questions, and if you keep them separate the whole chapter stays straight: how does it get there (propagation) and what does it do once it is there (payload). Everything else — virus, worm, Trojan, bot, rootkit, ransomware — is a combination of an answer to each.
From L05: the Morris Worm (1988), Code Red (2001), Slammer (2003), and Sasser (2004) all spread by exploiting buffer overflows — this lecture tells you what those worms were, not just how they got in. From L01a: confidentiality, integrity, and availability; the NIST malware definition below is written in exactly those terms. From L04a and L04b: a rootkit's whole purpose is to defeat the reporting mechanisms of the OS you studied there, and a virus attached to a program inherits that program's privileges — which is the setuid problem all over again.
This table is the single most likely source of a matching or definition question on the exam. Read it once as a list, then read it again grouped the way the next slide groups it — by propagation versus payload.
| Name | Description |
|---|---|
| Advanced Persistent Threat (APT) | Cybercrime directed at business and political targets, using a wide variety of intrusion technologies and malware, applied persistently and effectively to specific targets over an extended period, often attributed to state-sponsored organizations. |
| Adware | Advertising that is integrated into software. It can result in pop-up ads or redirection of a browser to a commercial site. |
| Attack kit | Set of tools for generating new malware automatically using a variety of supplied propagation and payload mechanisms. |
| Auto-rooter | Malicious hacker tools used to break into new machines remotely. |
| Backdoor (trapdoor) | Any mechanism that bypasses a normal security check; it may allow unauthorized access to functionality in a program or onto a compromised system. |
| Downloaders | Code that installs other items on a machine that is under attack. It is normally included in the malware code first inserted onto a compromised system to then import a larger malware package. |
| Drive-by-download | An attack using code on a compromised website that exploits a browser vulnerability to attack a client system when the site is viewed. |
| Exploits | Code specific to a single vulnerability or set of vulnerabilities. |
| Flooders (DoS client) | Used to generate a large volume of data to attack networked computer systems by carrying out some form of denial-of-service attack. |
| Keyloggers | Captures keystrokes on a compromised system. |
| Logic bomb | Code inserted into malware by an intruder. A logic bomb lies dormant until a predefined condition is met; the code then triggers some payload. |
| Macro virus | A type of virus that uses macro or scripting code, typically embedded in a document or document template and triggered when the document is viewed or edited, to run and replicate itself into other such documents. |
| Mobile code | Software (e.g. script and macro) that can be shipped unchanged to a heterogeneous collection of platforms and executed with identical semantics. |
| Rootkit | Set of hacker tools used after an attacker has broken into a computer system and gained root-level access. |
| Spammer programs | Used to send large volumes of unwanted e-mail. |
| Spyware | Software that collects information from a computer and transmits it to another system by monitoring keystrokes, screen data, and/or network traffic, or by scanning files on the system for sensitive information. |
| Trojan horse | A computer program that appears to have a useful function but also has a hidden and potentially malicious function that evades security mechanisms, sometimes by exploiting legitimate authorizations of a system entity that invokes it. |
| Virus | Malware that, when executed, tries to replicate itself into other executable machine or script code; when it succeeds, the code is said to be infected. When the infected code is executed, the virus also executes. |
| Worm | A computer program that can run independently and can propagate a complete working version of itself onto other hosts on a network by exploiting software vulnerabilities in the target system or using captured authorization credentials. |
| Zombie, bot | Program installed on an infected machine that is activated to launch attacks on other machines. |
Malware is classified first by how it spreads or propagates to reach the desired targets, and then by the actions or payloads it performs once a target is reached. Two more cuts are given alongside:
| Cut | Needs a host program (parasitic) | Independent, self-contained |
|---|---|---|
| Examples | Viruses | Worms, Trojans, bots |
| Cut | Does not replicate | Replicates |
|---|---|---|
| Examples | Trojans, spam e-mail | Viruses, worms |
"Virus" is not a synonym for malware. A virus needs a host — it infects something that already exists and runs when that thing runs. A worm is a complete program that runs and spreads by itself. A Trojan does not replicate at all; it spreads because people choose to install it. Getting these three straight is worth more exam points than memorising any single attack name.
| Propagation mechanisms | Payload actions |
|---|---|
| Infection of existing content by viruses, subsequently spread to other systems | Corruption of system or data files |
| Exploit of software vulnerabilities by worms or drive-by-downloads, allowing the malware to replicate | Theft of service: make the system a zombie agent of attack as part of a botnet |
| Social engineering attacks that convince users to bypass security mechanisms to install Trojans or respond to phishing | Theft of information from the system, e.g. keylogging |
| Stealthing: hiding its presence on the system |
Memory hook: three ways in — infect, exploit, trick. Four things to do — corrupt, conscript, steal, hide.
Initially, developing and deploying malware required considerable technical skill. Virus-creation toolkits in the early 1990s and more general attack kits in the 2000s changed that. These toolkits, often called crimeware, include a variety of propagation mechanisms and payload modules that even novices can deploy. The defensive problem is not the kit itself but the number of variants it generates: each one looks different to a signature scanner. Examples named on the slide: Zeus and Angler.
The other significant development is who is writing malware. The slide describes a shift from individuals motivated to demonstrate technical competence to their peers, to more organised and dangerous sources:
This changed both the resources available and the motivation, and produced a large underground economy trading attack kits, access to compromised hosts, and stolen information.
An APT is the well-resourced, persistent application of a wide variety of intrusion technologies and malware to selected targets, usually business or political, and is typically attributed to state-sponsored organizations and criminal enterprises. What makes APTs different from other attacks is not sophistication for its own sake: it is careful target selection plus stealthy intrusion over extended periods. High-profile examples: Aurora, RSA, APT1, Stuxnet.
APT attacks aim at anything from theft of intellectual property and security- or infrastructure-related data through to the physical disruption of infrastructure. The techniques are the ordinary ones — social engineering, spear-phishing e-mails, drive-by-downloads from compromised sites the target's personnel are likely to visit. The intent is to infect the target with sophisticated malware having multiple propagation mechanisms and payloads; once initial access is gained, a further range of attack tools is used to maintain and extend that access.
A spear-phishing e-mail with a malicious attachment is technologically unremarkable — the same trick has worked since the 1990s. What makes it an APT is the preparation around it: the attacker knows which engineer opens contract PDFs, the e-mail quotes a real project, the payload calls back over HTTPS so it blends into normal traffic, and the operators keep quiet for months. The sophistication is in the campaign, not the exploit. That is why the slide says individual components "may not necessarily be technically advanced."
A virus is a piece of software that infects programs: it modifies them to include a copy of the virus, then replicates and goes on to infect other content. It spreads easily through network environments. When attached to an executable program, a virus can do anything that the program is permitted to do, and it executes secretly when the host program is run. Viruses are specific to an operating system and hardware, because they take advantage of their details and weaknesses.
"A virus can do anything that the program is permitted to do" is the same sentence as the setuid problem from L04a, viewed from the other side. Access control decides what a program may do, not what a person intends; infect a privileged program and the virus inherits the privilege. Least privilege is therefore an anti-virus measure, not just an administration measure.
| Component | What it is | Also called |
|---|---|---|
| Infection mechanism | The means by which the virus spreads or propagates | Infection vector |
| Trigger | The event or condition that determines when the payload is activated or delivered | Logic bomb |
| Payload | What the virus does besides spreading; may be damage, or benign but noticeable activity | — |
| Phase | What happens |
|---|---|
| Dormant | The virus is idle and will eventually be activated by some event. Not all viruses have this stage. |
| Propagation | The virus places a copy of itself into other programs or into certain system areas on the disk. The copy may not be identical to the propagating version. Each infected program now contains a clone of the virus which will itself enter a propagation phase. |
| Triggering | The virus is activated to perform the function for which it was intended; can be caused by a variety of system events. |
| Execution | The function is performed. It may be harmless or damaging. |
The slides present these in the order dormant → triggering → propagation → execution. The logical life cycle is dormant → propagation → triggering → execution, since a virus normally spreads for a while before its trigger fires. Either ordering is defensible on an exam; what you must be able to say is that four phases exist, that the dormant phase is optional, and which phase does the copying.
NISTIR 7298 defines a macro virus as "a virus that attaches itself to documents and uses the macro programming capabilities of the document's application to execute and propagate." Macro viruses infect the scripting code used to support active content in user document types. They are threatening for five reasons:
The figure is a single block of pseudo-code for a macro named Document_Open — a handler the application calls automatically every time a document is opened. Reading it line by line shows all three virus components in about twenty lines:
macro Document_Open
disable Macro menu and some macro security features
if called from a user document
copy macro code into Normal template file
else
copy macro code into user document being opened
end if
if registry key "Melissa" not present
if Outlook is email client
for first 50 addresses in address book
send email to that address
with currently infected document attached
end for
end if
create registry key "Melissa"
end if
if minute in hour equals day of month
insert text into document being opened
end if
end macro
What each part is doing, and why it is on the exam:
Take 3 September at 14:03. The minute in the hour is 3; the day of the month is 3; they are equal, so the payload fires. At 14:04 on the same day it does not. Over a whole day the condition is true for exactly one minute in each hour, 24 minutes in all, and on the 31st of a month it is true at :31 past each hour. The point of the exercise is that a trigger does not have to be clever to be effective — it only has to be rare enough that the virus spreads before anyone notices it.
| By target | |
|---|---|
| Boot sector infector | Infects a master boot record or boot record and spreads when a system is booted from the disk containing the virus. |
| File infector | Infects files that the operating system or shell considers to be executable. |
| Macro virus | Infects files with macro or scripting code that is interpreted by an application. |
| Multipartite virus | Infects files in multiple ways. |
| By concealment strategy | |
|---|---|
| Encrypted virus | A portion of the virus creates a random encryption key and encrypts the remainder of the virus. |
| Stealth virus | A form of virus explicitly designed to hide itself from detection by anti-virus software. |
| Polymorphic virus | A virus that mutates with every infection. |
| Metamorphic virus | A virus that mutates and rewrites itself completely at each iteration, and may change behavior as well as appearance. |
Polymorphic changes its appearance — typically a new encryption key and a new decryptor each time, so the body differs byte-for-byte but does the same thing. Metamorphic rewrites itself completely and may change behaviour as well as appearance. Polymorphic defeats byte-signature scanning; metamorphic also defeats behavioural signatures. Note that an encrypted virus still has to decrypt itself before it runs, which is precisely what sandbox analysis exploits.
A worm is a program that actively seeks out more machines to infect; each infected machine serves as an automated launching pad for attacks on other machines. It exploits software vulnerabilities in client or server programs, uses network connections to spread from system to system, and also spreads through shared media (USB drives, CD and DVD data disks). E-mail worms spread in macro or script code included in attachments and in instant messenger file transfers. Upon activation the worm may replicate and propagate again, and it usually carries some form of payload. The first known implementation was done at Xerox Palo Alto Labs in the early 1980s.
| Route | How the copy travels |
|---|---|
| Electronic mail or instant messenger | The worm e-mails a copy of itself to other systems, or sends itself as an attachment via an instant message service. |
| File sharing | Creates a copy of itself, or infects a file as a virus does, on removable media. |
| Remote execution capability | The worm executes a copy of itself on another system. |
| Remote file access or transfer capability | The worm uses a remote file access or transfer service to copy itself from one system to the other. |
| Remote login capability | The worm logs on to a remote system as a user and then uses commands to copy itself across. |
Scanning (or fingerprinting) is the first function in the propagation phase of a network worm: it searches for other systems to infect. Four strategies are given, and they are a nice illustration of the trade-off between speed and stealth.
| Strategy | How it picks targets | Consequence |
|---|---|---|
| Random | Each compromised host probes random addresses in the IP address space, using a different seed. | Produces a high volume of Internet traffic, which may cause generalized disruption even before the actual attack is launched. Loud, and slow to find the good targets. |
| Hit-list | The attacker first compiles a long list of potentially vulnerable machines; once compiled, each infected machine is given a portion of the list to scan. | A very short scanning period, which may make it difficult to detect that infection is taking place. Fast and quiet, but requires reconnaissance in advance. |
| Topological | Uses information contained on the infected victim machine to find more hosts to scan. | Targets are real and reachable (address books, known hosts, peer lists), so hit rates are high and no scanning noise is generated. |
| Local subnet | If a host behind a firewall can be infected, that host looks for targets in its own local network using the subnet address structure. | Finds hosts that would otherwise be protected by the firewall. This is why a perimeter firewall is not a defence once one machine inside is infected. |
The figure is a graph with time on the horizontal axis and "fraction" from 0 to 1.0 on the vertical. Two curves cross in the middle: one rising from 0 to 1.0 labelled fraction of hosts infected, and its mirror image falling from 1.0 to 0 labelled fraction of hosts not infected. They cross at 0.5. Across the top, three braces divide the time axis into labelled phases:
| Phase | Shape of the curve | Why it looks like that |
|---|---|---|
| Slow start | Nearly flat near zero | Very few infected machines exist, so very few scans are happening. Growth is exponential but from a tiny base, so in absolute terms almost nothing is visible. |
| Fast spread | Steep, nearly vertical through the 0.5 crossing | Many infected machines are scanning at once and most of the addresses they hit are still vulnerable. This is where the whole Internet notices. |
| Slow finish | Flattens asymptotically toward 1.0 | Almost every vulnerable host is already infected, so most scans land on a machine that is already compromised or was never vulnerable. Effort is wasted, and the curve saturates. |
This is a logistic (S-shaped) curve, the same shape as an epidemic. The defensive lesson is the one the shape forces: a response that only begins in the fast-spread phase is already too late, because the fast-spread phase is measured in minutes for a well-engineered worm. Slammer in 2003 reached most of its victims in about ten minutes. Detection has to happen in the flat slow-start region, which is exactly where there is least evidence.
The earliest significant worm infection, released by Robert Morris in 1988, designed to spread on UNIX systems. It used three attack routes:
fingerd buffer overflow from Table 10.1 in L05.)A successful attack achieved communication with the operating system command interpreter, and the worm then sent the interpreter a bootstrap program to copy the rest of the worm over. The bootstrap pattern — get a tiny foothold, then pull down the real payload — is exactly what Table 6.1 calls a downloader, and it is still how most malware is staged today.
| Name | Year | Description |
|---|---|---|
| Melissa | 1998 | E-mail worm; the first to include virus, worm, and Trojan in one package. |
| Code Red | July 2001 | Exploited a Microsoft IIS bug; probes random IP addresses; consumes significant Internet capacity when active. |
| Code Red II | August 2001 | Also targeted Microsoft IIS; installed a backdoor for access. |
| Nimda | September 2001 | Had worm, virus, and mobile code characteristics; spread using e-mail, Windows shares, Web servers, Web clients, and backdoors. |
| SQL Slammer | Early 2003 | Exploited a buffer overflow vulnerability in SQL Server; compact, and spread rapidly. |
| Sobig.F | Late 2003 | Exploited open proxy servers to turn infected machines into spam engines. |
| Mydoom | 2004 | Mass-mailing e-mail worm; installed a backdoor in infected machines. |
| Warezov | 2006 | Creates executables in system directories; sends itself as an e-mail attachment; can disable security-related products. |
| Conficker (Downadup) | November 2008 | Exploits a Windows buffer overflow vulnerability; most widespread infection since SQL Slammer. |
| Stuxnet | 2010 | Restricted its rate of spread to reduce the chance of detection; targeted industrial control systems. |
Read the table as a story rather than a list. 1998–2006 is the age of worms that wanted to be as fast and as visible as possible. Stuxnet in 2010 inverts the entire design goal: it deliberately slows itself down, because the objective is not scale but one specific target. That inversion is the bridge from "worms" to "APTs," and it is a very likely short-answer question.
A ransomware attack that spread extremely fast over a period of hours to days, infecting hundreds of thousands of systems belonging to both public and private organizations in more than 150 countries. It spread as a worm, aggressively scanning both local and random remote networks (note: local subnet and random scanning, two of the four strategies at once), attempting to exploit a vulnerability in the SMB file sharing service on unpatched Windows systems. Its rapid spread was only slowed by the accidental activation of a "kill-switch" domain by a UK security researcher. Once installed, it also encrypted files and demanded a ransom payment to recover them.
WannaCry is the example that refuses to fit in one box: worm propagation, ransomware payload. That is the point of the two-axis classification.
Five properties characterise modern worm technology, and each one is a defence being defeated:
| Property | What it defeats |
|---|---|
| Multiplatform | "We don't run Windows, so we're fine." |
| Multi-exploit | Patching the one vulnerability you heard about. |
| Ultrafast spreading | Human-speed incident response. |
| Polymorphic | Byte-signature scanning. |
| Metamorphic | Behavioural signatures as well. |
NIST SP 800-28 defines mobile code as "programs that can be shipped unchanged to a heterogeneous collection of platforms and executed with identical semantics." It is transmitted from a remote system to a local system and then executed locally. It often acts as a mechanism for a virus, worm, or Trojan horse, and takes advantage of vulnerabilities to perform its own exploits.
| Popular vehicles | Most common malicious uses |
|---|---|
| Java applets | Cross-site scripting |
| ActiveX | Interactive and dynamic websites |
| JavaScript | E-mail attachments |
| VBScript | Downloads from untrusted sites or of untrusted software |
Cabir was the first mobile phone worm, in 2004; Lasco and CommWarrior followed in 2005. They communicate through Bluetooth wireless connections or MMS, and the target is the smartphone. They can completely disable the phone, delete data on it, or force the device to send costly messages. CommWarrior is the one to remember in detail: it replicates by Bluetooth to other phones, sends itself as an MMS file to contacts, and sends itself as an auto-reply to incoming text messages — three propagation routes in one piece of malware, two of which (contacts, auto-reply) are topological target discovery.
Exploits browser and plugin vulnerabilities so that when the user merely views a webpage controlled by the attacker, code in the page exploits the browser bug to download and install malware without the user's knowledge or consent. In most cases the malware does not actively propagate as a worm does: it spreads when users visit the malicious page. The propagation mechanism is the web's ordinary traffic.
A variant of drive-by-download used in highly targeted attacks. The sequence is:
The name is the whole explanation: rather than hunting the animal, poison the water hole it comes to.
Places malware on websites without actually compromising them. The attacker pays for advertisements that are highly likely to be placed on their intended target websites and incorporates malware in them; visitors to sites displaying those ads get infected. The malware code may be dynamically generated, either to reduce the chance of detection or to infect only specific systems. It has grown rapidly because it is easy to place on desired websites with few questions asked and hard to track, and because attackers can buy the ads for as little as a few hours — timed to when their intended victims are browsing — which greatly reduces the ads' visibility to defenders.
The reputable site is not compromised, so scanning it finds nothing. The ad is served by a third-party network the site operator does not control, so blocking at the site is not possible. And the ad may be live for hours, so by the time anyone reports it, the evidence is gone. This is the clearest example in the lecture of an attack that exploits a business relationship rather than a software bug.
Also known as a user-interface (UI) redress attack. It is a vulnerability used by an attacker to collect an infected user's clicks. The attacker can force the user to do a variety of things, from adjusting the user's computer settings to unwittingly sending the user to websites that might have malicious code. By taking advantage of Adobe Flash or JavaScript, an attacker can place a button under or over a legitimate button, making it difficult for users to detect. A typical attack uses multiple transparent or opaque layers to trick a user into clicking a button or link on another page when they intended to click on the top-level page — the attacker is hijacking clicks meant for one page and routing them to another.
Using a similar technique, keystrokes can also be hijacked: a user can be led to believe they are typing the password to an e-mail or bank account, but is instead typing into an invisible frame controlled by the attacker.
Social engineering is "tricking" users into assisting in the compromise of their own systems. It is the third propagation mechanism, and the only one that does not require a software vulnerability at all.
| Vehicle | Description |
|---|---|
| Spam | Unsolicited bulk e-mail. A significant carrier of malware, and the delivery mechanism for phishing attacks. |
| Trojan horse | A program or utility containing harmful hidden code, used to accomplish functions that the attacker could not accomplish directly. |
| Mobile phone Trojans | First appeared in 2004 (Skuller); the target is the smartphone. |
Table 6.1 says a Trojan "evades security mechanisms, sometimes by exploiting the legitimate authorizations of a system entity that invokes it." That clause is the important one. The Trojan does not break access control; it borrows yours. When you run it, it runs with your rights — it can read every file you can read. No exploit, no privilege escalation, no bug. This is why access control alone cannot stop a Trojan, and why the "confused deputy" idea from L04a keeps reappearing.
Four payload categories, matching the four actions listed at the start: system corruption, attack agents, information theft, and stealthing.
| Example | Year | What it did |
|---|---|---|
| Chernobyl virus | 1998 | A destructive parasitic memory-resident Windows 95/98 virus. Infects executable files when they are opened. When a trigger date is reached, it deletes data by overwriting the first megabyte of the hard drive with zeroes, causing massive corruption of the entire file system. It also rewrites BIOS code — real-world damage to physical equipment. |
| Klez | October 2001 | Mass-mailing worm infecting Windows 95 through XP. Spreads by e-mailing copies of itself to addresses found in the address book and in files on the system. Can stop and delete some anti-virus programs. On the trigger date, causes files on the hard drive to become empty. |
| Ransomware | 1989 onward | Encrypts the user's data and demands payment for the key needed to recover it. The PC Cyborg Trojan (1989) is the first example. From mid-2006 a number of worms and Trojans appeared using public-key cryptography with increasingly larger key sizes to encrypt data; the user had to pay a ransom or make a purchase from certain sites to receive the decryption key. |
| Stuxnet | 2010 | Targets specific industrial control system software; the lecture notes concerns about using sophisticated targeted malware for industrial sabotage. |
A logic bomb is code embedded in the malware that is set to "explode" when certain conditions are met. It is not a separate kind of malware — it is the trigger component, named in isolation.
Early ransomware used symmetric encryption, which meant the decryption key had to be somewhere on the victim's machine — so researchers could extract it and publish a free decryptor. With public-key crypto, the malware carries only the attacker's public key, encrypts with that, and the matching private key never leaves the attacker. There is nothing on the victim's machine to recover. This is the one slide in the chapter where L03a's RSA pays off directly, and it is a very good exam question: why did ransomware become unrecoverable?
WannaCry is used again as the modern illustration: it infected a large number of systems in many countries in May 2017, encrypted a large number of files, and demanded payment in Bitcoins. Recovery was generally only possible if the organization had good backups and an appropriate incident response and disaster recovery plan. Targets have widened beyond personal computers to mobile devices and Linux servers, and tactics such as threatening to publish sensitive personal information, or to permanently destroy the encryption key after a short period, are increasingly used to increase pressure on the victim to pay.
A bot takes over another Internet-attached computer and uses that computer to launch or manage attacks. A botnet is a collection of bots capable of acting in a coordinated manner. The uses listed are:
The remote control facility is what distinguishes a bot from a worm. A worm propagates itself and activates itself; a bot is initially controlled from some central facility. Memorise this one sentence — it is the cleanest single-line distinction in the chapter.
| Generation of command and control | How it works | Weakness it fixes |
|---|---|---|
| IRC server | Bots join a specific channel on the server and treat incoming messages as commands. | (The original.) Easy to build, but obvious traffic and one server to seize. |
| Covert channels over HTTP | More recent botnets hide commands inside ordinary web traffic. | IRC traffic stands out on a corporate network; HTTP does not. |
| Peer-to-peer protocols | Distributed control mechanisms. | Removes the single point of failure — there is no one server for law enforcement to take down. |
| Mechanism | What it captures |
|---|---|
| Keylogger | Captures keystrokes to allow an attacker to monitor sensitive information. Typically uses a filtering mechanism that only returns information close to keywords such as "login" and "password" — because sending every keystroke home is both enormous and conspicuous. |
| Spyware | Subverts the compromised machine to allow monitoring of a wide range of activity: monitoring the history and content of browsing activity, redirecting certain webpage requests to fake sites, and dynamically modifying data exchanged between the browser and certain websites of interest. |
That last clause describes a man-in-the-browser attack: the data is changed after TLS has decrypted it, so encryption in transit does not help. Worth connecting back to L03a's secure channels — a secure channel protects the wire, not the endpoints.
| Phishing | Spear-phishing |
|---|---|
| Exploits social engineering to leverage the user's trust by masquerading as communication from a trusted source. Typically: include a URL in a spam e-mail linking to a fake website that mimics the login page of a banking, gaming, or similar site; suggest that urgent action is required by the user to authenticate their account; then exploit the account using the captured credentials. | Recipients are carefully researched by the attacker. The e-mail is crafted to specifically suit its recipient, often quoting a range of information to convince the recipient of its authenticity. |
Phishing is broadcast; spear-phishing is aimed. Note how the urgency clause works: it is there to prevent the one behaviour that would defeat the attack, which is stopping to check.
A backdoor (also known as a trapdoor) is a secret entry point into a program allowing the attacker to gain access and bypass the security access procedures. A maintenance hook is a backdoor used by programmers to debug and test programs — i.e. backdoors are not always malicious in origin, which is exactly why they survive into shipped code. It is difficult to implement operating system controls for backdoors in applications, because from the OS's point of view the application is simply doing what applications do.
A rootkit is a set of hidden programs installed on a system to maintain covert access to that system. It hides by subverting the mechanisms that monitor and report on the processes, files, and registries on a computer, and gives administrator (or root) privileges to the attacker, who can then add or change programs and files, monitor processes, send and receive network traffic, and get backdoor access on demand.
The figure has two halves showing the same kernel memory before and after a rootkit named knark is installed.
(a) Normal kernel memory layout. On the left is a tall box labelled system call table, containing entries drawn in order with gaps between them: fork entry, read entry, then a vertical ellipsis, then execve entry, chdir entry. Each entry holds a pointer, drawn as a dot, and an arrow runs from each dot to a box on the right holding the real kernel function: sys_fork(), sys_read(), sys_execve(), sys_chdir(). Four entries, four arrows, four real functions — the table is just an array of function pointers indexed by system-call number.
(b) After knark install. The table looks identical from the outside — same entries, same order. But the arrows from fork entry, read entry, and execve entry now point to a new grey block containing knark_fork(), knark_read(), and knark_execve(). The original sys_fork(), sys_read(), and sys_execve() boxes are still there, untouched, with nothing pointing at them. Only chdir entry still points to its real sys_chdir().
What this buys the attacker, and why this figure is in the chapter:
ls, ps, and a file-integrity scanner all go through read and execve eventually; the rootkit's versions call the real function and then edit the results — removing its own files from directory listings, its own processes from the process list.chdir is left alone because there is nothing to hide there; each hook costs performance and adds a chance of a bug that crashes the kernel and reveals the rootkit.In L04a the system call interface was the trust boundary: user mode asks, kernel mode decides. Figure 6.3 is what happens when the attacker gets to the other side of that boundary. The access control is still working perfectly; it is the reporting that has been subverted. "Security mechanisms you can no longer trust the output of" is a different failure from "security mechanisms that were bypassed."
The ideal solution to the threat of malware is prevention, with four main elements:
| Element | What it means in practice |
|---|---|
| Policy | Rules about what may be installed, run, and connected. |
| Awareness | Training users, because social engineering targets people, not software. |
| Vulnerability mitigation | Patching and hardening, so exploits have nothing to exploit. |
| Threat mitigation | Controls that limit what malware can do if it does arrive. |
If prevention fails, technical mechanisms support three threat mitigation options, in order: detection, identification, removal. (Detection: something is wrong. Identification: it is this. Removal: it is gone. You cannot skip a step — removal requires identification, which requires detection.)
| Generation | Name | How it works | Limitation |
|---|---|---|---|
| First | Simple scanners | Requires a malware signature to identify the malware. | Limited to the detection of known malware; defeated by polymorphism. |
| Second | Heuristic scanners | Uses heuristic rules to search for probable malware instances. Another approach is integrity checking. | False positives; integrity checking can be subverted by a rootkit (see Figure 6.3). |
| Third | Activity traps | Memory-resident programs that identify malware by its actions rather than its structure in an infected program. | The malware must already be running. |
| Fourth | Full-featured protection | Packages combining a variety of anti-virus techniques: scanning and activity-trap components plus access control capability. | Complexity and performance cost. |
The progression is a single idea applied harder: the first generation asks what does it look like, the third asks what does it do, and the fourth adds what is it allowed to do. Each generation exists because the previous one was defeated by a concealment strategy from the virus classification table.
Running potentially malicious code in an emulated sandbox or on a virtual machine. This allows the code to execute in a controlled environment where its behavior can be closely monitored without threatening the security of a real system. It enables the detection of complex encrypted, polymorphic, or metamorphic malware — because however the body is disguised, it must eventually decrypt and run, and then its behaviour is visible.
The most difficult design issue with sandbox analysis is deciding how long to run each interpretation. Run too briefly and you miss malware that waits; run too long and you cannot process the volume. This is also the obvious attacker response: delay, or detect the sandbox and behave.
Integrates with the host operating system and monitors program behavior in real time for malicious action, blocking potentially malicious actions before they have a chance to affect the system. Because it blocks in real time, it has an advantage over detection techniques such as fingerprinting or heuristics.
Limitation: because malicious code must run on the target machine before all its behaviors can be identified, it can cause harm before it has been detected and blocked. Compare with sandbox analysis, which has the same insight — watch the behaviour — but pays for safety with the timing problem instead.
Anti-virus software is typically included in e-mail and Web proxy services running on an organization's firewall and IDS, and may also be included in the traffic analysis component of an IDS. It may include intrusion prevention measures, blocking the flow of any suspicious traffic. The approach is limited to scanning malware.
| Monitor | Where it sits | What it looks for |
|---|---|---|
| Ingress monitors | At the border between the enterprise network and the Internet. | Incoming traffic — one technique is to look for traffic addressed to unused local IP addresses, since legitimate traffic has no reason to go there, but a scanning worm will try them. |
| Egress monitors | At the egress point of individual LANs as well as at the enterprise border. | Outgoing traffic, for signs of scanning or other suspicious behavior. |
Recall local-subnet target discovery: a worm behind the firewall scans its own LAN, so none of that traffic ever crosses the perimeter. An ingress monitor sees nothing. An egress monitor placed at the exit of each LAN sees one machine suddenly talking to hundreds of its neighbours, which is not what a workstation normally does. The slide's phrase "at the egress point of individual LANs as well as at the border" is doing real work: perimeter-only monitoring cannot see an inside-out infection.
| Term | Definition | Example | Memory trick | Similar | Opposite |
|---|---|---|---|---|---|
| Malware | Program inserted covertly to compromise CIA or disrupt the victim | NIST SP 800-83 | Mal- = bad software | Malicious software | Benign software |
| Virus | Replicates into other executable or script code; needs a host | Melissa, Chernobyl | Infects a host cell | Parasitic malware | Worm |
| Worm | Independent program that propagates a working copy of itself over a network | Morris, Slammer | Crawls on its own | Self-contained malware | Virus |
| Trojan horse | Useful-looking program with a hidden malicious function | Skuller (2004) | Gift with soldiers inside | Non-replicating malware | Worm |
| Bot / zombie | Program on an infected machine, activated to attack others | Botnet DDoS | Remote-controlled robot | Agent | Worm (self-activating) |
| Botnet | Collection of bots acting in a coordinated manner | IRC, HTTP, P2P C&C | Net of bots | Zombie army | Single compromised host |
| Remote control facility | Central command channel; the thing that distinguishes a bot from a worm | IRC channel commands | Someone holds the remote | Command and control | Self-activation |
| APT | Persistent, well-resourced, carefully targeted intrusion campaign | Aurora, RSA, APT1, Stuxnet | Advanced, Persistent, Threat | State-sponsored attack | Opportunistic malware |
| Attack kit / crimeware | Toolkit generating new malware from supplied modules | Zeus, Angler | Malware with a GUI | Exploit kit | Hand-written exploit |
| Infection mechanism | How a virus spreads | Copy into Normal template | The vector | Infection vector | Payload |
| Trigger | Condition that activates the payload | Minute = day of month | Pulling the trigger | Logic bomb | Infection mechanism |
| Payload | What the malware does besides spreading | Encrypt files and demand ransom | The cargo | Warhead | Propagation |
| Logic bomb | Dormant code that fires when a predefined condition is met | Trigger date deletes files | Bomb on a timer | Trigger | Immediate payload |
| Macro virus | Virus in document macro or scripting code | Melissa in Word | Lives in the document | Scripting virus | File infector |
| Boot sector infector | Infects the master boot record; spreads at boot | Boots before the OS | Infects the ignition | File infector | Macro virus |
| Multipartite virus | Infects files in multiple ways | Boot sector plus executables | Multiple parts | Hybrid | Single-vector virus |
| Encrypted virus | Encrypts its own body under a random key | New key each infection | Hides its body | Polymorphic | Plaintext virus |
| Stealth virus | Explicitly designed to hide from anti-virus software | Hooks file reads | Hides from the scanner | Rootkit technique | Noisy malware |
| Polymorphic virus | Mutates with every infection (appearance changes) | New decryptor per copy | Many shapes | Metamorphic | Fixed signature |
| Metamorphic virus | Rewrites itself completely; behavior may change too | Recompiles itself | Changes form entirely | Polymorphic | Static malware |
| Scanning / fingerprinting | First function in a network worm's propagation phase | Probing IP addresses | Looking for doors | Target discovery | Passive spread |
| Hit-list scanning | Pre-built list of vulnerable machines, split among infected hosts | Very short scanning period | Guest list, not door-knocking | Topological | Random scanning |
| Topological scanning | Uses information on the victim to find more hosts | Address book, known_hosts | Follow the contacts | Hit-list | Random scanning |
| Local subnet scanning | Infected host scans its own LAN from behind the firewall | WannaCry on SMB | Attack from inside the wall | Topological | Internet-wide scanning |
| Worm propagation model | Logistic curve: slow start, fast spread, slow finish | Figure 6.2 | S-curve epidemic | Epidemic model | Linear growth |
| Mobile code | Programs shipped unchanged across platforms, executed with identical semantics | JavaScript, ActiveX, applets | Code that travels | Active content | Native binary |
| Drive-by-download | Browser/plugin exploit that installs malware on page view | Compromised website | Infected by driving past | Watering hole | User-installed Trojan |
| Watering-hole attack | Targeted drive-by on a site the intended victims visit | Only infects the target org | Poison the water hole | Drive-by | Broadcast attack |
| Malvertising | Malware delivered through paid ads, without compromising the site | Ads bought for a few hours | Malicious advertising | Drive-by | Site compromise |
| Clickjacking | UI redress attack using transparent layers to steal clicks | Invisible button over a real one | Hijacking the click | Keystroke hijacking | Honest UI |
| Spear-phishing | Phishing crafted for a researched individual | Quotes a real project | A spear, not a net | Phishing | Bulk spam |
| Keylogger | Captures keystrokes, usually filtered around keywords | "login", "password" | Logs the keys | Spyware | — |
| Spyware | Monitors browsing, redirects requests, modifies exchanged data | Man-in-the-browser | Spies on everything | Keylogger | — |
| Ransomware | Encrypts data and demands payment for the key | PC Cyborg 1989; WannaCry 2017 | Kidnaps your files | System corruption | Data theft |
| Backdoor / trapdoor | Secret entry point bypassing security access procedures | Maintenance hook | Door round the back | Rootkit component | Authenticated access |
| Rootkit | Hidden programs maintaining covert root access by subverting monitoring | knark, Figure 6.3 | Kit for keeping root | Stealth virus | Visible malware |
| Kernel-mode rootkit | Rootkit operating inside the kernel, e.g. hooking the system call table | knark_read() | Owns the referee | VM-based rootkit | User-mode rootkit |
| Sandbox analysis | Running suspect code in an emulator or VM to observe behavior | Detects polymorphic malware | Play in the sandbox | Dynamic analysis | Signature scanning |
| Behavior-blocking software | Host software that blocks malicious actions in real time | Blocks before damage | Stops the action, not the file | Activity trap | Signature scanner |
| Ingress monitor | Border monitor watching incoming traffic, e.g. to unused IPs | Perimeter IDS | In-gress = coming in | Egress monitor | — |
| Egress monitor | Monitor at each LAN's exit watching outgoing traffic for scanning | Detects inside-out infection | E-gress = going out | Ingress monitor | — |
Malware is any program inserted covertly to compromise confidentiality, integrity, or availability (NIST SP 800-83), and it is classified first by how it propagates — infect existing content (virus), exploit a vulnerability (worm, drive-by-download), or trick the user (social engineering, Trojan) — and then by its payload: corrupt, conscript into a botnet, steal, or hide. A virus needs a host and has three components (infection mechanism, trigger, payload) and four phases (dormant, propagation, triggering, execution); it is classified by target (boot sector, file, macro, multipartite) and by concealment (encrypted, stealth, polymorphic = changes appearance, metamorphic = rewrites itself and may change behaviour). A worm is independent, replicates over five routes, and finds targets by random, hit-list, topological, or local-subnet scanning; its propagation follows an S-curve with slow start, fast spread, slow finish, which is why detection must happen while there is almost no evidence. Morris (1988) started it, Slammer (2003) perfected speed, Stuxnet (2010) deliberately slowed down to stay hidden, and WannaCry (2017) combined worm propagation with a ransomware payload. Client-side propagation adds mobile code, drive-by-downloads, watering holes, malvertising, and clickjacking. Payloads run from system corruption (Chernobyl, Klez, ransomware made unrecoverable by public-key crypto) through bots — distinguished from worms purely by the remote control facility, which moved from IRC to HTTP to peer-to-peer to kill the single point of failure — to information theft (keyloggers, spyware, phishing and spear-phishing) and stealthing (backdoors and rootkits, where Figure 6.3 shows a kernel rootkit swinging system-call-table pointers at its own functions so every tool on the box reports a clean system). Countermeasures start with prevention (policy, awareness, vulnerability mitigation, threat mitigation) and fall back to detection, identification, removal via four generations of anti-virus (signature scanners → heuristics and integrity checking → activity traps → full-featured packages), sandbox analysis (whose hard problem is how long to run), host-based behavior blocking (whose weakness is that the code must run first), and perimeter ingress and egress monitors.
Network security I: computer networks
Source: L06b-Ch04-NetworkSecurity-I.pdf — 143 slides, "Computer Networks," dated 28 May 2026. Roughly the first half is a networking refresher; the second half is the attack catalogue built on top of it.
This deck has an unusual shape and it is worth knowing that before you start. Slides 1–83 teach you how the Internet works, bottom to top: link, network, transport, application. Slides 84–143 then go back through the same stack and show that almost every one of those protocols was designed with no authentication at all. ARP believes any reply. IP believes any source address. DNS believes any response with the right 16-bit number. TCP believes any packet in the right sequence window. The attacks are not clever exploits of bugs; they are the protocols working exactly as specified.
From L01a: confidentiality, integrity, availability — this lecture is mostly about the third. From L03a: a MAC or signature gives you origin authentication, which is precisely what every protocol below is missing; DNSSEC is that fix applied to DNS. From L05: Slammer and Code Red were buffer overflows, and here you see what they did with the network once inside. From L06a: worm scanning, botnets, and the remote control facility — Mirai at the end of this deck is the botnet chapter made concrete.
| Circuit switching | Packet switching |
|---|---|
| Legacy phone network | The Internet |
| A single route through a sequence of hardware devices is established when two nodes start communicating | Data is split into packets |
| Data is sent along that route | Packets are transported independently through the network |
| The route is maintained until communication ends | Each packet is handled on a best efforts basis, and packets may follow different routes |
Slides 3–6 are one picture advanced four times. Host A sits on the left with three packets queued beside it, labelled 3 2 1. A mesh of routers (B, C, D) lies in the middle and host F sits on the right. On each successive slide one packet leaves A and appears in a growing list at F: first 1, then 1 2, then 1 2 3. The packets visibly take different paths through the mesh.
Two things the animation is designed to make you notice: the link between A and the first router is free to carry someone else's traffic between packet 1 and packet 2 (that is the efficiency argument), and because the packets travelled different routes, nothing guarantees they arrive in the order they were sent (that is the bill you pay, and the reason the transport layer exists).
| Advantages | Disadvantages |
|---|---|
| No wasted bandwidth — links are not reserved during idle periods. (The slide itself adds "not entirely true.") | No guaranteed bandwidth, making it harder to build applications requiring QoS. |
| Multiplexing: frequency, time, and statistical. | Per-packet overhead: every packet needs a header with source and destination addresses. |
| Service: more connections of lesser quality; no blocking of users. | Complex end-to-end control: packets can be lost, corrupted, or delivered out of order. |
| Adaptation: can adapt to network congestion and failures. | Delay and congestion: with no congestion control, arbitrary delays and packet drops. |
Two reasons the slide is hedging. First, every packet carries a header, so some of the bandwidth you "saved" goes to addressing — a 40-byte TCP/IP header on a 40-byte payload is 50% overhead. Second, when demand exceeds capacity, packets are dropped and retransmitted, so the bandwidth used to carry the dropped copy was wasted outright. Circuit switching wastes capacity predictably during silence; packet switching wastes it unpredictably during congestion.
The picture shows three pairs of senders and receivers sharing the same physical link, and a switch multiplexing packets from the different senders into one packet stream.
| Method | How the picture is drawn | How it divides the link |
|---|---|---|
| Time Division Multiplexing (TDM) | A single horizontal time axis carrying the repeating sequence S1 S2 S3 S1 S2 S3 … | Each sender gets the whole link for a fixed, repeating slot. A silent sender's slot is still reserved and goes empty. |
| Frequency Division Multiplexing (FDM) | Frequency on the vertical axis, time on the horizontal; three horizontal bands f1, f2, f3 each occupied continuously by S1, S2, S3 | Each sender gets its own slice of the spectrum, all the time. Again, an idle sender's slice is wasted. |
| Statistical multiplexing | (Named in the advantages list rather than drawn.) | Capacity is handed out on demand, packet by packet. This is what packet switching actually does, and it is the only one of the three in which an idle sender costs nothing. |
| # | Internet (TCP/IP) — 5 layers | What it does | Examples | OSI equivalent |
|---|---|---|---|---|
| 5 | Application | Supporting network applications | FTP, SMTP, HTTP | Application (7), Presentation (6), Session (5) |
| 4 | Transport | Process-to-process data transfer | TCP, UDP | Transport (4) |
| 3 | Network | Routing of datagrams from source to destination | IP, routing protocols | Network (3) |
| 2 | Link | Data transfer between neighbouring network elements | PPP, Ethernet | Data Link (2) |
| 1 | Physical | Bits "on the wire" | Copper, fibre, radio | Physical (1) |
The slide asks: "anything missing?" The answer is OSI's presentation and session layers. The Internet stack does not have them; whatever they would have done (character encoding, compression, encryption, session management) is left to the application. That is why TLS is "an application-layer thing" from IP's point of view, and why each application reinvents its own session handling.
Hold on to the three-part definition — format, order, actions — because every attack later in the deck is a message that is correctly formatted, arrives in a plausible order, and therefore gets the standard action applied to it. Nothing in the definition says anything about where a message came from.
The figure shows a message travelling down the sending host's stack, across a switch and a router, and back up the receiving host's stack. On the source side, each layer prepends its own header:
| Layer | What the box contains | Name of the unit |
|---|---|---|
| Application | M | message |
| Transport | Ht M | segment |
| Network | Hn Ht M | datagram |
| Link | Hl Hn Ht M | frame |
| Physical | bits | — |
In the middle of the figure, the switch only goes up as far as the link layer — it receives Hl Hn Ht M, reads the link header, and sends it on. The router goes one layer higher: it strips the frame header, looks at Hn Ht M, makes a routing decision, and builds a new frame for the next hop. At the destination the headers come off in reverse order until the application gets back its original M.
A second slide draws the same idea with named fields: an application packet, wrapped in a TCP header + TCP data, wrapped in an IP header + IP data, wrapped in a frame header + frame data + frame footer (the frame is the only one with a trailer, because that is where the Ethernet checksum lives).
A router rewrites the link-layer header at every hop but leaves the IP header alone. That is why MAC addresses are hop-local and IP addresses are end-to-end — and why a MAC address is meaningless for identifying a remote attacker, while a source IP address is attacker-controlled and therefore meaningless for identifying anybody at all.
A diagram shaped like an hourglass: at the top, applications (FTP, HTTP, TFTP, DNS); below them two transports, TCP and UDP; below those a single narrow waist, IP; and below that, many link technologies (Ethernet, FDDI, …). The waist is the point: everything runs over IP, and IP runs over everything. One universal, minimal network layer is what lets a new application work on a network technology that did not exist when the application was written.
| Part of the structure | What lives there |
|---|---|
| Network edge | Applications and hosts. End systems run application programs (Web, email) at the "edge of the network." |
| Access networks, physical media | Wired and wireless communication links connecting the edge to the core. |
| Network core | A mesh of interconnected routers; a network of networks. |
Two models at the edge: client/server, where a client host requests and receives service from an always-on server (Web browser/server, email client/server), and peer-to-peer, with minimal or no use of dedicated servers (Skype, BitTorrent).
The fundamental question the slide poses is "how is data transferred through the net?" — circuit switching (a dedicated circuit per call, as in the telephone network) or packet switching (data sent through the net in discrete chunks). The packet-switching slide spells out the consequences, and the professor's handwriting strikes a red circle-and-slash through the bottom third of it:
| Packet switching does this | …and pays this price (resource contention) |
|---|---|
| Each end-to-end data stream is divided into packets | Aggregate resource demand can exceed the amount available |
| User A's and user B's packets share network resources | Congestion: packets queue, waiting for link use |
| Each packet uses the full link bandwidth | Store and forward: packets move one hop at a time; a node receives the complete packet before forwarding it |
| Resources are used as needed |
The crossed-out box reads "bandwidth division into pieces / dedicated allocation / resource reservation" — i.e. everything circuit switching does, which packet switching explicitly does not do. The red slash is the professor's way of saying: those three phrases are the wrong answer on a packet-switching question.
Different links have different transmission ranges because of signal attenuation.
Note the security implication the slide does not say out loud: a shared wireless medium means every frame is receivable by anyone in range, with no physical access to a cable required. Everything in the ARP-spoofing section becomes much easier on wireless.
Link layer topics: medium access control, MAC address, Ethernet, error detection, reliable transmission.
The context figure shows the full stack on the left, the frame Hl Hn Ht M crossing a physical link in the middle via the data link protocol, and the stack on the right receiving it — with the whole link layer implemented on the adapter card (the NIC), not in the operating system proper.
Many nodes attached to the same link: Ethernet, token rings, wireless networks (WiFi). The problem is who gets to send a frame, because multiple senders lead to a collision. The general solution named is multiple access with collision detect (CSMA/CD).
The attributes of the channel allocation problem, as listed: dynamic or static allocation; a single channel is available; time is continuous versus slotted; carrier sensing (CS); collision detection (CD).
Topologies drawn: a bus LAN (coaxial cable with everything tapped onto it), a ring LAN, a star (a switch in the centre), and shared RF (802.11 WiFi).
A collision domain is a region in which any host hears any other host. It can be a single segment, multiple segments connected by repeaters, or multiple segments connected by a hub. All these hosts are competing for access to the same link, and as a consequence are said to be in the same collision domain. A switch breaks a collision domain into separate ones; a hub does not, because a hub just repeats every bit to every port.
Step 2 is the carrier sense; step 4 is the collision detect. The jam signal exists so that every other station on the segment learns about the collision too, rather than only the two that collided.
Bus topology was popular through the mid-90s; today star topology prevails, with an active switch in the centre and each "spoke" running a separate Ethernet protocol. Wireless LANs are 802.11.
Each adapter on a LAN has a unique LAN address (and also has an IP address). The LAN (or MAC, or physical) address is used to get a datagram from one interface to another physically connected interface on the same network. It is a 48-bit MAC address for most LANs, burned into the adapter ROM.
Every host has a unique 48-bit Ethernet address. The slide works one example all the way through:
00001000 00000000 00101011 11100100 10110001 00000010
08 : 00 : 2b : e4 : b1 : 02
Each group of eight bits becomes two hex digits — 00001000 is 0000=0 and 1000=8, hence 08. (This is the binary-to-hex conversion from L00, used for real.) Three rules follow:
FF:FF:FF:FF:FF:FF) is the broadcast address. An address whose first bit is 1 is a multicast address.The slide also poses a question worth being able to answer: why separate MAC and IP addresses? Because they answer different questions. The MAC address is burned in and belongs to the hardware, so it cannot change when you move the machine — which makes it useless for routing, since routing needs addresses that reflect where you are. The IP address is assigned by the network you are attached to and so encodes location; that is what makes hierarchical routing possible. One identifies the box, the other identifies the place.
The question ARP answers: how do I determine the MAC address of B, knowing B's IP address? Each IP node (host or router) on a LAN has an ARP table holding IP/MAC mappings for some LAN nodes, each entry of the form <IP address; MAC address; TTL>, where the TTL is the time after which the mapping will be forgotten — typically 20 minutes.
FF-FF-FF-FF-FF-FF. All machines on the LAN receive the query.ARP is "plug-and-play": nodes create their ARP tables without intervention from a network administrator. That convenience is the vulnerability — see ARP spoofing.
The figure shows two IP networks joined by one router R. On the left network 111.111.111.x: host A at 111.111.111.111 with MAC 74-29-9C-E8-FF-55, another host at 111.111.111.112 with MAC CC-49-DE-D0-AB-7D, and R's left interface at 111.111.111.110 with MAC E6-E9-00-17-BB-4B. On the right network 222.222.222.x: R's right interface at 222.222.222.220 with MAC 1A-23-F9-CD-06-9B, host B at 222.222.222.222 with MAC 49-BD-D2-C7-56-2A, and another host at 222.222.222.221 with MAC 88-B2-2F-54-1A-0F. R has two ARP tables, one for each IP network. The slide carries a hand-added note: "This is a really important example — make sure you understand!"
Assume A knows B's IP address. The seven steps:
111.111.111.110.While the Ethernet frame leaving A has the router's MAC address as its destination, the IP datagram inside it still has B's IP address as its destination. Two destination addresses in the same packet, meaning different things: the MAC address says "next hop," the IP address says "final destination." Every hop rewrites the first and preserves the second. If you can say that sentence, you can answer almost any question about how a packet crosses a network.
How many ARP request/reply pairs does the walkthrough need, on a cold start? Two. A→R resolves 111.111.111.110 on the left LAN; R→B resolves 222.222.222.222 on the right LAN. A never ARPs for B, and cannot — the ARP broadcast would never leave the left LAN, because a router does not forward broadcasts. That is also why A must be configured with a default gateway: it has to know which local IP address to ARP for when the destination is off-net.
The network layer transports a segment from the sending to the receiving host. On the sending side it encapsulates segments into datagrams; on the receiving side it delivers segments to the transport layer. Network-layer protocols are in every host and every router, and a router examines header fields in all IP datagrams passing through it.
The slide labelled "Network service model" is marked CRUCIAL question! — what service model should the "channel" transporting packets from sender to receiver provide? Guaranteed bandwidth? Preservation of inter-packet timing (no jitter)? Loss-free delivery? In-order delivery? Congestion feedback to the sender? The most important abstraction provided by the network layer is the choice between a virtual circuit and a datagram service. The Internet chose datagram.
The delivery service of IP is minimal. IP provides an unreliable, connectionless, best-effort service, also called datagram service.
The deck states the same thing again later as a yes/no list:
| IP does | IP does not |
|---|---|
| Routing: if a host knows the destination host's IP, route the packet to it. | Everything else. No ordering. No retransmission. No (real) error checking. No acknowledgement of receipt. No "connections." No security. Just packets. |
| Fragmentation and reassembly: split data into packets and reassemble. | |
| Error reporting: maybe, if you're lucky, tell the source it dropped your packet. |
An IP address is a 32-bit identifier for a host or router interface. An interface is the connection between a host or router and a physical link; routers typically have multiple interfaces, and a host may have multiple interfaces. IP addresses are associated with an interface, not with a host or router.
The worked conversion on the slide:
223.1.1.1 = 11011111 00000001 00000001 00000001
223 1 1 1
An IP address has a network part (high-order bits) and a host part (low-order bits). The slide's question — what is a network, from the IP address perspective? — is answered: device interfaces with the same network part of their IP address, which can physically reach each other without an intervening router. The example picture has a network of three IP networks (223.1.1.x, 223.1.2.x, 223.1.3.x), where for addresses starting with 223 the first 24 bits are the network address.
| Forwarding | Routing | |
|---|---|---|
| What it is | Move packets from a router's input to the appropriate router output | Determine the route taken by packets from source to destination |
| Plane | Data plane | Control plane |
| Who does it | An individual router, using a forwarding table | Routers talking amongst themselves; each router then creates its forwarding table |
| Road-trip analogy | Getting through a single interchange | Planning the trip from source to destination |
| Timescale | Per packet, nanoseconds | Per topology change, seconds to minutes |
The interplay figure shows a routing algorithm feeding a local forwarding table that maps header values to output links (0100→3, 0101→2, 0111→2, 1001→1); an arriving packet with header value 0111 is sent out link 2.
The header "instructs routers and hosts what to do with a packet," and all values are filled in by the sending host. The figure is a grid, 32 bits wide, with byte offsets down the left side and bit numbers 0–31 along the bottom; a brace on the right marks the first 20 bytes as the fixed header, and IHL (Internet Header Length) as the measure of the full length including options.
| Byte offset | Fields in that 32-bit word |
|---|---|
| 0 | Version · IHL (header length) · Type of Service (TOS) · Total Length |
| 4 | Identification · IP Flags (x, D, M) · Fragment Offset |
| 8 | Time To Live (TTL) · Protocol · Header Checksum |
| 12 | Source Address (32 bits) |
| 16 | Destination Address (32 bits) |
| 20 | IP Option — variable length, optional, not common |
The deck then singles out three fields on their own slides, and the wording matters:
The client is trusted to embed the correct source IP, and it is easy to override using lower-level network sockets. Libnet is named as a library for formatting raw packets with arbitrary IP headers. Anyone who owns their own machine can send packets with an arbitrary source IP, which enables:
Spoofing only works for attacks that do not need the reply. That single limitation explains a surprising amount of the rest of the deck: it is why TCP connection spoofing is hard (you need the server's sequence number, which comes back to the real owner of the address), why amplification attacks use UDP (no handshake to complete), and why GET floods cannot use random source IPs (they must complete a real TCP connection).
The transport layer is responsible for the realization of host-to-host packet delivery. Its services:
Why we need it at all is the mirror image of that list — the limitations of the network layer: it can drop messages (routers may incur buffer overflow), reorder messages, deliver duplicate copies of the same message, and limit messages to a finite size (IP's size is limited). The network layer is intended to hide the different technologies at link level and provide a transparent end-to-end routing service; the transport layer provides an end-to-end transport service to the application layer.
| Internet transport protocol | What it gives you |
|---|---|
| UDP — datagram messaging service | "Best-effort" IP end-to-end service, with ports added. |
| TCP — reliable, in-order delivery | Connection set-up; discarding of corrupted packets; retransmission of lost packets; flow control; congestion control. |
| RPC — remote procedure call | Request/reply service. |
The slide asks parenthetically, "congestion control — what is the difference?" Flow control protects the receiver from being sent data faster than it can take it; congestion control protects the network from being sent data faster than it can carry it. Same symptom, different victim, different feedback signal: flow control is told the receiver's window explicitly; congestion control has to infer the network's state from loss and delay.
UDP is a process-to-process communication service. Processes are identified by incoming ports (sockets in Windows). There are 16 bits for each field, yielding 64K different identifiers, and the <IP, port> combination allows de-multiplexing at the receiving host. The "UDP message queue" figure shows the obvious consequence: one queue per bound port, and the kernel drops the datagram if nobody is listening.
Applications that use UDP, and why:
| Application | Reasons given |
|---|---|
| Simple query protocols like DNS | Delay for connection establishment is too large; queries are small and UDP adds only a small header overhead; it is easier to have the application retransmit if needed; a query usually fits within a single UDP packet, so there is no out-of-order danger. |
| Multimedia applications | Retransmitting lost or corrupted packets is not worthwhile — by the time the packet is retransmitted it is too late. Telephone calls, video conferencing, gaming. Certain loss is acceptable since voice and picture are still discernible. |
| Property | What it means |
|---|---|
| Connection oriented | Explicit set-up and tear-down of the TCP session. |
| Stream-of-bytes service | Sends and receives a stream of bytes, not messages. (So "one send = one receive" is not guaranteed; message boundaries are the application's problem.) |
| Reliable, in-order delivery | Checksums to detect corrupted data; acknowledgments and retransmissions for reliable delivery; sequence numbers to detect losses and data reordering. |
| Flow control | Prevents overflow of the receiver's buffer. |
| Congestion control | Adapts to network congestion "for the greater good." |
The deck asks a good question: does TCP duplicate lower-layer services? Both use a sliding-window algorithm with ACKs, NACKs, retransmissions, timeouts, and ordering. Four differences:
Why are packets lost? Limited storage, so packets are discarded in congestion; outages, which eventually reroute around the failure (hopefully in seconds); and drops at the end system, e.g. on the NIC.
ARQ (automatic request repeat): the sender puts sequence numbers on packets; the receiver positively or negatively acknowledges correct receipt; the sender starts a logical timer for each packet and retransmits on timeout. (The slide asks "why" sequence numbers — because without them the receiver cannot tell a retransmission from new data, and the sender cannot tell which packet an ACK refers to.)
Two time-sequence charts side by side, each with a sender timeline on the left and a receiver timeline on the right, blue arrows going right for data and green arrows going left for ACKs. The professor has circled the lost messages in red.
(c) lost ACK. Sender sends pkt0; receiver receives it and sends ACK0; sender gets ACK0 and sends pkt1; receiver receives pkt1 and sends ACK1 — but that ACK is lost (marked with a red X). The sender times out and resends pkt1. The receiver gets it, detects the duplicate, discards the data, and re-sends ACK1. Now the sender gets ACK1 and moves on to pkt0.
(d) premature timeout. Same start, but this time nothing is lost — the sender's timer simply fires too early, before ACK1 arrives. The sender resends pkt1; the receiver detects the duplicate and ACKs again; both the original and the duplicate ACK arrive, and the extra ACK is ignored. Nothing breaks, but the channel carried two useless packets.
The lesson both halves teach: the sender cannot distinguish "my packet was lost" from "the ACK was lost" from "I was impatient." All three look identical — silence. Because the only response available is to retransmit, the protocol must make duplicates harmless, which is exactly what the alternating sequence numbers are for. This is also why premature timeouts make congestion worse rather than better, which motivates the congestion control that follows.
The alternative to retransmission: add redundancy to recover from losses. The figure shows an original file of n blocks going through encoding into a potentially infinite number of blocks, passing through a lossy channel, and decoding once the receiver eventually has n(1+ε) blocks — slightly more than the original count, but it does not matter which ones. No feedback channel is needed and no retransmission ever occurs, which is why this approach suits one-way and real-time links where an ARQ round trip would be too slow.
| End-to-end congestion control | Network-assisted congestion control |
|---|---|
| No explicit feedback from the network. Congestion is inferred from end-system observed loss and delay. This is the approach taken by TCP. | Routers provide feedback to end systems: either a single bit indicating congestion (SNA, DECbit, TCP/IP ECN, ATM), or an explicit rate the sender should send at. |
TCP congestion control works by "probing" for usable bandwidth: ideally transmit as fast as possible — make Congwin as large as possible — without loss; increase Congwin until loss occurs (congestion); on loss, decrease Congwin and begin probing upward again. There are two phases, slow start and congestion avoidance, and the important variables are Congwin (the congestion window) and threshold, which defines the boundary between the slow-start phase and the congestion-control phase.
TCP's congestion control is voluntary. It lives in the sender's own stack, and nothing in the network enforces it. A well-behaved host backs off when it sees loss; an attacker's host simply does not. Every flooding attack later in this deck is, at bottom, a host declining to run the algorithm everyone else runs.
Client on the left, server on the right, three diagonal arrows between them. The state labels are the part to memorise:
| # | Message | Client state after | Server state after |
|---|---|---|---|
| 1 | Client → Server: SYN, seq = 100 | SYN-SENT | SYN-RECEIVED |
| 2 | Server → Client: SYN-ACK, seq = 200, ack = 101 | ESTABLISHED | SYN-RECEIVED |
| 3 | Client → Server: ACK, seq = 101, ack = 201 | ESTABLISHED | ESTABLISHED |
Check the arithmetic, because exams ask for it: the server's ack = 101 is the client's seq + 1, because the SYN flag consumes one sequence number even though it carries no data. Likewise the client's final ack = 201 is the server's seq + 1. After step 2 the client is ESTABLISHED but the server is not — it is still waiting for the third packet, holding state for a connection that may never complete. That asymmetry is the entire SYN flood attack, so note it now.
To close: send a packet with the FIN flag set; it must have the ACK flag with a valid sequence number. The peer receiving the FIN acknowledges receipt with an ACK. The FIN "consumes" one byte of sequence number (just as SYN does). Eventually the other side sends its own packet with FIN set, which terminates the session.
TCP is designed to handle the possibility of spurious TCP packets, e.g. from previous connections. Packets that are invalid given the current state of the session generate a reset: if a connection exists it is torn down, and a packet with the RST flag is sent in response. If a host receives a TCP packet with the RST flag, it tears down the connection.
Can we impersonate another host when initiating a connection? An off-path attacker can send the initial SYN to the server, but cannot complete the three-way handshake without seeing the server's sequence number — the SYN-ACK goes to the real owner of the spoofed address. If the initial sequence number is chosen uniformly at random, the attacker has a 1 in 232 chance of guessing right.
Can we reset an existing TCP connection? That is easier, for three reasons the slide lists:
Spoofing a new connection costs you 232 guesses on the sequence number alone. Resetting an existing one costs 216+32/W. With W = 64K = 216, that is 248/216 = 232 — the same order, but now you may also know the server port (knock 216 off) and may be able to guess the client port range, which brings a blind reset into the realm of a few million packets rather than billions. The security lesson is general: any protocol that accepts a range of valid values instead of one exact value has multiplied the attacker's chances by the size of that range. A large receive window is a performance optimisation that quietly costs security.
This is the deck's synthesis slide — a user types a URL and the browser downloads a page. Four slides trace every protocol involved, and it is the best single revision exercise in the lecture because it touches every layer at once.
gaia.cs.umass.edu at 128.119.30.30.Count the protocols that had to work for one page load: DHCP, DNS, UDP, ARP, Ethernet, IP, TCP, HTTP, plus the routing protocols (OSPF/RIP/IS-IS/BGP) that built the tables, plus SNMP counting along the way. Of those, in their original form, only one has any authentication at all. That is the setup for the rest of the lecture.
Application-layer protocols (and people) usually refer to an Internet host by host name, e.g. google.com. DNS is a delegatable, hierarchical name space. A DNS server has a set of records it authoritatively knows about.
The slide shows real dig output, which is worth being able to read line by line:
$ dig bob.ucsd.edu
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 30439
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 3, ADDITIONAL: 6
;; QUESTION SECTION:
;bob.ucsd.edu. IN A
;; ANSWER SECTION:
bob.ucsd.edu. 3600 IN A 132.239.80.176
;; AUTHORITY SECTION:
ucsd.edu. 3600 IN NS ns0.ucsd.edu.
ucsd.edu. 3600 IN NS ns1.ucsd.edu.
ucsd.edu. 3600 IN NS ns2.ucsd.edu.
id: 30439 is the query ID — remember this number, it is the only thing authenticating the response.aa in the flags means authoritative answer.QUERY: 1, ANSWER: 1, AUTHORITY: 3, ADDITIONAL: 6 name the four sections of a DNS packet. The six additional records are not shown — and the additional section is where cache poisoning lives.3600 is the TTL in seconds, i.e. one hour. A maps a name to an IPv4 address; NS names an authoritative server for a zone.There are 13 main DNS root servers, each named with a letter from A to M.
A 12-byte header grid in the same style as the IPv4 header:
| Byte offset | Fields |
|---|---|
| 0 | Identification (16 bits) · QR · Opcode · DNS Flags (AA, TC, RD, RA, Z, AD, CD) · RCode |
| 4 | Total Questions · Total Answers |
| 8 | Total Authority Resource Records · Total Additional Resource Records |
Summary of the three facts the slide wants: DNS requests are sent over UDP; there are four sections (questions, answers, authority, additional records); and the query ID is a 16-bit random value that links the response to the query.
Any host on the LAN can send ARP requests and replies, so any host can claim to be another host on the local network. This is ARP spoofing, and it allows any host X to force IP traffic between any two other hosts A and B to flow through X — a man-in-the-middle:
Why it works, stated four different ways across two slides:
Before. Two machines, 192.168.1.1 with MAC 00:11:22:33:44:01 and 192.168.1.105 with MAC 00:11:22:33:44:02, exchange data directly. Each holds the other's correct mapping in its ARP cache.
After. A third machine appears, 192.168.1.106 with MAC 00:11:22:33:44:03. It sends two gratuitous replies: to the first machine, "192.168.1.105 is at 00:11:22:33:44:03"; to the second, "192.168.1.1 is at 00:11:22:33:44:03." Both caches now map the other party's IP to the attacker's MAC, and both data arrows bend through the attacker. Note that the IP addresses in the caches are still correct — nothing an IP-level tool checks has changed.
Three demo slides build the attack on a LAN 192.168.1.x with Alice (client, .10, MAC 00:0A:E4:2E:9B:11), Bob (server, .100, MAC 00:0A:E4:3B:47:7E), and the attacker (.1, MAC 00:22:64:34:60:88), all plugged into a switch.
adduser. The slide points out the expectation: "in a switched network, packets are sent only to the destination computer; one would think that another computer plugged into the switch cannot sniff traffic."arpspoof <victim ip> <gateway ip> in each direction. The attacker now acts as a router between them.dsniff -n extracts the credentials used to log in to the telnet service.The conclusion the demo is built to deliver: a switch is not a security boundary. It separates collision domains, not trust domains. The only reason the password was readable is the protocol in the middle — which brings us to telnet.
Telnet (RFC 854) is a protocol providing general, bi-directional, not encrypted communication. It is a generic TCP client: it allows a computer to connect to another, provides remote login, sends whatever you type, prints whatever comes back, and is useful for testing ASCII-based TCP servers. Everything including the password travels in cleartext — which is why SSH replaced it, and why telnet survives only as a debugging tool.
Wireshark is a packet sniffer and protocol analyzer that captures and analyzes frames and supports plugins. It usually requires administrator privileges. Setting the network interface in promiscuous mode captures traffic across the entire LAN segment, not just frames addressed to the machine — which is exactly the "each frame is received by EVERY adaptor" fact from the Ethernet addressing slide, with the normal discarding turned off. It is freely available from wireshark.org. The UI figure labels five regions: the menu, the main toolbar, the filter toolbar, the packet list pane, the packet details pane, the packet bytes pane, and the status bar.
The second demo is availability rather than confidentiality. Two hosts 192.168.1.101 and 192.168.1.102 are on a switch wired with a cable loop. A single ping provokes an ARP request, which is a broadcast; the loop causes that broadcast to be forwarded around the loop forever and multiplied at every pass — a broadcast storm that saturates the segment. The slide ends with the exam question: "How can it be avoided?" The standard answer is the Spanning Tree Protocol, which detects loops and disables redundant links so the active topology is always loop-free; broadcast storm control and rate-limiting on switch ports are the complementary mitigations.
BGP (Border Gateway Protocol) is the protocol that allows routers to exchange information about their routing tables. Each router announces what it can route to all of its neighbours, and every router maintains a global table of routes. There is no authentication of those announcements — the same design flaw as ARP, at Internet scale.
208.65.153.0/24.Why a more specific prefix wins: BGP prefers the longest matching prefix, so a /24 announcement beats the legitimate shorter announcement covering the same space, everywhere the hijacked route reaches. And note the shape of the incident — the originator made a local decision (blocking YouTube domestically), and the absence of validation one hop upstream turned it into a global outage. Trust in BGP is transitive by default, which is exactly what "every router maintains a global table of routes" implies.
Why DNS is worth attacking: users and hosts trust the host-address mapping provided by DNS, and that mapping is used as the basis for many security policies — the browser same-origin policy and the URL address bar are the two named. Interception of requests, or compromise of DNS servers, can result in incorrect or malicious responses. If you can decide what bank.com means, you do not need to break any cryptography.
DNS query results include an Additional Records section, which exists to provide records for the anticipated next resolution step. Early servers accepted and cached all additional records provided in a query response — so a malicious server, asked an innocent question, could attach an answer to a question nobody asked.
The illustration follows six numbered steps across a drawing of a targeted name server:
www.BadGuysAreUs.com.www.BadGuysAreUs.com into an IP address.www.paypal.com.www.paypal.com, they are directed to a website that looks like PayPal's but works only to harvest their usernames and passwords.Three things to notice. The attacker never touched PayPal or the victims. The attack is triggered by the victim's own lookup of an attacker-controlled name, so the attacker only has to get one person to load an image. And the poison then serves everyone using that resolver until the TTL expires — and the TTL is chosen by the attacker.
The obvious fix — stop using the additional section, only accept answers from authoritative servers — does not work, because of glue records: non-authoritative records that are necessary to contact the next hop in the resolution chain. (If ns0.ucsd.edu is the nameserver for ucsd.edu, you need its address to ask it anything, and only it can tell you — a loop that glue records break.) They are necessary given the current design of DNS.
The real fix is bailiwick checking: only accept additional records that are for a domain in the original question. Under bailiwick checking, the attack above fails immediately — a server answering about BadGuysAreUs.com has no business supplying a record for paypal.com, and the record is dropped.
Scenario: a DNS client issues a query to a server. The attacker would like to inject a fake reply, and crucially the attacker does not see the query or the real response — this is an off-path (blind) attack. So: how does the client authenticate the response? Only two checks:
That is the whole of DNS authentication: 16 bits, plus a port number the attacker could often guess. 1 in 65,536 — and the attacker can send thousands of guesses in the time the real answer takes to arrive. Whoever's answer arrives first and matches, wins; the real answer that comes later is discarded as a duplicate.
The figure shows four boxes: user browser → local DNS resolver → .com, with an attacker box at the lower right. Across the top: "victim machine visits attacker's web site, downloads JavaScript." The flow:
a.bank.com..com with query ID x₁, and waits for the response.y₁, y₂, …. Each forged response contains (highlighted in yellow on the slide) two records: NS bank.com = ns.bank.com and A ns.bank.com = attackerIP.The next slide is titled simply "Try Again!" and shows the identical diagram. That repetition is the attack. Kaminsky's insight was not a better guess — it was removing the penalty for guessing wrong:
a.bank.com, then b.bank.com, then c.bank.com, and so on. Each is a fresh name that is not in the cache, so each forces a new outbound query with a new query ID to guess.bank.com zone plus the glue A record pointing at the attacker, one success hands over every name under bank.com, not just the random one that was queried.A 1-in-65,536 chance that you may retry a thousand times per second is not a 1-in-65,536 chance. That is the lesson.
The slide poses the problem directly: we want to increase the query ID space, but how? We do not want to change the packet format — that would mean upgrading every DNS implementation in the world at once. The answer:
The figure shows a user at a browser on the left, a tall red firewall bar down the middle, and on the right ns.evil.com (the attacker's DNS server, marked with a star) and www.evil.com's web server at 171.64.7.115. Below the user, inside the firewall, sits a corporate web server at 192.168.0.100. Across the top: <iframe src="http://www.evil.com">. A note on the right says "DNS-SEC cannot stop this attack."
The sequence of arrows:
www.evil.com?" and ns.evil.com answers 171.64.7.115 with TTL = 0.www.evil.com.192.168.0.100 — an address inside the corporate network.www.evil.com, which now resolves to the internal server. The browser considers this "same origin" — the caption reads "Read permitted: it's the same origin" — so the script may read the response.The attacker has used the victim's browser as a proxy to read a server the attacker cannot reach through the firewall. And the note is right that DNSSEC does not help: every answer the attacker's server gave was authentic, correctly signed, and for a domain it genuinely owns. The flaw is that the same-origin policy keys on the name while the connection goes to an address, and the attacker controls the binding between them.
| Rebinding defense | Problem with it |
|---|---|
| Browser: refuse to switch IPs mid-session (DNS pinning) | Interacts poorly with proxies, VPNs, CDNs, etc. Not consistently implemented in any browser. |
Server: check the Host header for unrecognized domains | Requires every internal server to do it. (It works because the browser sends Host: www.evil.com to the internal server, which is not a name that server answers to.) |
| Server: authenticate users with something beyond IP address | Requires giving up "it's on the internal network, so it's trusted" — which is the real root cause. |
DNSSEC adds authentication and integrity to DNS responses. Authoritative DNS servers sign DNS responses using a cryptographic key, and clients can verify that a response is legitimate by checking the signature through a PKI similar to HTTPS. This is precisely the origin-authentication primitive from L03a applied to the one protocol in this lecture that most needed it.
The slide's verdict is blunt: "Most people don't use DNSSEC and never will. Use TLS instead." The reasoning behind that opinion is worth knowing for a discussion question — DNSSEC authenticates the name-to-address mapping but provides no confidentiality and does not protect the connection, whereas TLS authenticates the server you actually ended up talking to and encrypts the session. If TLS is doing its job, being sent to the wrong IP address fails safely: the certificate will not match.
| Type | Description |
|---|---|
| DoS Bug | A design flaw that allows one machine to disrupt a service. Generally a protocol asymmetry — e.g. easy to send a request, difficult to create a response, or the request requires server state. |
| DoS Flood | Control a large number of requests from a botnet of machines you control. |
Denial of service is possible at every layer:
| Layer | The attack |
|---|---|
| Link layer | Send too much traffic for switches and routers to handle. (Demo 2's broadcast storm is exactly this.) |
| TCP/UDP | Require servers to maintain a large number of concurrent connections or state. |
| Application layer | Require servers to perform expensive queries or cryptographic operations. |
The figure has a client timeline C on the left and a server timeline S on the right. Five arrows labelled SYN_C1 through SYN_C5 run from left to right, and beside the server's timeline a blue bar grows taller with each arrival — the backlog queue filling up. The bullets on the right:
Trace it against the three-way handshake above. The server receives a SYN, allocates state, enters SYN-RECEIVED, and sends a SYN-ACK — to a random address that never asked and will never answer. The entry sits in the backlog until it times out. The attacker spent one small packet and no state; the server spent an entry in a finite queue and a timeout's worth of memory. That is the asymmetry.
Problem: the server commits resources (memory) before confirming the identity of the client — before the client responds.
Bad solutions: increase the backlog queue size (the attacker just sends more); decrease the timeout (now legitimate clients on slow links fail).
Real solution: avoid state until the three-way handshake completes.
Instead of storing SNC and SNS, send a cookie back to the client. The server computes:
L = MAC_key( SAddr, SPort, DAddr, DPort, SN_C, T )
key: picked at random during boot
T = 5-bit counter incremented every 64 secs
SN_s = ( T || mss || L )
An honest client sends back an ACK with AN = SN_s and SN = SN_C + 1. The server allocates space for the socket only if SNs is valid — that is, only if it recomputes the MAC and gets a match.
The server does not save state (at the cost of losing TCP options).
Why this works, in one line: the server's own initial sequence number is the state, encoded and authenticated with a key only the server knows. The client hands it back in the third packet, and the server reconstructs everything it needed to remember. T, a 5-bit counter ticking every 64 seconds, is there to expire old cookies — it wraps every 32 × 64 s ≈ 34 minutes, so a cookie cannot be replayed indefinitely. This is a MAC from L03a used as a memory-saving device, which is a genuinely elegant idea and a very likely exam question.
The figure shows an attacker at the top left controlling a bot, which sends small spoofed requests ("DNS ANY example.com") to three open DNS resolvers in the middle, each marked with a warning triangle. From each resolver a thick arrow of large response data — columns of MX mx1.example.com and A 1.2.3.x records — converges on the target victim at the right. A wide arrow across the bottom reads 60–70× increase in size.
The two ingredients, both of which are required:
| Common UDP amplifier | The trick |
|---|---|
| DNS | An ANY query returns all records the server has about a domain. |
| NTP | MONLIST returns a list of the last 600 clients who asked for the time recently. |
| Memcache | Retrieve a large record: the server responds by firing back as much as 50,000 times the data it received. |
Note that all three are UDP. TCP cannot be used this way: the handshake would have to complete, and the SYN-ACK goes to the spoofed address, so the attacker could never finish a connection.
The defensive conclusion is uncomfortable and worth stating: the victim can do nothing. The fix is BCP 38 ingress filtering at the networks that permit spoofed source addresses to leave, plus closing open resolvers — both of which have to be done by parties who suffer no harm from the attack. This is the clearest example in the course of a security problem whose costs and whose remedies sit with different people.
The October 2016 attacks on OVH, Dyn, and Krebs are the deck's case study. The Akamai quote on the Krebs slide reads, in part, that the attacks of that final week were significantly larger than the majority Akamai normally sees, and that the attack of 20 September was the largest ever mitigated by Akamai, while the 22 September attack peaked at 555 Gbps and would have held the record at any other time. Verisign's estimate was up to 100,000 malicious endpoints, with unverified reports in the 1.2 Tbps range.
The botnet figure shows a bot master at the left and about 200K IoT devices in the middle, firing GRE, HTTP, and TLS traffic at the targets. The slide's one-line analysis is the important part: "Not amplification. Flood with SYN, ACK, UDP, and GRE packets." With 200,000 real hosts you do not need an amplifier — you have the volume already, and the traffic comes from real addresses, which makes it harder to filter rather than easier.
The architecture figure has four horizontal bands — Attacker, Infrastructure, Devices, DDoS Target — with seven numbered arrows:
Separating the scanning bot from the loader is the design detail worth noticing: the device that finds the victim is not the device that infects it, so the infection traffic all comes from a small number of well-connected machines rather than from thousands of slow cameras. Compare this with the remote control facility from L06a — this is that abstract definition drawn as a system diagram.
The population slide reports roughly 600K devices compromised over the life of the botnet (against ~200K participating in a given attack). Booter services — DDoS-for-hire — are named as the commercial layer on top.
Google Project Shield is given as the defence: DDoS attacks are often used to censor content, as in Mirai's case where Brian Krebs's blog was under attack, and Project Shield uses Google's bandwidth to shield vulnerable websites such as news outlets, blogs, and human rights organisations. The framing matters — availability attacks are a free-speech problem, and the practical defence is to borrow capacity from someone who has more of it than the attacker.
Command the bot army to complete a real TCP connection, complete the TLS handshake, and then GET a large image or other content. This bypasses flood protections — every request is indistinguishable from a real user's — but there is a cost: the attacker can no longer use random source IPs, because a real connection requires receiving the replies. Consequently the victim site can block or rate-limit the bots once it identifies them.
That trade-off is the same one from the IP spoofing slide, now visible in both directions: spoofing buys anonymity but only works for attacks that need no reply; completing the handshake buys legitimacy but costs anonymity. An attacker cannot have both.
The GitHub attack is given as an example of an application-layer flood: 1.35 Tbps against GitHub, caused by JavaScript injected into HTTP web requests — ordinary users' browsers turned into attackers by modifying unencrypted pages in transit. The Chinese government was widely suspected to be behind the attack. (The lesson hiding in that sentence: the injection was possible because the pages were served over plain HTTP. HTTPS everywhere is a DDoS defence as well as a privacy measure.)
The idea: force every client to do a moderate amount of work for every connection they make.
C.X such that LSB_n( SHA-1( C || X ) ) = 0ⁿ — i.e. the last n bits of the hash of the challenge concatenated with the answer are all zero.Assumption: the puzzle takes 2n work for the client to compute (about 0.3 s on a 1 GHz core for the parameters assumed), while the solution is trivial for the server to check — a single SHA-1.
This is the DoS asymmetry reversed: now the requester does the expensive work and the server does the cheap check. It is the same construction as proof-of-work in Bitcoin, and it relies on the one-wayness of the hash function from L03a — there is no shortcut to finding X but checking it is one hash.
| Benefits | Limitations |
|---|---|
| Can change n based on the amount of attack traffic — the defence scales itself with the attack. | Requires changes to both protocols, clients, and servers. |
| Hurts low-power legitimate clients during an attack, e.g. phones. (A botnet of 200,000 devices can absorb the cost far better than one phone on battery.) |
The slide's own verdict: client puzzles are not frequently used in the real world.
| Term | Definition | Example | Memory trick | Similar | Opposite |
|---|---|---|---|---|---|
| Circuit switching | A route is established and held for the duration of the communication | Legacy phone network | A wire just for you | Virtual circuit | Packet switching |
| Packet switching | Data split into packets routed independently, best effort | The Internet | Postcards, not phone calls | Datagram service | Circuit switching |
| Statistical multiplexing | Link capacity handed out on demand, packet by packet | What packet switching does | Share as needed | TDM, FDM | Dedicated allocation |
| Store and forward | A node receives the complete packet before forwarding it | Every router hop | Read it all, then pass it on | Hop-by-hop | Cut-through |
| Encapsulation | Each layer wraps the layer above in its own header | M → Ht M → Hn Ht M → Hl Hn Ht M | Envelopes inside envelopes | Layering | Decapsulation |
| Segment / datagram / frame | The unit at the transport / network / link layer | TCP segment in IP datagram in Ethernet frame | S-D-F going down | PDU | — |
| Protocol | Defines format and order of messages, and actions taken | HTTP, TCP, ARP | Format, order, action | Standard | — |
| MAC address | 48-bit hardware address burned into adapter ROM | 08:00:2b:e4:b1:02 | Identifies the box | LAN / physical address | IP address |
| Broadcast address | MAC address of all 1s; received by every adaptor | FF:FF:FF:FF:FF:FF | All ones, all hosts | Multicast (first bit 1) | Unicast |
| Collision domain | Region where any host hears any other host | Hub or repeated segment | Everyone shouting in one room | Shared medium | Switched port |
| CSMA/CD | Sense the channel, transmit, abort and jam on collision | Ethernet transmitter algorithm | Listen, talk, stop if clashed | Multiple access | Token passing |
| Promiscuous mode | NIC keeps frames not addressed to it | Wireshark on a LAN segment | Keeps everything it hears | Packet sniffing | Normal filtering |
| ARP | Resolves an IP address to a MAC address on the same LAN | Broadcast query, unicast reply | Who has this IP? | Plug-and-play | DNS (name → IP) |
| Soft state | Cached information that expires unless refreshed | ARP entry, TTL ≈ 20 min | Forgets unless reminded | Cache TTL | Static entry |
| Gratuitous ARP | An ARP reply sent without any request | The poisoning primitive | Answering a question nobody asked | ARP spoofing | Solicited reply |
| ARP spoofing / poisoning | Claiming another host's IP to redirect traffic through you | arpspoof + dsniff demo | Lie about who you are | Man-in-the-middle | Static ARP table |
| IP service model | Unreliable, connectionless, best-effort datagram delivery | "Just packets" | Try, don't promise | Datagram service | Virtual circuit |
| Source IP spoofing | Sending packets with an arbitrary source address | Libnet; amplification attacks | Fake return address | — | Ingress filtering |
| Forwarding | Data plane: move a packet to the right output link | Forwarding table lookup | One interchange | Switching | Routing |
| Routing | Control plane: compute the paths packets will follow | OSPF, RIP, IS-IS, BGP | Planning the trip | Path computation | Forwarding |
| BGP | Routers exchange routing table information by announcement | Pakistan–YouTube, 2008 | Everybody advertises | Inter-domain routing | OSPF (intra-domain) |
| Route hijack | Announcing a prefix you do not own; longest prefix wins | 208.65.153.0/24 | Claim the address space | ARP spoofing at scale | Route validation |
| Flow control | Prevents overflow of the receiver's buffer | TCP receive window | Protects the receiver | Congestion control | — |
| Congestion control | Prevents overload of the network; TCP infers it from loss | Congwin, threshold, slow start | Protects the network | Flow control | Network-assisted (ECN) |
| ARQ | Sequence numbers, ACK/NACK, timers, retransmission | rdt3.0 | Ask again if silent | Sliding window | Forward error correction |
| Forward error control | Add redundancy so losses are recovered without retransmission | n(1+ε) blocks suffice | Send spares, not repeats | Erasure coding | ARQ |
| Three-way handshake | SYN → SYN-ACK → ACK, with states SYN-SENT / SYN-RECEIVED / ESTABLISHED | seq 100 / 200, ack 101 / 201 | Hello, hello back, got it | Connection setup | FIN teardown |
| RST | Flag that tears down a connection on an invalid packet | TCP reset attack | Hang up immediately | FIN (graceful) | — |
| TCP reset attack | Forging a RST within the receive window to kill a connection | 1 in 2^(16+32)/W | Window = extra chances | Connection spoofing | — |
| DNS query ID | 16-bit random value linking a response to its query | id: 30439 | The only authentication there is | Source port entropy | DNSSEC signature |
| DNS cache poisoning | Getting a resolver to cache a false record | Malicious additional records | Poison the well | DNS spoofing | Bailiwick checking |
| Glue record | Non-authoritative record needed to reach the next hop in resolution | Address of ns0.ucsd.edu | Breaks the chicken-and-egg | Additional section | Authoritative record |
| Bailiwick checking | Only accept additional records for a domain in the original question | Drops the paypal.com record | Stay in your lane | Response validation | Accept-all caching |
| Kaminsky attack | Forged responses to a stream of fresh subdomains, winning an NS record | a.bank.com, b.bank.com, … | Retry until you win | Cache poisoning | Source port randomization |
| Source port randomization | ~11 extra bits of entropy on DNS queries | Attack takes hours, not seconds | Widen the haystack | Query ID | Fixed source port |
| DNS rebinding | Short-TTL name re-pointed at an internal IP to abuse same-origin | TTL = 0, then 192.168.0.100 | Same name, new address | SSRF via browser | DNS pinning |
| DNSSEC | Cryptographic signatures on DNS responses, verified via a PKI | Authoritative servers sign | Signed answers | TLS certificates | Plain DNS |
| DoS bug | A design flaw letting one machine disrupt a service | SYN flood | Asymmetry, not volume | Protocol asymmetry | DoS flood |
| DoS flood | Overwhelming volume from a botnet you control | Mirai, 200K devices | Volume, not cleverness | DDoS | DoS bug |
| SYN flood | Spoofed SYNs filling the backlog queue before the handshake completes | SYN_C1 … SYN_C5 | Half-open the door forever | State exhaustion | SYN cookies |
| SYN cookie | Encoding the connection state in a MAC-protected initial sequence number | SN_s = (T ‖ mss ‖ L) | Let the client hold the state | Stateless validation | Backlog queue |
| Amplification attack | Small spoofed request producing a large response aimed at a victim | DNS ANY, 60–70× | Small in, huge out | Reflection attack | Direct flood |
| Open resolver | Misconfigured DNS server answering queries from anyone | 31,000 of them, 2013 | Unlocked megaphone | NTP MONLIST, memcache | Closed resolver |
| Mirai | IoT botnet built by guessing default telnet passwords | OVH, Dyn, Krebs, 2016 | Cameras as cannons | Botnet | — |
| GET flood | Real TCP + TLS connections requesting expensive content | Bypasses flood protection | Attack by being a customer | Application-layer DoS | SYN flood |
| Client puzzle | Server-set hash puzzle the client must solve per connection | LSBₙ(SHA-1(C‖X)) = 0ⁿ | Pay to knock | Proof of work | Free connection setup |
The Internet is packet switched: data is split into packets routed independently and best-effort, which buys statistical multiplexing and adaptation at the price of no guaranteed bandwidth, per-packet overhead, reordering, loss, and congestion. It is organised as a five-layer stack (application, transport, network, link, physical — OSI's presentation and session layers are missing), each layer encapsulating the one above, so a message becomes a segment, then a datagram, then a frame; a switch works to layer 2, a router to layer 3, and every hop rewrites the MAC address while preserving the IP address. At the link layer, 48-bit MAC addresses are burned into the adaptor, every adaptor hears every frame on the segment (which is what promiscuous mode exploits), and CSMA/CD resolves collisions by sense-transmit-abort-jam. ARP maps IP to MAC by broadcast query and unicast reply, caches the answer as soft state, and authenticates nothing — so a gratuitous ARP reply poisons any cache and puts the attacker in the middle, as the arpspoof/dsniff demo shows against cleartext telnet. IP is unreliable, connectionless, best-effort, splits a 32-bit address into network and host parts, separates forwarding (data plane) from routing (control plane), and lets the sender fill in a source address that routers do not verify — the single fact behind spoofing, DoS, and amplification. BGP has the same credulity at Internet scale (Pakistan/YouTube, 2008). UDP adds only ports; TCP adds connections, byte streams, sequence numbers, flow control (protects the receiver) and congestion control (protects the network, inferred from loss, Congwin and threshold, slow start and congestion avoidance). The three-way handshake leaves the server holding state after SYN-ACK, which is the SYN flood; the fix is to hold no state until the handshake completes, via SYN cookies that put the state inside a MAC-protected sequence number. DNS authenticates a response with a matching port and a 16-bit query ID, which gives cache poisoning through the additional records section (blocked by bailiwick checking) and the Kaminsky attack, which wins by querying endless fresh subdomains so a wrong guess costs nothing — mitigated by source port randomization, while DNS rebinding defeats the same-origin policy with a zero-TTL name and is not stopped by DNSSEC. Finally, denial of service is the search for asymmetry at every layer: DoS bugs (one machine, a protocol flaw) versus DoS floods (a botnet), amplification via open DNS, NTP MONLIST and memcache, the Mirai IoT botnet that needed no amplification at all, GET floods that trade anonymity for legitimacy, and client puzzles that reverse the asymmetry but hurt phones and are rarely deployed.
dig output: identify the query ID, the aa flag, the four sections, and the TTL.Formula sheet
Every formula from the lectures so far, in one place. Grows as lectures are added.
| Formula | Meaning | From |
|---|---|---|
Grade = .30A + .05P + .25M + .25F + .15T | Weighted course grade | L00 |
Σ dᵢ·bⁱ | Value of a base-b numeral | L00 |
Risk ≈ Impact × Likelihood | Conceptual risk score | L01a |
C = E(M), M = D(C) | Encryption/decryption | L01a |
C = (P + k) mod 26 | Caesar cipher | L01a |
yᵢ = xᵢ ⊕ sᵢ | Binary stream cipher (XOR is self-inverse) | L01a, L03a |
n(n−1)/2 vs n pairs (2n keys) | Keys for n users: symmetric vs public key | L01a, L03a |
2ᵏ keys; avg time 2ᵏ⁻¹ / rate | Brute-force key search | L01a, L03a |
N = Cᴸ | Password search space | L01a |
rate = Cᴸ / T (T = 5,184,000 s for 60 days) | Guesses/s needed to exhaust before password change | L01a |
D_K(E_K(P)) = P | Consistency | L03a |
valid fraction = 1 / 2^((1−β)n), β ≈ 0.16 | Share of n-bit strings that are English | L03a |
n = k / (1 − β) | Unicity distance (bits) | L03a |
n ≤ bm ≤ n + b | Block count with padding | L03a |
C[i] = E_K(P[i]) | ECB mode | L03a |
C[i] = E_K(C[i−1] ⊕ P[i]); P[i] = C[i−1] ⊕ D_K(C[i]); C[−1] = IV | CBC mode | L03a |
C[i] = P[i] ⊕ E_K(t + i) | CTR mode | L03a |
C₁ ⊕ C₂ = P₁ ⊕ P₂ | Key-stream reuse leak | L03a |
X₀ = P ⊕ K; C = X₁₀ | AES-128 round structure | L03a |
gcd(a,b) = gcd(b, a mod b) | Euclid; O(log max(a,b)) steps | L03a |
gcd(a,b) = ia + jb | Extended Euclid / Bézout → inverses | L03a |
x^(p−1) ≡ 1 (mod p); x⁻¹ = x^(p−2) | Fermat's little theorem | L03a |
x^φ(n) ≡ 1 (mod n); φ(pq) = (p−1)(q−1) | Euler's theorem and totient | L03a |
n = pq; ed ≡ 1 (mod φ); C = Mᵉ mod n; M = Cᵈ mod n | RSA | L03a |
ed = kφ(n) + 1 ⇒ M^(ed) ≡ M | RSA correctness | L03a |
F_k ≈ e^(−k(k−1)/2m); k ≈ 1.17√m | Birthday bound; b-bit hash ≈ b/2 bits security | L03a |
h((K⊕A) ‖ h((K⊕B) ‖ M)) | HMAC | L03a |
xₙ = r; xᵢ = h(xᵢ₊₁) | Hash chain | L03a |
xₙ₊₁ = 0; xᵢ = h(pᵢ ‖ xᵢ₊₁) | Validation chain | L03a |
Qᵢ = (Qᵢ₋₁² mod n)·a^bit mod n | Repeated squaring, O(log p) | L03a |
t = ⌈k / log₂(1/q)⌉, error ≤ 2⁻ᵏ | Randomized primality test rounds | L03a |
digit = 4r + 2w + 1x | UNIX octal permission digit | L04a |
These notes teach the material rather than summarize the slides: every bullet from lecture is expanded into full explanation, and background your professor assumed is filled in. Read a chapter like a textbook chapter, then come back and use the colored boxes as your review layer.
Plain English, the exam wording, and why it matters.
A concrete scenario walked through step by step.
The wrong-but-tempting idea, corrected.
Exact phrasings, comparisons, and gotchas that show up on tests.
A section distilled to the one thing to remember.
Study order. Chapters 1–2 build the mental model (what an OS is and how it is built). Chapter 3 is the C toolkit every later chapter relies on. Chapters 4–7 are the heart of the course: processes, threads, scheduling, and synchronization, the most heavily tested topics. Chapters 8–9 finish the concurrency arc with the classical synchronization problems and deadlocks. Chapters 10–11 cover main memory, the newest material and a stated focus of the cumulative final.
Introduction to Operating Systems
Chapter 1
Almost every program you will ever write runs on top of an operating system (OS). You rarely talk to the hardware directly; you ask the OS, and the OS talks to the hardware for you. That is the entire point of the course: to understand the layer of software that sits between raw silicon and the apps people actually use.
Historically, OS development was driven by the need to use very expensive hardware efficiently — a 1960s mainframe cost a fortune, so you could not let the CPU sit idle. Today, the driving goal has flipped: an OS exists to relieve users and programmers from having to deal with the messy details of the hardware architecture. You should not need to know how a specific SSD controller works just to save a file.
There is no single universally accepted definition of an operating system, because the idea evolved alongside people's needs. Two useful approximations your professor gave:
Your professor framed OS knowledge as benefiting three groups, and you sit in at least the middle one:
A computer system can be divided into four layers. Memorize these four — they are a classic exam question, and the ordering (hardware at the bottom, users at the top) matters.
| Component | Role | Examples |
|---|---|---|
| Hardware | Provides the basic computing resources. | CPU, memory (RAM), I/O devices, GPUs |
| Operating System | Controls and coordinates hardware use among applications and users. | Windows, macOS, Linux/Android |
| Applications / Programs | Define how system resources are used to solve users' problems. | Word processors, compilers, browsers, databases, games |
| Users | The agents that direct the work. | People, other programs, other computers |
Notice that “users” are not only people. A program can be a user of another program, and a remote computer can be a user of your machine over a network. This broad definition matters later when we discuss protection and security.
Most computers follow the John von Neumann architecture, also called the stored-program computer. Background you need: in this design, both the program instructions and the data live in the same main memory, and the CPU fetches instructions one at a time, decodes them, and executes them. The four hardware pieces this architecture defines are:
The single most important idea in this chapter: the OS fills the gap between what the hardware provides and what applications require. Hardware offers crude, device-specific capabilities; applications want clean, uniform services. The OS translates between them. Concretely, filling the gap means the OS provides:
Classic question: “What is abstraction and give an example.” Answer with the definition plus a concrete case — e.g., the OS exposes a file (open/read/write/close) so you never deal with disk sectors, RPM, or controller commands. The word the slides attach to this is Abstraction.
The same OS looks different to different audiences. Exam questions love to contrast these viewpoints:
The slides draw a careful boundary. Get the three categories straight:
| Category | What it is |
|---|---|
| Kernel | The one program running at all times; the core of the OS. |
| System program | Ships with the OS but is NOT part of the kernel (e.g., utilities). |
| Application program | Any program not associated with the OS at all. |
Modern general-purpose and mobile operating systems also include middleware — a set of software frameworks providing extra services to app developers, such as databases, multimedia, and graphics. Apple's iOS and Google's Android are the canonical examples.
“The kernel is the whole operating system.” No. The kernel is the always-running core. The full OS also bundles system programs and (today) middleware. “Kernel” and “OS” are not synonyms — confusing them is a common slip on definition questions.
A huge part of what the OS does is give every running program the illusion that it owns the whole machine. The slides call this virtualizing the machine. The vehicle for that illusion is the process.
Program vs. process — the distinction the exam will test: A program is a passive entity — just a file of instructions sitting on disk. A process is an active entity — that program loaded into main memory and executing. A program becomes a process only when it is loaded and begins to run.
A process needs resources to do its job: CPU time, memory, I/O, files, and initialization data. When a process terminates, the OS must reclaim any reusable resources it held.
Each running program runs in its own process, and processes provide a nicer interface than raw hardware. From inside a process, a program sees clean abstractions instead of physical hardware:
The OS sits in the middle, translating between the hardware interface (below) and the application interface (above). It gives each program its own process, switches the CPU between processes, and enforces protection so one process cannot corrupt another.
Protection in action. If a process tries to read or write memory it does not own, the OS stops it and the program dies with:
Segmentation Fault (core dumped)That message — which you will see often in C — is the OS's protection mechanism doing exactly its job: keeping one process from trampling another's (or the OS's) memory.
Computers run programs out of fast, rewritable main memory — the hardware people call RAM (random-access memory). The catch: RAM is volatile, meaning its contents vanish the instant power is lost. It has also historically been expensive and limited in size.
To compensate, secondary storage extends main memory. It is cheap, roughly permanent (non-volatile), and comes in two main variants:
| Mechanical Hard Disk (HDD) | Solid-State Drive (SSD) | |
|---|---|---|
| How it works | Rotating magnetic platters; read/write heads on a moving actuator arm. | No moving parts; data in flash memory cells. |
| Limited by | Seek time (head movement) + rotational latency (waiting for a sector). | Near-zero seek time. |
| Controller does | — | Wear leveling, logical-to-physical mapping, error correction. |
| Access pattern | Sequential fast, random slow. | Massive random I/O performance. |
Storage forms a hierarchy: as you move toward the CPU, storage gets faster, smaller, and more expensive per byte; as you move away, it gets slower, larger, and cheaper. Crucially, note who manages each level — a favorite exam detail.
| Level / Name | Typical Size | Access Time | Managed By |
|---|---|---|---|
| 1 · Registers | < 1 KB | 0.25–0.5 ns | Compiler |
| 2 · Cache | < 16 MB | 0.5–25 ns | Hardware |
| 3 · Main Memory | < 64 GB | 80–250 ns | Operating System |
| 4 · Solid-State Drive | < 1 TB | 25,000–50,000 ns | Operating System |
| 5 · Magnetic Disk | < 10 TB | ~5,000,000 ns | Operating System |
Memorize the “Managed By” column. Registers → compiler; cache → hardware; main memory and below → operating system. Also be ready to state the ordering and the trend (faster+smaller+pricier near the CPU).
These five terms are easy to blur together, and the exam exploits exactly that. Keep them distinct:
| Term | Definition |
|---|---|
| CPU | The hardware that executes instructions. |
| Processor | A physical chip that contains one or more CPUs. |
| Core | The basic computation unit of the CPU. |
| Multicore | Multiple computing cores on the same CPU/chip. |
| Multiprocessor | A system that includes multiple processors (chips). |
“Multicore means every core can see the whole cache.” Not necessarily. Cache layout depends on the chip's design — especially in heterogeneous designs like ARM's big.LITTLE or Intel's P-cores and E-cores. Separate multiprocessor systems naturally have separate caches and even separate main memory.
Add too many CPUs to one shared bus and competition for that bus becomes a bottleneck, degrading performance. The fix is NUMA (Non-Uniform Memory Access): give each CPU (or group of CPUs) its own local memory reached by a fast local bus. Memory access time then depends on which memory you touch — hence “non-uniform.”
Clustered systems go further: instead of multiple CPUs in one box, you join two or more complete computers (nodes) together, each typically multicore, usually connected over a Local Area Network (LAN). That is the difference from a multiprocessor — clusters are separate whole systems working as a team.
Operating systems are interrupt-driven. Rather than constantly polling every device (“Hey USB, got anything? Hey keyboard, anything yet?”), the OS sits idle until a device interrupts it to say something happened. This event-based design is far more efficient than busy-checking.
| Term | Meaning |
|---|---|
| Interrupt-Request Line | The hardware wire on which interrupts are signaled to the CPU. |
| Interrupt Address / Number | A value sent with the signal that identifies which device/source raised it. |
| Interrupt Service Routine (ISR) | The OS routine that runs when an interrupt arrives (a.k.a. interrupt handler). |
| Interrupt Vector | An OS table, indexed by interrupt number, holding the addresses of the ISRs. |
| Trap / Exception | A SOFTWARE-generated interrupt — caused by an error or a user request (e.g., a system call). |
A trap (also called an exception) is the software cousin of a hardware interrupt: it is raised by an error (divide-by-zero, page fault) or deliberately by a system call. Same handling machinery, different trigger.
Keyboard keystroke (a read interrupt), step by step:
(1) You press a key; the keyboard controller detects it and stores the keycode in its local buffer. (2) The controller raises an interrupt: “I've got data for you!” (3) The CPU pauses its current task and runs the keyboard ISR. (4) The ISR moves the keycode to a designated place. (5) The OS passes the data to the foreground app — often via the special input file stdin. (6) The app (say, MS Word) reads it and updates the document. (7) The CPU resumes the paused task until the next interrupt.
Most CPUs have two interrupt-request lines:
There are also two ways to handle the I/O that an interrupt accompanies:
For asynchronous I/O, the kernel keeps a device-status table with one entry per I/O device, recording each device's status and parameters.
Interrupt-driven I/O is great for small transfers (a keystroke, a mouse move) where you would generate one interrupt per byte. But for high-speed devices moving large blocks, one-interrupt-per-byte would bury the CPU. DMA solves this.
The payoff: only one interrupt per block, not one per byte, and the CPU is free to do other work while the device controller handles the transfer. This is why your video keeps playing smoothly while other things happen — the heavy data movement is offloaded from the CPU to DMA.
A single user cannot keep the CPU and I/O devices busy all the time — you think, you read, you wait. The OS exploits those gaps.
The slides tie four key OS mechanisms to the goals of a multitasking system — each is a future chapter:
Multiprogramming vs. multitasking — the distinction. Multiprogramming just keeps the CPU busy by switching to another job when the current one blocks. Multitasking/timesharing goes further: it switches rapidly and on a timer so each job gets frequent small slices, producing interactive responsiveness. A common short-answer: “Benefit of multiprogramming over sequential processing?” → both better CPU utilization and reduced total computation time, because CPU work and I/O work overlap.
A cache is always smaller than the storage it caches, so the design question is the cache size and replacement policy (what to evict when full). Two coherence concerns arise once data can live in several places at once:
To protect itself and other programs, the OS runs the machine in two modes, distinguished by a hardware mode bit.
When user code runs, the mode bit is set to user; when kernel code runs, it is set to kernel. A program cannot simply flip the bit to kernel itself — that would defeat the whole scheme. Instead, a system call switches the mode to kernel, and the return from the call resets it to user. Instructions marked privileged only execute in kernel mode.
But what if a user program never gives control back — an infinite loop, or a process hogging resources? The timer prevents this. The OS sets a counter (via a privileged instruction); the physical clock decrements it; when it hits zero it raises an interrupt. By setting the timer before scheduling a process, the OS guarantees it can regain control or terminate a program that overruns its time slice.
“A program can switch itself to kernel mode whenever it wants.” No. The only legitimate route into kernel mode is a system call (or a trap/interrupt), which the OS controls; the mode bit resets to user on return. That gate is precisely what keeps user code from running privileged instructions.
The chapter closes by previewing the OS's major management duties — each becomes a later topic.
A single-threaded process has one program counter and executes instructions sequentially to completion; a multi-threaded process has one program counter per thread. A system typically runs many processes concurrently (some user, some OS) by multiplexing the CPU among them. OS duties include creating/deleting processes, suspending/resuming them, and providing mechanisms for synchronization, communication, and deadlock handling.
To execute, a program's instructions and needed data must be in memory. Memory management decides what is in memory and when, to optimize CPU utilization and responsiveness. Duties: track which parts of memory are used and by whom; decide what to move in/out; allocate and deallocate space; and protect memory from unauthorized access.
The OS gives a uniform logical view of storage by abstracting physical media into files. Each medium (HDD, SSD) has its own properties — capacity, transfer rate, and access method (sequential or random). Files are organized into directories, with access controls (file permissions; chmod on Linux) deciding who can access what. The OS creates/deletes files and directories, maps files onto secondary storage via a filesystem format (FAT32, NTFS, ext4, ZFS), and backs them up to stable storage. Under RAID, one file need not live entirely on one device.
One job of the OS is to hide each device's peculiarities from the user. The I/O subsystem manages device memory through three techniques you should be able to name and distinguish:
Only the device driver knows a specific device's quirks; generic standards (like USB) handle simple peripherals, while complex hardware (GPUs, network cards, printers) needs vendor-specific drivers.
Distinguish three related ideas: Emulation simulates hardware in software and is used when the host CPU type differs from the target (e.g., Apple's x86→ARM migration) — generally the slowest. Interpretation runs a language not compiled to native CPU code. Virtualization runs guest OSes that are themselves natively compiled for the host CPU; a Virtual Machine Manager (VMM) provides the virtualization services (e.g., VMware running Linux guests on a Linux host).
An OS is the abstraction layer between hardware and applications. It virtualizes the machine into processes, is interrupt-driven, uses dual-mode operation + a timer for protection, keeps the CPU busy via multiprogramming/multitasking, exploits a storage hierarchy with caching, and manages processes, memory, files, I/O, and virtualization.
Operating System Structures
Chapter 2
Chapter 1 asked what an OS is. Chapter 2 asks how it is built and how programs reach its services. Two threads run through it: the menu of services the OS offers, and the system-call mechanism that lets your code request those services. It ends with the major ways OS code can be structured (monolithic, layered, microkernel, modular, hybrid).
An OS provides an environment for executing programs and a set of services for users and for other programs. The slides group the services; you should be able to list and briefly explain each.
| Service | What it provides |
|---|---|
| User interface | Almost every OS has one (CLI, GUI, or touch). |
| Program execution | Load a program into memory, run it, and terminate it (normally or abnormally). |
| I/O operations | Let a running program access files or I/O devices. |
| File-system manipulation | Read/write/create/delete/search files & directories; set permissions. |
| Communications | Let processes exchange information within one system or across a network. |
| Error detection | Constantly watch for errors in CPU/memory, I/O devices, and user programs, and respond appropriately. |
| Resource allocation | Allocate and coordinate CPU, memory, storage, and I/O among concurrent jobs/users. |
| Logging | Track who used how much of which resources. |
| Protection & security | Control access to resources; authenticate users; defend against outsiders. |
Protection vs. security — keep them distinct. Protection means ensuring all access to system resources is controlled (internal: stopping concurrent processes from interfering with one another). Security means defending the system from outsiders, which requires user authentication and defending external I/O devices (e.g., network adapters) from invalid access.
On errors: hardware errors include memory errors or power failure; I/O errors include a network connection failure or a printer out of paper; user-program errors include arithmetic overflow or an attempt to access an illegal memory location. Good debugging support greatly enhances a programmer's ability to use the system.
The kernel uses the same data structures you learned in your data-structures course — arrays, lists, stacks, queues, trees, hash maps, and bitmaps. A few specifics the lecture highlighted:
O(n); a balanced BST searches in O(log n).hash_value = key % 10), giving fast key→value lookup such as username→password. A hash collision is when two distinct inputs produce the same output slot.In Linux these live in C headers such as <linux/list.h>, <linux/kfifo.h>, and <linux/rbtree.h>.
Likely gotcha: “Search performance of a BST?” The general BST is O(n) (it can degenerate into a linked list); a balanced BST is O(log n). Do not write O(log n) for a plain BST.
A CLI lets a user type commands directly. It may be built into the kernel or — more flexibly — implemented as a separate system program. Unix-like systems have several shells: the C Shell, the Bourne-Again Shell (bash), and the Korn Shell.
A shell's job is to interpret commands and execute them. Some commands are built-in (part of the shell itself); others are simply the names of programs/utilities on disk. The second design is powerful: to add a new command you just add a new program — no need to modify the shell. Commands are maintained as ordinary system programs.
You almost never invoke a system call directly. You call a friendly API (Application Programming Interface) function, and it invokes the real system call on your behalf. The three most common APIs:
On UNIX/Linux, the C library that provides the API is libc. Example of the layering: the Windows function CreateProcess() actually invokes the NTCreateProcess() system call inside the Windows kernel. The system-call names used in this course are generic/POSIX-style; each real OS has its own names.
The beauty: the caller knows nothing about the implementation. A programmer only has to obey the API and understand what the OS will do as a result. The Runtime Environment (RTE) — compilers, interpreters, libraries, loaders — provides the system-call interface that links the API to actual system calls; it intercepts API function calls and invokes the necessary kernel calls.
“You must know how a system call works internally to use it.” Exactly backwards. The whole design hides the implementation; you only obey the API. The OS defines the convention, the compiler follows it, and the application stays blissfully unaware.
A system call often needs more than just its number — it needs parameters. There are three standard methods:
| Method | How it works | Note |
|---|---|---|
| Registers | Pass parameters directly in CPU registers. | Simplest; limited by register count. |
| Memory block / table | Store parameters in memory; pass the block's address in a register. | Used by Linux; no limit on count/length. |
| Process stack | Program pushes parameters onto the stack; OS pops them off. | No limit on count/length. |
The method is fixed by the OS and architecture (the calling convention). The OS defines it, the compiler adheres to it for all programs, and users are kept unaware of the details. The block and stack methods are favored when parameters are numerous or large because they do not limit the number or length of parameters.
A recurring pattern (you will see it again in Chapters 3 and 4): to launch a brand-new program you use fork() followed by exec().
fork() clones the currently executing process, producing a near-identical child.exec() overlays a different executable on top of the calling process, replacing its code.#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // clone this process
if (pid < 0) { // fork failed
perror("Fork Failed\n");
exit(1);
} else if (pid == 0) { // child branch
printf("I am process %d, about to exec ls -l\n", getpid());
execl("/bin/ls", "ls", "-l", NULL); // replace child with ls
printf("This line should never be executed\n");
}
return 0;
}
The printf after execl never runs: if execl succeeds, the child's code is entirely replaced by ls, so there is no “original” code left to return to.
Most users' view of “the OS” is actually shaped by system programs (also called system utilities), not by raw system calls. They provide a convenient environment for development and execution, in categories: file manipulation, status information, programming-language support, program loading/execution, communications, background services, and application programs.
Some services are “oneshot”: launched only for startup, then they terminate. These facilitate booting — setting kernel variables, mounting drives, loading drivers — and then exit. Key exam point: daemons run in user mode, not kernel mode.
How does source code become a running program? Through compiling, linking, and loading. Get the vocabulary and order right.
The pipeline: reusable source (like a library) is compiled into relocatable object files — designed to be loaded at any physical memory location. The linker combines object files into one executable on secondary storage. To run, the loader brings it into memory and relocates it.
Modern systems usually do not link libraries directly into executables. In Windows, dynamically linked libraries (DLLs) are loaded as needed and shared by all programs using the same version — loaded only once. This shrinks program sizes because the library routines are stored separately. The linker inserts relocation information so a library (e.g., the math library) can be dynamically linked and loaded at runtime rather than baked into the executable. Object and executable files use standard formats so each OS knows how to load and start them.
The shell pipeline you already use: when you run gcc -o pun pun.c, the compiler turns pun.c into object code, the linker joins it with the standard I/O library's object code, and the result is the executable pun. Later, typing ./pun invokes the loader, which places it in memory as a process.
A program compiled for Windows usually will not run on Linux. Why? Because it was built against a specific OS's system calls, binary format, and ABI. The slides list the ways apps can become non-OS-specific (i.e., cross-platform):
API vs. ABI. API = source-level interface for programmers. ABI = binary-level interface for compiled code on a specific OS+CPU. “The ABI is the architecture equivalent of an API” is the exact phrasing to remember.
A general-purpose OS is an enormous program, and there are several ways to organize its internal structure. Know all five, with an example of each.
The original UNIX (and MS-DOS) were monolithic: an “all-in-one” kernel containing everything below the system-call interface and above the hardware — file system, CPU scheduling, memory management, and every other OS function in one big program. Monolithic kernels are hard to implement and extend, but they have very little overhead in the system-call interface, and communication within the kernel is fast. On Linux, glibc (the GNU C Library) provides the interface between C programs and the kernel.
In theory layering simplifies construction and debugging because each layer worries only about its own data and operations. In practice it is hard: deciding how many layers and what goes in each is difficult, and it is slow — a user request must cross multiple layers, each handing off to the next, to reach an OS service.
Microkernel benefits and the downside, as the slides frame them:
macOS's kernel, Darwin, is partly based on the Mach microkernel. In a microkernel, client programs and services never interact directly — the microkernel facilitates all communication between them.
Many modern OSes use loadable kernel modules (LKMs): a core kernel plus additional services linked in as modules, loaded at boot or at runtime (Linux, macOS, Solaris, Windows). Each module talks to others through known interfaces. It is like a layered-microkernel approach but more flexible — any module can call any other without message passing.
Real operating systems are not one pure model — they combine approaches for performance, security, and usability:
macOS/iOS structure, top to bottom: the User Experience layer (how users interact), the Application Framework layer (Cocoa / Cocoa Touch APIs for Objective-C and Swift), the Core Frameworks (graphics and media), and the Kernel Environment (Darwin). Darwin exposes two system-call interfaces: Mach system calls (“traps”) and BSD system calls (POSIX functionality), and provides core services — memory management, CPU scheduling, and inter-process communication (IPC).
| Structure | Idea | Example |
|---|---|---|
| Monolithic | Everything in one big kernel. | Original UNIX, MS-DOS |
| Layered | Stack of layers, each using only lower ones. | Abstraction-based designs |
| Microkernel | Tiny kernel; services in user space via messages. | Mach |
| Modular (LKM) | Core kernel + dynamically loadable modules. | Linux, Windows, macOS, Solaris |
| Hybrid | Combination of the above. | Linux, Windows, macOS |
The OS offers services; programs reach them through system calls wrapped in APIs; linkers/loaders turn source into a running process; apps are OS-specific because of system calls and the ABI; and the kernel can be structured as monolithic, layered, microkernel, modular, or hybrid.
C Review
Chapter 3
Operating systems are written in C, and your programming assignments run on the GSU Snowball server and are graded there. C gives you direct control over memory and a thin layer over the hardware — exactly what an OS needs. This chapter is your toolkit for every later chapter: fork/exec, pipes, shared memory, and Pthreads are all C. If pointers and dynamic memory feel shaky, the process and thread chapters will too — so invest here.
If you know Java, much of C's syntax will feel familiar; the big differences are manual memory management, pointers, no classes (only structs), and no garbage collector.
#include <stdio.h> // preprocessor directive: import I/O library
/* My first C program prints a pun */
int main(void) { // main returns an int, takes no args
printf("To C, or not to C: that is the question.\n");
return 0; // 0 means "finished without errors"
}
Stored as pun.c — the filename does not matter, but the .c extension usually does. Notice there is no class or module name wrapping printf: C functions are free-standing. return 0 from main reports success to the OS.
# (called directives).stdio) into a complete executable.The pre-processor is usually integrated with the compiler. In large projects, object code is cached — only changed .c files are recompiled. Compile with gcc (the GNU Compiler Collection — the one to use on Snowball):
cc pun.c # produces a.out by default
cc -o pun pun.c # -o names the executable 'pun'
gcc -o pun pun.c # gcc works the same way; use this on Snowball
Directives begin with #, are one line long by default, and have no semicolon at the end. The < > around a header name (e.g., #include <stdio.h>) mark it as a standard library header rather than a programmer-defined one. Directives can also define macros:
#define ARRAY_SIZE 1024 // object-like macro for a constant
#define N 100
C's integer sizes can vary by platform (32-bit vs 64-bit), which matters for OS code. Note especially that long is 4 bytes on 32-bit but 8 bytes on 64-bit — a classic source of portability bugs.
| Type | Bytes (32-bit) | Bytes (64-bit) | Approx. range |
|---|---|---|---|
| char | 1 | 1 | -128 … 127 |
| short | 2 | 2 | -32,768 … 32,767 |
| int | 4 | 4 | -2.15B … 2.15B |
| long | 4 | 8 | (grows on 64-bit) |
| long long | 8 | 8 | -2^63 … 2^63 - 1 |
| float | 4 | 4 | 3.4e-38 … 3.4e38 |
| double | 8 | 8 | 1.7e-308 … 1.7e308 |
Check any type's or variable's size with the sizeof operator: sizeof(int), sizeof(x). Integers can be signed (default) or unsigned (non-negative only, doubling the positive range): an unsigned int spans 0 … ~4.29 billion. float and double have no unsigned variants; there is a long double (16 bytes) for extended precision.
Classic C has no Boolean type (C99 adds _Bool). Three ways to fake true/false:
0 is false, anything non-zero is true.enum { false = 0, true = 1 };#define true 1 and #define false 0.Sizeof and unsigned traps. Expect a question on long's size difference (4 vs 8 bytes) and on the fact that 0 is false and any non-zero value is true in C. Also: float/double cannot be unsigned.
Variables must be declared before use (like Java). Critical C-only hazard: uninitialized variables are NOT set to 0. They contain whatever “junk” was already in that memory. Reading an uninitialized variable yields unpredictable garbage.
int height, length = 12, width = 10; // declare; length & width initialized
float profit = 2150.48f; // 'f' suffix marks a float literal
int volume = height * length * width; // height is UNINITIALIZED -> garbage!
Character constants use single quotes ('A'); string literals use double quotes ("text"). Escape sequences: \n newline, \t tab, \\ backslash, \" double quote.
Input/output use scanf() and printf(). Both take a format string first, containing ordinary characters plus format specifiers (starting with %) that get replaced by the argument values.
| Specifier | Meaning | Specifier | Meaning |
|---|---|---|---|
| %c | character | %u | unsigned integer |
| %d or %i | signed integer | %ld / %li | long / unsigned long |
| %e or %E | scientific notation | %lf | double |
| %f | float | %Lf | long double |
| %s | string | %% | a literal % sign |
Printing a float:
float pi = 3.14;
printf("The value of Pi is %f", pi); // %f is replaced by pi's valueWhen you mix types in an expression, C automatically promotes narrower types to wider ones. The promotion chains:
int → float/doubleint → unsigned int → long int → unsigned long intfloat → double → long doubleThe reverse (wider → narrower) is not automatic. Also, chars are an integer type and can be implicitly converted — 'a' has the numeric value 97, so num + letter adds 97 to num.
To force a conversion, cast with (type)(expression). Moving from wider to narrower loses information:
float → int truncates the fractional part (does not round).double → float rounds digits.long int → int drops higher-order bits.int a = 1, b = 0;
double c = 2.55;
b = (int)(a + c); // a promoted to double -> 3.55, then cast to int -> 3
“float → int rounds to nearest.” No — it truncates. (int)3.99 is 3, not 4. (Only double→float rounds.)
Pointers are the single most important — and most tested — topic in this chapter. Background first: main memory is divided into bytes, each with a unique numeric address. Every variable occupies one or more bytes, and the address of its first byte is “the address of the variable.” Typical sizes: char 1 byte, int 4 bytes, double 8 bytes, and a pointer is 8 bytes.
Example: if int i = 1; lives at address 2000, a pointer p can store the value 2000 — we say “p points to i.” Every pointer has a reference type (the type it points to), declared by preceding the name with *:
int *p; // p points to an int
char *str; // str points to a char
double *q; // q points to a double
int i, a[10], *p, *q; // p and q point to int
A pointer can even point to another pointer. (On some old CPUs, pointers were offsets combined with a segment register rather than full addresses — Intel x86 “real mode” — but you can treat pointers as addresses.)
| Operator | Name | Does |
|---|---|---|
| & | Address-of | Returns the address of a variable (a pointer of that variable's type). |
| * | Indirection / dereference | Returns the VALUE stored at the address the pointer holds. |
You have already used & with scanf: scanf("%d", &num1); passes the address of num1 so scanf knows where to store the value it reads. The * operator turns a pointer back into the value it points to; the reference type determines how many bytes are read.
int i = 2, j, *p; // p points nowhere yet
p = &i; // p now holds the address of i
j = *p; // *p reads the value at that address -> same as j = i;
int k = 2, *q = &k; // you can initialize a pointer at declaration
q = p vs *q = *p — do not confuse these. q = p is a pointer assignment (q now points where p points). *q = *p copies the value p points to into the location q points to. Totally different effects.
int *p; printf("%u", *p); may print garbage, crash, or worse — p points nowhere meaningful.int *p; *p = 1; writes to a random address. If it belongs to another program or the OS, you get a Segmentation Fault (crash); if it happens to belong to your program, the result is undefined.int *p = &i; the * just means “p is a pointer.” In a statement *p = &i; the * performs indirection — here it wrongly tries to store an address into the int that p points to.“All uninitialized pointers are automatically NULL.” Some compilers zero them, but not all. Never rely on it. Always initialize pointers, and after freeing memory set the pointer to NULL.
C passes arguments by value — a function gets a copy of each argument, in a fresh memory location. So a function cannot change the caller's variable… unless you pass a pointer. A copy of a pointer is still the same address, so the function can reach back and modify the original.
decompose() splits a number into integer and fractional parts by writing through pointers:
void decompose(double x, long *int_part, double *frac_part) {
*int_part = (long) x; // write the integer part back to caller
*frac_part = x - *int_part; // write the fractional part back
}
// caller:
int whole; double frac;
decompose(3.14159, &whole, &frac); // pass ADDRESSES so changes stick
// after the call: whole == 3, frac == 0.14159Forgetting the & — calling decompose(3.14159, whole, frac) — passes values, not addresses, and leads to wrong results or a crash. A function prototype lets the compiler catch the type mismatch (but it cannot catch a missing & for scanf, whose arguments are untyped).
C has no classes, but it has structures (structs). Arrays force two restrictions: every element must be the same type, and you select elements by position. Structures lift both: their members can have different types, and you select a member by name.
Use a struct when several data items only make sense together. Example: a warehouse part needs a number (int), a name (char array), and a quantity on hand (int) — none means much alone.
struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} part1 = {528, "Disk drive", 10},
part2 = {914, "Printer cable", 5};
Members are stored in memory in declaration order. Initialization uses braces, and the values must appear in the same order as the members, and must be constant expressions. Each struct is its own scope / namespace — a member named number in part never clashes with number in employee or with ordinary variables.
Access a member with the dot operator: struct_name.member. Members can be read, assigned, incremented, etc.:
printf("Part number: %d\n", part1.number);
part1.number = 258; // assign
part1.on_hand++; // increment
scanf("%d", &part1.on_hand); // & and . together
Operator precedence detail the slides stress: the dot operator outranks &, so &part1.on_hand computes the address of on_hand (what we want), not of part1.
part2 = part1; copies every member. This works because a struct is a known, fixed-size type (member arrays are always the same size), unlike a bare array.= — but only between structs of the same length/type.== or != on whole structs. You must compare member by member.Two classic struct traps. (1) You can assign structs with = but cannot compare them with ==. (2) Forgetting the semicolon after a struct's closing brace produces a cryptic error reported far away (often at the next function's return), nowhere near the real mistake.
Two anonymous structs with identical members are not compatible types — you cannot assign one to the other or pass them interchangeably. The fix is to name the structure. A structure tag names a kind of struct:
struct part { // 'part' is a struct TAG
int number;
char name[NAME_LEN+1];
int on_hand;
}; // <-- semicolon REQUIRED
struct part part1, part2; // must keep the keyword 'struct'
With a tag you must always write struct part. A typedef lets you drop the keyword and use the name like a built-in type:
typedef struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} Part;
Part part1, part2; // no 'struct' keyword needed
Functions can take structs as arguments and return them. But passing or returning a struct copies the entire struct — expensive for large structs (especially with big arrays). To avoid that overhead — and to let a function modify the caller's struct — pass a pointer to the struct instead.
“when a struct contains a pointer to itself, typedef is fine.” The lecture notes that to declare a self-referential node (a pointer to the same struct type), you generally use the struct tag form, because the typedef name is not yet defined at the point the member is declared. This is why linked-list nodes use struct node *next;.
C's data structures are normally fixed in size: an array's length is set at compile time, forcing you to guess the maximum and waste space — and it can still fill up. Dynamic storage allocation lets you request memory during execution, so data structures can grow and shrink as needed (lists, trees, etc.).
Three allocation functions live in <stdlib.h>:
| Function | Prototype | Behavior |
|---|---|---|
| malloc | void *malloc(size_t bytes) | Allocates a block; does NOT initialize it. Most used (fastest). |
| calloc | void *calloc(size_t n, size_t bytes) | Allocates AND zero-clears the block. Slower for large blocks. |
| realloc | void *realloc(void *ptr, size_t bytes) | Resizes a previous block; may copy/move data (can be slow). |
All three return a void * (“generic” pointer) — an address with no reference type that you cast to whatever you need (int *, char *, struct part *). If no large-enough block is available, they return NULL (“a pointer to nothing”, like Java's null or Python's None). Always check the return value against NULL before using it.
Here size_t is an unsigned integer type (a typedef alias for unsigned int) meant to represent discrete counts of things, like a number of bytes.
Allocating an array of 10 ints with malloc:
#define ARR_SIZE 10
int *num_arr;
num_arr = (int *) malloc(sizeof(int) * ARR_SIZE); // size * count
if (num_arr == NULL) exit(1); // always check!
num_arr[0] = 15; num_arr[5] = 1;The calloc version takes (count, size) and zeroes the block: calloc(ARR_SIZE, sizeof(int)).
String concatenation that allocates exactly the space needed:
char *concat(const char *s1, const char *s2) {
char *result = malloc(strlen(s1) + strlen(s2) + 1); // +1 for '\0'
if (result == NULL) { printf("malloc failed\n"); exit(EXIT_FAILURE); }
strcpy(result, s1);
strcat(result, s2);
return result;
}The +1 is the null terminator — forgetting it is a classic buffer-overrun bug.
C has no garbage collector — you must hand memory back yourself with free():
void free(void *ptr); // returns ptr's block to the heap
Only call free on an address that came from malloc/calloc/realloc. Two failure modes dominate:
p = malloc(...);
q = malloc(...);
p = q; // the block p used to point to is now UNREACHABLE -> leak
char *ptr = malloc(4);
free(ptr); // memory released, but ptr STILL holds the old address
strcpy(ptr, "abc"); // *** dangling-pointer use: risky! ***
// correct habit:
free(ptr);
ptr = NULL; // now ptr is safe to test and won't dangleFor nested dynamic memory (a struct holding pointers that were themselves malloc'd), free the inner pointers first, then the struct — otherwise you lose the only references to the inner blocks and leak them:
free(myname->first_name); // free inner pointers FIRST
free(myname); // then the struct itself
Leak vs. dangling — know the difference cold. A memory leak = you still have memory but lost the pointer (can't free it). A dangling pointer = you freed the memory but still have the pointer (point at nothing valid). Two opposite mistakes.
A linked list is a chain of structs called nodes; each node holds data plus a pointer to the next node. The last node's next pointer is NULL (drawn as a diagonal line). Because nodes are self-referential, you must use the struct tag form:
struct node {
int value; // the data
struct node *next; // pointer to the next node (self-reference)
};
struct node *head = NULL; // tracks the start of the list
Creating a node = allocate memory, store data, then insert. The -> (arrow) operator accesses a member through a pointer — it is shorthand for dereference-then-dot:
struct node *new_node = malloc(sizeof(struct node));
(*new_node).value = 10; // verbose way
new_node->value = 10; // identical, using the -> operator
Inserting at the front of a list (returns the new head):
struct node *add_to_list(struct node *list, int n) {
struct node *new_node = malloc(sizeof(struct node));
if (new_node == NULL) exit(EXIT_FAILURE);
new_node->value = n;
new_node->next = list; // new node points at the old head
return new_node; // new node becomes the new head
}This section is the bridge into Chapter 4 — the C system calls that create processes. In UNIX, a new process is born from fork().
When fork() is called, the OS: allocates a slot in the process table; assigns a unique process ID; copies the parent's process image (except shared memory); puts the child in the Ready-to-Run state; and returns the child's PID to the parent and 0 to the child.
The key to telling parent from child is the return value:
fork() returns 0.fork() returns the child's PID (a positive number).fork() returns -1 and no child is created.The child is an exact but separate copy — an independent process image, not a shared one.
Distinguishing parent and child after fork():
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0)
printf("I am the child process\n");
else if (pid > 0)
printf("I am the parent process\n");
else
printf("the fork() has failed\n");
}Both processes run the same code after the fork; they differ only in pid, so the if sends each down its own branch. The code before the fork runs once; the code after runs in both.
To run a different program, a process uses the exec() family to replace its current code, data, and stack with a new executable. Crucially, only the code being executed changes — the PID and PPID stay the same. The exec() functions (execl, execv, execlp, etc.) are C library wrappers around the single real system call execve(), which is rarely called directly.
int execl(const char *path, const char *arg0, ..., const char *argn, NULL);
// path = absolute path to executable; arg0 = program name; args end with NULL
// returns -1 if the executable is not found
The standard pattern combines all three calls — fork to create, exec in the child to run a new program, wait in the parent to synchronize:
int main() {
pid_t pid = fork();
if (pid < 0) { // error
fprintf(stderr, "Fork Failed");
exit(-1);
} else if (pid == 0) { // child: become 'ls'
execlp("/bin/ls", "ls", NULL);
} else { // parent: wait for child
wait(NULL);
printf("Child Complete");
exit(0);
}
}
Two coordination calls round this out:
void exit(int status) terminates a process; status is its return value, announcing success or failure.pid_t wait(int *status) suspends the parent until a child terminates, returning that child's PID (or -1 if no children exist) and writing the child's status into status.The “fork() prints” question. Given printf("One\n"); fork(); printf("Two\n"); — “One” prints once (before the fork) and “Two” prints twice (parent and child each run the code after the fork). Be ready to count outputs across multiple forks, where n forks create 2^n processes.
C gives you manual control: typed variables (mind sizeof and uninitialized junk), pointers (& address-of, * dereference) for pass-by-reference, structs (assign yes, compare no), malloc/free on the heap (watch leaks & dangling pointers), and the fork/exec/wait trio that creates and runs processes.
Processes
Chapter 4
The process is the OS's fundamental unit of work, and this chapter is the spine of the course. You will learn what a process is made of, the states it moves through, the PCB the OS uses to track it, how the scheduler shuffles processes through queues, what a context switch costs, how processes are created and terminated, and finally how cooperating processes talk to each other via IPC (shared memory, message passing, and pipes).
Program vs. process, again, because the exam loves it: a program is a passive file on disk; a process is active. You can start a program via GUI clicks, a command-line name, or the exec() system call. And one program can become many processes — e.g., several users running the same shell each get a separate process.
A process in memory has four sections (plus current activity). Know all of them and what each holds:
| Section | Contains |
|---|---|
| Text | The program code (the compiled instructions). |
| Data | Global variables (split into initialized and uninitialized). |
| Heap | Memory dynamically allocated at runtime (malloc). |
| Stack | Temporary data: function parameters, return addresses, local variables. |
| (Current activity) | Program counter + processor registers. |
The heap and stack grow toward each other into the free space between them. The global data section divides into initialized and uninitialized data, and a separate area holds the argc/argv command-line parameters.
You can inspect a compiled program's section sizes with the Linux size utility (size executable_name). It reports text (code), data, bss (Block Started by Symbol — uninitialized data), and the dec/hex totals.
Slide wording quirk to watch. The lecture's size slide labels data as “uninitialized” and bss as “initialized,” but standard convention is the reverse: data = initialized globals, bss = uninitialized globals. Learn the standard meaning (bss = uninitialized) and be aware the slide swapped the labels.
As a process runs it changes state. Memorize the five-state model — it is guaranteed exam material and underpins Chapter 6.
| State | Meaning |
|---|---|
| New | The process is being created. |
| Ready | Waiting to be assigned to a processor (in memory, runnable). |
| Running | Instructions are currently being executed on a CPU. |
| Waiting | Blocked, waiting for some event (e.g., I/O completion). |
| Terminated | The process has finished execution. |
Ready vs. Waiting — the distinction students miss. A Ready process could run right now but is waiting only for a free CPU. A Waiting (blocked) process cannot run yet because it is waiting for an event like I/O. After the event completes, a Waiting process moves back to Ready, not straight to Running.
A PCB stores, at minimum:
So far each process had a single thread (one program counter). A multithreaded process has multiple program counters — one per thread — so multiple locations can execute at once (e.g., a word processor handling input, spell-checking, and saving simultaneously). The PCB must then store details for each thread. The number of processes currently in memory is the degree of multiprogramming.
The process scheduler selects which available process runs next on each CPU core, with the goal of maximizing CPU utilization by switching quickly when a process blocks or terminates. It keeps processes organized in queues:
Processes migrate among the queues over their lifetime: a running process that issues I/O moves to a wait queue; when the I/O completes it returns to the ready queue. When a process finally terminates, it is removed from all queues and its PCB is deallocated.
The crucial exam point: context-switch time is pure overhead — no useful work happens during it. The more complex the OS and the PCB, the longer a switch takes. It also depends on hardware support: some CPUs provide multiple register sets, so several contexts can be loaded at once and a switch becomes as cheap as changing a pointer to the active register set.
“Context switching gets work done faster.” A context switch itself accomplishes zero useful computation — it is overhead. The benefit is keeping the CPU busy across many processes; the switch itself is a cost you want to minimize.
Processes form a tree (“process genealogy”): a parent creates children, which create their own children. Each process has a process identifier (PID). On creation, the OS decides resource sharing and execution policy:
In UNIX, fork() creates the new process, exec() (after the fork) replaces the child's memory with a new program, and the parent calls wait() to wait for the child. On Linux, pstree -p shows the tree: systemd is the root parent, logind manages logins, and from a bash shell a user spawns processes like ps and vim.
A process finishes by executing its last instruction and calling exit(), which returns status data to the parent (via wait()) and lets the OS deallocate its resources. A parent can forcibly terminate a child with abort() if: the child exceeded its allocated resources; the child's task is no longer needed; or the parent is exiting and the OS does not allow a child to outlive its parent.
Mobile OSes (Android) reclaim memory by killing processes, starting with the least important. Know the ranking, most to least important:
Older browsers ran as a single process; Google Chrome uses multiple processes for robustness and security — a great real-world illustration of process isolation:
So a crash or compromise in one tab's renderer is contained — it cannot easily take down the whole browser or reach the disk.
Processes are either independent or cooperating. Cooperating processes can affect or be affected by each other (e.g., by sharing data), and they need IPC. Reasons to cooperate: information sharing, computation speedup (splitting subtasks across cores), modularity, and convenience.
There are two IPC models. The comparison is a guaranteed exam question:
| Shared Memory | Message Passing | |
|---|---|---|
| Speed | Faster. | Slower. |
| Kernel involvement | Only to set up the shared region; then routine memory access, no kernel. | Each message needs kernel intervention (time-consuming). |
| Data size | Good for large amounts of data. | Useful for smaller amounts of data. |
| Coordination | Programmer must synchronize access (risk of race conditions). | No conflict avoidance needed by the programmer. |
| Distributed systems | Harder. | Easier to implement across machines. |
Why is shared memory faster? Because after the region is established, all accesses are ordinary memory reads/writes with no system calls and no kernel intervention. Message passing must trap into the kernel for every send/receive.
Two buffer variations:
A clean bounded-buffer solution uses a circular array with two indices: in (next free slot) and out (first filled slot). The buffer is full when (in + 1) % BUFFER_SIZE == out and empty when in == out. This version is correct but can only use BUFFER_SIZE - 1 elements (one slot is sacrificed to distinguish full from empty).
/* Producer */ /* Consumer */
while (1) { while (1) {
/* produce next_produced */ while (in == out) ; // empty? busy-wait
while ((in+1)%SIZE == out) ; next_consumed = buffer[out];
buffer[in] = next_produced; out = (out + 1) % SIZE;
in = (in + 1) % SIZE; /* consume next_consumed */
} }
To use all of the buffer, add a counter of filled slots: the producer waits while counter == BUFFER_SIZE and increments after producing; the consumer waits while counter == 0 and decrements after consuming. But this shared counter introduces a race condition.
Why counter++ is dangerous: it is not atomic. It compiles to three steps — register = counter; register = register + 1; counter = register. With counter = 5 and the producer's ++ interleaved with the consumer's --, you can end up with counter = 6 or 4 depending on interleaving — a lost update.
A losing interleaving (counter starts at 5):
S0 producer: reg1 = counter (reg1 = 5)
S1 producer: reg1 = reg1 + 1 (reg1 = 6)
S2 consumer: reg2 = counter (reg2 = 5)
S3 consumer: reg2 = reg2 - 1 (reg2 = 4)
S4 producer: counter = reg1 (counter = 6)
S5 consumer: counter = reg2 (counter = 4) <-- should be 5!The correct answer is 5 (one produced, one consumed), but the race yields 4. The first version (using only BUFFER_SIZE-1 slots) had no race because producer and consumer never wrote the same variable — the producer only touched in, the consumer only touched out.
Why was there no race in the first solution? Because the two processes never modified a shared variable. With the counter, both write it, so their unsynchronized read-modify-write sequences interleave badly. This is the setup for synchronization tools in the next chapter.
In message passing, processes communicate without shared variables, through two operations: send(message) and receive(message). To communicate, processes P and Q must establish a link and then exchange messages. Messages may be fixed or variable size.
| Direct | Indirect | |
|---|---|---|
| Naming | Processes name each other: send(P, msg), receive(Q, msg). | Messages go to/from mailboxes (ports) with unique IDs: send(A, msg). |
| Link established | Automatically, between exactly one pair. | Only if processes share a common mailbox. |
| Processes per link | Exactly two. | A link may be associated with many processes. |
| Links per pair | Exactly one. | Several (one per shared port). |
Indirect communication raises a sharing puzzle: if P1, P2, P3 all share mailbox A and P1 sends one message while P2 and P3 both try to receive, who gets it? Solutions: limit a link to at most two processes; allow only one receive at a time; or let the system pick the receiver arbitrarily and tell the sender who got it. Mailbox operations: create a mailbox, send/receive through it, and delete it.
Commonly, sending is non-blocking but receiving is blocking. If both send and receive block, that meeting point is called a rendezvous.
A queue of messages (a buffer) can attach to a link in three capacities:
POSIX shared memory uses shm_open (create/open a named shared object), ftruncate (size it), and mmap (map it into the address space). A producer writes; a consumer reads and unlinks it.
/* Producer (excerpt) */
const int SIZE = 4096; const char *name = "OS";
int fd = shm_open(name, O_CREAT | O_RDWR, 0666); // create shared object
ftruncate(fd, SIZE); // set its size
char *ptr = mmap(0, SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
sprintf(ptr, "%s", "Hello"); ptr += strlen("Hello");
/* Consumer (excerpt) */
int fd = shm_open(name, O_RDONLY, 0666);
char *ptr = mmap(0, SIZE, PROT_READ, MAP_SHARED, fd, 0);
printf("%s", ptr); // read what the producer wrote
shm_unlink(name); // remove the object from the system
On Snowball you must link the real-time library with -lrt: gcc -o producer producer.c -lrt. Each memory map corresponds to a file under /dev/shm/ — run ls -l /dev/shm/ to see the "OS" object.
In a shell, the | character creates a pipe: who | wc -l feeds the output of who into wc -l, counting logged-in users. Both the reader and writer execute concurrently, and the pipe controls them — blocking the writer if the buffer is full, the reader if it is empty.
An unnamed pipe is a unidirectional link that automatically buffers input (buffer size varies, ~5000 bytes). Each end has a file descriptor — a read end and a write end — and it uses the ordinary read()/write() system calls. Bidirectional communication needs two pipes.
int fd[2];
pipe(fd); // create the pipe
// fd[0] = READ end ; fd[1] = WRITE end
Because unnamed pipes use file descriptors, access can be passed to descendants via fork(). Typical parent–child usage:
Pipe ends — do not flip them. fd[0] is the READ end; fd[1] is the WRITE end. Unnamed pipes are unidirectional and used between a process and its descendants; bidirectional needs two pipes.
A process = program in execution with text/data/heap/stack; it moves through New→Ready→Running→Waiting→Terminated; the PCB tracks it; the scheduler shuffles it through ready/wait queues using costly context switches; fork/exec/wait create and end it; and cooperating processes use shared memory, message passing, or pipes — where unsynchronized sharing breeds race conditions.
Threads & Concurrency
Chapter 5
In Chapter 4 you learned that a process is a program in execution, carrying its own memory (text, data, heap, stack), its own state (ready/running/waiting), and its own kernel record (the PCB). That is a heavy, self-contained container. This chapter zooms inside the process and asks a sharper question: what is the smallest unit the CPU can actually run? The answer is the thread. Everything in this chapter — concurrency, parallelism, Amdahl's Law, threading models, Pthreads — is about splitting one process into many independently runnable streams of execution so your program can do several things at once and exploit multiple CPU cores.
Big picture: a process is the container; a thread is a worker inside that container. A single-threaded process has exactly one worker that runs the code top to bottom. A multithreaded process has several workers running different parts of the same code at the same time, all sharing the process's memory and resources. This is the central idea of the chapter — hold onto it.
| Each thread has its OWN | All threads SHARE (per process) |
|---|---|
| Thread ID (TID) | Code / text section |
| Program counter (PC) | Data section (globals) |
| Register set | Heap |
| Stack (its own call frames / locals) | Open files, signals, other OS resources |
The classic exam line: "Threads of the same process share the heap and global data but each has its own stack and registers." That is why two threads can clobber a shared global (race condition) but cannot accidentally overwrite each other's local variables — locals live on each thread's private stack.
The instructor's diagram shows two boxes. The left box (single-threaded) has one column: code, data, files across the top, then a single registers/stack/PC unit and one wavy thread line. The right box (multithreaded) has the same shared row across the top — code, data, files — but underneath it shows multiple registers+stack+PC units side by side, each with its own wavy thread line. What the instructor wants you to notice: the top row (code/data/files) is drawn once and shared; the registers/stack/PC unit is duplicated per thread. That picture is the whole 'private vs. shared' table in visual form.
Big picture: almost every modern application is multithreaded because a single thread leaves the machine underused and the user interface frozen. A word processor, for example, can use separate threads to update the display, fetch data, run the spell checker, and answer a network request — all at once.
| Benefit | What it means |
|---|---|
| Responsiveness | If one part of a process blocks (e.g., waiting on disk), other threads keep running. Crucial for UIs — the interface stays alive while work happens in the background. |
| Resource sharing | Threads share the process's memory and resources by default — easier and cheaper than setting up shared memory or message passing between processes. |
| Economy | Creating a thread is cheaper than creating a process; switching between threads has lower overhead than a full process context switch. |
| Scalability | A multithreaded process can spread its threads across multiple cores and run them truly in parallel; a single-threaded process cannot. |
The Multithreaded Server Architecture diagram (Slide 7) makes the point concrete: a server thread sits in a loop waiting for client requests. When a request arrives, instead of handling it itself (and making everyone else wait), the server creates a new thread (or hands the request to an existing one) to service that client, then immediately loops back to wait for the next request. Result: many clients served concurrently, and the listening thread is never blocked.
Big picture: these two words are constantly confused, and distinguishing them is a guaranteed exam question. They are NOT synonyms.
The Slide 10 diagrams: Concurrent execution on a single core is drawn as one timeline where threads T1, T2, T3, T4 take turns — T1 T2 T3 T4 T1 T2 T3 T4… — interleaved on the same core. Parallelism on a multicore system is drawn as two timelines stacked: core 1 runs T1 T3 T1 T3… while core 2 runs T2 T4 T2 T4… simultaneously. Notice in the parallel picture two threads occupy the same vertical time slice; in the concurrent picture only one ever does.
"Concurrent means parallel." No. Concurrency can happen on a single core through time-slicing — tasks make progress by taking turns, not by running at the same instant. You only get parallelism when you have multiple cores actually executing at the same time.
| Type | Idea | Example |
|---|---|---|
| Data parallelism | Split the SAME data across cores; each core runs the SAME operation on its chunk. | Sum a huge array by giving each core a slice to sum, then combine. |
| Task parallelism | Distribute THREADS across cores; each thread performs a UNIQUE operation (possibly on the same data). | One thread sorts, another compresses, another encrypts. |
Memory trick: DATA parallelism = same operation, different DATA. TASK parallelism = different TASK (operation), possibly same data.
Multicore systems put pressure on programmers. The instructor lists five challenges you should be able to name:
Big picture: you bought a 16-core machine. Does your program get 16× faster? Almost never. Amdahl's Law tells you the ceiling, because the part of your program that must run serially (one step after another) cannot be sped up by adding cores. This is the marquee equation of the chapter — expect to compute it by hand.
1
speedup = -------------
(1 - S)
S + -------
N
| Symbol | Meaning | Units / range |
|---|---|---|
| S | Serial portion — the fraction of the program that must run sequentially (cannot be parallelized). | A fraction between 0 and 1 (e.g., 0.25 = 25%). |
| 1 - S | Parallel portion — the fraction that CAN be split across cores. | Fraction between 0 and 1. |
| N | Number of processing cores you run the parallel portion on. | A positive integer (1, 2, 4, …). |
| speedup | How many times faster the whole program runs vs. on a single core. | A unitless ratio (e.g., 1.6 means 1.6× faster). |
Think of total work on one core as taking time 1. That work splits into two pieces: the serial piece S, which always takes time S no matter how many cores you have, and the parallel piece (1 - S), which can be divided evenly across N cores, so it now takes (1 - S) / N. The new total time is S + (1 - S)/N. Speedup is old time over new time = 1 / (S + (1 - S)/N). The serial term S never shrinks — that is the anchor dragging your speedup down.
Example 1 (the deck's own numbers): 75% parallel, 25% serial, 1 → 2 cores.
Here S = 0.25, (1 - S) = 0.75, N = 2.
Denominator = 0.25 + 0.75/2 = 0.25 + 0.375 = 0.625.
speedup = 1 / 0.625 = 1.6. So two cores give a 1.6× speedup, not 2× — exactly what the slide states.
Example 2: the ceiling as N → ∞ (50% serial).
As you add infinite cores, (1 - S)/N → 0, so speedup → 1 / S.
If S = 0.50, maximum speedup = 1 / 0.5 = 2.0× — no matter how many cores you throw at it. Half-serial code can never beat 2× total. This is the slide's headline warning.
Example 3 (harder): 90% parallel, 4 cores.
Here S = 0.10, N = 4. Denominator = 0.10 + 0.90/4 = 0.10 + 0.225 = 0.325.
speedup = 1 / 0.325 ≈ 3.08×. And the ceiling for this program (N → ∞) is 1/0.10 = 10×.
Mistake 1 — using percentages instead of fractions. Plug in 0.25, not 25. The formula needs fractions of 1.
Mistake 2 — dividing the serial part by N. Only the parallel part (1 - S) is divided by N. The serial term S stands alone.
Mistake 3 — thinking N cores means N× speedup. That is only true if S = 0 (fully parallel). Any serial fraction caps you below N.
The serial portion of an application has a disproportionate effect on the gain from extra cores; the absolute ceiling is 1/S.
Big picture: threads can be managed in two different places — by a library running in user space, or by the kernel itself. Which one you have determines how concurrency really behaves.
If threads live purely in user space, the library — not the kernel — is responsible for everything:
Big picture: there must be a relationship — a mapping — between user threads and the kernel threads that actually get CPU time. There are four ways to draw that mapping, and each has a concurrency consequence.
The Slide 17–18 diagrams draw user threads as circles (k or user thread labels) on top, kernel threads as boxes below, and lines connecting them. The number and pattern of those connecting lines is the model: many lines into one box = many-to-one; one line each = one-to-one; many lines into several boxes = many-to-many.
Guaranteed question: "In the many-to-one model, what happens when one thread blocks?" Answer: every thread in the process blocks, because they all share a single kernel thread. Contrast with one-to-one, where only the blocking thread is stuck.
Big picture: a thread library gives the programmer an API to create and manage threads. There are three you must know by name, and one structural choice (async vs. sync) about how the parent behaves after spawning children.
| Library | Key facts |
|---|---|
| POSIX Pthreads | An IEEE POSIX standard (1003.1c). It is a SPECIFICATION, not an implementation — it defines the API's behavior and leaves the implementation to the OS. May be user-level or kernel-level. Common on Linux & macOS. Now part of glibc. |
| Windows threads | The native threading API on Windows. |
| Java threads | Managed by the JVM, typically built on the underlying OS's thread model. |
True/False from the slides: "It is possible to create a thread library without any kernel-level support." True — a library can live entirely in user space.
| Asynchronous threading | Synchronous threading |
|---|---|
| After the parent creates a child, the parent RESUMES execution. Parent and child run concurrently and independently. | The parent creates one or more children and then WAITS for all of them to finish before it continues. Children run concurrently, but the parent is blocked until they all terminate. |
The deck walks through a complete Pthreads program that sums the integers 1..N in a separate thread. The four calls to know: pthread_attr_init (set default attributes), pthread_create (start the thread running a given function), pthread_join (wait for it to finish), and pthread_exit (the thread ends, optionally returning a value).
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int sum; /* shared by the thread(s) */
void *runner(void *param);
int main(int argc, char *argv[]) {
pthread_t tid; /* the thread identifier */
pthread_attr_t attr; /* set of thread attributes */
pthread_attr_init(&attr); /* default attrs */
pthread_create(&tid, &attr, runner, argv[1]); /* start */
pthread_join(tid, NULL); /* wait for exit */
printf("sum = %d\n", sum);
}
void *runner(void *param) {
int i, upper = atoi(param);
sum = 0;
for (i = 1; i <= upper; i++)
sum += i;
pthread_exit(0);
}
Note sum is a global, so it lives in the shared data section — that is why the child thread can write it and main can read it after the join. If sum were a local in runner, it would die with the thread's private stack.
Why void *? A thread function has a fixed signature: it takes a void * and returns a void *. void * is a generic pointer — it can point to any type. To return an int, you allocate it on the heap (so it survives the thread), store its address, and the joiner casts the pointer back:
/* in the worker: return 42 via the heap */
int *result = malloc(sizeof(int));
*result = 42;
pthread_exit((void *) result);
/* in main: collect it */
void *return_value;
pthread_join(tid, &return_value);
int *result = (int *) return_value;
printf("The returned value: %d\n", *result);
Why malloc instead of returning a local's address? If the worker did int r = 42; pthread_exit(&r);, r lives on the thread's stack, which is gone the moment the thread exits — the joiner would read freed memory. Heap memory persists until you free it, so it survives the handoff.
To join many threads (Slide 29), keep them in an array and loop: for (i = 0; i < NUM_THREADS; i++) pthread_join(workers[i], NULL);
| Concept | Windows | Java |
|---|---|---|
| Create a thread | CreateThread(...) | new Thread(new Task()); worker.start(); |
| Wait for a thread | WaitForSingleObject(handle, INFINITE) | worker.join(); |
| Thread function / body | DWORD WINAPI Summation(LPVOID Param) | implement Runnable's run() method |
| Generic pointer type | LPVOID (= void *) | (handled by objects) |
| 32-bit unsigned int type | DWORD ("double word") | (int) |
Java specifics: a thread body is written by implementing the Runnable interface (public void run()) — standard practice — or by extending the Thread class. You start it with worker.start() and wait with worker.join() (wrapped in a try/catch for InterruptedException).
Big picture: as thread counts grow, explicit thread management becomes error-prone. Implicit threading shifts the creation and management of threads away from the programmer and onto compilers and run-time libraries. The deck explores five methods.
Create a number of threads up front in a pool, where they wait for work. When a request arrives, it is handed to an idle pool thread; if none is free, the request is queued until one finishes and returns to the pool.
Windows uses QueueUserWorkItem(&PoolFunction, NULL, 0) — passing a function pointer, a parameter, and flags — to run a function on a pool thread.
Multiple tasks are forked (spawned) and later joined (their results combined). The general recipe is divide-and-conquer:
Task(problem)
if the problem is small enough
solve it directly
else
subtask1 = fork(new Task(subset of problem))
subtask2 = fork(new Task(subset of problem))
result1 = join(subtask1)
result2 = join(subtask2)
combine result1 and result2
Java added a fork-join library in 1.7 for recursive divide-and-conquer algorithms (Quicksort, Mergesort, array summation). ForkJoinTask is the abstract base; RecursiveTask returns a result (via compute()), while RecursiveAction returns nothing. A ForkJoinPool runs the tasks. The summation example splits the array at the midpoint, forks both halves, and joins them — recursing until a chunk is below THRESHOLD (1000), where it sums directly.
A set of compiler directives and an API for C, C++, and FORTRAN, for shared-memory parallelism. You annotate parallel regions and the compiler creates the threads:
#pragma omp parallel /* one thread per core */
{
printf("I am a parallel region.");
}
#pragma omp parallel for /* split loop across threads */
for (int i = 0; i < N; i++)
c[i] = a[i] + b[i];
/* compile: */ gcc -fopenmp openmp_ex.c -o openmp
#pragma omp parallel creates as many threads as there are cores; #pragma omp parallel for additionally divides the loop iterations among those threads. Available on many compilers for Linux, Windows, and macOS.
^{ ... } syntax, e.g. ^{ printf("I am a block"); }.A C++ template library for parallelism. A serial loop for (int i=0;i<n;i++) apply(v[i]); becomes parallel_for(size_t(0), n, [=](size_t i){ apply(v[i]); });. TBB splits the iterations into chunks and creates tasks for them. Supported on Linux, Windows, and macOS.
Big picture: threads complicate several classic OS mechanisms. The deck lists six issues you should be able to discuss.
fork() duplicate only the calling thread, or all threads of the process? Some UNIXes provide two versions: fork() and vfork().exec() works as usual: the new program replaces the entire process, including all of its threads.exec() is called immediately after forking, duplicating all threads is wasteful, so duplicating only the calling thread is appropriate. If the child does not call exec(), it should ideally duplicate all threads.vfork() is faster than fork() because the parent's memory space is not copied.A signal notifies a process that an event occurred; a signal handler (default or user-defined) processes it. The hard question with threads is: which thread receives the signal? The options are: deliver to the thread the signal applies to; deliver to every thread; deliver to certain threads; or assign one thread to receive all signals.
In Pthreads, pthread_cancel(tid) only requests cancellation — whether and when it actually happens depends on the target thread's cancellation state. You still pthread_join afterward to wait for it to terminate.
static __thread int threadID;Both the many-to-many and two-level models need the user library and kernel to communicate about how many kernel threads to allocate. They use an intermediate structure called a lightweight process (LWP), which appears to the application as a virtual processor it can schedule a user thread onto. Each LWP attaches to a kernel thread. A good starting heuristic is one LWP per available core.
Scheduler activations provide upcalls — a communication mechanism from the kernel up to an upcall handler in the thread library — so the application can keep the right number of kernel threads as conditions change (e.g., when a thread blocks).
A thread is the basic unit of CPU scheduling: it privately owns a TID, PC, registers, and stack, and shares code, data, heap, and files with its siblings. Threads buy you responsiveness, resource sharing, economy, and scalability. Concurrency (progress via interleaving, even on one core) is not parallelism (true simultaneous execution on many cores). Amdahl's Law — speedup = 1 / (S + (1−S)/N) — caps that parallelism at 1/S because the serial fraction never shrinks. Threads are managed as user or kernel threads and mapped via many-to-one, one-to-one, many-to-many, or two-level models. Pthreads, Windows, and Java are the three libraries; implicit threading (thread pools, fork-join, OpenMP, GCD, TBB) hands the hard parts to the runtime. Finally, threads complicate fork/exec, signals, cancellation, TLS, and scheduler activations.
CPU Scheduling
Chapter 6
Chapter 5 gave you threads — many runnable streams of execution. But a single core can run only one at a time. CPU scheduling is the policy that decides which ready process (or thread) gets the CPU next, and for how long. Get this wrong and short jobs wait forever behind long ones; get it right and the machine stays busy and responsive. This chapter builds the vocabulary, the criteria, and your first two algorithms — FCFS and SJF — with full Gantt-chart worked examples.
Heads-up on scope: this lecture deck ends after SJF and the start of CPU-burst prediction. These notes cover everything the slides present — basic concepts, the process lifecycle, scheduling vs. dispatching, preemptive vs. non-preemptive, the dispatcher, scheduling criteria, priority, FCFS, SJF, and the burst-prediction (exponential averaging) formula. Later algorithms (Round Robin, Priority queues, multilevel queues) belong to the next part of the chapter and are not in this deck.
Big picture: scheduling exists only because the CPU would otherwise sit idle. Whenever a running process pauses to do I/O, the scheduler can slot in another ready process so no CPU cycles are wasted. This is the entire payoff of multiprogramming.
Histogram of CPU-burst times (Slide 5): the instructor shows a frequency curve that is high on the left and tails off to the right — meaning there are many short CPU bursts and relatively few long ones. This 'many short, few long' shape is why algorithms that favor short jobs (like SJF) tend to perform well in practice.
The deck shows a detailed UNIX-style state diagram — richer than Chapter 4's five states. Trace the arrows: a program starts at New (e.g., you run ./my_prog), then becomes Ready to Run (in memory). If memory is short it can be Ready to Run (swapped) out to disk. From ready it is dispatched to Running (user mode); a system call or interrupt moves it to Running (kernel mode). A preempt sends it back to ready; a sleep() or I/O wait sends it to Asleep (in memory) — or Asleep (swapped) if memory is short. When the event completes it returns to ready. On exit it becomes a Zombie (terminated but its exit status not yet collected by the parent) before final removal. What the instructor wants you to notice: the user-mode/kernel-mode split, the swapped variants driven by 'not enough memory,' and the zombie step — none of which appeared in the simpler five-state model.
Big picture: 'choosing who runs next' and 'actually putting them on the CPU' are two different jobs. Mixing them up is a common exam trap.
Allocating ready processes/threads to available processors is split into two parts:
The CPU scheduler selects from the ready queue and assigns a core. A scheduling decision is triggered when a process:
The four-points question is a favorite. For situations 1 and 4 there is no choice — the running process can't continue, so a new one must be selected (or the CPU goes idle). For situations 2 and 3 there is a choice — the scheduler may keep the current process or switch. That distinction is exactly what defines preemptive vs. non-preemptive scheduling below.
Because resource sharing is the whole point of multiprogramming, the scheduler must be able to remove (preempt) a process in response to changing circumstances, such as:
Preemption isn't free. Preemptive scheduling can cause race conditions on shared data: while process A is mid-update of a shared variable, it gets preempted, and process B reads the variable in its inconsistent, half-updated state. This is exactly the producer/consumer race from Chapter 4. The tools to fix it (locks, semaphores) come in the next chapter.
Big picture: to compare scheduling algorithms you need yardsticks. The deck lists five. Some you want to maximize, some you want to minimize — and knowing which is which is a classic question.
| Criterion | Definition | Goal |
|---|---|---|
| CPU utilization | Fraction of time the CPU is kept busy. | MAXIMIZE |
| Throughput | Number of processes that complete per time unit. | MAXIMIZE |
| Turnaround time | Total time to execute a process — from submission to completion. | MINIMIZE |
| Waiting time | Total time a process spends sitting in the ready queue. | MINIMIZE |
| Response time | Time from submitting a request until the first response is produced (not full completion). | MINIMIZE |
Turnaround vs. waiting vs. response — don't blur them. Turnaround = the whole lifetime (waiting + executing + I/O). Waiting = only the time stuck in the ready queue. Response = time until the first output, which matters for interactive systems where users want quick feedback even if the full job takes longer.
Which process runs is generally based on some measure of priority. System measures that can set a process's priority level include: external priority, attained service time, total service time, deadline, real-time in the system, periodicity, and memory requirements. Many of these are dynamic, so a process's priority can change over time.
Big picture: the simplest possible policy — whoever asks first, runs first, to completion. It is non-preemptive and intuitive, but it has a nasty failure mode called the convoy effect.
A Gantt chart is a horizontal timeline. Each process occupies a block whose width equals its CPU burst time; the numbers under the boundaries are the clock times when each process starts/stops. Waiting time for a process = the time it sits before it first starts running (in simple no-arrival-time problems, that's just the sum of the burst times of everyone ahead of it). Turnaround time = the time from start (t=0) until that process finishes = its waiting time + its own burst.
| Process | Burst Time |
|---|---|
| P1 | 24 |
| P2 | 3 |
| P3 | 3 |
Gantt chart:
Waiting times: P1 = 0 (it runs first); P2 = 24 (waits for all of P1); P3 = 27 (waits for P1 + P2).
Average waiting time = (0 + 24 + 27) / 3 = 17.
Turnaround times: P1 = 24; P2 = 27; P3 = 30.
Average turnaround = (24 + 27 + 30) / 3 = 27.
Nothing about the processes changed — only the order. Watch the average waiting time collapse.
Waiting times: P2 = 0; P3 = 3; P1 = 6.
Average waiting time = (6 + 0 + 3) / 3 = 3 — down from 17!
Turnaround times: P1 = 30; P2 = 3; P3 = 6 → average = (30 + 3 + 6)/3 = 13 (down from 27).
The lesson the instructor is driving at: Example A vs. B uses the identical workload yet average waiting time swings from 17 to 3 purely from ordering. FCFS's quality depends entirely on arrival order — that fragility is the convoy effect, and it's why we need a smarter rule.
Big picture: if putting short jobs first helped so much in Example B, why not always do it? That is exactly SJF — and it is provably optimal for average waiting time.
| Process | Burst Time |
|---|---|
| P1 | 6 |
| P2 | 8 |
| P3 | 7 |
| P4 | 3 |
SJF orders by shortest burst: P4 (3) → P1 (6) → P3 (7) → P2 (8). Gantt chart:
Waiting times (time before each process starts): P4 = 0; P1 = 3; P3 = 9; P2 = 16.
Average waiting time = (3 + 16 + 9 + 0) / 4 = 7.
Turnaround times: P4 = 3; P1 = 9; P3 = 16; P2 = 24 → average = (9 + 24 + 16 + 3)/4 = 13.
"Shortest job" means shortest NEXT CPU burst, not shortest total program. SJF compares the upcoming burst, which is why predicting that burst (next section) is the whole practical challenge.
Big picture: SJF needs the length of the next CPU burst, but the future is unknown. The standard trick is to predict it from the history of past bursts, weighting recent bursts more heavily. This is the formula the deck transitions into at its final slides.
T(n+1) = a * t(n) + (1 - a) * T(n)
| Symbol | Meaning | Range / units |
|---|---|---|
| t(n) | The length of the nth CPU burst — the ACTUAL most recent measured burst. | Time units (e.g., ms). |
| T(n) | The PREDICTED value for the nth burst (our previous guess). | Time units. |
| T(n+1) | The new prediction for the next burst — what SJF will use. | Time units. |
| a (alpha) | Weight given to the most recent actual burst vs. accumulated history. | 0 ≤ a ≤ 1 (commonly 0.5). |
The new guess is a blend of two things: the actual last burst t(n) (recent reality) and the old guess T(n) (everything that came before, already summarized). The knob a sets the balance. With a = 0.5, the latest burst and the entire past history count equally. Because the old guess T(n) itself was built the same way, older bursts fade out exponentially — recent behavior dominates, ancient behavior barely registers. That is why it's called exponential averaging.
Worked example with a = 0.5.
Suppose our previous prediction was T(n) = 10 ms, and the burst that just finished was actually t(n) = 6 ms.
T(n+1) = 0.5 * 6 + 0.5 * 10 = 3 + 5 = 8 ms. The prediction moved from 10 toward the new reality of 6, landing at 8 — halfway, because a = 0.5.
If the next actual burst is t = 4: T = 0.5*4 + 0.5*8 = 6 ms. It keeps gliding toward recent values.
Edge cases to remember: if a = 0, the prediction never changes (T(n+1) = T(n) — history only, recent burst ignored). If a = 1, only the most recent actual burst matters (T(n+1) = t(n) — no memory). a = 0.5 is the common middle ground.
Scheduling fills idle CPU cycles created by the CPU–I/O burst cycle. Scheduling (choosing order) is separate from dispatching (binding to the CPU, costing dispatch latency). Decisions happen at four points; situations 1 & 4 force a switch (non-preemptive), 2 & 3 offer a choice (preemptive). Modern OSes are preemptive — which buys responsiveness but risks race conditions. Judge algorithms by utilization, throughput, turnaround, waiting, and response time. FCFS is simple but suffers the convoy effect (average wait swung 17 → 3 purely on order). SJF runs the shortest next burst first and is optimal for average waiting time, with the preemptive SRTF variant; its one problem is knowing the next burst, solved by exponential averaging: T(n+1) = a·t(n) + (1−a)·T(n).
Synchronization Tools
Chapter 7
Chapters 4–6 kept running into the same wall: whenever concurrent processes or threads share data, the final result can depend on the order in which their instructions interleave — a race condition. This chapter finally builds the toolbox that fixes it. The arc: define the critical section problem precisely, state the three requirements any valid solution must satisfy, then climb the ladder of solutions — software algorithms (turn variable, Peterson's), memory barriers, hardware instructions (test-and-set, compare-and-swap), atomic variables, mutex locks, semaphores, and finally monitors.
MIDTERM ALERT (from Slide 2): Thursday, July 9th · 1 hr 40 min · Multiple-choice + short answer. Topics: OS Structures, Processes, Threads, CPU Scheduling, and the first half of Synchronization Tools (this chapter through roughly mutex locks/semaphores). Cheat sheet allowed: ONE letter-sized page, handwritten, front & back — no typed or photocopied notes.
Big picture: nothing here is new hardware or new OS machinery — it's a consequence of what you already know. Processes execute concurrently and can be interrupted at any time, partially completing their work. If two of them touch the same shared data mid-update, the data can end up inconsistent.
The paradigm: a producer process produces information consumed by a consumer process (in practice, one process may be both). Two shared-memory variations: an unbounded buffer has no size limit — the producer never waits, the consumer waits only when the buffer is empty; a bounded buffer has a fixed size — the producer must also wait when the buffer is full.
The bounded-buffer code, with a shared counter tracking filled slots (in = next free location, out = first data location):
/* PRODUCER */ /* CONSUMER */
item next_produced; item next_consumed;
while (1) { while (1) {
/* produce an item */ while (counter == 0); /* busy wait */
while (counter == BUFFER_SIZE); next_consumed = buffer[out];
/* busy wait */ out = (out + 1) % BUFFER_SIZE;
buffer[in] = next_produced; counter--;
in = (in + 1) % BUFFER_SIZE; /* consume the item */
counter++; }
}
counter == BUFFER_SIZE means full (producer spins); counter == 0 means empty (consumer spins). The bug: both processes write the shared variable counter.
A single line like counter++ is NOT one machine instruction — it compiles to three: load, add, store. Same for counter--:
counter++ => register1 = counter counter-- => register2 = counter
register1 = register1 + 1 register2 = register2 - 1
counter = register1 counter = register2
The killer interleaving (counter = 5 initially):
S0: producer register1 = counter {register1 = 5}
S1: producer register1 = register1 + 1 {register1 = 6}
S2: consumer register2 = counter {register2 = 5}
S3: consumer register2 = register2 - 1 {register2 = 4}
S4: producer counter = register1 {counter = 6}
S5: consumer counter = register2 {counter = 4} ← should be 5!
One item produced + one consumed should leave counter unchanged at 5. The interleaving yields 4 (and a different interleaving could yield 6). The result depends on execution order — the definition of a race condition.
A simpler kernel-level example (Slide 9): processes P0 and P1 both call fork(). Inside the kernel, both read/update the shared variable next_available_pid. Without a mechanism preventing simultaneous access, the same PID could be assigned to two different processes — races aren't just an application-programmer problem; the kernel itself must synchronize.
Why is it so fragile? A program is a sequence of machine instructions executed sequentially. We may assume an individual instruction is atomic, but a process may be interrupted after any one instruction — including in the most inconvenient places: mid-way through evaluating a while/for/if condition, or halfway through inserting a node into a tree, linked list, or heap.
Two processes each execute x = x + 1, which is really three machine steps: R = x (load), R = R + 1 (increment), x = R (store). With x = 0 initially, three interleavings:
| Sequence | Interleaving | Final x |
|---|---|---|
| #1 | P1 runs all three steps, THEN P2 runs all three. | 2 (correct) |
| #2 | P2 runs all three steps, THEN P1 runs all three. | 2 (correct) |
| #3 | P1 loads x (R1=0), then P2 runs all three (x=1), then P1 finishes with its stale R1 → x = 1. | 1 (lost update!) |
Sequence #3 is the lost update: P1's stale register overwrites P2's work. The question the rest of the chapter answers: how do we prevent multiple processes from being active in the CS at once?
Consider n processes {P0…Pn}, each with a CS. The protocol: each process must ask permission to enter in an entry section, follow the CS with an exit section signaling it has left, then continue in its remainder section. The entry and exit sections are standardized across all cooperating processes:
while (true) {
[entry section] /* ask permission */
critical section
[exit section] /* announce departure */
remainder section
}
| Requirement | Meaning |
|---|---|
| Mutual Exclusion | If Pi is executing in its critical section, no other process can be executing in its critical section. |
| Progress | If no process is in its CS and some process wants in, the selection of who enters next cannot be postponed indefinitely. |
| Bounded Waiting | There must be a bound on how many times OTHERS can enter their CS before a requesting process gets its turn. (Assumes each process runs at nonzero speed; no assumption about relative speeds.) |
Deadlock vs. livelock: in deadlock, processes are stuck waiting (blocked, doing nothing). In livelock, they are actively running — constantly reacting and yielding to each other — yet still making no progress. Think two people in a hallway endlessly stepping the same way to let each other pass.
Idea: entry section = disable interrupts; exit section = enable interrupts. The running process can't be preempted, so mutual exclusion holds. But (Slides 14 & 19): what if the CS runs for an hour? Processes can starve. What if there are two or more CPUs? Disabling interrupts on one core doesn't stop the other core from entering the CS. Not scalable — fails on multiprocessor systems. It's used only for tiny sections inside uniprocessor kernels.
Big picture: before hardware help, people tried to solve the CS problem with plain shared variables. The two-process attempts below assume load and store instructions are atomic (cannot be interrupted mid-instruction).
Two processes share one variable: int turn; whose value is only ever i or j, indicating whose turn it is to enter. Initially turn = i.
/* Process Pi */
while (true) {
while (turn == j); /* ENTRY: busy wait until it's my turn */
/* critical section */
turn = j; /* EXIT: hand the turn to the other */
/* remainder section */
}
Correctness check: Mutual exclusion holds — Pi enters only if turn == i, and turn cannot be both i and j at once. But Progress fails: the processes are forced into strict alternation. If it's Pj's turn but Pj never wants to enter (it's busy in its remainder section), Pi is locked out even though the CS is empty — the selection of who enters next is postponed indefinitely by a process that isn't even competing.
/* Process Pi (Pj is symmetric, with i and j swapped) */
while (true) {
flag[i] = true; /* ENTRY: I am ready */
turn = j; /* ...but I defer to you */
while (flag[j] && turn == j); /* spin only if Pj is ready */
/* AND it's Pj's turn */
/* critical section */
flag[i] = false; /* EXIT: I'm done */
/* remainder section */
}
How to read the entry section: "I raise my hand (flag[i]=true), then politely give you the turn (turn=j). I wait only while you are BOTH ready AND it's your turn. If you're not ready, I go in. If we're both ready, whoever set turn last loses the tie and waits."
flag[j] == false (Pj not competing) or turn == i. Both can't pass simultaneously because turn is a single variable — it can't be i and j at once.flag[j] == false) — no strict alternation, so a non-competing process never blocks anyone.Peterson's is great for teaching but not guaranteed to work on modern hardware. To improve performance, processors and/or compilers may reorder operations that have no dependencies (i.e., don't touch the same data). For single-threaded code that's fine — the result is identical. For multi-threaded code, reordering can produce inconsistent, unexpected results.
The reordering demo (Slides 27–28). Shared: boolean flag = false; int x = 0;
Thread 1: while (!flag); print(x); Thread 2: x = 100; flag = true;
Expected output: 100 — Thread 2 sets x before raising flag. But x and flag are independent, so Thread 2's two writes may be reordered to flag = true; x = 100;. If Thread 1 wakes between them, it prints 0.
In Peterson's, reordering the writes to flag[i] and turn can let BOTH processes into their critical sections simultaneously.
/* Thread 1 */ /* Thread 2 */
while (!flag); x = 100;
memory_barrier(); memory_barrier();
print(x); flag = true;
For Thread 1, the barrier guarantees flag is loaded before x. For Thread 2, it ensures the assignment to x happens before the assignment to flag. Output is now guaranteed to be 100.
Big picture: software-only solutions are fragile, so hardware gives us special atomic instructions that test-and-modify a word, or swap two words, as one uninterruptible step. On uniprocessors the old trick was disabling interrupts (not scalable); on modern machines the two workhorses are test-and-set and compare-and-swap, plus atomic variables built on top of them.
boolean test_and_set(boolean *target) {
boolean rv = *target; /* remember the OLD value */
*target = true; /* always set flag to true */
return rv; /* return the OLD value */
} /* the whole thing executes ATOMICALLY */
Three properties to recite: it executes atomically (one indivisible step); it returns the original value of *target; and it always leaves *target true. It is the classic building block for spinlocks.
Lock with TAS — shared boolean lock = false:
do {
while (test_and_set(&lock)); /* busy wait */
/* critical section */
lock = false;
/* remainder section */
} while (true);
Why it works: if the lock was free (false), TAS returns false — the loop exits and you're in, and the lock is now true so everyone else spins. If it was taken (true), TAS returns true and you keep spinning. Verdict vs. the three requirements: mutual exclusion ✓, progress ✓, but bounded waiting ✗ — an unlucky process can lose the TAS race forever while others repeatedly grab the lock.
int compare_and_swap(int *value, int expected, int new_value) {
int temp = *value; /* remember original */
if (*value == expected) /* swap ONLY if it matches */
*value = new_value;
return temp; /* return original value */
} /* executes ATOMICALLY */
Three properties: atomic; returns the original value; and the swap is conditional — it happens only when *value equals expected. CAS is the foundation of modern lock-free algorithms.
Lock with CAS — shared int lock = 0:
while (true) {
while (compare_and_swap(&lock, 0, 1) != 0); /* busy wait */
/* critical section */
lock = 0;
/* remainder section */
}
Read the spin condition aloud: "try to change lock from 0 to 1; if the returned original wasn't 0, someone else holds it — spin." Same verdict as TAS: mutual exclusion ✓, progress ✓, bounded waiting ✗.
To satisfy all three requirements, add shared boolean waiting[n] (all false — nobody waiting at startup) and int lock = 0 (lock starts free, so the first process can acquire it immediately):
while (true) {
/* ---- ENTRY SECTION ---- */
waiting[i] = true;
key = 1;
while (waiting[i] && key == 1)
key = compare_and_swap(&lock, 0, 1);
waiting[i] = false;
/* critical section */
/* ---- EXIT SECTION ---- */
j = (i + 1) % n;
while ((j != i) && !waiting[j]) /* scan for next waiter */
j = (j + 1) % n;
if (j == i) lock = 0; /* nobody waiting: free the lock */
else waiting[j] = false; /* hand CS directly to Pj */
/* remainder section */
}
The idea: a process escapes the entry loop either by winning the CAS (key == 0) or by being chosen (waiting[i] set false by the exiting process). On exit, the leaver scans the waiting array in circular order from i+1 and passes the CS to the first waiter it finds — so every waiter is reached within at most n − 1 turns. That circular scan is exactly what buys bounded waiting.
void increment(atomic_int *v) {
int temp;
do {
temp = *v; /* snapshot */
} while (temp != compare_and_swap(v, temp, temp + 1)); /* retry */
}
How the retry loop works: snapshot the value, then CAS attempts to bump it from that snapshot. If another thread changed *v in between, CAS returns something ≠ temp — the swap didn't happen — so loop and try again with a fresh snapshot. Eventually one clean attempt lands.
Big picture: TAS/CAS solutions are complex and easy to get wrong, so OS designers wrapped them in a simpler tool programmers can actually use — the mutex (MUTual EXclusion) lock.
/* usage */ /* implementation */
while (true) { acquire() {
acquire(lock); while (!available); /* spin */
/* critical section */ available = false;
release(lock); }
/* remainder section */ release() {
} available = true;
}
"Spinlock" isn't a different tool — it's a mutex that busy-waits. While the lock is held, waiting threads loop re-checking it. That wastes CPU cycles, which is fine for very short critical sections (spinning is cheaper than a context switch) but terrible for long ones. The no-busy-wait alternative appears in the semaphore section next.
Big picture: a mutex is a door that's open or shut. A semaphore is a counter of available resources — strictly more powerful. Invented by Edsger Dijkstra in 1965; the original operation names P() and V() come from Dutch: proberen ('test') and verhogen ('increase').
/* busy-waiting definition */
wait(s) { signal(s) {
while (s <= 0); /* spin */ s++;
s--; }
}
| Type | Value range | Use |
|---|---|---|
| Counting semaphore | Unrestricted integer domain | Controlling access to a pool of N identical resources. |
| Binary semaphore | Only 0 or 1 | Same as a mutex lock. (A counting semaphore can even be built from binary ones.) |
With semaphores we can solve the classic synchronization problems previewed for next chapter: the Bounded-Buffer, Readers–Writers, and Dining-Philosophers problems.
Pattern 1 — mutual exclusion. Create semaphore mutex = 1 (the number of 'resources' = 1 CS slot):
wait(mutex); /* critical section */ signal(mutex);
Pattern 2 — enforcing order (S1 before S2). P1 must run S1 before P2 runs S2. Create semaphore sync = 0:
P1: S1; signal(sync); P2: wait(sync); S2;
Because sync starts at 0, P2's wait() blocks until P1's signal() — regardless of scheduling order. Initializing to 0 = 'the event has not happened yet.'
wait() and signal() must themselves be mutually exclusive — no two processes may execute them on the same semaphore simultaneously. So the semaphore implementation is itself a critical-section problem, with wait/signal code in the CS. The naive version busy-waits; that's tolerable when critical sections are short and rarely occupied, but applications that spend a lot of time in their CS make spinning a very poor solution.
The fix (Slides 51–53): give each semaphore a waiting queue. Each queue entry holds a value and a pointer to the next record. Two new kernel operations: block() — put the invoking process on the semaphore's waiting queue (suspend it); wakeup(P) — remove process P from the waiting queue and move it to the ready queue.
typedef struct {
int value;
struct process *list; /* the waiting queue */
} semaphore;
wait(semaphore *s) { signal(semaphore *s) {
s->value--; s->value++;
if (s->value < 0) { if (s->value <= 0) {
(s->list).append(P); P = (s->list).pop();
block(); /* suspend */ wakeup(P);
} }
} }
Read the negative value! In this version, s->value may go negative — and its magnitude equals the number of processes waiting on the semaphore. E.g., value = −3 means three processes are blocked in the queue. In signal(), the test value <= 0 (after the increment) means "at least one process was waiting — wake one up." Instructors love asking what a negative semaphore value means.
| Mistake | Consequence |
|---|---|
| signal(mutex) … then wait(mutex) — reversed order | Several processes may be in their CS simultaneously — mutual exclusion violated. |
| wait(mutex) … then wait(mutex) again — double wait | The process permanently blocks on the second wait; the semaphore is now unavailable to everyone. |
| Omitting wait() and/or signal() | Either mutual exclusion is violated, or processes permanently block. |
These bugs motivate the final tool: push the discipline into the language/compiler so programmers can't forget or misorder the calls — the monitor.
monitor monitor-name {
/* shared variable declarations */
initialization_code (...) { ... }
procedure_p1 (...) { ... }
procedure_p2 (...) { ... }
...
}
Schematic (Slide 56): picture the monitor as a box holding shared data and its procedures. Processes wanting in line up in a single entry queue outside; at most one is active inside at any moment. That entry queue is the mutual exclusion made visible.
x.signal() is NOT semaphore signal(). A semaphore's signal() always increments — it is 'remembered' even if nobody is waiting. A condition variable's x.signal() with no waiters does nothing at all — it evaporates. This is the most-tested difference between the two tools.
Ordering with a condition variable (Slide 60): require S1 (in P1) before S2 (in P2). Build a monitor with a shared done = false and condition x:
F1() { S1; done = true; x.signal(); } F2() { if (done == false) x.wait(); S2; }
If P2 arrives first, done is false, so it waits on x. When P1 finishes S1 it sets done and signals. Note the done flag handles the case where P1 ran first — then P2 skips the wait entirely (remember: a signal with no waiter would have vanished).
Simple version (mutual exclusion only): one semaphore mutex = 1; every monitor procedure body is wrapped as wait(mutex); …body…; signal(mutex); — mutual exclusion within the monitor is ensured.
Full version (with signaling): add semaphore next = 0 and int next_count = 0 — the count of processes suspended inside the monitor after signaling. A signaler must yield the monitor to the process it woke, then wait on next until it can resume. Each procedure exit becomes:
wait(mutex);
... body of P ...
if (next_count > 0)
signal(next); /* let a suspended signaler resume */
else
signal(mutex); /* otherwise open the front door */
For each condition variable x, keep semaphore x_sem = 0 and int x_count = 0:
/* x.wait() */ /* x.signal() */
x_count++; if (x_count > 0) {
if (next_count > 0) next_count++;
signal(next); signal(x_sem); /* wake waiter */
else wait(next); /* I step aside */
signal(mutex); next_count--;
wait(x_sem); /* sleep on x */ }
x_count--;
The logic in one breath: x.wait() = register as a waiter, release the monitor to someone (a suspended signaler first, else the entry queue), then sleep on x_sem. x.signal() = if anyone is waiting on x, wake exactly one and suspend yourself on next until the monitor is handed back — preserving the one-active-process rule at every instant.
The deck ends here — 'to be continued, resumed before the exam.' Next lecture: Chapter 7 continued / Synchronization Examples (Bounded-Buffer, Readers–Writers, Dining Philosophers).
Concurrent access to shared data causes race conditions because even counter++ is three interruptible instructions. Code touching shared data is a critical section, and a valid protection scheme must guarantee mutual exclusion, progress, and bounded waiting while avoiding starvation, deadlock, and livelock. A bare turn variable fails progress; Peterson's solution (flag[] + turn) provably satisfies all three — but modern CPUs reorder independent instructions, so it needs memory barriers to force visibility. Hardware gives atomic test_and_set and compare_and_swap (mutual exclusion but unbounded waiting until you add the circular waiting[] scan) and atomic variables (CAS retry loops). The OS wraps these into mutex locks — acquire/release spinlocks — and semaphores (Dijkstra 1965; atomic wait/signal; counting vs. binary; init 1 = mutual exclusion, init 0 = ordering; the blocked-queue version's negative value counts the waiters). Misused semaphores deadlock or break exclusion, so monitors enforce one-active-process automatically, with condition variables (x.wait/x.signal — a signal with no waiter evaporates) for waiting on state inside.
Synchronization Examples
Chapter 8
Chapter 7 built the toolbox: hardware gives us atomic test_and_set and compare_and_swap, the OS wraps those into mutex locks and semaphores, and languages wrap everything into monitors with condition variables. Chapter 8 is where the toolbox gets used. First we solve the three classical synchronization problems — Bounded-Buffer, Readers–Writers, and Dining Philosophers — which are the standard benchmarks every new synchronization scheme is tested against. Then we look at how real systems — Windows, Linux, POSIX, and Java — actually implement these tools.
The classical problems matter for two reasons. First, they are abstractions of situations that appear constantly in real systems (a print queue is a bounded buffer; a database is a readers–writers situation). Second, they are the single most common source of exam questions on synchronization: you will be asked to write or trace the semaphore code, explain why a wait comes before another wait, or spot the deadlock in a broken variant.
Take-Home Quiz 4 covers Lectures 8 and 9 — exactly the material in this chapter and the next. Homework 3 (the process-synchronization program with reads/writes on a common file) requires POSIX semaphores, which are covered in §8.6. Read that section with the assignment open next to it.
One recap fact worth restating before the problems: every valid critical-section solution must give Mutual Exclusion, Progress, and Bounded Waiting, and the hardware primitives alone give you the first two but not the third. The classical problems below are where these requirements stop being abstract and start deciding whether your code deadlocks.
You met the producer–consumer setup in Chapters 4 and 7: a producer creates items and inserts them into a shared buffer of fixed size n; a consumer removes them. Two things can go wrong: the producer must not insert into a full buffer, the consumer must not remove from an empty one — and neither may touch the buffer while the other is mid-operation. The semaphore solution handles all three concerns with exactly three semaphores.
int n; /* buffer holds n items */
semaphore mutex = 1; /* mutual exclusion on the buffer */
semaphore empty = n; /* counts EMPTY slots (starts n) */
semaphore full = 0; /* counts FULL slots (starts 0) */
Read the initial values as an inventory: at the start there are n empty slots and 0 full slots, and the buffer itself is free (mutex = 1). mutex is a binary semaphore used for mutual exclusion; empty and full are counting semaphores used for ordering — the two canonical usage patterns from §7.7, side by side in one problem.
/* PRODUCER */ /* CONSUMER */
while (true) { while (true) {
/* produce item in next_produced */ wait(full);
wait(empty); wait(mutex);
wait(mutex); /* remove item to next_consumed */
/* add next_produced to buffer */ signal(mutex);
signal(mutex); signal(empty);
signal(full); /* consume item in next_consumed */
} }
Trace the symmetry: the producer consumes an empty slot (wait(empty)) and produces a full slot (signal(full)); the consumer does the mirror image. Each process signals the semaphore the other process waits on — that is what makes them cooperate. The mutex wait/signal pair is nested inside the empty/full pair, wrapping only the few lines that actually touch the buffer.
A classic exam question swaps the producer's two waits: wait(mutex) before wait(empty). Trace it with a full buffer: the producer acquires mutex, then blocks on empty (no empty slots). The consumer now runs wait(full) — fine — then blocks on wait(mutex), which the sleeping producer still holds. Each waits for an event only the other can cause: deadlock. Rule to memorize: acquire the resource-counting semaphore first, the mutex last — never sleep on a counting semaphore while holding the mutex. (Swapping the two signals only hurts efficiency; swapping the two waits kills the program.)
Bounded buffer = one binary semaphore for exclusion + two counting semaphores that count empty and full slots. Initial values are the inventory (n, 0), each side waits on what it needs and signals what it creates, and the mutex is always the innermost pair.
A dataset is shared among concurrent processes of two kinds. Readers only read — they never update, so any number of them can be in the dataset simultaneously without harm. Writers read and write, so a writer needs the dataset exclusively: no other writer, and no readers either. The problem is to maximize concurrency (let readers overlap) while preserving safety (writers alone).
In plain English: Many processes share data; the ones that only look can share freely, but the ones that change it need everyone else out of the room.
Technical definition: A synchronization problem in which any number of reader processes may access a shared dataset concurrently, but each writer process requires exclusive access (no concurrent readers or writers).
Why it matters: It models real systems directly — databases, file systems, caches — and it motivated the reader–writer locks that Linux and Pthreads provide as first-class primitives.
There are two standard fairness policies, and the exam expects you to know both by name. Reader-priority (the "first" readers–writers problem): no reader waits unless a writer has already obtained permission — readers never wait merely because a writer is waiting. Writer-priority (the "second" problem): once a writer is ready, it writes as soon as possible — no new readers may start after a writer is queued. Each policy starves the other side: reader-priority can starve writers under a steady stream of readers; writer-priority can starve readers.
semaphore rw_mutex = 1; /* readers vs. writers: who owns the dataset */
semaphore mutex = 1; /* protects updates to read_count */
int read_count = 0; /* how many readers are inside right now */
Keep the two semaphores straight — this is the part students mix up. rw_mutex guards the dataset itself; a writer holds it for the whole write, and the reader group holds it collectively while any reader is inside. mutex guards only the little integer read_count, because read_count++ is itself a critical section (it is exactly the counter++ race from §7.1).
while (true) {
wait(rw_mutex); /* claim exclusive access */
/* writing is performed */
signal(rw_mutex); /* release it */
}
while (true) {
wait(mutex); /* permission to update read_count */
read_count++;
if (read_count == 1) /* FIRST reader... */
wait(rw_mutex); /* ...locks out the writers */
signal(mutex);
/* reading is performed */
wait(mutex);
read_count--;
if (read_count == 0) /* LAST reader... */
signal(rw_mutex); /* ...lets the writers back in */
signal(mutex);
}
Reader R1 arrives: read_count becomes 1, so R1 (the first reader) does wait(rw_mutex), acquiring the dataset for the reader group. R2 and R3 arrive: read_count becomes 2, then 3; the if fails both times, so they walk straight in — three readers overlap, which is the whole point.
Writer W arrives: wait(rw_mutex) blocks, because the reader group holds it. R1 and R2 leave (read_count 2, then 1) — nothing happens to rw_mutex. R3 leaves: read_count hits 0, so R3 (the last reader) does signal(rw_mutex), and W finally proceeds. Note it is not the first reader who releases — it is whoever happens to leave last.
"Each reader locks rw_mutex on the way in." No — only the first reader locks it and only the last releases it; the ones in between just increment/decrement the count under mutex. If every reader took rw_mutex, readers would exclude each other and you would have built an ordinary mutex with extra steps, losing all reader concurrency.
Two favorite short-answers: (1) Why does this version starve writers? Because a continuous overlapping stream of readers keeps read_count above 0 forever, so signal(rw_mutex) never runs and the writer waits indefinitely — reader-priority sacrifices writer bounded waiting. (2) What does the inner mutex protect and why is it needed? Only read_count — because ++/-- are not atomic, two readers updating simultaneously could lose an update, and then the first/last-reader tests fire at the wrong times.
Because the pattern is so common, many systems ship it pre-built as a reader–writer lock: acquire in read mode (shared, many holders) or write mode (exclusive). Pthreads provides pthread_rwlock_t, and Linux provides reader–writer versions of both its spinlocks and its semaphores (§8.5) — useful whenever readers clearly outnumber writers.
Five philosophers sit around a circular table with one bowl of rice in the middle and five single chopsticks, one between each pair of neighbors. A philosopher alternates forever between thinking and eating, never interacting with neighbors directly. To eat, a philosopher needs both adjacent chopsticks, picked up one at a time, and releases neither until finished eating. The puzzle: design a protocol so no philosopher starves.
Why anyone cares: it is the cleanest possible model of a set of processes that each need multiple shared resources at once. Every deadlock you will ever debug is dining philosophers wearing a costume.
semaphore chopstick[5]; /* all initialized to 1 */
/* philosopher i */
while (true) {
wait(chopstick[i]); /* pick up left */
wait(chopstick[(i + 1) % 5]); /* pick up right */
/* eat for a while */
signal(chopstick[i]); /* put down left */
signal(chopstick[(i + 1) % 5]); /* put down right */
/* think for a while */
}
This is asked verbatim. The answer: it guarantees mutual exclusion on each chopstick (no two neighbors eat at once), but it can deadlock. If all five philosophers get hungry simultaneously and each grabs their left chopstick, all five then block on wait(chopstick[(i+1) % 5]) — every chopstick is held, every philosopher waits for a resource held by their neighbor, and the circular wait never breaks. Note this is precisely the four deadlock conditions of Chapter 9 in action: mutual exclusion (a chopstick has one holder), hold-and-wait (hold left, wait for right), no preemption (you cannot snatch a chopstick), circular wait (the ring).
The monitor tracks each philosopher's state — THINKING, HUNGRY, or EATING — and grants chopsticks in one atomic decision instead of two separate grabs. A philosopher may move to EATING only when neither neighbor is EATING. Each philosopher has a condition variable self[i] to wait on when hungry but blocked.
monitor DiningPhilosophers
{
enum {THINKING, HUNGRY, EATING} state[5];
condition self[5];
void pickup(int i) {
state[i] = HUNGRY;
test(i); /* try to eat right now */
if (state[i] != EATING)
self[i].wait(); /* otherwise sleep until fed */
}
void putdown(int i) {
state[i] = THINKING;
test((i + 4) % 5); /* maybe left neighbor can eat now */
test((i + 1) % 5); /* maybe right neighbor can eat now */
}
void test(int i) {
if ((state[(i + 4) % 5] != EATING) && /* left not eating */
(state[i] == HUNGRY) && /* i wants to eat */
(state[(i + 1) % 5] != EATING)) { /* right not eating */
state[i] = EATING;
self[i].signal(); /* wake i if it was waiting */
}
}
initialization_code() {
for (int i = 0; i < 5; i++)
state[i] = THINKING;
}
}
A philosopher uses it as: DiningPhilosophers.pickup(i); ... eat ... ; DiningPhilosophers.putdown(i); — and the monitor's built-in mutual exclusion (only one process active inside at a time, §7.8) makes each pickup/putdown decision atomic. The clever part is putdown(): a finishing philosopher runs test() on behalf of both neighbors, which is what wakes a hungry neighbor whose earlier test() failed. Note (i + 4) % 5 is just "left neighbor" written without negative numbers — it equals (i − 1) mod 5.
"The monitor solution fully solves dining philosophers." Not quite — it eliminates deadlock (chopstick pairs are granted atomically, so the hold-and-wait on single chopsticks never happens) but starvation is still possible: an unlucky philosopher can wait forever while their two neighbors alternate eating, since nothing guarantees the waiter is ever chosen. Deadlock-free and starvation-free are different guarantees — the exam loves this distinction.
One semaphore per chopstick + grab-left-then-right = possible deadlock (all five hold left). Fixes all break one deadlock condition: limit to four diners, order the grabs asymmetrically, or grant both chopsticks atomically. The monitor does the last one via state[] and test(), giving no deadlock — but still possible starvation.
The classical problems are solved with idealized semaphores and monitors. Real kernels have to build those tools out of what the hardware offers, and the design differs by OS and by uniprocessor vs. multiprocessor — a distinction that echoes §7.2's point that disabling interrupts only works on one core.
The type atomic_t is an integer whose operations are guaranteed indivisible without an explicit lock — the "atomic variables" of §7.5 as a concrete kernel API. Given atomic_t counter; and int value;, the basic operations behave as follows:
| Operation | Effect | Value of counter |
|---|---|---|
| atomic_set(&counter, 5); | counter = 5, atomically | 5 |
| atomic_add(10, &counter); | counter = counter + 10 | 15 |
| atomic_sub(4, &counter); | counter = counter − 4 | 11 |
| atomic_inc(&counter); | counter = counter + 1 | 12 |
| value = atomic_read(&counter); | value = counter (atomically read) | 12 |
Atomic integers are ideal when the shared data is just a counter (reference counts, statistics): they are far cheaper than taking a lock. The moment an update involves more than one variable, or any decision logic, you are back to locks — atomicity of single operations does not compose into atomicity of sequences (the same lesson as the CAS discussion in §7.5).
The POSIX API (Pthreads and friends) is what you program against on UNIX, Linux, and macOS — and what Homework 3 requires. It provides three tools: mutex locks, semaphores, and condition variables. POSIX is a specification for user-level code; underneath, each OS implements these with the kernel machinery of §8.5.
#include <pthread.h>
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); /* create + initialize; NULL = default attrs */
pthread_mutex_lock(&mutex); /* acquire (blocks if unavailable) */
/* critical section */
pthread_mutex_unlock(&mutex); /* release */
POSIX provides two versions of semaphores. Named semaphores have a filesystem-style name and therefore can be shared by unrelated processes — two programs that have never met can both sem_open("SEM", ...) and synchronize. Unnamed semaphores live in a variable, so other processes can only get them by inheritance across fork() (or via placement in shared memory).
#include <semaphore.h>
/* NAMED: usable by unrelated processes */
sem_t *sem;
sem = sem_open("SEM", O_CREAT, 0666, 1); /* create, perms 0666, initial value 1 */
/* UNNAMED: shared only via fork() or shared memory */
sem_t sem2;
sem_init(&sem2, 0, 1); /* 0 = shared between THREADS of this process; 1 = initial
value */
/* Both versions are used identically afterwards: */
sem_wait(sem); /* = wait(): acquire */
/* critical section */
sem_post(sem); /* = signal(): release */
Vocabulary mapping trips people up: POSIX spells wait() as sem_wait() and signal() as sem_post() — there is no function called sem_signal. And the named-vs-unnamed distinction is a ready-made short answer: named → unrelated processes via a string name; unnamed → related processes only, inherited through fork(). For Homework 3 (separate processes sharing a file), named semaphores are the natural fit.
C has no monitors, so a POSIX condition variable does not live inside one — instead it is explicitly paired with a pthread mutex, and you supply the mutual exclusion the monitor would have provided. The waiting thread must hold the mutex when it calls pthread_cond_wait(), which atomically releases the mutex and sleeps; when signaled, it reacquires the mutex before returning.
#include <pthread.h>
pthread_mutex_t mutex;
pthread_cond_t cond_var;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond_var, NULL);
/* WAITING side: sleep until a == b */ /* SIGNALING side */
pthread_mutex_lock(&mutex); pthread_mutex_lock(&mutex);
while (a != b) a = b;
pthread_cond_wait(&cond_var, &mutex); pthread_cond_signal(&cond_var);
pthread_mutex_unlock(&mutex); pthread_mutex_unlock(&mutex);
"You can test the condition with an if." Always a while. Between the moment a waiter is signaled and the moment it actually reacquires the mutex and runs, another thread may have slipped in and falsified the condition again (or the wait may return spuriously). The while re-checks on every wakeup; an if would proceed on a stale condition. This single line is one of the most-tested details in POSIX synchronization.
Java bakes synchronization into the language. It offers Java monitors (via the synchronized keyword), plus library classes for reentrant locks (mutex locks), semaphores, and condition variables. The monitor mechanism is the part the lecture develops, because it is the closest thing in mainstream languages to Chapter 7's monitor concept.
Each object actually has two waiting rooms, and telling them apart is the standard exam question. The entry set holds threads waiting to acquire the lock (they tried to enter a synchronized method that was busy). The wait set holds threads that had the lock and gave it up by calling wait() because some condition they needed was false. When a thread calls wait(): (1) it releases the object's lock, (2) its state is set to blocked, (3) it is placed in the wait set. A call to notify() picks one thread from the wait set and moves it to the entry set, where it competes to reacquire the lock.
public class BoundedBuffer<E> {
private static final int BUFFER_SIZE = 5;
private int count, in, out;
private E[] buffer;
public BoundedBuffer() {
count = 0; in = 0; out = 0;
buffer = (E[]) new Object[BUFFER_SIZE];
}
/* Producers call this */
public synchronized void insert(E item) {
while (count == BUFFER_SIZE) {
try { wait(); } /* buffer full: sleep */
catch (InterruptedException ie) {}
}
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
count++;
notify(); /* wake a waiting consumer */
}
/* Consumers call this */
public synchronized E remove() {
E item;
while (count == 0) {
try { wait(); } /* buffer empty: sleep */
catch (InterruptedException ie) {}
}
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
notify(); /* wake a waiting producer */
return item;
}
}
Compare this with §8.2: synchronized replaces the mutex semaphore, and the wait()/notify() loops on count replace the empty/full counting semaphores. Same problem, one buffer class, no explicit semaphores — the monitor absorbs them. Note the wait() sits inside a while, for exactly the reason given in the POSIX misconception above, and notify() at the end of each method plays the role of putdown()'s neighbor tests in §8.4: the state change might be exactly what a sleeping thread was waiting for.
Four platforms, one toolbox. Windows: interrupt masks (uniprocessor), spinlocks that are never preempted (multiprocessor), dispatcher objects in signaled/non-signaled states. Linux: fully preemptive since 2.6, with mutexes, semaphores, spinlocks, atomic_t, and reader–writer variants — spinlocks become preemption-disabling on one CPU. POSIX: mutexes, named/unnamed semaphores (sem_wait/sem_post), condition variables paired with a mutex and tested in a while. Java: every object is a monitor with one lock, an entry set (waiting for the lock) and a wait set (called wait()); synchronized + wait/notify rebuild the bounded buffer with no explicit semaphores.
Deadlocks
Chapter 9
Chapter 8 ended with the dining philosophers frozen, each clutching one chopstick. This chapter is the full theory of that freeze: what deadlock is, the four conditions that must all hold for it to occur, how to draw it (resource-allocation graphs), how to detect it (graph reduction, cycles, and knots), and the three strategic responses — detection and recovery, avoidance (the Banker's Algorithm), and prevention. Deadlock received formal treatment early — Dijkstra (1968) and Holt (1971, 1972) — because it is a fundamental problem of concurrent systems, not an edge case.
A system consists of resource types R₁, R₂, …, Rₘ — CPU cycles, memory space, I/O devices — where each type Rᵢ has Wᵢ instances (e.g., two identical printers = one type, two instances). Every process uses a resource in the same three-step pattern, and all deadlock theory hangs off step one and step three:
The lecture makes a distinction most textbooks skip, and it matters later (§9.5–§9.6): resources come in two fundamentally different kinds.
| Kind | Definition | Examples |
|---|---|---|
| Reusable | A fixed number of units; each unit is either available or allocated to exactly one process, and is returned intact after use. | Devices (printers, tapes, disks), memory, service routines / critical sections |
| Consumable | Units are produced and consumed dynamically — the count varies over time; a consumed unit ceases to exist. | Messages, signals, interrupts / events, dynamic data structures |
A lock is reusable — you give it back. A message is consumable — receiving it destroys it, and the sender can mint new ones. Deadlock can involve either kind (Example 3 below deadlocks on messages alone), but the clean classical theory is stated for reusable resources; consumable resources complicate detection in a specific way we will hit in §9.6.
In plain English: A group of processes stuck waiting on each other in a circle, where nobody can move because the thing each one needs is held by another member of the stuck group.
Technical definition: A set of processes is deadlocked when every process in the set is blocked waiting for an event that can be caused only by another process in the set. Equivalently: the permanent blocking of a set of processes that either compete for system resources or communicate with each other.
Why it matters: Deadlocked processes never run again without outside intervention — no error is raised, no timeout fires by default; the system just silently stops making progress on those processes.
In plain English: Everyone is moving but nobody is getting anywhere — like two people in a hallway who keep stepping the same direction to let each other pass.
Technical definition: A liveness failure in which a thread continuously attempts an action that fails; the threads are not blocked, but make no useful progress.
Why it matters: Livelock is the trap hiding inside naive deadlock fixes — e.g., philosophers who politely put their chopstick down and immediately retry in lockstep forever. It burns CPU while deadlock does not, yet delivers the same zero progress.
Three distinct liveness failures; be able to separate them in one sentence each. Deadlock: blocked forever, waiting in a cycle — no one runs. Livelock: running constantly, retrying and failing — no one progresses. Starvation: the system makes progress but one unlucky process is indefinitely postponed (the waiting philosopher of §8.4, the writer of §8.3). All deadlocked processes are starved; starved processes are usually not deadlocked.
The lecture's three examples are chosen to show deadlock arising from different resource kinds — the third one has no devices or locks in it at all.
To reason precisely, the lecture models execution as a state space: Σ is the set of all possible resource-allocation states, and processes' actions (request, acquire, release) are transitions between states. The two-process disk/tape program of Example 1 generates a grid of states — P1's progress on one axis, P2's on the other — and tracing it shows every path eventually funnels into total deadlock, which is why the slide annotates that state diagram with "there are no safe/secure states!"
Blocked: Process Pᵢ is blocked in state S if no action by Pᵢ can change the state — Pᵢ has no move.
Deadlocked: Pᵢ is deadlocked in S if it is blocked in S and in every state T reachable from S — no future sequence of other processes' actions can ever unblock it. (Merely blocked is temporary; deadlocked is forever.)
Total deadlock: S is a total deadlock state if all processes are deadlocked in S.
Safe (secure) state: S is safe if neither S nor any state reachable from S is a deadlock state — from a safe state, deadlock is unreachable no matter what happens.
Why it matters: "Safe state" is the load-bearing concept of deadlock avoidance (§9.8): the Banker's Algorithm is nothing more than a rule that refuses to leave the set of safe states.
"Blocked and deadlocked are the same thing." No — blocked is a snapshot, deadlocked is a destiny. A process waiting for a printer is blocked but not deadlocked if the printer will eventually be released. It is deadlocked only if in every reachable future it stays blocked. This quantifier ("in every state reachable from S") is exactly what makes deadlock detection require search rather than a glance.
Deadlock can arise only if all four of the following conditions hold simultaneously. They are necessary conditions: each one is a load-bearing wall, and knocking out any single one makes deadlock impossible — which is precisely the design space of prevention (§9.10).
| # | Condition | What it means |
|---|---|---|
| 1 | Mutual Exclusion | At least one resource is held in a non-sharable mode — held exclusively, unavailable to others while held. |
| 2 | Hold and Wait (partial allocation) | A process holds at least one resource while waiting to acquire additional resources held by others. |
| 3 | No Preemption | Resources cannot be forcibly taken away; only the holding process may release them, voluntarily, when finished. |
| 4 | Circular Wait | A circular chain of processes exists — P₀ waits for a resource held by P₁, P₁ for one held by P₂, …, Pₙ for one held by P₀. |
"Define the four necessary conditions that characterize deadlock" is a stated chapter objective — expect it verbatim, worth easy points if you can produce all four names and one-line definitions. Two follow-ups to prepare: (1) Necessary, not sufficient — all four can hold and the system may still avoid deadlock (see the cycle-without-deadlock graphs of §9.5); the four holding makes deadlock possible, not certain. (2) Violating any ONE condition guarantees a deadlock-free system — the sentence the slide flags with a lightning bolt.
Map them onto the dining philosophers for an instant example bank: chopsticks are exclusive (1), a philosopher holds left while waiting for right (2), you cannot snatch a chopstick (3), and five of them wait in a ring (4).
Deadlocks are easiest to see when drawn. A resource-allocation graph is a directed graph G = (V, E) whose vertex set is the union of process nodes (drawn as circles: P₁, P₂, …) and resource nodes (drawn as rectangles, with a dot inside for each instance of that type). Three kinds of edges encode who wants what and who has what:
| Edge | Direction | Meaning |
|---|---|---|
| Request edge | Pᵢ → Rⱼ | Process Pᵢ has requested an instance of Rⱼ and is waiting for it. |
| Assignment edge | Rⱼ → Pᵢ | An instance of Rⱼ is currently allocated to Pᵢ. (Drawn from the specific instance dot.) |
| Producer edge | Rⱼ ↔ producing P | Connects a consumable resource to the process that produces its units — only exists in the general (consumable) model. |
Take R with two instances. P₁ holds one instance of R and requests S; P₂ holds S and requests R — that is a cycle P₁ → S → P₂ → R → P₁. But suppose P₃, outside the cycle, holds R's second instance and requests nothing. P₃ will finish and release its instance of R; P₂'s request is then granted, P₂ finishes and releases S, and P₁ proceeds. The cycle existed, the deadlock never did — this is exactly the "graph with a cycle but no deadlock" figure from the slides, and it is why multiple instances demote a cycle from proof to suspicion.
Draw it: circles request rectangles (Pᵢ → Rⱼ), rectangles are assigned to circles (Rⱼ → Pᵢ). Then read it: no cycle → safe; cycle with single-instance resources → deadlocked; cycle with multi-instance resources → run the detection algorithm.
Deadlock detection answers one question: given the current resource graph, is this state deadlocked or not? The tool is graph reduction — repeatedly simulating "this process could finish" and erasing it, then seeing who is left. To state the results precisely, the lecture first recalls some graph-theory vocabulary.
Facts about knots worth writing on your cheat sheet: in a directed graph, a knot implies a cycle (but not vice versa); no node in a knot is a sink and no path leads from a knot node to a sink — a sink would be an exit, and knots have none; and a digraph is knot-free iff every node is a sink or has a path to a sink.
Given resource graph G:
repeat until no unblocked process remains:
select any unblocked process p /* its requests can all be granted */
remove p and ALL of its request and assignment edges
If G is completely reducible (all processes removed) -> state is NOT deadlocked
If G is NOT completely reducible -> state IS a deadlock state;
the irreducible processes
are the deadlocked ones
The intuition: removing an unblocked process simulates "let it run to completion and hand everything back." Its released resources may unblock others, who get removed next, and so on. Whoever can never be removed is waiting on something that will never appear — deadlocked. For reusable-only systems the slide adds a reassuring note: all reduction sequences of a given graph lead to the same final graph, so you may erase eligible processes in any order you like.
The lecture then sets a trap: consider P₁ and P₂ where P₁ requests one unit each of R₁ and R₂, P₂ requests one unit each of R₁ and R₂ — and R₁ is consumable with P₁ as its producer. Reduce by P₁ first: as producer, P₁ sets R₁'s count to ∞ on removal, P₂'s requests become grantable, and the graph reduces completely. Reduce by P₂ first: P₂ consumes the available units, and the reduction can strand P₁. Same graph, different orders, different verdicts: when consumable resources are involved, reducibility may be order-dependent. Is the textbook wrong? No — it simply neglects the more general case; its any-order claim is stated (and true) for reusable resources only.
For a non-isolated, unblocked process Pi:
Step 1: delete every request edge (Pi, Rj);
if Rj is consumable, decrement rj by the number of deleted requests
Step 2: delete every assignment/producer edge (Rj, Pi);
if Rj is reusable, increment rj (units returned)
if Rj is consumable and Pi is its producer, set rj = INFINITY
In plain English: A system that never sits on a grantable request — if it can say yes, it says yes immediately.
Technical definition: A system is expedient if all satisfiable resource requests are granted immediately; consequently the resource graph never contains a satisfiable request edge, and every process with an outstanding request is blocked.
Why it matters: Real OSes are expedient (why would you delay a grantable request?), and expediency buys a powerful shortcut: in an expedient system, a knot is a *sufficient* condition for deadlock.
Putting the pieces together gives the chapter's central chain of implications — learn it as four one-liners:
Practical reading: cycle detection is the fast first check (necessary condition — no cycle, stop worrying); knot analysis gives the stronger guarantee when the system is expedient.
| Restriction | What simplifies | Detection result |
|---|---|---|
| Single-unit resources (each resource class has exactly one unit) | Collapse the graph to a wait-for graph: edge Pᵢ → Pⱼ means Pᵢ waits for a resource Pⱼ holds. | Cycle ⟺ deadlock — cycle becomes sufficient as well as necessary; detection reduces to cycle detection. |
| Single-unit requests (each process may have at most one outstanding request, for one unit) in an expedient system | Any two reduction sequences reach the same irreducible state; Pᵢ is not deadlocked iff Pᵢ is a sink or on a path to a sink. | Knot ⟺ deadlock — knot detection is both necessary and sufficient. Proof sketch that no knot ⟹ reducible: a knot-free graph has a sink (or paths to sinks); by expediency that sink must be a process (a resource with free units would have granted someone), and a sink process is unblocked — reduce by it and repeat. |
Keep the quantifiers straight — this is where points are lost. Cycle: necessary always, sufficient only with single-instance/single-unit resources. Knot: sufficient only with expediency, necessary only in the single-unit-request case. And the order-dependence question has a two-word trigger: the moment a problem says "consumable resources," answer that reduction order may matter and that complete reducibility is no longer equivalent to freedom from deadlock — only the one-directional corollary survives.
Detection without recovery is just bad news. Once a deadlock is found, the system must break it, and there are exactly two approaches: process termination and resource preemption. Both are expensive, and the lecture is blunt about why: time is spent detecting and removing the deadlock; the deadlocked processes have already consumed resources; resource state may have been changed and must be rolled back; and other processes that ran during the deadlock may need to be undone too — a cascading rollback.
In plain English: An action is idempotent if doing it twice is the same as doing it once — safe to redo.
Technical definition: An execution sequence is idempotent if re-executing it (after partial completion) yields the same final state with no additional side effects.
Why it matters: Process execution sequences are generally NOT idempotent — a process that has printed a page, sent a message, or moved money cannot simply be killed and restarted without repeating side effects. Interleaving with other processes makes this even harder. This is the fundamental reason "just restart it" is not a free recovery strategy.
Rather than letting deadlock happen and paying for recovery, avoidance enforces runtime rules that keep the system from ever entering an unsafe state (recall §9.3: from a safe state, deadlock is unreachable). It is defined for reusable resources, and it charges rent: the avoidance algorithm runs on every resource request. The core idea: delay granting any request that might lead to deadlock, using advance knowledge of each process's worst-case future needs. That declared upper bound is the Maximum Claim.
In plain English: Every process must announce, up front, the most of each resource it could ever ask for; the system then plans for the worst case at every step.
Technical definition: The maximum claim is a per-process upper bound on units of each resource type the process may request. The Maximum Claim Graph extends the resource graph with dashed potential request edges showing every request the process could still make; the total number of edges between a process and the resources stays constant throughout execution — edges only change kind (potential → request → assignment → back to potential).
Why it matters: The MCG lets the system test the worst case before saying yes: if the graph remains completely reducible even when everyone might demand their maximum, the grant is safe.
When a process makes a request, its potential edge becomes an actual request edge, and the avoidance algorithm asks: if I grant this, is the resulting worst case still reducible? It tentatively grants and performs graph reduction on the MCG. Completely reducible → even worst-case future requests cannot deadlock → SAFE → grant. Not reducible → UNSAFE → defer the request (the process waits — it is not denied forever, just until the state changes).
The Banker's Algorithm is deadlock avoidance made concrete — dynamic avoidance using the Maximum Claim Graph. The name is the metaphor: a banker never commits cash in a way that could leave every customer's declared credit line unsatisfiable; the OS never allocates in a way that could leave every process's maximum claim unsatisfiable.
| Structure | Meaning |
|---|---|
| C — maximum claim matrix | C[i][j] = maximum demand of process Pᵢ for resource type Rⱼ (declared on entry). |
| Avail — available vector | Avail[j] = number of currently unallocated units of Rⱼ: [R₁, R₂, …, Rₖ]. |
| Alloc — allocation matrix | Alloc[i][j] = units of Rⱼ currently held by Pᵢ. |
| Need = C − Alloc | Need[i][j] = units of Rⱼ that Pᵢ may still request — the remaining claim. |
Setup: 2 processes (P₁, P₂), 2 resource types. R₁ has 3 units available, R₂ has 2. Maximum claims: P₁ may need [1, 2] (1 of R₁, 2 of R₂) and P₂ may need [2, 2].
Scenario A — P₂ requests one unit of R₁. Tentatively assign it and reduce the MCG assuming worst-case futures. Worst case: P₁ demands its full claim (1 R₁, 2 R₂) — with 2 R₁ and 2 R₂ still free, grantable; remove P₁, returning its units. Now P₂'s worst case (1 more R₁, 2 R₂) is grantable; remove P₂. Completely reducible → SAFE → grant.
Scenario B — P₁ now requests a unit (with P₂ already holding one R₁). Tentatively grant and try to reduce. In the worst case each process may still demand enough of R₂ (both claim 2, only 2 exist — one full claim at a time is fine, but with the tentative grant in place neither process's entire remaining claim can be guaranteed grantable in any order: the MCG cannot be reduced. UNSAFE → defer P₁'s request until it is safe to do so.
The takeaway shape of every Banker's question: tentatively grant → reduce under maximum claims → reducible means grant, irreducible means defer. Note what deferral means: nothing is "denied," and no deadlock exists — the system is simply refusing to enter a state from which deadlock could become unavoidable.
"Unsafe state = deadlocked state." No. Unsafe means deadlock is now possible in some worst-case future, not that it has happened — processes might never actually request their maxima, and the system could get lucky. The Banker's Algorithm is conservative on purpose: it refuses unsafe states so that luck is never required. Safe ⊂ deadlock-free futures guaranteed; unsafe = guarantee lost, not doom certain.
Prevention is the design-time strategy: structure the system so that one of the necessary conditions can never hold (the lecture focuses on attacking mutual exclusion, hold-and-wait, and circular wait). No condition, no deadlock — guaranteed statically, with no per-request runtime check. The catch is the price each attack exacts:
| Condition attacked | How | Verdict |
|---|---|---|
| Mutual Exclusion | Make resources sharable so no one needs exclusive access. | Generally impossible — some resources are inherently non-sharable (a printer mid-job, a mutex by definition). |
| Hold and Wait | Require each process to request all resources at startup; or require a process to release everything it holds before requesting anything new. | Works but neither approach is truly feasible in practice: all-at-startup yields terrible utilization (resources held long before use) and future needs are often unknown; release-before-request forces redoing work and risks livelock/starvation. |
| Circular Wait | Impose a total ordering: assign a sequence number to every resource type and require every process to request resources in ascending order only. | The practical winner. A circular chain cannot form — in any would-be cycle, some process would have to request a lower-numbered resource while holding a higher-numbered one, which the rule forbids. (This is exactly the odd/even philosopher fix of §8.4 generalized.) |
Prevention breaks a necessary condition by design, needs no runtime information, no per-request algorithm — but is restrictive and can waste resources. Avoidance allows all four conditions but uses runtime information (the maximum claim) to sidestep unsafe states, running a check (Banker's) on every request — flexible, but pays runtime overhead and requires processes to declare maxima up front. Detection & recovery permits deadlock, finds it after the fact, and pays the (potentially cascading) recovery bill. One sentence each is a guaranteed exam answer.
Deadlocks are a fundamental problem of concurrent systems, not an edge case. A set of processes deadlocks when each waits for an event only another member can cause; it requires mutual exclusion, hold-and-wait, no preemption, and circular wait to hold simultaneously. Resource-allocation graphs make the situation visible — no cycle → no deadlock; cycle + single instances → deadlock; cycle + multiple instances → run graph reduction, remembering that consumable resources can make reduction order-dependent and that in an expedient system a knot is sufficient for deadlock. The three strategies are complementary, and each has a bill: recovery is costly due to rollback and cascading effects; avoidance (Banker's Algorithm on the Maximum Claim Graph) adds per-request overhead but guarantees safety; prevention is cleanest but often too restrictive for real workloads. Bottom line, straight from the last slide: deadlocks are hard to deal with — but they are a matter of fact, and understanding the theory is what enables informed system-design choices.
Four conditions make deadlock possible; a cycle is necessary, a knot (in an expedient system) is sufficient, and graph reduction decides the ambiguous middle. Then pick your poison: prevent (break one condition — usually by ordering resources), avoid (Banker's: tentatively grant, reduce the MCG, defer if unsafe), or detect and recover (terminate incrementally or preempt with checkpoints, and budget for cascading rollback).
Main Memory, Part 1
Chapter 10
The course now moves from who runs (scheduling) and how they cooperate (synchronization) to where they live: memory management. A program on disk is just a file; to run, it must be brought into main memory and placed within a process. This chapter covers the foundations — protection, address binding, the logical/physical address split, and the MMU — then two generations of allocation strategy: contiguous allocation (with its fragmentation diseases) and paging (the cure, and the basis of everything modern). Part 2 (Chapter 11) finishes with advanced page-table structures, swapping, and the Intel and ARM architectures as case studies.
The final exam has moved to the last lecture day: Thursday, July 30th. Format: cumulative with a focus on newer topics — which makes Chapters 10–11 (and Chapter 12, Virtual Memory) the highest-yield material in these notes. Duration 1 hour 40 minutes; sections are Definitions, MCQ, and Short Answers; a cheat sheet and calculator are allowed. The calculator is a hint: expect address-translation and effective-access-time arithmetic like §10.11 and §10.13. Mid-term solution keys: definitions and MCQ will be posted online; short answers will not.
Two hardware facts drive the entire chapter. First, main memory and registers are the only storage the CPU can access directly — a register in one CPU clock (or less), but main memory in many cycles, causing a stall while the CPU waits (the cache sits between them to soften this). Second, correctness demands protection: a process must be confined to its own addresses, and that check is performed by the memory hardware on every access, not by the OS in software — the OS could never afford to intercept every load and store.
The simplest protection scheme uses a pair of registers that together define a process's logical address space: the base register holds the smallest legal physical address, and the limit register holds the size of the range. The CPU checks every memory access generated in user mode: legal means base ≤ address < base + limit; anything else traps to the kernel as an addressing error. Crucially, the instructions that load the base and limit registers are privileged — only the OS, in kernel mode, may execute them. If a user process could set its own base and limit, the protection would be decoration.
Base register = 300040, limit register = 120900. The program may legally access every address from 300040 through 300040 + 120900 − 1 = 420939, inclusive. Address 420940? One past the end — trap. Address 299999? Below base — trap. Note the limit is a count, not an upper address; the upper bound is base + limit − 1. Expect exactly this arithmetic as a short answer.
Programs waiting on disk to be brought into memory form the input queue. In principle a process could be loaded anywhere in physical memory — the machine's address space may start at 00000, but the first address of the user process need not be. That flexibility raises the central question: when does an address in the program get pinned to a real memory location? An address is represented differently at each stage of a program's life: source code uses symbolic addresses (the variable count); the compiler binds those to relocatable addresses ("14 bytes from the beginning of this module"); the linker or loader binds relocatable addresses to absolute addresses (74014). Each binding is a mapping from one address space to another — and the interesting design choice is how late you can delay the final one.
| Binding time | How it works | Consequence |
|---|---|---|
| Compile time | If the memory location is known a priori, the compiler generates absolute code with real addresses baked in. | If the starting location ever changes, you must recompile. Rigid. |
| Load time | Location unknown at compile time → compiler generates relocatable code; final binding is delayed until the loader places it. | Process can load anywhere — but once loaded, it cannot move. |
| Execution time | Binding delayed until runtime; the process can be moved during execution from one memory segment to another. | Requires hardware support for address maps (base/limit, MMU). Most operating systems use this method. |
These appeared as in-class MCQs and are prime exam recycling: (1) Absolute code can be generated for → compile-time binding (only then is the location known early enough to hard-code). (2) In a dynamically linked library → a stub is included in the image for each library routine (see §10.5 — loading postponed until execution describes dynamic loading; the stub is the DLL signature). (3) The mapping of a logical address to a physical address is done in hardware by the → memory-management unit (MMU) — not the relocation register alone (that is a component the MMU uses), and not the memory address register.
In plain English: The logical address is the address as the program sees it; the physical address is where the bytes actually sit in RAM. The program lives its whole life never knowing the difference.
Technical definition: A logical address (also called a virtual address) is generated by the CPU. A physical address is the address seen by the memory unit. The logical address space is the set of all logical addresses generated by a program; the physical address space is the set of all physical addresses corresponding to them.
Why it matters: The concept of a logical address space bound to a separate physical address space is central to proper memory management — it is what lets the OS place, move, and protect processes without their knowledge or cooperation.
The two spaces are identical under compile-time and load-time binding (static binding) — the address the CPU emits is the memory address. They differ under execution-time binding, and that gap is bridged by hardware: the Memory-Management Unit (MMU), a hardware device that maps virtual to physical addresses at run time. Many mapping methods exist — the rest of this chapter and the next is essentially a catalog of them.
Generalize the base register and rename it the relocation register: its value is added to every address generated by the user process at the moment the address is sent to memory. A process compiled to run at logical addresses 0…max executes correctly at physical addresses R…R+max, where R is whatever the OS loaded into the relocation register. The user program deals only in logical addresses and never sees the real physical addresses — execution-time binding happens on each reference.
"The logical address is just a fancy name for the physical address." Only in the static-binding schemes. The moment binding moves to execution time, they are genuinely different numbers, and the program cannot discover — and has no reason to care — where it physically resides. Every address it can print, store in a pointer, or compare is logical. This is also why the OS can later move the process (compaction, §10.8; swapping, §11.5) by changing one register instead of rewriting the program.
Static linking combines system libraries and program code into the binary image at build time — every program carries its own copy of printf. Dynamic linking postpones linking until execution time: the image instead contains a small piece of code called a stub for each library routine. On first call, the stub locates the memory-resident library routine (the OS checks whether it is already in the process's address space and maps it in if not), then replaces itself with the address of the routine and executes it — so every later call jumps straight there, paying the lookup cost once. Dynamically linked libraries are also called shared libraries; Windows is famous for this model with its DLLs. One operational wrinkle: patching a shared library affects every program that uses it, so systems handle updates by distributing different versions of libraries and letting programs bind to the one they expect.
Dynamic loading delays bringing a routine into memory until it is called (a program-design trick, no OS needed). Dynamic linking delays resolving the reference itself until runtime via a self-replacing stub (an OS-supported mechanism enabling shared libraries). Same instinct — pay costs lazily — applied to two different steps.
Main memory must hold both the OS and the user processes, and it is a limited resource the OS must allocate efficiently. Contiguous allocation is the early method: memory is divided into two partitions — the resident OS in low memory (with the interrupt vector) and user processes in high memory — and each process is contained in a single contiguous section of memory. Protection reuses §10.2's hardware per process: the relocation register holds the process's smallest physical address, the limit register holds the range of legal logical addresses (every logical address must be less than the limit), and the MMU maps dynamically. A side benefit of dynamic mapping: the kernel itself can be transient — kernel code can come and go and the kernel can change size, since nothing is pinned by compiled-in addresses.
In fixed-partition memory, the user region is divided in advance into blocks — either equal-sized or variable-sized chunks — and a process is placed into a partition at least as large as its need. Fixed partitions suited old batch multiprogramming systems (job sizes were known, turnover was slow) but are a poor fit for timesharing. In variable-partition (multiple-partition) allocation, partitions are sized to each process's needs at arrival. The degree of multiprogramming is limited by the number of partitions. A hole is a block of available memory; holes of assorted sizes end up scattered throughout memory. An arriving process is carved out of a hole large enough to accommodate it; an exiting process frees its partition, and adjacent free partitions are combined. The OS bookkeeping is two lists: allocated partitions and free partitions (holes).
Given a request of size n and a list of free holes, which hole do you use? This is the dynamic storage-allocation problem, and the lecture presents four placement strategies — the first three are the classic trio the exam asks about by name.
| Strategy | Rule | Properties |
|---|---|---|
| First-fit | Allocate the first hole that is big enough (search from the start). | Fast — stops at the first success; no full scan needed. |
| Best-fit | Allocate the smallest hole that is big enough. | Must search the entire list (unless it is ordered by size) — but produces the smallest leftover hole. |
| Worst-fit | Allocate the largest hole. | Also searches the whole list — produces the largest leftover hole (hoping the remainder stays usable). |
| Next-fit | Like first-fit, but start searching from the last placement instead of the beginning. | Spreads allocations around memory instead of clustering them near the front. |
The verdict from analysis and simulation: first-fit and best-fit are better than worst-fit in both speed and storage utilization. The two in-class review questions map straight onto the table: smallest leftover hole → best-fit; largest leftover hole → worst-fit.
In plain English: External fragmentation is having enough total free memory but in useless scattered slivers; internal fragmentation is wasted space inside an allocation because you were given more than you asked for.
Technical definition: External fragmentation — total memory space exists to satisfy a request, but it is not contiguous. Internal fragmentation — allocated memory may be slightly larger than requested memory; the size difference is memory internal to a partition that is not being used.
Why it matters: External fragmentation is the disease of variable partitions (and the reason paging exists); internal fragmentation is the disease of fixed-size units (fixed partitions, and pages themselves — §10.10). Every allocation scheme trades one for the other; know which scheme suffers which.
How bad does it get? Statistical analysis of first-fit gives the 50-percent rule: given N allocated blocks, another 0.5 N blocks are lost to fragmentation — i.e., one-third of memory may be unusable.
External fragmentation can be reduced by compaction: shuffle memory contents to place all free memory together in one large block. But compaction is possible only if relocation is dynamic and done at execution time — moving a process means changing its physical location mid-run, which only works when addresses are translated through a relocation register the OS can update (a load-time-bound process would carry stale absolute addresses). Compaction also collides with I/O: a device may be writing into a process's buffer by physical address while the OS wants to move the process. Two fixes: latch the job in memory while it is involved in I/O (forbid moving it), or do I/O only into OS buffers and copy to the process afterward. And a sobering closing note from the slide: the backing store has the same fragmentation problems — moving the mess to disk does not dissolve it. This appeared as a review MCQ: the true statement about compaction is that it is possible only if relocation is dynamic and done at execution time.
Paging is the escape from contiguity — and from external fragmentation. The idea: let the physical address space of a process be non-contiguous, allocating physical memory wherever it happens to be available, and the varying-sized-chunk problem disappears entirely.
In plain English: Chop physical memory into equal fixed-size slots (frames) and chop each program's memory into pieces of exactly the same size (pages); then any piece fits in any slot.
Technical definition: Divide physical memory into fixed-sized blocks called frames, with size a power of 2 between 512 bytes and 16 MB; divide logical memory into blocks of the same size called pages. The backing store is likewise split into pages.
Why it matters: Because every page fits every frame, allocation becomes trivial bookkeeping (a free-frame list) instead of hole-hunting — no external fragmentation, no compaction, ever.
The OS keeps track of all free frames. To run a program of N pages, it finds N free frames — any N, anywhere — loads the pages, and builds a page table for the process: the translation from logical pages to physical frames. The price of the fixed unit is the return of internal fragmentation: the last page of a process is rarely exactly full, wasting on average half a frame per process (bounded by number of processes × frame size).
Every CPU-generated address is split in two: a page number (p) — used as an index into the page table, which holds the base address of each page in physical memory — and a page offset (d) — concatenated with that base to form the physical address sent to the memory unit. For a logical address space of 2ᵐ and page size 2ⁿ: the high-order m − n bits are the page number and the low n bits are the offset. Translation never touches the offset — a byte keeps its position within its page; only which frame the page sits in changes.
Setup: n = 2, m = 4 — page size 4 bytes, logical space 16 bytes (4 pages), physical memory 32 bytes (8 frames). Page table: page 0 → frame 5, page 1 → frame 6, page 2 → frame 1, page 3 → frame 2. How does logical address 13 map?
Split 13: page = 13 ÷ 4 = 3, offset = 13 mod 4 = 1 (binary 1101 → p = 11, d = 01). Look up page 3 → frame 2. Physical address = frame × page size + offset = 2 × 4 + 1 = 9. General recipe for the exam (calculator allowed!): p = ⌊addr / pagesize⌋, d = addr mod pagesize, physical = frame(p) × pagesize + d.
Page size = 2,048 bytes; process size = 72,766 bytes. Then 72,766 ÷ 2,048 = 35 full pages with 72,766 − 35 × 2,048 = 1,086 leftover bytes, so the memory footprint is 36 logical pages → 36 physical frames.
Internal fragmentation, actual: the last frame holds 1,086 useful bytes out of 2,048 → 2,048 − 1,086 = 962 bytes wasted. Worst case: a process needing one byte more than a page multiple wastes one frame minus 1 byte. Average case: half a frame per process.
"So smaller frame sizes are better?" — the slide's rhetorical trap. Smaller pages do cut internal fragmentation, but they multiply the number of pages, and therefore the size of the page table (§10.12's problem) and the per-page management overhead. Page size is a trade-off, and its historical drift has been upward.
The page table itself lives in main memory. Two registers describe it: the page-table base register (PTBR) points to the table, and the page-table length register (PTLR) gives its size in entries — a context switch just reloads these two registers, which is what makes switching address spaces cheap. But now count the cost of one load instruction: every data/instruction access requires two memory accesses — one to read the page-table entry, one for the data itself. Memory just got twice as slow.
The fix is a special fast-lookup hardware cache: the translation look-aside buffer (TLB), also called associative memory — a small set of page-table entries searched in parallel by page number. TLB facts to know:
The hit ratio is the percentage of translations found in the TLB. Model each access as: TLB hit → one memory access; TLB miss → two (page table, then data). With 10 ns memory access time:
EAT = hit_ratio * (1 memory access) + miss_ratio * (2 memory accesses)
80% hit ratio: EAT = 0.80 * 10 + 0.20 * 20 = 12 ns -> 20% slowdown
99% hit ratio: EAT = 0.99 * 10 + 0.01 * 20 = 10.1 ns -> 1% slowdown
EAT is the single most likely calculation on the final (calculator allowed, formula on your cheat sheet). Be ready for variants: some problems add an explicit TLB-lookup time ε to both cases — EAT = h·(ε + m) + (1 − h)·(ε + 2m) — but the lecture's version treats TLB lookup as free; match whichever the question states. And be able to interpret the result: the slowdown relative to bare memory access is (EAT − m)/m, which is why 80% hits → 20% slower but 99% hits → only 1% slower. Realistic TLBs live at the 99% end, which is why paging is affordable at all.
Base-and-limit protected a contiguous block; paging protects per page, using extra bits in each page-table entry. A protection bit on each frame marks the page read-only or read-write (and more bits can mark execute-only, etc.). Separately, a valid–invalid bit on each entry says whether the page is in the process's logical address space at all: valid = a legal page; invalid = not part of this process's space. An alternative to per-entry valid bits is simply the PTLR — any page number beyond the table length is illegal. Any violation traps to the kernel.
A system has a 14-bit address space (addresses 0 to 16,383) with 2 KB pages, but the process only uses addresses 0 to 10,468. Pages 0 through 5 cover addresses 0–12,287 and are mapped normally with valid bits; pages 6 and 7 are marked invalid, and any address generated in them traps.
Notice the leak: page 5 spans 10,240–12,287, but the program is only entitled up to 10,468 — addresses 10,469–12,287 are inside a valid page and slip through. Page-granular protection cannot catch overruns within the last page; this residue is internal fragmentation's security-flavored twin.
Paging makes sharing natural. Shared code: one copy of read-only (reentrant) code — text editors, compilers, window systems — is mapped into many processes' page tables at once, similar to multiple threads sharing one process image; sharing read-write pages is likewise useful for inter-process communication if allowed. Private code and data: each process keeps its own copy, and those pages can appear anywhere in its logical address space. The slide's example: if libc occupies four pages, forty processes using it need four physical frames for libc, not one hundred sixty — each process's page table simply points its libc pages at the same frames.
Straightforward paging has a hidden memory bill. Take a modern-looking configuration: a 32-bit logical address space with 4 KB pages (2¹²). The page table needs 2³² / 2¹² = 2²⁰ ≈ 1 million entries; at 4 bytes each, that is 4 MB of physical memory for the page table alone — per process. We do not want to allocate that much contiguous space in main memory, so the table itself must be broken into smaller units. Three families of solutions: hierarchical paging (below), hashed page tables, and inverted page tables (both in Chapter 11).
A two-level page table breaks the logical address space into multiple page tables by applying paging to the page table itself. The outer page table's entries give the starting address of a page of the page table; the inner page table's entries give actual frame addresses. On the 32-bit / 4 KB machine, the 20-bit page number splits into a 10-bit outer index p₁ and a 10-bit inner index p₂, followed by the 12-bit offset d — p₁ indexes the outer table, p₂ is the displacement within that page of the inner table. This is called a forward-mapped page table. The payoff: only the outer table must exist in full; inner page tables are created only for active pages, so a process using a sliver of a 4 GB address space pays for a sliver of a page table.
Scale to a 64-bit logical address with 4 KB pages: 2⁵² page-table entries. Two levels with 2¹⁰-entry inner tables leaves an outer table of 2⁴² entries ≈ 2⁴⁴ bytes — absurd. Add a second outer level (three-level paging) and that second outer table is still 2³⁴ bytes, and each translation now costs up to four memory accesses. Hierarchies alone do not tame 64 bits; that is exactly why Chapter 11 opens with hashed and inverted page tables.
Everything in this chapter is one arms race. Base+limit protects a contiguous process; variable partitions cause external fragmentation; compaction needs execution-time binding; paging eliminates external fragmentation by fixing the unit size (accepting internal fragmentation in the last page); the page table makes memory 2× slower, so the TLB caches translations (EAT = h·m + (1−h)·2m); and flat page tables grow monstrous, so we page the page table — until 64-bit spaces demand the hashed and inverted structures of Chapter 11.
Main Memory, Part 2
Chapter 11
Part 1 ended on a cliffhanger: flat page tables collapse under 32-bit address spaces and even hierarchies strain under 64-bit ones. Part 2 delivers the two remaining page-table structures — hashed and inverted — then covers swapping (using disk as overflow memory, and why modern systems mostly don't), and closes with two real architectures as case studies: Intel IA-32/x86-64 (segmentation plus paging, PAE, four-level tables) and ARM (the mobile world's answer). These case studies exist because the lecture's stated objective is a detailed description of the Intel architecture supporting both pure segmentation and segmentation with paging — expect the exam to probe the IA-32 translation pipeline.
Warm up with the lecture's paging exercise, since it is the mechanical skill everything else builds on:
Assuming a 1-KB (1,024-byte) page size, find the page number and offset of these decimal addresses (page numbers start at 0):
Address 3085: page = ⌊3085 / 1024⌋ = ⌊3.012…⌋ = 3; offset = 3085 mod 1024 = 13. Reference: page 3, offset 13.
Address 42095: page = ⌊42095 / 1024⌋ = ⌊41.108…⌋ = 41; offset = 42095 mod 1024 = 111. Reference: page 41, offset 111.
Divide for the page, take the remainder for the offset — with a power-of-two page size this is just splitting the binary representation, which is why hardware does it for free.
Hashed page tables are common for address spaces larger than 32 bits. Instead of indexing a giant array by page number, the virtual page number is hashed into a table of buckets, and each bucket holds a chain of elements that hashed to the same location. Each element carries three fields: (1) the virtual page number, (2) the value of the mapped page frame, (3) a pointer to the next element in the chain. Translation walks the chain comparing virtual page numbers until a match is found, then extracts the corresponding physical frame. The table is sized to the pages actually in use, not to the whole address space — sparse 64-bit spaces stop being a problem.
A variation for 64-bit addresses is the clustered page table: like a hashed table, but each entry refers to several pages (e.g., 16) instead of one. Clustering is especially useful for sparse address spaces, where memory references are non-contiguous and scattered — one entry covers a small neighborhood of pages, cutting both table size and chain-walking.
In plain English: Instead of every process keeping a directory of its own pages, the system keeps one master directory of physical frames, each entry recording which process's which page currently lives there.
Technical definition: A single table with one entry for each real page (frame) of memory; each entry contains the virtual address of the page stored in that frame and information about the process that owns it (its pid). Rather than each process tracking all its possible logical pages, the system tracks all physical frames and who references them.
Why it matters: Table size now scales with physical memory, not with the number of processes times the size of their (possibly enormous, possibly sparse) virtual spaces — one table for the whole machine.
The trade is time for space: the table is indexed by frame, but translation starts from a virtual address ⟨pid, p, d⟩, so a lookup must search the table for an entry matching (pid, p) — the matching entry's position i is the frame number, giving physical address ⟨i, d⟩. Searching every frame per memory reference would be crippling, so implementations hash the (pid, p) pair to limit the search to one — or at most a few — entries, and the TLB absorbs most lookups before they touch the table at all. One structural casualty: shared memory gets awkward. A frame has exactly one table entry, so it can record only one mapping of one virtual address — the multiple-processes-point-at-one-frame trick of §10.11 has no natural home in a pure inverted table.
A system has a 32-bit logical address, a 4-KB page size, but only supports 512 MB of physical memory. How many entries in each table?
Conventional single-level page table: one entry per logical page = 2³² / 2¹² = 2²⁰ = 1,048,576 entries (per process!) — physical memory size is irrelevant.
Inverted page table: one entry per physical frame = 512 MB / 4 KB = 2²⁹ / 2¹² = 2¹⁷ = 131,072 entries (for the whole system).
The contrast is the whole argument for inversion: 8× fewer entries here, and the gap explodes as logical spaces grow to 64 bits while physical memory stays finite.
Know which quantity sizes which table: conventional → logical address space ÷ page size, per process; inverted → physical memory ÷ frame size, per system. And the two canonical inverted-table trade-offs, one sentence each: it decreases memory needed for page tables but increases search time per reference (mitigated by hashing + TLB); and it complicates shared memory because each frame can record only one virtual mapping.
Swapping lets the sum of processes' memory exceed physical memory: a process can be swapped temporarily out of memory to a backing store and later brought back in to continue executing. The backing store is a fast disk, large enough to hold copies of all memory images for all users, with direct access to those images. The system keeps a ready queue of ready-to-run processes whose images are on disk. One named variant: roll out, roll in — swapping driven by priority-based scheduling, where a lower-priority process is swapped out so a higher-priority one can be loaded and run.
If the next process to run is not in memory, the OS must swap one out and swap the target in — and the major part of swap time is transfer time, proportional to the amount of memory swapped.
Hard drive at 140 MB/s: swapping out a 100 MB process takes 100/140 ≈ 714 ms; add the swap-in of a same-sized process → 1,428 ms ≈ 1.4 seconds of context-switch time. Compare a normal context switch measured in microseconds.
NVMe SSD at 14,700 MB/s: the same swap-out takes ≈ 6.8 ms, total ≈ 13.6 ms — a hundred times better, still enormous next to a switch without swapping.
Reduction lever: swap less. If the OS knows how much memory is really being used, it moves only that — hence system calls like request_memory() and release_memory() by which a process informs the OS of its actual memory use.
The verdict on classic swapping: standard swapping — whole processes shuttled to disk — is not used in modern operating systems. What survives is a modified version found in UNIX, Linux, and Windows: swapping is normally disabled, is started only when free memory falls below a threshold (allocated memory exceeds the threshold), and is disabled again once demand drops back down. Modern systems also swap at page granularity rather than whole processes (swapping combines with paging — the natural bridge to virtual memory, Chapter 12).
Mobile OSes typically do not support swapping at all, for flash-memory reasons: small capacity, a limited number of write cycles before the flash wears out, and poor throughput between flash and CPU on mobile platforms. Instead, when memory runs low they reclaim it by other means: iOS asks applications to voluntarily relinquish allocated memory — read-only data (like code) is thrown out and reloaded from flash if needed — and an app that fails to free memory can be terminated. Android goes straight to terminating apps when memory is low, but first writes application state to flash for a fast restart. Despite rejecting swap, both OSes support paging — the translation machinery is universal even where the disk-overflow habit is not.
"Swapping and paging are the same thing." They solve different problems. Paging is an address-translation scheme — it removes the contiguity requirement and exists on essentially every modern system, including phones. Swapping is a capacity scheme — moving memory contents to disk to overcommit RAM. You can page without ever swapping (iOS, Android), and historical systems swapped whole processes without paging. Modern desktops combine them: paging supplies the unit (the page) that the swap mechanism moves.
Intel chips dominate the industry: 32-bit Pentium-class CPUs are the IA-32 architecture, and current 64-bit Intel CPUs are IA-64 / x86-64. IA-32 is the textbook's showpiece because it supports both pure segmentation and segmentation with paging — the CPU runs two translation stages back to back, and together the segmentation and paging units form the equivalent of the MMU.
Stage 2: the linear address is fed to the paging unit, which produces the physical address. Page sizes are 4 KB or 4 MB. For 4-KB pages, IA-32 uses a two-level scheme: the 10 high-order bits index the outermost table — Intel calls it the page directory — whose entry points to an inner page table; the next 10 bits index that page table; and the low 12 bits are the offset within the 4-KB page. If the directory entry is flagged as a 4-MB page, the page directory points directly at the page frame and the remaining 22 bits are all offset — one level shallower.
The 4 GB ceiling of 32-bit addressing pushed Intel to add PAE, letting 32-bit applications reach beyond 4 GB of physical memory. Paging becomes a 3-level scheme: the top 2 bits of the linear address index a page-directory pointer table, which points to a page directory, then a page table. Page-directory and page-table entries widen to 64 bits, so frame addresses can exceed 32 bits: the net effect is 36-bit physical addressing = up to 64 GB of physical memory. (Virtual addresses are still 32 bits — PAE grows the machine, not the process.)
ARM is the dominant mobile-platform chip — Apple iOS and Google Android devices — built as modern, energy-efficient CPUs. Its memory system in brief:
IA-32: logical (selector, offset) → segmentation unit → linear address → paging unit → physical address; selector = 13-bit s + 1-bit g + 2-bit p; 8 K LDT (private) + 8 K GDT (shared) segments of up to 4 GB; pages 4 KB (two-level, 10/10/12) or 4 MB (directory-direct, 22-bit offset); PAE → 3 levels, 64-bit entries, 36-bit/64 GB physical. x86-64: 48-bit virtual, 52-bit physical, 4 levels, 4 KB/2 MB/1 GB pages. ARM: 4 KB/16 KB pages + 1 MB/16 MB sections; micro TLBs (data + instruction) → main TLB → hardware page-table walk. Any of these numbers can be a one-point definition or MCQ — they fit on a cheat-sheet corner.
When flat page tables outgrow memory, three structures answer: hierarchical tables page the page table (great until 64-bit spaces demand too many levels); hashed tables size the structure to pages in use, chaining collisions (with clustered variants for sparse 64-bit spaces); inverted tables keep one entry per physical frame — small and global, but slower to search (hash + TLB to the rescue) and awkward for sharing. Swapping stretches memory onto a backing store, but transfer time makes whole-process swapping brutally slow (1.4 s for 100 MB over disk), so modern systems keep only a threshold-triggered, page-granular version — and mobile systems skip it entirely, reclaiming memory by asking (iOS) or by terminating with saved state (Android). The IA-32 pipeline — selector through segmentation to a linear address, then two-level (or PAE three-level) paging to a physical address — and the x86-64 and ARM schemes show every idea of Chapters 10–11 shipped in silicon.
Next stop, the course's final topic: Chapter 12 — Virtual Memory (textbook Chapter 10), where the valid–invalid bit, the backing store, and swapping-with-paging fuse into demand paging. And circle the date: the final exam is Thursday, July 30th, 2:00–3:40 pm — cumulative with roughly 80% new content, one-page handwritten cheat sheet allowed. Chapters 10 and 11 are squarely inside that 80%.
This is the comprehensive study guide rebuilt as readable notes. Everything from the guide is here, with the reasoning filled in: why each formula works, what each instruction field means, and where the traps are. The colored boxes are your review layer.
Plain English, the exam wording, and why it matters.
Numbers carried all the way through.
The mistake the guide flags, corrected.
Questions with the answer hidden until you open it.
A procedure you may need to reproduce.
The guide's own advice: review Units 3–6 most heavily. Unit 7 is the cram sheet.
Computer architecture & performance
Unit 1 of the CSC 3210 study guide
A computer is any mechanism designed to process data through arithmetic or logical operations, covering input, processing, and output. In the Von Neumann model the core components are the processor (CPU), memory, and I/O, all connected by the system bus.
Because instructions and data share one path to memory, the CPU cannot fetch an instruction and its operand at the same instant. That limit is nicknamed the Von Neumann bottleneck, and it is a big reason caches and pipelining (great ideas 4 and 6) exist.
| # | Great idea | What it means / example |
|---|---|---|
| 1 | Abstraction | Hide lower-level complexity behind clean interfaces. The OS hides hardware from apps; a high-level language hides the ISA. |
| 2 | Make the common case fast | Optimize frequent operations even at some cost to rare ones. 12-bit immediate operands are fast because small constants are common. |
| 3 | Parallelism | Execute multiple operations simultaneously: multi-core CPUs, GPU SIMD, pipelined execution. |
| 4 | Pipelining | Overlap instruction stages (fetch, decode, execute, writeback) like an assembly line. |
| 5 | Prediction | Branch prediction pre-fetches the likely next instruction to avoid pipeline stalls. |
| 6 | Memory hierarchy | Layer storage (registers → cache → RAM → disk) to balance speed and cost. |
| 7 | Redundancy | Duplicate components to detect and correct errors and improve dependability, as in RAID. |
Memory hook: Abstraction, Common case, Parallelism, Pipelining, Prediction, Hierarchy, Redundancy. Ideas 3, 4, and 5 all buy speed; 6 and 7 trade extra hardware for speed and safety.
| Term | Definition | Better when |
|---|---|---|
| Response time (latency) | Time to complete one task from start to finish | Lower |
| Throughput | Number of tasks completed per unit time | Higher |
| Performance | Formally 1 / execution time | Higher |
| Elapsed time | Wall-clock time including CPU, I/O, OS overhead, and idle time | Lower |
| CPU time | Only the time the CPU spends on your program (user CPU time + system CPU time) | Lower |
| Clock period | Duration of one clock cycle, e.g. 250 ps = 0.25 ns | Lower |
| Clock frequency | Cycles per second, e.g. 4.0 GHz = 4×10⁹ cycles/s; frequency = 1 / period | Higher |
Treating CPU time and elapsed time as the same thing. Elapsed time includes waiting on disk, the network, and other programs; CPU time excludes all of that. A program can have tiny CPU time and terrible elapsed time.
Variables. IC = instruction count, the number of instructions actually executed. CPI = average cycles per instruction. Cycle Time = 1 / clock rate.
Why it works. Multiply how many instructions you run by how many cycles each takes to get total cycles (CPU Cycles = IC × CPI), then multiply by how long a cycle lasts. Three independent knobs, one product.
Weighted CPI = Σ (CPIᵢ × fractionᵢ) across instruction types, when different instruction classes take different numbers of cycles.
1. IC = 2×10⁹, CPI = 1.5, clock = 3 GHz. CPU time = (2×10⁹ × 1.5) / 3×10⁹ = 1.0 s.
2. Solve for CPI. A program runs in 2 s at 4 GHz with IC = 4×10⁹. Cycles = 2 × 4×10⁹ = 8×10⁹, so CPI = 8×10⁹ / 4×10⁹ = 2.0.
3. Weighted CPI. 50% ALU at CPI 1, 30% loads at CPI 5, 20% branches at CPI 2: CPI = 0.5(1) + 0.3(5) + 0.2(2) = 0.5 + 1.5 + 0.4 = 2.4.
4. Trade-off. A compiler change cuts IC by 20% but raises CPI from 2.0 to 2.4. New CPU time ∝ 0.8 × 2.4 = 1.92 vs. old 1 × 2.0 = 2.0, so it is about 4% faster. Always multiply all three factors before declaring a win.
| If this is high | Root cause | Fix |
|---|---|---|
| Instruction count | Inefficient algorithm; poor compiler | Improve the algorithm; enable compiler optimization |
| CPI | Many slow instructions (memory loads, branches) | Add cache; restructure code; reduce branch mispredictions |
| Cycle time | Low clock rate (hardware limit) | Faster technology or a different processor |
Worked example. Computer A runs a program in 20 s, computer B in 40 s. Speedup = 40/20 = 2.0, so A is 2 times faster than B. Note the times flip when you move from performance to time.
Variables. P = the fraction of the program that can be parallelized (or improved); N = the number of processors.
Why it works. Total work takes time 1. The serial share (1 − P) is untouched; the parallel share P is divided across N. Add the two pieces to get the new time, then invert.
Ceiling. As N → ∞ the term P/N → 0, so Speedup → 1 / (1 − P).
1. The guide's example. P = 0.80, N = 4: 1 / [0.20 + 0.80/4] = 1 / [0.20 + 0.20] = 1 / 0.40 = 2.5×.
2. The ceiling. Same P = 0.80 with infinite cores: 1 / 0.20 = 5×, no matter how much hardware you buy.
3. Harder. P = 0.95, N = 8: 1 / [0.05 + 0.11875] = 1 / 0.16875 ≈ 5.93×; the ceiling is 1/0.05 = 20×.
4. Backwards. You want 4× on 8 cores. Solve 4 = 1/[(1−P) + P/8] → (1−P) + 0.125P = 0.25 → 1 − 0.875P = 0.25 → P ≈ 0.857. You would need almost 86% of the program parallelized.
Dividing the whole program by N, or plugging in percentages instead of fractions. Only the parallel part is divided by N, and P must be a fraction of 1 (0.80, not 80). Also: N cores never gives N× speedup unless P = 1. (CSC 4320 writes the same law with S for the serial fraction: speedup = 1/(S + (1−S)/N). Here P = 1 − S.)
Moore's law: transistor count doubles roughly every two years with stable cost per transistor.
| Year | Technology | Performance vs. 1951 | Notes |
|---|---|---|---|
| 1951 | Vacuum tubes | 1× (baseline) | First generation |
| 1965 | Transistors | 35× | Discrete components |
| 1975 | Integrated circuits | 900× | ICs |
| 1995 | VLSI | 2.4 million× | Very large scale integration |
| 2013 | Ultra-large ICs | 250 billion× | Billions of transistors per chip |
| Level | Size | Speed | Volatile? | Purpose |
|---|---|---|---|---|
| Registers | 32–64 bits each | Fastest | Yes | Immediate CPU operands |
| L1/L2/L3 cache (SRAM) | KB → MB | Faster than RAM | Yes | Reduce memory latency |
| Main memory (DRAM) | GB | ~100 ns access | Yes | Running programs and data |
| Secondary (SSD/HDD) | TB+ | Milliseconds | No | Long-term storage |
The pattern: closer to the CPU means faster, smaller, and more expensive per byte. This is great idea 6, and it is the same hierarchy CSC 4320 covers from the OS side.
MIPS (millions of instructions per second) sounds like a performance number but is unreliable:
Use execution time instead: it captures IC × CPI × cycle time together.
Time ∝ CPI / clock rate. A: 2.0/4 = 0.5 ns per instruction. B: 1.2/3 = 0.4 ns. B is faster by 0.5/0.4 = 1.25×, even though A has the higher clock rate.
Because MIPS counts instructions, not work. A RISC machine may need more, simpler instructions to do the same job, and MIPS ignores CPI differences entirely. Compare execution time.
Number systems & arithmetic
Unit 2 of the CSC 3210 study guide
| System | Base | Digits | Example |
|---|---|---|---|
| Decimal | 10 | 0–9 | 2345₁₀ = 2×10³ + 3×10² + 4×10¹ + 5×10⁰ |
| Binary | 2 | 0, 1 | 1011₂ = 8 + 0 + 2 + 1 = 11₁₀ |
| Octal | 8 | 0–7 | 1271₈ = 512 + 128 + 56 + 1 = 697₁₀ |
| Hexadecimal | 16 | 0–9, A–F | A110₁₆ = 10×4096 + 256 + 16 + 0 = 41,232₁₀ |
Hex digits: 0–9 as usual, then A = 10, B = 11, C = 12, D = 13, E = 14, F = 15.
101011₂ = 32 + 8 + 2 + 1 = 43₁₀.100110₂.ABCF₁₆ = 1010 1011 1100 1111₂.4D2₁₆. Check: 4×256 + 13×16 + 2 = 1234.Convert 1011011101₂. Pad on the left to a multiple of four: 0010 1101 1101. Each group: 0010 = 2, 1101 = D, 1101 = D. Answer: 2DD₁₆. Check: 2×256 + 13×16 + 13 = 512 + 208 + 13 = 733, and the binary is 512+128+64+16+8+4+1 = 733. ✓
Grouping bits from the left. Always group from the right and pad the left with zeros; 4 binary digits = 1 hex digit only when the groups line up with the low end of the number.
One's complement: 10110101. Add 1: 10110110. The original is 74, so this is −74.
Verify by adding them: 01001010 + 10110110 = 1 00000000; the low 8 bits are zero, which is exactly what x + (−x) should give. ✓
| Bits | Unsigned range | Signed range | Notes |
|---|---|---|---|
| 4 | 0 to 15 | −8 to +7 | MSB is the sign bit |
| 5 | 0 to 31 | −16 to +15 | |
| 8 | 0 to 255 | −128 to +127 | ASCII fits in unsigned 8-bit |
| 16 | 0 to 65,535 | −32,768 to +32,767 | |
| 32 | 0 to 4,294,967,295 | −2,147,483,648 to +2,147,483,647 | RISC-V word size |
Notice the asymmetry: the signed range has one more negative value than positive, because zero uses a slot on the positive side.
Binary addition rules: 0+0 = 0, 0+1 = 1, 1+0 = 1, and 1+1 = 10₂ (write 0, carry 1). Add column by column from the LSB, propagating carries.
10110111₂ (183) + 01101110₂ (110) = 293, which needs 9 bits. In 8 bits you keep 00100101₂ (37) and a carry-out of 1: 293 − 256 = 37. That carry-out is unsigned overflow.
6A2₁₆ + 49A₁₆, working right to left, carrying when a column reaches 16:
Units: 2 + A(10) = 12 → C, no carry. Sixteens: A(10) + 9 = 19 → 19 − 16 = 3, carry 1. 256s: 1 + 6 + 4 = 11 → B. Result: B3C₁₆. Check in decimal: 1698 + 1178 = 2876 = 11×256 + 3×16 + 12. ✓
100 = 01100100. Invert: 10011011. Add 1: 10011100. Check: 128+16+8+4 = 156, and 156 − 256 = −100. ✓
As unsigned, no: 127 + 1 = 128 fits in 0–255. As signed, yes: two positives produced a negative (127 + 1 gave −128). Whether it is overflow depends on the interpretation.
RISC-V ISA & instruction formats
Unit 3 of the CSC 3210 study guide
lw, sw).| Register | ABI name | Saver | Purpose |
|---|---|---|---|
| x0 | zero | — | Hardwired zero: reads return 0, writes are discarded |
| x1 | ra | Caller | Return address: holds PC+4 after jal |
| x2 | sp | Callee | Stack pointer: top of the stack |
| x3 | gp | — | Global pointer: references the static data segment |
| x4 | tp | — | Thread pointer |
| x5–x7 | t0–t2 | Caller | Temporaries; a callee may overwrite them |
| x8 | s0/fp | Callee | Saved register / frame pointer |
| x9 | s1 | Callee | Saved register |
| x10–x11 | a0–a1 | Caller | Function arguments and return values |
| x12–x17 | a2–a7 | Caller | Additional arguments (8 total in registers) |
| x18–x27 | s2–s11 | Callee | Saved registers; callee must save and restore |
| x28–x31 | t3–t6 | Caller | More temporaries |
Mnemonic: t = temporary = trashable by the callee; s = saved = safe across calls.
| Format | Primary use | Immediate | Field layout |
|---|---|---|---|
| R | Register-register ALU (add, sub, and, or, xor, sll) | None | funct7 | rs2 | rs1 | funct3 | rd | opcode |
| I | Immediate arithmetic and loads (addi, lw, lb, jalr) | 12-bit signed | imm[11:0] | rs1 | funct3 | rd | opcode |
| S | Stores (sw, sb, sh) | 12-bit, split | imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode |
| B | Conditional branches (beq, bne, blt, bge) | 13-bit, PC-relative | imm[12|10:5] | rs2 | rs1 | funct3 | imm[4:1|11] | opcode |
| U | Upper immediate (lui, auipc) | 20-bit upper | imm[31:12] | rd | opcode |
| J | Jumps (jal) | 21-bit, PC-relative | imm[20|10:1|11|19:12] | rd | opcode |
| Field | Bits | Description |
|---|---|---|
| opcode | 7 | Identifies the instruction type (0110011 = R-type ALU, 0010011 = I-type ALU) |
| rd | 5 | Destination register number (0–31) |
| rs1 | 5 | First source register |
| rs2 | 5 | Second source register |
| funct3 | 3 | Distinguishes variants within a format (add vs. xor) |
| funct7 | 7 | Further distinguishes: add = 0000000, sub = 0100000 |
| immediate | 12 | Sign-extended constant in I and S formats; split across fields in S |
Sanity check: 7 + 5 + 3 + 5 + 5 + 7 = 32 bits for R-type, and 12 + 5 + 3 + 5 + 7 = 32 for I-type. Every format fills exactly one 32-bit word.
It looks arbitrary, but the split keeps rs1, rs2, and funct3 in the same bit positions in every format. The decoder can read the register numbers before it even knows the instruction type, which saves hardware. The B-type's low bit is dropped (offsets are always even), which buys one extra bit of range.
It adds an immediate, so it is I-type, opcode 0010011, funct3 000.
imm[11:0] rs1 funct3 rd opcode
000000001010 00000 000 00101 0010011
10 x0 addi x5 I-ALU
Regroup into bytes: 0000 0000 1010 0000 0000 0010 1001 0011
Hex: 0x00A00293
0x002082B3 = 0000000 00010 00001 000 00101 0110011
funct7 rs2 rs1 f3 rd opcode
opcode 0110011 → R-format (register-register ALU)
funct3 000 + funct7 0000000 → ADD (funct7 0100000 would be SUB)
rd 00101 → x5 rs1 00001 → x1 rs2 00010 → x2
Assembly: add x5, x1, x2.
| Instruction | Format | Operation / notes |
|---|---|---|
add rd, rs1, rs2 | R | rd = rs1 + rs2 |
sub rd, rs1, rs2 | R | rd = rs1 − rs2 (funct7 = 0100000) |
and/or/xor rd, rs1, rs2 | R | Bitwise AND / OR / XOR |
sll/srl rd, rs1, rs2 | R | Shift left/right logical by the amount in rs2 |
addi rd, rs1, imm | I | rd = rs1 + sign_extend(imm), a 12-bit constant |
lw rd, imm(rs1) | I | rd = Memory[rs1 + sign_extend(imm)], 32-bit word |
lb / lbu rd, imm(rs1) | I | Load byte: lb sign-extends, lbu zero-extends |
sw rs2, imm(rs1) | S | Memory[rs1 + sign_extend(imm)] = rs2 |
beq / bne rs1, rs2, L | B | Branch if equal / not equal (PC-relative) |
blt / bge rs1, rs2, L | B | Branch if less / greater-or-equal, signed |
bltu / bgeu rs1, rs2, L | B | Unsigned variants |
jal rd, label | J | rd = PC+4, then jump (PC-relative, ±1 MB) |
jalr rd, imm(rs1) | I | rd = PC+4, then jump to rs1 + imm (returns, computed jumps) |
lui rd, imm | U | rd = imm << 12 (load upper 20 bits) |
slli rd, rs1, shamt | I | rd = rs1 << shamt (multiply by 2^shamt) |
srli rd, rs1, shamt | I | rd = rs1 >> shamt (logical, zero-fills) |
S-type. A store has two source registers (address base and data) and no destination, so the 5 bits that I-type uses for rd are needed for the low half of the immediate. That is why the S immediate is split.
R-type: funct7 0000000, rs2 = x12 = 01100, rs1 = x11 = 01011, funct3 000, rd = x10 = 01010, opcode 0110011. Bits: 0000000 01100 01011 000 01010 0110011 = 0000 0000 1100 0101 1000 0101 0011 0011 = 0x00C58533.
Bitwise operations & masking
Unit 4 of the CSC 3210 study guide
| A | B | A AND B | A OR B | A XOR B | NOT A |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 1 |
| 0 | 1 | 0 | 1 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 | 0 |
Read them as rules, not rows: AND is 1 only when both are 1, OR is 1 unless both are 0, XOR is 1 when the inputs differ.
| Operation | Effect | Typical mask | Use case |
|---|---|---|---|
| AND | Keeps bits where the mask is 1, clears where it is 0 | 0x000000FF | Extract or isolate specific bits |
| OR | Sets bits where the mask is 1, leaves others unchanged | 0x00000001 | Force specific bits to 1 |
| XOR | Toggles bits where the mask is 1, leaves others unchanged | Any pattern | Flip bits; simple encryption |
| Mask | Effect on a 32-bit register |
|---|---|
0x000000FF | Keeps the lowest 8 bits (byte 0) |
0x0000FF00 | Keeps the second byte (bits 15:8) |
0x0000FFFF | Keeps the lowest 16 bits |
0xFFFFFF00 | Clears the lowest 8 bits |
0xFFFF0000 | Keeps the upper 16 bits |
A favorite exam constraint: build constants using nothing but addi and shifts. The trick is that addi x, x0, -1 gives all ones, and shifting walks the boundary to where you need it.
addi x10, x0, 0xFF # 255 fits in a 12-bit signed immediate → 0x000000FF
addi x11, x0, -1 # x11 = 0xFFFFFFFF (all ones)
slli x11, x11, 8 # shift left 8 → 0xFFFFFF00
addi x12, x0, -1 # x12 = 0xFFFFFFFF
srli x12, x12, 16 # shift right 16 → 0x0000FFFF
Task 1 — extract the lowest byte into x6:
addi x10, x0, 0xFF
and x6, x5, x10 # x6 = 0x00000078
Task 2 — clear the lowest byte into x7:
addi x11, x0, -1
slli x11, x11, 8 # mask = 0xFFFFFF00
and x7, x5, x11 # x7 = 0x12345600
Task 3 — extract the second byte (bits 15:8) into x8:
addi x10, x0, 0xFF
srli x8, x5, 8 # x8 = 0x00123456 (shift the field down first)
and x8, x8, x10 # x8 = 0x00000056
The general recipe for extracting a field: shift it down to bit 0, then AND with a mask of that many ones.
Using OR to clear bits or AND to set bits. AND clears (where the mask is 0), OR sets (where the mask is 1), XOR toggles. To clear with AND you need a mask with zeros in the target positions, which is why 0xFFFFFF00 clears the low byte.
| Instruction | Direction | Fills with | Effect |
|---|---|---|---|
slli rd, rs1, n | Left | 0s on the right | Multiply by 2ⁿ |
srli rd, rs1, n | Right | 0s on the left | Divide by 2ⁿ (unsigned) |
srai rd, rs1, n | Right | Copies of the sign bit | Divide by 2ⁿ (signed, arithmetic) |
sll / srl / sra rd, rs1, rs2 | Either | As above | Shift amount comes from a register |
Why srai exists: shifting −8 (0xFFFFFFF8) right by 1 with srli gives a huge positive number, while srai keeps the sign and gives −4.
XOR's three properties: A ⊕ A = 0, A ⊕ 0 = A, and it is commutative and associative. Together they let you swap two registers without a third.
xor x5, x5, x6 # x5 = A⊕B
xor x6, x5, x6 # x6 = (A⊕B)⊕B = A (original x5)
xor x5, x5, x6 # x5 = (A⊕B)⊕A = B (original x6)
The same self-inverse property is the basis of XOR encryption: Cipher = Plaintext ⊕ Key, so Plaintext = Cipher ⊕ Key. (Your security course uses this for stream ciphers.)
Bit 3 is the value 8: addi x10, x0, 8 then or x5, x5, x10. OR sets, and every other mask bit is 0, so nothing else changes.
Shifting right 4 gives 0x0000000F; ANDing with 0x0F leaves 0x0000000F, so x6 = 15. You extracted the high nibble of the low byte.
Procedures, stack & memory layout
Unit 5 of the CSC 3210 study guide
x10–x17 (a0–a7). Extra arguments go on the stack.jal x1, ProcedureLabel, which saves the return address PC+4 in x1 (ra).addi sp, sp, -N where N is the bytes needed.x10 (a0); a second return value goes in x11 (a1).jalr x0, 0(x1), jumping to the address in ra and discarding the new return address into x0.| Leaf procedure | Non-leaf procedure | |
|---|---|---|
| Calls others? | No | Yes, at least one other function |
| Save x1 (ra)? | No: x1 is never overwritten | Yes: a nested jal overwrites ra |
| Stack needed for | Callee-saved registers it modifies | ra + callee-saved registers + extra args + locals |
| Complexity | Simple, few stack operations | More stack management |
| Example | A simple math helper | Recursive factorial, or anything that calls another function |
C source: int leaf_example(int g, int h, int i, int j) { int f = (g + h) - (i + j); return f; }
leaf_example: # g=x10, h=x11, i=x12, j=x13
addi sp, sp, -8 # allocate 8 bytes of stack frame
sw x5, 4(sp) # save temp x5 (this code chooses to preserve it)
sw x6, 0(sp) # save temp x6
add x5, x10, x11 # x5 = g + h
add x6, x12, x13 # x6 = i + j
sub x10, x5, x6 # return value in a0 = (g+h) - (i+j)
lw x6, 0(sp) # restore x6
lw x5, 4(sp) # restore x5
addi sp, sp, 8 # free the frame
jalr x0, 0(x1) # return
Note the symmetry: allocate then save at the top, restore then deallocate at the bottom, in reverse order.
Before any jal, save what the call would destroy; after it returns, restore:
sw x1, N(sp) # save return address before the nested call
sw x10, M(sp) # save an argument you still need afterward
jal x1, other_function
lw x1, N(sp) # restore return address
lw x10, M(sp) # restore the argument
In recursion every call gets its own frame, which is exactly what keeps each invocation's state independent.
Forgetting to save ra in a non-leaf procedure. The nested jal overwrites x1 with its return address, so when you finally execute jalr x0, 0(x1) you jump back into the middle of your own function instead of to your caller. Infinite loops and wild crashes come from this one line.
| # | Reason | Explanation |
|---|---|---|
| 1 | Return address preservation | Nested jals overwrite x1, so ra is saved before each call |
| 2 | Callee-saved register preservation | s0–s11 modified by a callee must be saved and restored so the caller's values survive |
| 3 | Extra arguments | Beyond the 8 in a0–a7, arguments are passed on the stack |
| 4 | Local variables | Arrays, structs, and data too large for registers live on the stack |
| 5 | Recursion support | Each recursive call needs its own independent state, which frames provide |
Higher addresses
+---------------------------------+ <- caller's sp (before the call)
| caller's saved data |
+---------------------------------+
| return address (ra / x1) | <- non-leaf only
+---------------------------------+
| saved s-registers |
+---------------------------------+
| local variables / arrays |
+---------------------------------+
| extra arguments (if more than 8)| <- sp + 0
+---------------------------------+ <- callee's sp after addi sp, sp, -N
Lower addresses (the stack grows DOWN)
| Segment | Contains | Notes |
|---|---|---|
| Stack | Local variables, saved registers, return addresses | Grows downward from high addresses; managed by sp (x2) |
| Heap | Dynamically allocated memory | Grows upward; managed by the programmer (malloc/new) |
| Static data | Global and static variables, constants, string literals | Referenced via the global pointer x3 |
| Text | Program machine instructions | Read-only at run time; loaded by the OS |
| Reserved | OS and hardware use | Lowest addresses |
Heap and stack grow toward each other through the free space between them, which is the same picture CSC 4320 draws for a process's address space.
lw can target any byte address.| Address | Byte | Note |
|---|---|---|
| 0x1000 | 0x78 | Least significant byte, stored first |
| 0x1001 | 0x56 | |
| 0x1002 | 0x34 | |
| 0x1003 | 0x12 | Most significant byte, stored last |
Memory hook: little endian = little end first.
Architecturally RISC-V allows misaligned loads and stores, but real implementations often handle them slowly or trap to a handler that fixes them up. So the guide's claim is right for the exam, while real code still aligns data for speed.
| Instruction | Branches when | Comparison | Complement (exit) |
|---|---|---|---|
beq rs1, rs2, L | rs1 == rs2 | Either | bne |
bne rs1, rs2, L | rs1 != rs2 | Either | beq |
blt rs1, rs2, L | rs1 < rs2 | Signed | bge |
bge rs1, rs2, L | rs1 >= rs2 | Signed | blt |
bltu rs1, rs2, L | rs1 < rs2 | Unsigned | bgeu |
bgeu rs1, rs2, L | rs1 >= rs2 | Unsigned | bltu |
0xFFFFFFFF is −1 read as signed and 4,294,967,295 read as unsigned. So blt says 0xFFFFFFFF < 1 is true (−1 < 1), while bltu says it is false (a huge number is not less than 1). The bits are identical; only the instruction decides the meaning.
Four 4-byte words = 16 bytes: addi sp, sp, -16, then sw each at offsets 0, 4, 8, 12, and addi sp, sp, 16 before returning.
jal takes a fixed PC-relative label, but a function can be called from anywhere, so the return target is only known at run time, in ra. jalr jumps to a computed address, and writing the new return address to x0 discards it.
Branches, jumps & loading constants
Unit 6 of the CSC 3210 study guide
The immediate is a signed 13-bit value stored in the B-type instruction, and it counts half-words; multiplying by 2 converts to bytes. Branch not taken: PC = PC + 4. Branch taken: PC = PC + imm × 2.
A branch sits at PC = 0x00000100 and its target Loop is 0x000000D0.
Byte offset = 0xD0 − 0x100 = −0x30 = −48 bytes. Stored immediate = −48 / 2 = −24, in 13-bit two's complement. Decoding: −24 × 2 = −48, and 0x100 + (−48) = 0xD0. ✓
Treating the branch immediate as a byte offset. It is a half-word offset: divide the byte distance by 2 when encoding, multiply by 2 when decoding. This works because every instruction is 2- or 4-byte aligned, so the lowest bit is always 0 and storing it would waste range.
C: while (i < 10) { body; }
Loop:
bge x11, x12, Exit # if i >= 10, leave (INVERT the condition)
...loop body...
beq x0, x0, Loop # unconditional branch back (x0 == x0 always)
Exit:
Key rule: the C condition is inverted for the exit branch. "while i < 10" becomes "branch out when i >= 10", so you use bge.
C: if (i == j) f = g + h; else f = g - h; with i=x22, j=x23, f=x19, g=x20, h=x21.
bne x22, x23, Else # if i != j, skip the then-branch
add x19, x20, x21 # f = g + h
beq x0, x0, Exit # jump over the else
Else:
sub x19, x20, x21 # f = g - h
Exit:
Same inversion idea: the branch tests the opposite of the C condition, because the branch's job is to skip the code.
I-format immediates are only 12 bits signed (−2048 to +2047), so a full 32-bit constant is split: lui loads the upper 20 bits and addi adds the lower 12.
Load 0x12340050 into x19:
lui x19, 0x12340 # x19 = 0x12340000
addi x19, x19, 0x50 # x19 = 0x12340050
addi sign-extends its 12-bit immediate. If the lower 12 bits are ≥ 0x800, bit 11 is 1, so the immediate is treated as negative and subtracts from the upper part.
Wrong: to load 0x12340800, lui x6, 0x12340 then addi x6, x6, 0x800 gives 0x12340000 − 0x800 = 0x1233F800. ✗
Right: pre-compensate by adding 1 to the upper 20 bits: lui x6, 0x12341 then addi x6, x6, 0x800 gives 0x12341000 − 0x800 = 0x12340800. ✓
Rule: if lower12 ≥ 0x800, add 1 to the value used in lui.
| Instruction | Format | Operation and use |
|---|---|---|
jal rd, label | J | rd = PC+4; PC = PC + offset (20-bit, ±1 MB). Procedure call or unconditional jump. |
jalr rd, imm(rs1) | I | rd = PC+4; PC = rs1 + sign_extend(imm). Returns, computed jumps, switch-case. |
jalr x0, 0(x1) | I | The standard return: jump to ra, discard the new return address. |
lui + jalr | U + I | Load a full 32-bit target into a temporary, then jump, for unlimited range. |
addi x10, x0, 0 -+
addi x11, x0, 0 | Block 1: initialization
addi x12, x0, 10 -+
Loop: -+ Block 2: loop header
bge x11, x12, Exit -+ (label at start, branch at end)
add x10, x10, x11 -+
addi x11, x11, 1 | Block 3: loop body
beq x0, x0, Loop -+
Exit: -+ Block 4: post-loop
The boundaries are exactly where labels and branches sit.
Byte offset = 0x260 − 0x200 = 0x60 = 96 bytes. Immediate = 96 / 2 = 48.
Split the target at bit 12: upper20 = 0xABCDE, lower12 = 0xF00. Since 0xF00 ≥ 0x800, add 1 to the upper: use 0xABCDF.
lui x5, 0xABCDF # x5 = 0xABCDF000
addi x5, x5, 0xF00 # 0xF00 sign-extends to -256
# 0xABCDF000 - 0x100 = 0xABCDEF00Exam toolkit
Unit 7 of the CSC 3210 study guide: formulas, must-know facts, and traps
| Formula | Meaning |
|---|---|
CPU Time = IC × CPI × Cycle_Time = (IC × CPI) / Clock_Rate | The core performance equation |
Clock_Rate = 1 / Cycle_Time | 250 ps period → 4 GHz |
CPU Cycles = IC × CPI | Total cycles |
Weighted CPI = Σ (CPIᵢ × fractionᵢ) | Mixed instruction types |
Speedup(A over B) = Time_B / Time_A = Perf_A / Perf_B | "n times faster" |
Amdahl = 1 / [(1−P) + P/N] | Partial-improvement speedup |
Max Amdahl = 1 / (1−P) | The ceiling as N → ∞ |
Branch target = PC + imm × 2 | B-type addressing |
LUI: rd[31:12] = imm, rd[11:0] = 0 | Then add the lower 12 with ADDI |
Unsigned range = 0 … 2ⁿ − 1 | |
Signed range = −2^(n−1) … 2^(n−1) − 1 | |
Two's complement = invert all bits + 1 |
| 2ⁿ | Value | 2ⁿ | Value | 2ⁿ | Value | 2ⁿ | Value |
|---|---|---|---|---|---|---|---|
| 2⁰ | 1 | 2⁴ | 16 | 2⁸ | 256 | 2¹² | 4,096 |
| 2¹ | 2 | 2⁵ | 32 | 2⁹ | 512 | 2¹⁶ | 65,536 |
| 2² | 4 | 2⁶ | 64 | 2¹⁰ | 1,024 | 2²⁰ | ~1 M |
| 2³ | 8 | 2⁷ | 128 | 2¹¹ | 2,048 | 2³² | ~4 B |
addi sp, sp, -N.jal.bltu/bgeu for unsigned comparisons.| # | Mistake | Consequence |
|---|---|---|
| 1 | Forgetting to save ra before a jal in a non-leaf procedure | Returns to the wrong address |
| 2 | LUI + ADDI without the +1 correction when lower12 ≥ 0x800 | Constant is 0x1000 too low |
| 3 | Confusing signed and unsigned comparisons with 0xFFFFFFFF | Branch goes the wrong way |
| 4 | Treating the branch immediate as a byte offset | Target is twice as far as intended |
| 5 | Using OR to clear bits or AND to set bits | Mask does the opposite of what you want |
| 6 | Forgetting addi sp, sp, +N at the end of a function | Stack pointer drifts; caller's frame is corrupted |
| 7 | Treating CPU time and elapsed time as the same | Wrong performance conclusions |
| 8 | Assuming a higher clock rate always wins | CPI and IC matter just as much |
| 9 | Trying to store a value in x0 | Silently discarded |
| 10 | Flipping endianness | RISC-V is little endian: LSB at the lowest address |
| Encoding | Bits | Range | Notes |
|---|---|---|---|
| ASCII | 7 | 0–127 (128 chars) | Standard English characters; fits in an unsigned byte |
| Extended ASCII | 8 | 0–255 | Extra symbols and Latin characters |
| Latin-1 | 8 | 0–255 | Western European languages |
| Unicode | 32 | Millions of characters | Most world languages; handled with lb/lh/lw |
| Feature | RISC-V (RISC) | x86 (CISC) |
|---|---|---|
| Instruction length | Fixed 32-bit, simple decode | Variable 1–15 bytes, complex decode |
| Memory operands | Load/store only | Arithmetic can address memory directly |
| Register count | 32 general-purpose | 8–16, with more complex conventions |
| Instruction complexity | Simple, about one cycle each | Some instructions perform many operations |
| Proprietary? | No, open source (RISC-V International) | Yes, Intel/AMD controlled |
| Addressing modes | Primarily base + offset | Many: immediate, indirect, scaled, segment |
| ISA examples | RISC-V, ARM, MIPS | x86, x86-64 |
This is the comprehensive study guide rebuilt as readable notes: the same tables and commands, with the reasoning and the traps spelled out. C here is the same C that CSC 4320 assumes in its Chapter 3 review, so the two courses reinforce each other.
Plain English, the exam wording, and why it matters.
Code traced to its output.
The traps the guide flags as most-tested.
Questions with the answer hidden until you open it.
A procedure worth reproducing.
CLI & UNIX fundamentals
Unit 1 of the CSC 3320 study guide
| Component | Description |
|---|---|
| Hardware | Physical components: CPU, memory (RAM), storage, peripherals |
| Operating system | Software managing hardware, memory, files, scheduling, and user interfaces |
| Applications | Everything else running on top of the OS: games, editors, compilers |
| API | Application Program Interface: how applications reach OS services |
| UNIX | Major OS family; the foundation of macOS, Linux, and Android |
Snowball (snowball.cs.gsu.edu) is GSU's instructional server for C programming and UNIX coursework.
| Protocol | Security | Usage |
|---|---|---|
| SSH (Secure Shell) | Encrypted — secure | Remote terminal login |
| Telnet | Plaintext — insecure | Legacy remote login (avoid) |
| SFTP | Encrypted, runs over SSH | Secure file transfer |
| FTP | Plaintext — insecure | Legacy file transfer (avoid) |
ssh username@snowball.cs.gsu.edu
The first connection asks you to accept the server's key fingerprint; authentication uses your GSU single sign-on password.
SSH has no certificate authority by default, so the first time you connect your client shows the server's public key fingerprint and stores it. If it ever changes, SSH warns loudly, because a changed key can mean a man-in-the-middle. (Your security course covers exactly this problem and how certificates solve it.)
| Command | Purpose | Common flags | Example |
|---|---|---|---|
pwd | Print working directory | — | pwd → /home/student |
ls | List directory contents | -a -l -F -R -d -s -g | ls -la |
cd | Change directory | — | cd /home/student |
mkdir | Create directories | — | mkdir myproject |
rm | Remove files | -f -i -r | rm -i file.c |
rmdir | Remove an empty directory | — | rmdir emptydir |
cp | Copy files and directories | -i -r | cp src.c dest.c |
mv | Move or rename | -i | mv old.c new.c |
cat | Display file contents | — | cat main.c |
more / less | Paginate / scroll forward and back | — | less largefile.c |
head / tail | First / last n lines | -n | head -5 file.c |
man | Manual pages | -s section | man ls |
clear | Clear the terminal | — | clear |
wc | Word, line, byte count | -l -w -c | wc -l file.c |
script | Record a terminal session | — | script session.log |
chmod | Change permissions | — | chmod g+w file.c |
chgrp | Change group | -R | chgrp team file.c |
groups | List a user's groups | — | groups jsmith |
rm is irreversible: there is no recycle bin. rm -rf dir deletes a whole tree without asking. Use -i while learning, and read the path twice before pressing Enter.
| Shortcut | Action |
|---|---|
| CTRL-C | Interrupt (kill) the running process |
| CTRL-Z | Suspend (pause) the running process |
| CTRL-D | End of file (EOF); also logs out of a shell |
| CTRL-S | Stop/pause terminal output |
Exam contrast: CTRL-C kills, CTRL-Z only pauses (the job still exists), and CTRL-D signals end of input rather than killing anything.
/: /home/student/csc3320/main.ccsc3320/main.c| Symbol | Meaning |
|---|---|
/ | Root, the top-level ancestor of all files |
. | Current directory |
.. | Parent directory (one level up) |
~ | Home directory of the current user |
chmod <who><+/-><permission> <file>
Who: u user (owner), g group, o others, a all. Permissions: r read, w write, x execute. + adds, - removes. Separate multiple changes with commas.
| Command | Effect | Before | After |
|---|---|---|---|
chmod g+w file | Add write for the group | rw-r--r-- | rw-rw-r-- |
chmod u-rw file | Remove owner read and write | rw-r--r-- | ---r--r-- |
chmod u+x,g-r file | Add execute for owner, remove group read | rw-r--r-- | rwx---r-- |
The same permissions can be written numerically, with r = 4, w = 2, x = 1 per triplet. rw-r--r-- = 6 4 4 = chmod 644 file. rwxr-xr-x = 755. Symbolic mode changes one bit and leaves the rest alone; octal mode sets all nine bits at once.
| SFTP command | Action |
|---|---|
sftp username@snowball.cs.gsu.edu | Start an SFTP session |
ls / pwd / cd dirname | List, show, and change the remote directory |
get filename | Download from remote to local |
get -r dirname | Download a directory recursively |
put filename | Upload from local to remote |
put -r dirname | Upload a directory recursively |
reget / reput filename | Resume an interrupted download / upload |
exit | Close the session |
SSH is an encrypted remote terminal. SFTP is encrypted file transfer running over SSH. FTP and Telnet are unencrypted and should never be used on public networks. get (download) and put (upload) are the most tested commands. GUI alternatives: FileZilla (cross-platform) and WinSCP (Windows only).
get hw1.c. Think of the direction from your machine's point of view: get pulls down, put pushes up.
Introduction to C programming
Unit 2 of the CSC 3320 study guide
C's lineage: BCPL (1967) → B (Ken Thompson) → C (Dennis Ritchie, Bell Labs, 1973).
| Standard / event | Key facts |
|---|---|
| BCPL (1967) | Typeless systems language by Martin Richards |
| B | Created by Ken Thompson at Bell Labs, based on BCPL |
| C (1973) | Dennis Ritchie extended B into C |
| UNIX in C (1973) | UNIX was rewritten in C: the portability breakthrough |
| K&R C (1978) | Kernighan & Ritchie's book became the de facto standard |
| C89 / C90 | First formal ANSI/ISO standard |
| C99 (1999) | Added // comments, _Bool, inline, variable-length arrays |
| C11, C17, C23 | Further refinements for safety and modern features |
Why rewriting UNIX in C mattered: before that, an OS was written in the assembly language of one machine. In C, you port the compiler and recompile, which is how UNIX spread to every architecture.
| Language | Relationship to C |
|---|---|
| C++ | Extends C with classes, templates, and OOP; largely C-compatible |
| Java | Derived from C++: similar syntax, but garbage-collected with no pointers |
| C# | Influenced by C++ and Java; Microsoft's managed language |
| Python | CPython, the standard interpreter, is itself written in C |
| Property | Explanation |
|---|---|
| Low-level | Direct access to memory and OS APIs (system calls) |
| Efficient | Compiles to machine code; designed to replace assembly |
| Portable | Write once, compile on many platforms with minimal changes |
| Small language | Minimal built-in features; relies on standard libraries |
| Permissive | Trusts the programmer; no automatic safety checks |
| No OOP | No classes or inheritance; structs group data instead |
Strengths: efficiency, portability, power (rich operators and types), flexibility, a standard library, and direct UNIX integration. Weaknesses: error-prone (no bounds checking), complex pointer management, no OOP support, and harder to read than Python.
| Feature | C | Java | Python |
|---|---|---|---|
| OOP support | None (structs only) | Full OOP (classes) | Optional OOP |
| Type system | Static, explicit | Static, explicit | Dynamic, inferred |
| Memory management | Manual (malloc/free) | Garbage collected | Garbage collected |
| Pointers | Yes, explicit | No (references only) | No |
| Boolean type | int: 0 = false, non-zero = true | boolean | bool |
| String type | None (char arrays) | String object | str object |
| do-while loop | Yes | Yes | No |
| switch statement | Yes | Yes | match (3.10+) |
| Array bounds check | No: undefined behavior | Yes: exception | Yes: IndexError |
| Entry point | main() function | main() method in a class | Top-level script |
# directive: expands #include files, substitutes #define macros, and strips comments, producing pure C..o files). Syntax errors surface here.a.out by default).| Command | Action |
|---|---|
gcc file.c | Compile, producing a.out |
gcc -o myprog file.c | Compile, producing myprog |
gcc -lm file.c | Link the math library (<math.h>) |
cc file.c | Alternative compiler command on UNIX |
Missing semicolon → compiling. #include <stdioh> (typo) → preprocessing: the file cannot be found. Called sqrt() but forgot -lm → linking: "undefined reference to sqrt". Knowing the stage tells you where to look.
| Type | Size (32-bit) | Size (64-bit) | Range / notes |
|---|---|---|---|
char | 1 byte | 1 byte | −128 to 127; ASCII encoding |
short | 2 bytes | 2 bytes | −32,768 to 32,767 |
int | 4 bytes | 4 bytes | ±2.15 billion |
long | 4 bytes | 8 bytes | Expands on 64-bit systems |
long long | 8 bytes | 8 bytes | −2⁶³ to 2⁶³ − 1 |
float | 4 bytes | 4 bytes | ~3.4e−38 to 3.4e+38; F suffix |
double | 8 bytes | 8 bytes | ~1.7e−308 to 1.7e+308; the default for float literals |
| pointer | 4 bytes | 8 bytes | Stores a memory address |
unsigned variants hold only non-negative values, with double the positive range.sizeof(type) returns the size in bytes at compile time.double; use the F suffix for float: 3.14F.42, octal 052 (leading 0), hex 0x2A.That is the guide's wording and the exam answer, but read the table: pointers also change, 4 bytes on 32-bit and 8 on 64-bit. The precise claim is that long is the only basic arithmetic type that changes. Watch the leading-zero trap too: 052 is octal 42, not decimal 52.
#include <stdio.h> // preprocessor directive: pulls in the I/O library
#define MAX 100 // macro: MAX is replaced by 100 before compiling
int my_func(void) { // function defined before main
return 42;
}
int main(void) { // mandatory entry point
int result = my_func();
printf("%d\n", result);
return 0; // 0 = successful termination
}
int a, b, c = 5; sets only c; a and b are still garbage.C had no boolean type until C99 added _Bool. Before that (and still commonly), integers stand in: 0 is false, any non-zero value is true.
typedef int BOOL; // user-defined alias
BOOL flag = 1; // true
#define true 1
#define false 0
Garbage. Only c is initialized. Each variable needs its own initializer: int a = 0, b = 0, c = 5;
The C standard makes unsuffixed floating-point literals double for precision. Write 3.14F when you specifically want a float.
C fundamentals: I/O, strings & preprocessor
Unit 3 of the CSC 3320 study guide
| Directive | Purpose and example |
|---|---|
#include <stdio.h> | Include a standard library header, which declares printf and scanf |
#include "myfile.h" | Include a user-defined header from the current directory |
#define MAX 100 | Object-like macro: MAX is replaced by 100 everywhere before compiling |
#define SQ(x) ((x)*(x)) | Function-like macro: SQ(5) expands to ((5)*(5)) |
#ifdef / #endif | Conditional compilation: include code only if a macro is defined |
Header files (.h) declare function signatures only; the actual implementation lives in library code linked at the linking stage.
A macro is pure text substitution, so #define SQ(x) x*x turns SQ(2+3) into 2+3*2+3 = 11, not 25. Wrap every parameter and the whole body in parentheses: ((x)*(x)).
Both are declared in <stdio.h>. The OS automatically opens stdin (keyboard) and stdout (terminal) when a program starts.
printf("Name: %s, Age: %d, GPA: %.2f\n", name, age, gpa);
scanf("%d", &age); // & gives scanf the address to write into
scanf("%s", name); // arrays: NO & needed, the name is already a pointer
& for scalar variables (int, float, char), never for arrays or strings.scanf does not display a prompt; call printf first.%s reads until the next whitespace (so it cannot read a line with spaces).scanf("%10s", name);| Specifier | Type | Example | Notes |
|---|---|---|---|
%d / %i | int (signed) | printf("%d", 42) | The common integer specifier |
%u | unsigned int | printf("%u", 42u) | Non-negative integers |
%f | float / double | printf("%f", 3.14) | Fixed decimal, 6 places by default |
%e / %E | float / double | printf("%e", 3.14) | Scientific notation |
%g | float / double | printf("%g", 3.14) | Auto-selects %f or %e, drops trailing zeros |
%c | char | printf("%c", 'A') | Single character |
%s | char* (string) | printf("%s", name) | Reads until \0 |
%ld / %li | long int | printf("%ld", x) | |
%lf | double (scanf) | scanf("%lf", &x) | Use %lf to read a double |
%o | int as octal | printf("%o", 8) | Prints 10 |
%x / %X | int as hex | printf("%x", 255) | Prints ff / FF |
%% | literal % | printf("100%%") | Prints 100% |
Optional parameters follow the pattern %[flags][width][.precision][length]type: %-10.2f is left-justified in 10 columns with 2 decimals, %+d always shows a sign, and %08d pads with leading zeros to width 8.
| Escape | Meaning |
|---|---|
\n | Newline |
\t | Horizontal tab |
\\ | Literal backslash |
\" | Literal double quote |
\r | Carriage return |
\0 | Null character (string terminator) |
\a | Alert / bell |
\0 marks the end, so "Hello" occupies 6 bytes: {'H','e','l','l','o','\0'}.\0, and losing it means reading past the end of the array.char str1[6] = "Hello"; // fixed size; must include room for \0
char str2[] = "Hello"; // size inferred as 6
char *str3 = "Hello"; // pointer to a string literal (read-only!)
char str4[] = {'H','e','l','l','o','\0'}; // explicit character init
str1 = str2 is illegal: you cannot assign strings with =, because an array name is not a modifiable value. Use strcpy(str1, str2). Likewise s1 == s2 compares addresses, not contents; use strcmp.
| Function | Signature | Returns | Key notes |
|---|---|---|---|
strlen | strlen(char *s) | Length, excluding \0 | Does not count the terminator |
strcpy | strcpy(char *dst, char *src) | dst pointer | Copies including \0; no bounds check |
strncpy | strncpy(dst, src, int n) | dst pointer | Copies at most n chars; may omit \0 |
strcat | strcat(char *dst, char *src) | dst pointer | Appends src to dst; no bounds check |
strncat | strncat(dst, src, int n) | dst pointer | Appends at most n chars and always adds \0 |
strcmp | strcmp(char *s1, char *s2) | int | 0 = equal; positive or negative otherwise |
strncmp | strncmp(s1, s2, int n) | int | Compares only the first n characters |
| Expression | Result and reason |
|---|---|
strcmp("hello", "hello") | 0: identical |
strcmp("yello", "hello") | 17: 'y' − 'h' = 17 |
strcmp("Hello", "hello") | −32: 'H' − 'h' = −32 (uppercase sorts before lowercase in ASCII) |
strncmp("some diff", "some DIFF", 4) | 0: the first four characters match |
!strcmp(s1, s2) | True (1) when the strings match: the idiomatic equality test |
Only the sign is guaranteed by the standard; this course's exact differences come from subtracting ASCII codes.
strcpy(dst, src) has no size check; dst must hold src plus \0.dst[n-1] = '\0' yourself.==; use strcmp.%s on a buffer with no terminator is undefined behavior: printf runs off the end."Alexander" needs 10 bytes (9 characters plus \0) but the buffer holds 5, so strcpy writes 5 bytes past the end: a buffer overflow. It may crash, corrupt nearby variables, or appear to work. Use a bigger buffer or strncpy with manual termination.
strlen returns 5 (it does not count \0), but the array occupies 6 bytes. Off-by-one array sizing is the classic mistake.
Flow control & scope rules
Unit 4 of the CSC 3320 study guide
| Variable type | Declared where | Lifetime | Scope / visibility |
|---|---|---|---|
| Local (automatic) | Inside a function | That function's execution | That function only |
| Global (external) | Outside all functions | Entire program | Whole file; other files via extern |
| Static local | Inside a function, with static | Entire program | That function only, but persists between calls |
| Static global | Outside functions, with static | Entire program | That source file only (file-level encapsulation) |
Read the table as two independent questions: how long does it live (lifetime) and who can see it (scope). static extends lifetime for a local, and restricts visibility for a global.
// file1.c
int counter = 0; // definition: memory allocated here
// file2.c
extern int counter; // declaration: no memory, links to file1.c's counter
counter++; // modifies the same variable
void countCalls(void) {
static int count = 0; // initialized ONCE, retained between calls
count++;
printf("Called %d times\n", count);
}
// three calls print: Called 1 times, Called 2 times, Called 3 times
Without static, count would be re-created and reset to 0 on every call, printing 1 each time.
| Category | Operators | Notes | Example |
|---|---|---|---|
| Arithmetic | + - * / % ++ -- | % is remainder | 7 % 3 = 1 |
| Relational | < > <= >= == != | Return 0 (false) or 1 (true) | 5 > 3 → 1 |
| Logical | && || ! | && and || short-circuit | 1 && 0 → 0 |
| Assignment | = += -= *= /= %= | Compound assignment | x += 5 is x = x + 5 |
| Bitwise | & | ^ ~ << >> | Operate on individual bits | 5 & 3 = 1 |
| Ternary | expr ? a : b | Inline if-else | x > 0 ? x : -x |
| Address-of | &var | Returns a pointer to var | &x |
| Dereference | *ptr | The value at that address | *p |
sizeof | sizeof(type) | Size in bytes, at compile time | sizeof(int) = 4 |
| Expression | Result and explanation |
|---|---|
x & y | 0 — 0001 & 0010 = 0000 |
x | y | 3 — 0001 | 0010 = 0011 |
x ^ y | 3 — 0001 ^ 0010 = 0011 |
x << 1 | 2 — shifting left multiplies by 2 |
y >> 1 | 1 — shifting right divides by 2 |
~x | Inverts every bit; on any two's-complement machine (all of them in practice) ~1 is −2 |
Short-circuiting matters: in if (p != NULL && *p == 5) the dereference never runs when p is NULL, which is a standard safety idiom.
if (condition) {
// runs if condition != 0
} else if (condition2) {
// runs if condition2 != 0
} else {
// runs if all above were false (zero)
}
An else always pairs with the nearest unmatched if, no matter how the code is indented. In if (a) if (b) x = 1; else x = 2; the else belongs to if (b), not if (a). Always use braces.
switch (expression) { // evaluated ONCE
case constant1: // must be an integer or char constant
statements;
break; // without break, control FALLS THROUGH
case constant2:
statements;
break;
default: // optional; runs when nothing matches
statements;
}
| switch vs. if-else if | Details |
|---|---|
| switch is faster | The compiler can build a jump table, giving O(1) dispatch instead of sequential O(n) tests |
| Equality only | switch tests equality against constants; no ranges, no floats |
| Fall-through | A missing break continues into the next case |
| Python note | Python had no switch until match/case in 3.10 |
The jump table only appears when the case values are dense integers. For a handful of scattered values, compilers generate the same comparisons an if-else chain would. Know the guide's answer for the exam, and the nuance for real code.
| Loop | Skeleton | Condition checked | Minimum iterations |
|---|---|---|---|
while | while (cond) { body; } | Before each iteration | 0 |
do-while | do { body; } while (cond); | After each iteration | 1 |
for | for (init; test; update) { body; } | Before each iteration | 0 |
for (int i = 0; i < 10; i++) {
// ^^^^^^^^^ ^^^^^^ ^^^
// init test update
// (once) (before (after each
// each) iteration)
}
int input;
do {
printf("Enter a positive number: ");
scanf("%d", &input);
} while (input <= 0); // body always runs at least once
| Pitfall | Explanation and fix |
|---|---|
= vs. == | if (x = 5) assigns 5 and is always true. Use ==; defensively write if (5 == x) so a typo becomes a compiler error. |
Missing break | Execution falls through to the next case. |
| Integer division | 5/2 is 2, not 2.5. Cast: (double)5/2 = 2.5. |
| Non-zero is true | C treats any non-zero value as true, so if (-5) executes. |
| Precedence | & vs &&, | vs ||: use parentheses to force your intent. |
When the body must run at least once before the condition can be evaluated: menu loops and input validation, where you have to read a value before you can test it.
AB. Case 0 matches and prints A, then falls through to case 1 and prints B, where break finally stops it.
Arrays, functions & pointers
Unit 5 of the CSC 3320 study guide
Arrays are statically allocated, fixed-size collections of same-type elements stored consecutively in memory.
int a[10]; // 10 ints, UNINITIALIZED (garbage)
int b[5] = {1, 2, 3, 4, 5}; // fully initialized
int c[10] = {1, 2, 3}; // partial: the remaining 7 are 0
int d[10] = {0}; // all 10 elements = 0
int e[] = {10, 20, 30}; // compiler infers size 3
| Feature | C array | Java array | Python list |
|---|---|---|---|
| Is an object? | No | No | Yes |
| Has methods? | No | No | Yes (.append()) |
| Size flexibility | Fixed at declaration | Fixed at creation | Dynamic |
| Auto-initialized? | No: garbage | Yes: zeros/null | Built manually |
| Bounds checking? | No: undefined behavior | Yes: exception | Yes: IndexError |
| Index base | 0 | 0 | 0 |
a == &a[0].int n = sizeof(arr)/sizeof(arr[0]); (only where the real array is visible).int m[3][4]; is 12 elements laid out row by row.// PROTOTYPE (declaration) — tells the compiler the function exists
double average(double a, double b);
// DEFINITION
double average(double a, double b) {
return (a + b) / 2.0;
}
// CALL
double result = average(15.0, 35.0);
| Term | Definition |
|---|---|
| Parameter | The variable in the definition: a and b above |
| Argument | The actual value passed in the call: 15.0 and 35.0 |
| Return type | The type returned: double, int, or void |
| Prototype | A declaration before the definition, giving the compiler the signature |
void | Returns nothing (or, as a parameter list, takes nothing) |
scanf needs &: to change a caller's variable you must pass its address.void tryToChange(int x) { x = 999; } // changes the COPY only
int main(void) {
int a = 5;
tryToChange(a);
printf("%d\n", a); // still prints 5
}
void increment(int *x) { (*x)++; } // pointer to int
int main(void) {
int a = 5;
increment(&a); // pass the ADDRESS
printf("%d\n", a); // prints 6 — the original changed
}
An array passed to a function decays to a pointer to its first element. The function does not get a copy, and the size is not carried along, so you must pass the length yourself.
int sum_array(int arr[], int n) { // int arr[] is the same as int *arr
int total = 0;
for (int i = 0; i < n; i++) { // n must be passed separately
total += arr[i];
}
return total;
}
int main(void) {
int data[5] = {1,2,3,4,5};
int s = sum_array(data, 5); // pass the name (a pointer) plus the size
}
Inside the function, sizeof(arr) gives the pointer size (8 bytes on 64-bit), not the array size, so the sizeof element-count trick silently computes 2 instead of 5. Always pass the length as its own parameter. Because arrays decay to pointers, a function can modify the caller's array, which is the one exception that feels like pass-by-reference.
A recursive function calls itself. Every recursive solution needs a base case that stops the recursion and a recursive case that shrinks the problem.
int factorial(int n) {
if (n <= 1) return 1; // BASE CASE
return n * factorial(n - 1); // RECURSIVE CASE
}
// factorial(5) = 5 * 4 * 3 * 2 * 1 = 120
Each call gets its own stack frame with its own copy of n, which is exactly the mechanism CSC 3210 shows in assembly and CSC 4320 describes as the process stack. Miss the base case and the frames pile up until the stack overflows.
| Syntax | Name and purpose |
|---|---|
int *p; | Declaration: p is a pointer to int (the * here means "pointer to") |
p = &x; | Address-of: stores x's address in p |
*p | Dereference: the value stored at the address p holds |
*p = 42; | Dereference and assign: writes 42 into the location p points to |
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x; // p holds the address of x
printf("%d\n", x); // 10, direct access
printf("%d\n", *p); // 10, through the pointer
*p = 50; // modifies x through the pointer
printf("%d\n", x); // 50 — x changed
return 0;
}
Note the two meanings of *: in a declaration it means "pointer to"; in an expression it dereferences.
int *p; *p = 5; writes to a garbage address: crash or corruption.int *p = NULL; *p = 1; is a segmentation fault.Rule: always initialize pointers before use; set them to NULL if you have nothing to point at yet.
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
// call as: swap(&x, &y);Taking int a, int b instead would swap only the copies.
8 / 4 = 2, regardless of the real array length, because arr is a pointer here. This is why n is passed separately.
Exam toolkit
Quick reference and common pitfalls from the CSC 3320 study guide
| Command | Purpose | Key flags |
|---|---|---|
pwd | Print working directory | — |
ls | List files | -a hidden, -l long, -R recursive |
cd dir | Change directory | — |
mkdir dir | Create a directory | — |
rm file | Delete a file (irreversible) | -f force, -r recursive, -i prompt |
cp src dst | Copy | -i -r |
mv src dst | Move or rename | -i |
cat file | Display a whole file | — |
head / tail | First / last n lines | -n |
chmod | Change permissions | u/g/o/a +/- r/w/x |
wc file | Count lines, words, bytes | -l -w -c |
man cmd | Manual page | -s section |
script | Record a terminal session | — |
ssh user@host | Encrypted remote login | — |
sftp user@host | Secure file transfer | — |
get / put | SFTP download / upload | -r |
| Keyword / symbol | Category | Meaning |
|---|---|---|
#include | Preprocessor | Pulls in a library header |
#define | Preprocessor | Text substitution before compiling |
int / char / float / double | Types | Fundamental data types; know the sizes |
sizeof | Operator | Size in bytes at compile time |
printf() / scanf() | I/O | Formatted output / input; scanf needs & for scalars |
if / else | Control | Selection on a condition |
switch | Control | Jump to a matching integer or char constant; needs break |
while / do-while / for | Control | Pre-test / post-test / counted loops |
break / continue | Control | Exit a loop or switch / skip to the next iteration |
return | Function | Exit a function, optionally with a value |
void | Type | No value returned, or no parameters |
static | Storage | Persists between calls (local); file-only visibility (global) |
extern | Storage | Declares a variable defined in another file |
typedef | Type alias | typedef int BOOL; |
struct | Aggregate | Groups variables of different types: C's substitute for objects |
&var / *ptr | Pointer | Address-of / dereference |
NULL | Pointer | Points to nothing |
| Specifier | Type | Example output |
|---|---|---|
%d / %i | signed int | printf("%d", -42) → -42 |
%u | unsigned int | 42 |
%f | float/double, fixed | 3.140000 |
%e / %E | scientific | 3.140000e+00 |
%g | auto | 3.14 |
%c | char | A |
%s | string | Hi |
%ld | long int | 1000000 |
%lf | double (scanf) | scanf("%lf", &d) |
%o / %x | octal / hex | 10 / ff |
%% | literal percent | 100% |
= vs ==: if (x = 0) assigns; write if (0 == x) so typos fail to compile.else binds to the nearest unmatched if.break in a switch causes fall-through.int a[5]; a[5] = 10; is invalid (0–4) and C gives no error.strcpy buffer overflow: the destination must hold the source plus \0.strncpy may omit the null terminator; set dst[n-1] = '\0'.== compares addresses; use strcmp.sizeof on an array parameter gives the pointer size.5/2 = 2; cast one operand to double.int *p; *p = 5; writes to a garbage address.long is 4 bytes on 32-bit and 8 on 64-bit.strcmp returns 0 for equal, not 1; use !strcmp(s1, s2).str1 = str2 is illegal.static local persists between calls; static global limits visibility to one file.return 0 from main signals success to the OS; non-zero signals an error.