ยง16.3โ€“16.7What About The Stack? โ€ฆ Summary

Part I OSTEP pp. 159โ€“163 ยท ~8 min read

  • protection bits
  • coarse-grained segmentation
  • fine-grained segmentation
  • segment table
  • external fragmentation
  • compaction

Three loose ends, and each one teaches something: the stack grows the wrong way, sharing wants one more bit of hardware, and variable-sized segments leave physical memory full of holes.

16.3 What About The Stack?

In Figure 16.2 the stack lives at physical 26โ€“28KB โ€” but it grows backwards: it โ€œstartsโ€ at 28KB and grows toward lower addresses, backing virtual 16KB down to 14KB. Translation must proceed differently, and the hardware needs to know which way each segment grows โ€” one extra bit (Figure 16.4: 1 = positive, 0 = backwards).

For a backwards segment, the hardware computes a negative offset: the raw offset minus the maximum segment size. Virtual 15KB (binary 11 1100 0000 0000): selector 11 โ†’ stack; raw offset 3KB; 3KB โˆ’ 4KB = โˆ’1KB; physical = 28KB + (โˆ’1KB) = 27KB. The bounds check becomes |negative offset| โ‰ค current size (1KB โ‰ค 2KB โœ“). Run it โ€” then find the stackโ€™s limits:

Figures 16.4 + 16.5: segment registers with negative-growth support and protection โ€” the stack presets show the backwards arithmetic

virtual address

15 KB (15360)

11110000000000

โ†’

MMU (segmentation)

1. top 2 bits = 11 โ†’ Stack

2. offset = VA & 0xFFF = 3072; grows โ†“: 3072 โˆ’ 4 KB (4096) = -1024

3. bounds:ย |-1024| โ‰ค 2048 โœ“

4. PA = 28 KB (28672) + -1024 = 27648

โ†’

physical address

27 KB (27648)

SegmentselectorBaseSizeGrows positive?Protection
Code0032 KB (32768)2 KB (2048)1Read-Execute
Heap0134 KB (34816)3 KB (3072)1Read-Write
Stack โ—€1128 KB (28672)2 KB (2048)0 (backwards)Read-Write

The tour: 15360 (15KB) โ†’ offset 3KB, minus the 4KB max = โˆ’1KB, so 28KB โˆ’ 1KB = 27KB. 14336 (14KB) โ†’ โˆ’2KB โ€” the deepest legal byte (|โˆ’2KB| = the stackโ€™s whole 2KB size) โ†’ 26KB. 16000 โ†’ โˆ’384 โ†’ 28288. 13312 (13KB) โ†’ |โˆ’3KB| > 2KB โ†’ fault: below the stackโ€™s reach. 4200 โ†’ the heap, for contrast: positive segments just add.

(Pedantic footnote, straight from the book: the stackโ€™s โ€œstartโ€ at 28KB is really the byte just below the backwards region โ€” the first valid byte is 28KBโˆ’1. Saying it this way keeps the arithmetic clean: physical = base + negative offset, always.)

16.4 Support for Sharing

Segment registers made another economy possible: sharing โ€” code sharing especially, common then and still in use today. The extra hardware is protection bits per segment: may this program read, write, or execute these bytes? Set a code segment read-execute (Figure 16.5 โ€” the Protection column above), and the same physical segment can be mapped into multiple address spaces: each process still believes the memory is private, while the OS secretly shares one immutable copy. The illusion holds precisely because no one can write it.

The hardware check grows accordingly: besides in bounds?, now also permissible? Writing a read-only segment, or executing a non-executable one, raises an exception and hands the offender to the OS.

16.5 Fine-grained vs. Coarse-grained Segmentation

Three big segments (code, heap, stack) is coarse-grained โ€” the space is chopped into a few large pieces. Some early systems (Multics; the Burroughs B5000 with support for thousands of segments) went fine-grained : compilers chopped code and data into many small segments, and the hardware kept a segment table in memory to hold all those base/bounds pairs. The thinking: the more precisely the OS could see which pieces were in use, the better it could pack main memory.

16.6 OS Support

Segmentation hands the OS three new duties:

Context switches. The segment registers are per-process state now โ€” saved and restored with everything else. Nothing surprising, but easy to forget: run process B with Aโ€™s segment registers and every access lands in Aโ€™s memory.

Segment growth. A malloc() that outgrows the heap turns into a negotiation with the OS:

Growing a segment: what a malloc() that outgrows the heap sets in motion
Program(user mode)
malloc library(user mode)
OS(kernel mode)
malloc() needs more heap
step 1 / 8 ยท time flows downward

Program: call malloc(size) for a new object

And the big one: managing free space. Every address space used to be the same size โ€” physical memory was a row of identical slots. Now each process brings several segments, each a different size, and physical memory quickly becomes full of little holes:

Figure 16.6 (left): Non-compacted โ€” 24KB free, in three useless pieces
Operating System0KB(not in use) โ€” 8KB โ“˜16KBAllocated โ“˜24KB(not in use) โ€” 8KB โ“˜32KBAllocated40KB(not in use) โ€” 8KB โ“˜48KBAllocated56KB64KB

click a region marked โ“˜ for details

Figure 16.6 (right): Compacted โ€” same tenants, one big extent
Operating System0KBAllocated (slid together) โ“˜16KB(not in use) โ€” 24KB, contiguous โ“˜40KB64KB

click a region marked โ“˜ for details

That failure โ€” a 20KB request refused while 24KB sits free โ€” is external fragmentation , the sibling of chapter 15โ€™s internal fragmentation : that one wasted space inside an allocation; this one strands space between them. Compaction (right map) cures it by stopping processes, copying their segments together, and re-pointing their registers โ€” but itโ€™s expensive, and ironically it packs memory so tightly that growing a segment now forces yet more rearranging. The gentler path: a smarter free list algorithm that tries to keep large extents available โ€” best-fit, worst-fit, first-fit, the buddy algorithm, and literally hundreds more. Theyโ€™re the next chapterโ€™s whole subject; no matter how smart, external fragmentation still exists. A good algorithm merely minimizes it.

Tip: If 1000 Solutions Exist, No Great One Does

That so many algorithms try to minimize external fragmentation reveals a stronger truth: there is no one best way โ€” we settle for something reasonable and hope itโ€™s good enough. The only real solution is to avoid the problem altogether by never allocating memory in variable-sized chunks. Hold that thought for two chapters.

16.7 Summary

Segmentation earns its keep: sparse address spaces become affordable (the unused middle costs nothing), the arithmetic is fast and hardware-friendly, and read-only segments give us code sharing for free. But two problems stick. External fragmentation is fundamental to variable-sized allocation โ€” manageable, never solvable. And segmentation still isnโ€™t flexible enough: a large but sparsely-used heap in one segment must still be entirely resident. When the model of how memory is used doesnโ€™t match the segments, segmentation stops helping. We need new solutions. Ready to find them?

Homework: segmentation.py

Translate addresses by hand in a tiny two-segment space, find the highest/lowest legal virtual addresses, then reverse-engineer: pick base/bounds values that make a given address stream produce valid, valid, violation, โ€ฆ, violation, valid, valid โ€” and configure the simulator so ~90% of random addresses are valid. Get it at ostep-homework.

Check yourself

1.Virtual address 15KB (0x3C00) targets the backwards-growing stack (base 28KB, size 2KB, max segment 4KB). Walk the translation.

2.How do protection bits let two processes share one physical code segment without breaking isolation?

3.A new process needs a 20KB segment. Physical memory has 24KB free โ€” 8KB + 8KB + 8KB, in three separate holes. What happens, and what is this problem called?

4.Compaction would cure those three holes. Why isn't it the happy ending?

5.A process calls malloc(), and the heap has no room. Trace what happens under segmentation.

5 questions