C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
201: How a C Compiler Works: Lexing, Parsing, Codegen
I remember the first time I realized that the compiler wasn't some magical black box that just "understood" my intent. I had written a particularly nasty line of nested ternary operators, and the error message the compiler spat back was absolutely incomprehensible. It didn't point to the logic error; it complained about a missing semicolon three lines up. That's when I started wondering: How does this thing actually read my code?
Let's pull back the curtain. Instead of me just lecturing you on theory, let's take a specific line of C and pretend we're building a tiny compiler for it right now. Here is our target:
int result = 10 + 5 * 2;
The String Problem
To you and me, that looks like a variable declaration and some math. But to the computer, when the compiler first opens your .c file, it's just a long stream of characters. It sees 'i', then 'n', then 't', then a space, then 'r'... and so on. It has no idea that int is a keyword or that 10 is a number.
If I tried to process this character-by-character, my logic would be a nightmare of if statements. I'd be asking, "Is this '1'? Okay, is the next character also a digit? If so, keep going." That's tedious. I need a way to group these characters into meaningful "words."
Grouping the Chaos
This is where Lexing (or Tokenization) comes in. I'm going to write a mental script that scans the line and bundles characters into "tokens."
As I scan int result = 10 + 5 * 2;, I'll group them like this:
int→ [KEYWORD]result→ [IDENTIFIER]=→ [OPERATOR]10→ [INTEGER_LITERAL]+→ [OPERATOR]5→ [INTEGER_LITERAL]*→ [OPERATOR]2→ [INTEGER_LITERAL];→ [PUNCTUATION]
Now we're getting somewhere. We've moved from a sea of characters to a list of categorized tokens. But wait—we still don't have "meaning." If I accidentally wrote 10 = result int;, the lexer would be perfectly happy. It would just say, "Yep, I see an integer, an operator, an identifier, and a keyword." It doesn't know that the order is illegal.
Building a Mental Tree
To handle the order and the logic, we need Parsing. I can't just read the tokens left-to-right because math doesn't work that way. If I just processed 10 + 5 * 2 from left to right, I'd get 30. But C follows the order of operations, so I need 20.
I'll build a tree (specifically, an Abstract Syntax Tree, or AST). I'll start at the "root" of the expression and work my way down based on precedence. The lowest precedence operator (the one that happens last) becomes the top of the tree.
In our case, the assignment = is the very last thing to happen. So the tree looks like this:
[Assignment (=)]
/ \
[result] [Addition (+)]
/ \
[10] [Multiplication (*)]
/ \
[5] [2]
Now the compiler "understands" the structure. It knows that it must solve the multiplication branch before it can solve the addition branch, and it must solve the addition before it can assign the value to result. This is why a missing semicolon or a misplaced bracket throws the parser into a panic—it can't complete the tree.
The Final Translation
We have a tree, but the CPU can't execute a tree. The CPU wants a linear list of simple instructions. This is Codegen (Code Generation). I'm now going to traverse that tree and translate each node into assembly instructions for the target architecture (like x86 or ARM).
I'll start at the bottom of the tree and work up:
- Multiply:
mov eax, 5; imul eax, 2(Now eax is 10) - Add:
add eax, 10(Now eax is 20) - Assign:
mov [result_address], eax(Store 20 into memory)
And there it is. We went from a raw string of text, to categorized tokens, to a structural tree, and finally to machine-readable instructions. Every time you hit "build" in your IDE, this entire pipeline fires off in a fraction of a second.
📋 Practical Task
Analyzing GCC Assembly Output for a Custom Math Function
Instead of writing a compiler from scratch, you're going to use a real one to see the "Codegen" phase in action. Your goal is to observe how the C compiler translates a specific mathematical expression into assembly.
The Setup:
Create a file named calc.c with the following function:
int calculate() {
return (10 + 2) * 3;
}
The Task:
Run the following command in your terminal to tell GCC to stop after the compilation phase and output the assembly code instead of a binary:
gcc -S -O0 calc.c
(The -S flag triggers assembly output; -O0 disables optimizations so the assembly remains easy to read and follows your code literally.)
The Analysis:
Open the newly created calc.s file. Find the calculate: label and identify the following three things:
- Locate the instruction that handles the addition (
10 + 2). - Locate the instruction that handles the multiplication (
* 3). - Verify that the addition happens before the multiplication in the assembly sequence.
Submission: Copy the relevant 3-5 lines of assembly code from calc.s that perform these operations and explain which line corresponds to which part of your C expression.
There are no comments for now.