# Lecture 5 - Virtualization: CPU to Memory

Source: https://wisconsin.twango.dev/sp26-cs537/lectures/lecture-05

# Lecture 5 - Virtualization: CPU to Memory

**Instructor:** Michael Swift
**Topics:** CPU virtualization recap, memory virtualization, address spaces, static relocation, dynamic relocation, base and bounds, segmentation

> [!abstract]- Learning Outcomes
> - Recap of scheduling policies
> - What is the need for memory virtualization?
> - How to virtualize memory?

---

## Recap: CPU Virtualization

### Scheduling Mechanism

- **Process:** Abstraction to virtualize CPU
- Use **time-sharing** in OS to switch between processes
- **Limited Direct Execution:**
  - Use system calls to access devices etc. from user mode
  - Context-switch using interrupts for multi-tasking

### CPU Summary

- **Mechanism**
  - Process abstraction
  - System call for protection
  - Context switch to time-share
- **Policy**
  - Metrics: turnaround time, response time
  - Balance using MLFQ

---

## Motivation for Memory Virtualization

**Uniprogramming:** One process runs at a time — OS occupies the top of physical memory, user process occupies the rest.

Disadvantages:
- Only one process runs at a time
- Process can destroy OS

---

## Multiprogramming Goals

- **Transparency:** Process is unaware of sharing
- **Protection:** Cannot corrupt OS or other process memory
- **Efficiency:** Do not waste memory or slow down processes
- **Sharing:** Enable sharing between cooperating processes

---

## Address Space Abstraction

The **address space** is the OS abstraction for a process's view of memory. Each process has its own address space laid out in physical memory alongside other processes.

> [!note] Lecture Note
> Multiple processes are placed in different regions of physical memory. The right side of the diagram shows processes C, B, and A interleaved with free regions in a 512KB physical memory layout.

### What is in the Address Space?

- **Static:** Code and some global variables
- **Dynamic:** Stack and heap

| Segment | Contents | Growth Direction |
|---------|----------|-----------------|
| Code (top, 0KB–1KB) | Instructions (the code segment) | Fixed |
| Heap (below code) | `malloc()`'d data, dynamic data structures | Grows downward |
| Stack (bottom, 15KB–16KB) | Local variables, arguments, return values | Grows upward (toward lower addresses) |

> [!note] Lecture Note
> Static variables live in the code/data region at the top of the address space. The heap and stack grow toward each other in the free space between them.

---

## Stack Organization

- Stack grows downward (toward lower addresses)
- A pointer separates allocated and free space
- **Allocate:** Decrement pointer
- **Free:** Increment pointer
- No fragmentation — allocation and deallocation follow LIFO order

### What Goes on the Stack?

```c
main () {
    int A = 0;
    foo(A);
    printf("A: %d\n", A);
}

void foo (int Z) {
    int A = 2;
    Z = 5;
    printf("A: %d Z: %d\n", A, Z);
}
```

> [!note] Lecture Note
> The stack frame for `main` is at the base. When `foo` is called, its frame (containing local `A` and parameter `Z`) is pushed on top. The `printf` arguments are pushed above that. The stack pointer moves as frames are pushed/popped.

---

## Heap Organization

Allocate from any random location: `malloc()`, `new()`, etc.
- Heap memory consists of allocated and free areas (holes)
- Order of allocation and free is unpredictable

> [!note] Lecture Note
> Fragmentation occurs — free space exists between allocated blocks but may not be usable for new allocations because the free chunks are too small or non-contiguous.

---

## Memory Access

```c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  int x;
  x = x + 3;
}
```

Compiled to assembly:

```text
0x10: movl  -0x8(%rbp), %rdi
0x13: addl  $0x3, %rdi
0x19: movl  %rdi, -0x8(%rbp)
```

- `%rbp` is the **base pointer** — points to base of current stack frame
- `%rip` is the **instruction pointer** (program counter)

> [!example] Example
> With initial `%rip = 0x10`, `%rbp = 0x200`:
>
> 1. Fetch instruction at addr `0x10` → Exec: load from addr `0x1f8` (`0x200 - 0x8`)
> 2. Fetch instruction at addr `0x13` → Exec: no memory access (register add)
> 3. Fetch instruction at addr `0x19` → Exec: store to addr `0x1f8`

### Memory Practice

```c
int x;
int main(int argc, char *argv[]) {
    int y;
    int* z = malloc(sizeof(int));
}
```

| Address | Location |
|---------|----------|
| `x` | Static data/code |
| `main` | Static data/code |
| `y` | Stack |
| `z` | Stack |
| `*z` | Heap |

---

## How to Virtualize Memory

**Problem:** How to run multiple processes simultaneously when addresses are "hardcoded" into process binaries?

Possible solutions:
1. **Time sharing**
2. **Static relocation**
3. **Base** (dynamic relocation)
4. **Base+Bounds**
5. **Segmentation**

---

## Approach 1: Time Sharing Memory

Swap entire process memory to/from disk on each context switch.

> [!note] Lecture Note
> This is called **swapping** — writing the entire process state out to disk and loading the next process in. One process uses RAM at a time.

**Problem:** Ridiculously poor performance — copying entire memory to/from disk on every switch is far too slow.

**Better alternative:** Space sharing — at the same time, the space of memory is divided across processes. All remaining solutions use space sharing.

---

## Approach 2: Static Relocation

**Idea:** OS rewrites each program before loading it as a process in memory. Each rewrite for a different process uses different addresses and pointers.

Changes jumps, loads of static data:

```text
Original:               Rewritten for process 1:   Rewritten for process 2:
0x1010: movl 0x8(%rbp), %rdi   0x1010: movl 0x8(%rbp), %rdi   0x3010: movl 0x8(%rbp), %rdi
0x1013: addl $0x3, %rdi        0x1013: addl $0x3, %rdi        0x3013: addl $0x3, %rdi
0x1019: movl %rdi, 0x8(%rbp)   0x1019: movl %rdi, 0x8(%rbp)   0x3019: movl %rdi, 0x8(%rbp)
```

> [!note] Lecture Note
> Only code addresses, globals, and statics are rewritten. Stack-relative addresses (using `%rbp`) don't need rewriting because they are already relative to the base pointer.

### Static Relocation Disadvantages

- **No protection**
  - Process can destroy OS or other processes
  - No privacy
- **Cannot move** address space after it has been placed
  - May not be able to allocate new process

> [!note] Lecture Note
> A process could write `int x = 700; memset(x, 0, 1000)` and overwrite memory belonging to another process. There is no hardware enforcement.

---

## Approach 3: Dynamic Relocation (Base Register)

**Goal:** Protect processes from one another.

Requires hardware support:
- **Memory Management Unit (MMU)**

MMU dynamically changes process address at every memory reference:
- Process generates **logical** (or **virtual**) addresses (in their address space)
- Memory hardware uses **physical** (or **real**) addresses

```mermaid
graph LR
    A[CPU] -->|logical address| B[MMU]
    B -->|physical address| C[Memory]
```

### Virtual Addresses

- Virtual addresses are independent of location in physical memory (RAM) that referenced data lives
  - OS determines location in physical memory
- Instructions issued by CPU reference virtual addresses
  - e.g., pointers, arguments to load/store instruction, PC
- Virtual addresses are translated by hardware into physical addresses (with some help from OS)

### Hardware Support for Dynamic Relocation

**Privileged (protected, kernel) mode:** OS runs
- When enter OS (trap, system calls, interrupts, exceptions)
- Allows certain instructions to be executed (can manipulate contents of MMU)
- Allows OS to access all of physical memory

**User mode:** User processes run
- Perform translation of logical address to physical address

### Base Register Implementation

Translation on every memory access of user process:

> **physical address = logical address + base register**

- MMU checks mode — if kernel mode, address passes through unchanged
- If user mode, MMU adds base register to logical address to form physical address
- Each process has a different value in base register
- Dynamic relocation by changing value of base register

> [!example] Example
> P1 at physical 1KB–2KB (base = 1024), P2 at physical 4KB–5KB (base = 4096):
>
> | | Virtual | Physical |
> |---|---------|----------|
> | P1: load 100, R1 | 100 | load 1124, R1 |
> | P2: load 100, R1 | 100 | load 4196, R1 |
> | P2: load 1000, R1 | 1000 | load 5096, R1 |
> | P1: load 100, R1 | 100 | load 2024, R1 |

### Base Register Protection Problem

With only a base register, a process can still access memory beyond its allocation:

> [!note] Lecture Note
> P1 (base = 1024) could issue `store 3072, R1` → physical address = 3072 + 1024 = 4096, which falls in P2's region. Base register alone does **not** provide protection.

---

## Approach 4: Base+Bounds

**Idea:** Limit the address space with a **bounds register**.

- **Base register:** Smallest physical address (starting location)
- **Bounds register:** Size of this process's virtual address space
  - Sometimes defined as largest physical address (base + size)
- OS kills process if process loads/stores beyond bounds

### Base+Bounds Implementation

Translation on every memory access of user process:

1. MMU checks mode — if kernel mode, address passes through
2. If user mode, MMU compares logical address to bounds register
3. If logical address >= bounds → **generate error** (segfault)
4. If logical address < bounds → **physical address = logical address + base**

> [!example] Example
> P1 (base = 1024, bounds = 1024), P2 (base = 4096, bounds = 1024):
>
> P1 issues `store 3072, R1` → 3072 > 1024 (bounds) → **interrupt OS!** Protection enforced.

### Managing Processes with Base and Bounds

**Context-switch:** Add base and bounds registers to PCB.

Steps:
1. Change to privileged mode
2. Save base and bounds registers of old process
3. Load base and bounds registers of new process
4. Change to user mode and jump to new process

**Protection requirement:**
- User process cannot change base and bounds registers
- User process cannot change to privileged mode

### Base and Bounds Advantages

- Provides protection (both read and write) across address spaces
- Supports dynamic relocation
  - Can place process at different locations initially and also move address spaces
- Simple, inexpensive implementation: few registers, little logic in MMU
- Fast: add and compare in parallel

### Base and Bounds Disadvantages

- Each process must be allocated **contiguously** in physical memory
  - Must allocate memory that may not be used by process (free space between heap and stack is wasted)
- **No partial sharing:** Cannot share parts of address space

> [!note] Lecture Note
> The entire region from code through heap free space through stack must be allocated contiguously, even if the process only uses a small fraction of it.

---

## Approach 5: Segmentation

Divide address space into **logical segments**:
- Each segment corresponds to a logical entity in address space (code, stack, heap)
- Each segment has a **separate base + bounds register**

### Segmented Addressing

Process now specifies segment and offset within segment.

How does a process designate a particular segment?
- Use part of logical address
  - **Top bits** of logical address select segment
  - **Low bits** of logical address select offset within segment
- If small address space, not enough bits:
  - Implicitly by type of memory reference
  - Special registers

### Segmentation Implementation

MMU contains a **Segment Table** (per process):
- Each segment has its own base, bounds, and protection bits

> [!example] Example
> 14-bit logical address, 4 segments → 2 bits for segment, 12 bits for offset (1 hex digit = 4 bits)
>
> | Segment | Base | Bounds | R | W |
> |---------|--------|--------|---|---|
> | 0 | 0x2000 | 0x6ff | 1 | 0 |
> | 1 | 0x0000 | 0x4ff | 1 | 1 |
> | 2 | 0x3000 | 0xfff | 1 | 1 |
> | 3 | 0x0000 | 0x000 | 0 | 0 |

### Segmentation Translation Example

Segment numbers: 0 = code+data, 1 = heap, 2 = stack

> [!example] Example
> Given heap (seg 1) at physical 0x400, stack (seg 2) at physical 0x1600:
>
> | Virtual (hex) | Translation | Physical |
> |---------------|-------------|----------|
> | `load 0x2010, R1` | seg 2, offset 0x010 → 0x1600 + 0x010 | `0x1610` |
> | `load 0x1010, R1` | seg 1, offset 0x010 → 0x400 + 0x010 | `0x0410` |
> | `load 0x1100, R1` | seg 1, offset 0x100 → 0x400 + 0x100 | `0x0500` |

### How Do Stacks Grow?

Stacks grow downward, but the segment offset increases upward. Solution: add a **"grows down" bit** to the segment descriptor.

> [!example] Example
> Stack segment base = 28K, size = 4K (stack goes 16K → 12K virtual, 28K → 24K physical).
>
> Virtual address `0x3C00` = 15K → top 2 bits (0x3) = segment reference, offset = 0xC00 = 3K.
>
> **Negative offset** = subtract segment size from offset: 3K - 4K = -1K from top.
>
> Add to base: 28K - 1K = 27K → physical address `0x6C00`.

### Segmentation in x86

| Register | Purpose |
|----------|---------|
| SS (Stack Segment) | Pointer to the stack |
| CS (Code Segment) | Pointer to the code |
| DS (Data Segment) | Pointer to the data |
| ES (Extra Segment) | Pointer to extra data |
| FS (F Segment) | Pointer to more extra data |
| GS (G Segment) | Pointer to still more extra data |

### Advantages of Segmentation

- Enables **sparse allocation** of address space
- Stack and heap can grow independently
- Heap: if no data on free list, dynamic memory allocator requests more from OS (e.g., UNIX: `malloc` calls `sbrk()`)
- Stack: OS recognizes reference outside legal segment, extends stack implicitly
- **Different protection** for different segments
- Enables **sharing** of selected segments (e.g., read-only code)
- Supports dynamic relocation of each segment

### Disadvantages of Segmentation

- Each segment must be allocated **contiguously** in physical memory
- May not have sufficient physical memory for large segments
- **External fragmentation** — free memory is broken into non-contiguous chunks

---

## Summary

- Operating systems virtualize memory to support multiprogramming
  - Each process gets its own address space
  - Processes are prevented from accessing each other's memory
- Hardware support required for efficient execution

| Approach | Protection | Relocation | Sharing | Fragmentation |
|----------|-----------|------------|---------|---------------|
| Time sharing | N/A | N/A | No | N/A |
| Static relocation | No | No (fixed at load) | No | No |
| Base register | No | Yes | No | Internal |
| Base+Bounds | Yes | Yes | No | Internal |
| Segmentation | Yes (per-segment) | Yes | Partial | External |

---

## Related

- **Navigation:** [[lecture-04|← Lecture 4]] · [[lecture-06|Lecture 6 →]]
- **Exam:** [[exams/midterm-1/index|Midterm 1]]
- **Course:** [[README|CS 537 Map of Content]]
