Segmentation: Why I Needed to Understand It
When I finished learning about paging, something felt off. Sure, slicing memory into 4KB chunks is neat and efficient. But what happens when code and data end up in the same page? Code should be read-only, data should be read-write. Paging doesn't care about that—it just chops memory by physical size.
This actually bugged me while debugging my service. I wondered: "What if a buffer overflow bug overwrites data and accidentally hits the code section?" The thought was terrifying. That's when I dove into segmentation to figure out how operating systems actually protect memory regions.
The Struggle: Physical Chunks vs Logical Meaning
At first, I thought paging was the perfect solution. Fixed-size chunks mean no external fragmentation, and hardware MMUs can translate addresses blazingly fast. But then I realized: paging doesn't give a damn about how your program is actually structured.
Take a program I wrote recently:
// Code Segment (instruction area)
int add(int a, int b) {
return a + b;
}
// Data Segment (global variables)
int global_counter = 0;
int main() {
// Stack Segment (function call stack)
int local_var = 10;
global_counter = add(local_var, 5);
return 0;
}
There are clearly three logical regions here:
- Code Segment: Instructions like
add()andmain() - Data Segment: Global variables like
global_counter - Stack Segment: Local variables like
local_var
But paging doesn't recognize these semantic boundaries. It just goes "chop every 4KB!" and calls it a day. So you might get unlucky and have the tail end of your code segment and the start of your data segment crammed into the same page.
Why is this bad? Setting permissions becomes messy. You want code to be read-only, but if data is mixed in the same page, do you mark the page as read-only or read-write? It's a lose-lose.
I spent hours confused about this. "Does this mean paging has weak security?" I kept asking myself.
The 'Aha!' Moment: Just Split by Meaning!
Then it hit me. The answer was stupidly simple: "Split by semantic meaning, not physical size."
Instead of chopping programs into 4KB blocks, you divide them based on how they're actually organized:
- Code Segment: All instructions go here
- Data Segment: All global variables go here
- Stack Segment: All function call stacks go here
- Heap Segment: All dynamic memory allocations go here
This makes permissions ridiculously easy:
Code Segment: Read + Execute (can't write to it)
Data Segment: Read + Write (can't execute it)
Stack Segment: Read + Write (can't execute it)
Heap Segment: Read + Write (can't execute it)
Each segment gets its own permission set, so you can cleanly enforce "any attempt to write to code = instant error."
This analogy clicked for me:
"Paging is slicing a book by thickness (every 4KB). Segmentation is slicing it by chapters."
When you slice by thickness, one chapter might span multiple pieces. But when you slice by chapters, the semantic units stay intact. Chapter 1 is Chapter 1, Chapter 2 is Chapter 2.
Deep Dive: How Segmentation Actually Works
To really understand segmentation, you need to know about the Segment Table.
Segment Table Structure
While paging uses a Page Table, segmentation uses a Segment Table.
| Segment # | Base Address | Limit (Size) | Permissions |
|---|---|---|---|
| 0 (Code) | 0x1000 | 64KB | R-X |
| 1 (Data) | 0x11000 | 32KB | RW- |
| 2 (Stack) | 0x19000 | 16KB | RW- |
| 3 (Heap) | 0x1D000 | 128KB | RW- |
- Base Address: Where this segment lives in physical memory
- Limit: Size of this segment (boundary check)
- Permissions: Read (R) / Write (W) / Execute (X) flags
Address Translation Process
When a program accesses memory, the CPU translates like this:
Logical Address = <Segment #> + <Offset>
For example, to read "byte 100 of Segment 1 (Data)":
Logical Address: <1, 100>
1. Look up Segment 1 in Segment Table
- Base Address: 0x11000
- Limit: 32KB (32,768 bytes)
2. Check if Offset exceeds Limit
- 100 < 32,768 → OK!
- If Offset > Limit? → Segmentation Fault!
3. Calculate Physical Address
- Base + Offset = 0x11000 + 100 = 0x11064
Here's a code simulation of this:
#include <stdio.h>
#include <stdbool.h>
// Segment table entry
typedef struct {
unsigned int base;
unsigned int limit;
char permissions[4]; // "RWX"
} SegmentEntry;
// Segment table (4 segments for simplicity)
SegmentEntry segment_table[4] = {
{0x1000, 65536, "R-X"}, // Code
{0x11000, 32768, "RW-"}, // Data
{0x19000, 16384, "RW-"}, // Stack
{0x1D000, 131072, "RW-"} // Heap
};
// Translate logical address to physical address
bool translate_address(int segment_num, int offset, unsigned int* physical_addr) {
if (segment_num < 0 || segment_num >= 4) {
printf("Error: Invalid segment number %d\n", segment_num);
return false;
}
SegmentEntry* seg = &segment_table[segment_num];
// Limit check (if offset exceeds boundary → Segmentation Fault)
if (offset < 0 || offset >= seg->limit) {
printf("Segmentation Fault! Offset %d exceeds limit %d in segment %d\n",
offset, seg->limit, segment_num);
return false;
}
// Calculate physical address
*physical_addr = seg->base + offset;
printf("Logical Address <Segment %d, Offset %d> → Physical Address 0x%X\n",
segment_num, offset, *physical_addr);
return true;
}
int main() {
unsigned int phys_addr;
// Valid access: byte 100 of Data Segment
translate_address(1, 100, &phys_addr);
// Segmentation Fault: access beyond Data Segment boundary
translate_address(1, 50000, &phys_addr);
return 0;
}
Output:
Logical Address <Segment 1, Offset 100> → Physical Address 0x11064
Segmentation Fault! Offset 50000 exceeds limit 32768 in segment 1
The key here is the Limit check. If the offset exceeds the limit, you get an error. That's segmentation's security mechanism in action.
Why "Segmentation Fault" Happens
You've probably seen "Segmentation Fault (core dumped)" when writing C code. Now you know why it happens.
#include <stdio.h>
int main() {
int arr[10];
// Access beyond array bounds (goes past Stack Segment boundary)
for (int i = 0; i < 1000000; i++) {
arr[i] = i; // Eventually: Segmentation Fault!
}
return 0;
}
This code tries to write a million values into a 10-element array. At first, it luckily overwrites other parts of the stack, but eventually it crosses the Stack Segment's limit, and the CPU screams "Segmentation Fault!" and kills the program.
That's segmentation's protection in action. If a program tries to escape its segment boundaries, it gets shut down immediately.
The Fatal Flaw: External Fragmentation Returns
"Wait, so segmentation is way better than paging, right?"
I thought so too at first. But there's a huge downside: external fragmentation comes roaring back.
The biggest characteristic of segmentation is that segments have variable sizes.
- Code Segment: 100KB
- Data Segment: 200KB
- Stack Segment: 50KB
- Heap Segment: 300KB
Paging fixes everything at 4KB, so any 4KB hole can fit any page. But segmentation needs holes that are exactly the right size.
This analogy made it click:
"Segmentation is like organizing a warehouse where every item has a different size, so it's hard to find a hole that perfectly fits each item."
Imagine memory looks like this:
|--- Process A (100KB) ---|--- Free (150KB) ---|--- Process B (200KB) ---|
If Process A terminates:
|--- Free (100KB) ---|--- Free (150KB) ---|--- Process B (200KB) ---|
Now you have 250KB of free space total. But it's split into two chunks, so you can't fit a 200KB segment!
It won't fit in the 150KB hole, and it won't fit in the 100KB hole. You have enough space, but you can't use it. That's external fragmentation.
I ran into a similar issue at work once. Our server had 30% free memory, but we got "Out of Memory" errors. Turns out the memory was so fragmented that there was no contiguous space large enough for the allocation we needed.
Solving External Fragmentation: Compaction
The solution is compaction—pushing all segments to one side so free space consolidates.
Before Compaction:
|--- Process A ---|--- Free ---|--- Process B ---|--- Free ---|
After Compaction:
|--- Process A ---|--- Process B ---|------------- Free -------------|
But this is insanely expensive. You have to copy entire segments around in memory, and you have to pause programs while doing it.
It's like defragmenting a hard drive. I remember back in the Windows XP days, running defrag would lock up my computer for hours.
Segmentation vs Paging: Pros and Cons
Comparing the two made their tradeoffs crystal clear.
| Feature | Paging | Segmentation |
|---|---|---|
| Split Basis | Fixed size (4KB) | Variable size (logical units) |
| External Fragmentation | None | Yes |
| Internal Fragmentation | Yes (end of pages) | Almost none |
| Permission Setup | Hard (physical split) | Easy (logical split) |
| Sharing | Hard | Easy (share segments) |
| Hardware Complexity | Simple (Page Table) | Complex (Segment Table + Limit checks) |
Building this table made me realize: there's no perfect solution. Paging makes memory management easy but security harder. Segmentation makes security easy but memory management harder.
So modern OSes use both.
Paged Segmentation: Having Your Cake and Eating It Too
In the end, modern operating systems decided: "Why not both?" This is called Paged Segmentation.
How It Works
- First, segment: Divide the program into logical units (Code, Data, Stack, Heap).
- Then, page: Chop each segment into 4KB pages.
This way you get segmentation's benefits (security, sharing) and paging's benefits (no external fragmentation).
Program
├─ Code Segment (100KB)
│ ├─ Page 0 (4KB)
│ ├─ Page 1 (4KB)
│ ├─ ...
│ └─ Page 24 (4KB)
│
├─ Data Segment (200KB)
│ ├─ Page 0 (4KB)
│ ├─ ...
│ └─ Page 49 (4KB)
│
└─ Stack Segment (50KB)
├─ Page 0 (4KB)
├─ ...
└─ Page 12 (2KB) ← last page might have internal fragmentation
Address Translation (Two-Level)
Address translation becomes a two-step process.
Logical Address = <Segment #> + <Page #> + <Offset>
For example, to read "Segment 1 (Data), Page 5, Offset 100":
1. Look up Segment 1's Page Table in Segment Table
2. Look up Page 5's physical frame in Page Table
3. Physical Address = Frame # × 4KB + Offset
It's more complex, but you get security (segment permissions) and memory efficiency (page allocation) at the same time.
x86's Segmentation Legacy
Interestingly, x86 CPUs still have segmentation hardware built in. Registers like CS (Code Segment), DS (Data Segment), SS (Stack Segment) are leftovers from this era.
mov ax, 0x1000 ; Set Data Segment address to DS register
mov ds, ax
mov [0x100], bx ; Write to DS:0x100 address
But modern operating systems (Linux, Windows) barely use this feature. Instead, they set all segments to span from 0 to 4GB, effectively neutering segmentation. This is called the Flat Memory Model.
Why don't they use it?
- External fragmentation is too painful
- Paging alone is enough for security (you can add permission bits to Page Table Entries)
- Segmentation was basically deprecated when x86-64 came out
The x86-64 architecture almost completely removed segmentation. Only FS and GS registers remain for special purposes—everything else runs in flat mode.
When I first read assembly code and saw DS, CS registers, I was like "what are these?" Turns out they're fossils from the segmentation era. The hardware is still there, but the software doesn't use it. Weird situation.
Application: How This Matters for My Service
Understanding segmentation changed how I think about memory layout in my programs.
Debugging Segmentation Faults
My service used to crash occasionally with "Segmentation Fault" errors. At first I was like "why is it randomly dying?" But after learning segmentation, tracking down the cause became way easier.
// Example bug
char* ptr = NULL;
strcpy(ptr, "Hello"); // Segmentation Fault!
If ptr is NULL, you're trying to write to address 0x00000000. But this address doesn't belong to any segment, so the Segment Table lookup fails, causing a Segmentation Fault.
Now when I see a Segmentation Fault, I instantly think: "Ah, we tried to access memory outside our segment boundaries."
Docker Container Memory Limits
When you run Docker containers, you can limit memory with the --memory option:
docker run --memory="512m" my-app
I was curious how this worked internally. Turns out it uses Linux Cgroups to limit segment sizes for each process.
It's basically implementing segmentation's Limit concept in software. If the container tries to exceed its allocated memory, the OOM (Out of Memory) Killer forcibly terminates the process.
Code Reviews
Now when I review code, I can better spot memory safety issues.
// Dangerous code
char buffer[10];
scanf("%s", buffer); // Buffer overflow possible!
// Safe code
char buffer[10];
scanf("%9s", buffer); // Read max 9 chars (leave room for null terminator)
Before, I vaguely thought "we should limit size, I guess?" Now I understand concretely: "Going past the buffer can overwrite other parts of the stack segment, and in the worst case, corrupt the code segment."
City District Analogy: Final Summary
If I had to sum up segmentation in one sentence, I'd say: "It's like dividing a city into districts."
- Residential District (Stack Segment): Where people live. Read/write allowed.
- Commercial District (Data Segment): Stores, offices. Read/write allowed.
- Industrial District (Code Segment): Factories, power plants. Read-only (messing with it is dangerous).
- Park (Heap Segment): Flexible space that can grow or shrink as needed.
Each district has a different purpose and different regulations (Permissions). You can't build a factory in the residential district, and you can't build a house in the industrial district. Similarly, segmentation enforces what you can do in each memory region.
If paging is "cutting land into equal-sized plots," then segmentation is "zoning land by purpose." Both are necessary, which is why modern OSes mix them together.
This analogy made everything snap into place for me. At its core, segmentation is about "meaningful division."
Summary
- Segmentation divides memory into logical units (Code, Data, Stack, Heap).
- Pros: Easy security setup (per-segment permissions), easy sharing (share whole segments).
- Cons: Variable segment sizes cause external fragmentation.
- Segmentation Fault happens when you access memory beyond a segment's Limit.
- Modern OSes use Paged Segmentation: segment first, then page each segment.
- x86 Legacy: Segmentation hardware still exists, but modern OSes use Flat Memory Model to effectively disable it.
What I learned: paging and segmentation are complementary. Neither is perfect alone, but together they work beautifully. It's like city planning—you need both district zoning (Segmentation) and lot subdivision (Paging).