---
title: "Аппаратная трансляция адресов: base-and-bounds"
description: "В главе про адресное пространство мы договорились о важной вещи: каждый процесс думает, что владеет памятью единолично. Его код лежит «с нуля», стек — где-то наверху, куча …"
canonical: https://graphlms.ru/os/book/address-translation
course: "Операционные системы"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Аппаратная трансляция адресов: base-and-bounds

> Источник: https://graphlms.ru/os/book/address-translation — курс «Операционные системы», GraphLMS.

## Иллюзию надо чем-то поддерживать

В главе про [адресное пространство](/os/book/address-space) мы договорились о важной
вещи: каждый процесс думает, что владеет памятью единолично. Его код лежит «с нуля»,
стек — где-то наверху, куча — посередине, и адреса у всех процессов как будто
одинаковые. Это **иллюзия приватной памяти**.

Но физическая память (та самая планка RAM в сервере) — одна на всех. На ней
одновременно живут десятки процессов: ваш Go-сервис, БД, sidecar, агент мониторинга.
Их данные лежат вперемешку в реальных ячейках. Возникает вопрос:

> Процесс обращается к **виртуальному** адресу (что он «видит»), а данные физически
> лежат по **другому**, физическому адресу. Кто и как переводит одно в другое — да
> так быстро, чтобы это происходило на **каждое** обращение к памяти и не тормозило?

Этот перевод и называется **трансляцией адресов** (address translation). А занимается
им специальный кусок железа внутри процессора — **MMU** (Memory Management Unit,
блок управления памятью). В этой главе разберём самый простой механизм трансляции —
**base-and-bounds** (он же **динамическая релокация**). Он наивный, но именно на нём
проще всего понять разделение труда «железо vs ОС», которое тянется через все
следующие главы — [сегментацию](/os/book/segmentation) и [страницы](/os/book/paging).

## Три слова

Они будут повсюду в следующих главах.

- **Виртуальный адрес (VA, virtual address)** — адрес, который видит и использует
  программа. Когда в Go вы берёте `&x` или процессор читает следующую инструкцию —
  это всё виртуальные адреса. Процесс уверен, что они «настоящие».
- **Физический адрес (PA, physical address)** — реальный номер ячейки в RAM, куда
  в итоге пойдёт чтение или запись.
- **MMU** — железо в CPU, которое на лету превращает VA в PA по правилам, которые
  ему задала ОС.

```text
   Что видит программа              Что есть на самом деле
   (виртуальный мир)                (физический мир, RAM)

   адрес 0    ┌─────────┐                ┌─────────┐ 0 KB
              │  код    │                │  ОС     │
   адрес 100  │  данные │     MMU        │ (ядро)  │
              │  ...    │  ──────────▶   ├─────────┤
              │  куча   │   перевод      │ процесс │  ← реально тут
              │  ...    │   VA → PA      │  лежит  │
              │  стек   │                ├─────────┤
   адрес MAX  └─────────┘                │ другой  │
                                         │ процесс │
                                         └─────────┘
```

Главная мысль картинки: программа живёт в «удобном» виртуальном мире от нуля, а MMU
незаметно подменяет адреса так, чтобы они попадали в нужное место настоящей RAM.

## Идея base-and-bounds

Представьте тетрадь (RAM), страницы которой вы сдаёте в аренду. Пришёл процесс, вы
говорите: «ваш кусок начинается со страницы 320». Процесс продолжает нумеровать
свои страницы с нуля: 0, 1, 2... Чтобы найти его «страницу 5» в тетради, достаточно
прибавить: 320 + 5 = страница 325.

Вот и весь фокус. Заводим **два регистра** (две специальные ячейки прямо в CPU):

- **base (база)** — с какого физического адреса начинается память этого процесса.
- **bounds (граница, она же limit)** — насколько большой кусок ему выделен (размер).

Формула трансляции — одно сложение:

```text
   физический адрес = виртуальный адрес + base
```

Слово «динамическая» в названии — потому что перевод происходит **во время
выполнения** (runtime), на каждом доступе, а не один раз при загрузке программы.
Можно даже переместить процесс в другое место RAM на ходу: поменял base — и все его
адреса автоматически указывают на новое место. Программа об этом и не узнает.

## Разбор одного доступа по шагам

Пусть ОС загрузила процесс в RAM начиная с адреса `32 KB`. Значит:

```text
   base   = 32768   (это 32 KB)
   bounds = 16384   (процессу выделено 16 KB)
```

Процесс выполняет инструкцию, которая хочет прочитать виртуальный адрес `100`
(например, обращение к переменной). Смотрим, что делает MMU:

```text
   VA = 100
        │
        ▼
   ┌───────────────────────────────────────────┐
   │  1) проверка границы:  VA < bounds ?        │
   │        100 < 16384  →  ДА, всё ок           │
   │                                             │
   │  2) трансляция:        PA = VA + base       │
   │        PA = 100 + 32768 = 32868             │
   └───────────────────────────────────────────┘
        │
        ▼
   PA = 32868  →  идём в RAM по этому адресу
```

Программа думала, что читает ячейку 100. На самом деле прочиталась ячейка 32868. И
это нормально: внутри своего куска у процесса смещения те же самые (0, 100, 200...),
просто весь кусок целиком сдвинут на base.

Важно: эти два действия (проверка + сложение) MMU делает на **каждое** обращение к
памяти — а их миллиарды в секунду. Поэтому механизм обязан быть тупым и быстрым.
Сложение и сравнение — это как раз то, что железо умеет делать за доли наносекунды.

## Зачем нужен bounds: защита

Вторая половина названия — **bounds** — это не про адресацию, а про **безопасность**.
Без неё процесс мог бы выйти за пределы своего куска и полезть в чужую память (в БД,
в ядро ОС, в соседний контейнер). Поэтому перед каждым сложением MMU проверяет: а
влезает ли виртуальный адрес в выделенный размер?

```text
   Процесс с base=32KB, bounds=16KB

   VA = 100        → 100 < 16384      → OK   → PA = 32868
   VA = 16383      → 16383 < 16384    → OK   → PA = 49151  (последний свой байт)
   VA = 20000      → 20000 < 16384    → НЕТ! → ПРЕРЫВАНИЕ
```

Если адрес вылез за границу, MMU не делает трансляцию. Вместо этого он генерирует
**аппаратное прерывание** (exception, исключение) и передаёт управление ОС. ОС видит:
«процесс полез не туда» — и обычно убивает его, послав сигнал `SIGSEGV`. Снаружи это
выглядит как **segmentation fault** (segfault, «ошибка сегментации»).

```text
   Процесс лезет за bounds
   ──────────────────────────────────────────────
   CPU: VA = 20000
        │
        ▼  MMU: 20000 >= bounds → НАРУШЕНИЕ!
        │
        ▼  железо генерит exception, прыгает в ОС
        │
   ОС:  «нарушитель границ» → kill(SIGSEGV)
        │
        ▼
   Процесс падает:  "Segmentation fault (core dumped)"
```

Паника Go `invalid memory address or nil pointer dereference` устроена так же:
железо поймало недопустимое обращение, ядро ОС послало `SIGSEGV`, рантайм Go
перехватил сигнал и превратил его в панику. В современных системах недопустимость
адреса около нуля определяет не регистр bounds, а страничная трансляция (страница 0
просто не отображена, см. [страничная организация](/os/book/paging)), но разделение
труда то же: проверяет железо, решает ОС.

## Кто что делает: ОС против железа

Ключ к пониманию всей темы — чёткое разделение ролей. Есть вещи, которые делаются
**редко** (при создании процесса, при переключении) — их берёт на себя медленная,
но умная **ОС** (это программа). А есть то, что делается **на каждый доступ** — это
отдаёт быстрому, но тупому **железу** (MMU). Связку «железо делает рутину, ОС
настраивает правила и разбирает исключения» мы подробно ввели в главе
[прямое исполнение с ограничениями](/os/book/limited-direct-execution), и здесь она
продолжается.

| Когда | Кто | Что делает |
|-------|-----|------------|
| Старт системы | ОС | настраивает обработчики прерываний (что делать при выходе за bounds) |
| Создание процесса | ОС | находит в RAM свободный кусок нужного размера, запоминает где |
| Запуск процесса | железо | переключается в режим пользователя, передаёт управление коду |
| **Каждый доступ к памяти** | **железо (MMU)** | проверяет `VA < bounds`, считает `PA = VA + base` |
| Переключение контекста | ОС | сохраняет старые base/bounds, ставит новые из PCB следующего процесса |
| Выход за границу | железо → ОС | железо ловит, ОС убивает процесс (SIGSEGV) |
| Завершение процесса | ОС | освобождает кусок RAM обратно в пул свободного |

Регистры base и bounds процесс сам поменять не может: запись в них —
привилегированная инструкция, доступная только в режиме ядра. Иначе любой процесс
поставил бы себе base = 0 и bounds = вся память, и изоляция бы исчезла.

Обратите внимание на строку про переключение контекста. Регистры base/bounds — это
часть **состояния процесса**. Они хранятся в его управляющем блоке (PCB,
[process control block](/os/book/process)). Когда планировщик переключается с
процесса A на процесс B, ОС перезаписывает base/bounds значениями B. После этого
ровно те же виртуальные адреса начинают указывать на кусок памяти B.

## Два процесса в одной RAM

Покажем главное преимущество механизма: оба процесса написаны так, будто живут с
нуля, а в реальности лежат в разных местах. Их виртуальные адресные пространства
одинаковы, а base — разный.

```text
   Виртуальный мир A        ФИЗИЧЕСКАЯ RAM            Виртуальный мир B
   (думает: с 0)                                      (думает: с 0)

   0    ┌────────┐          0  ┌──────────┐
        │ код A  │             │   ОС     │
        │ куча A │         16K ├──────────┤
        │ стек A │             │ процесс B│ ◀── base_B = 16K   0 ┌────────┐
   16K  └────────┘             │ (16K)    │                     │ код B  │
                          32K  ├──────────┤                     │ куча B │
                               │ процесс A│ ◀── base_A = 32K     │ стек B │
                               │ (16K)    │                 16K  └────────┘
                          48K  ├──────────┤
                               │ свободно │
                               └──────────┘

   A: VA 100 → PA 100 + 32K = 32868
   B: VA 100 → PA 100 + 16K = 16484
```

Один и тот же виртуальный адрес `100` у A превращается в `32868`, а у B — в `16484`.
Они физически не пересекаются, и bounds гарантирует, что один не залезет к другому.
Это и есть аппаратная основа изоляции процессов: дёшево, на одном сложении.

## Почему это быстро, но негибко

Плюс очевиден: трансляция — это **одно сложение и одно сравнение**. Дешевле некуда,
железо справляется без задержки на каждый доступ.

А теперь минус, ради которого вообще придумали следующие главы. Base-and-bounds
выделяет процессу память **одним сплошным куском** фиксированного размера (bounds).
Но реальное адресное пространство процесса устроено иначе: код и куча растут снизу
вверх, стек — сверху вниз, а **между ними — огромная пустота** (см.
[адресное пространство](/os/book/address-space)). Эту пустоту процесс не использует.

```text
   Виртуальное пространство A           Что лежит в RAM (base..base+bounds)

   0    ┌──────────┐ код             ┌──────────┐
        │ код+куча │ (используется)  │ код+куча │  ← реально нужно
        ├──────────┤                 ├──────────┤
        │          │                 │          │
        │  ПУСТОТА  │  ← её нет в    │  ПУСТОТА  │  ← а место в RAM
        │ (не юзаем)│    программе,   │ (занята  │     ЗАНЯТО ВПУСТУЮ
        │          │    но место под │  зря!)   │
        ├──────────┤    неё выделено ├──────────┤
        │  стек    │ (используется)  │  стек    │  ← реально нужно
   16K  └──────────┘                 └──────────┘
```

Поскольку весь диапазон от base до base+bounds резервируется целиком, эта дыра между
кучей и стеком физически занимает RAM, хотя там ничего нет. Это называется
**внутренняя фрагментация** (internal fragmentation) — память выделена процессу,
но простаивает внутри его куска. На каждый процесс — мегабайты впустую. На сервере с
тысячами процессов это гигабайты RAM, занятые пустотой.

Есть и второй минус: чтобы переместить или подгрузить процесс, нужен **непрерывный**
свободный блок нужного размера. А память со временем дробится на мелкие свободные
кусочки (об этом — глава [управление свободной памятью](/os/book/free-space)), и
большой непрерывный кусок может просто не найтись.

## Куда это ведёт

Корень проблемы — «вся память процесса одним куском». Напрашивается идея: а что если
выдавать память **не одним куском, а несколькими** — отдельно под код, отдельно под
кучу, отдельно под стек? Тогда пустоту между ними не придётся резервировать. Это и
есть [**сегментация**](/os/book/segmentation): по сути несколько пар base/bounds,
по одной на каждый «сегмент». Она лечит главную боль этой главы.

А когда и сегменты окажутся слишком крупными и неудобными, придёт идея резать память
на одинаковые мелкие кусочки фиксированного размера — это
[**страничная организация**](/os/book/paging). Но и там, и там фундамент тот же,
что мы разобрали здесь: **железо переводит виртуальный адрес в физический по
правилам, которые задаёт ОС**. Меняются только сами правила и их сложность.

> Интерактивный симулятор «base-and-bounds»: Двигайте виртуальный адрес ползунком. Железо проверяет его по bound и прибавляет base. Вышли за границу: segfault. Запустить: https://graphlms.ru/os/book/address-translation

> Квиз по разделу: см. вопросы в конце документа или на https://graphlms.ru/os/book/address-translation

## Частые ошибки

- Сравнивать с bounds физический адрес. Проверяется **виртуальный**: `VA < bounds`.
  Иначе процесс с base = 32 KB вообще не смог бы обратиться к своему первому байту.
- Писать условие как `VA <= bounds`. Последний допустимый адрес — `bounds − 1`,
  потому что адреса считаются с нуля.
- Считать, что ОС участвует в каждом обращении к памяти. ОС вступает только при
  нарушении или при переключении контекста.

## Проверьте себя

1. base = 49152 (48 KB), bounds = 4096. Переведите виртуальные адреса 0, 4095 и
   4096.
2. ОС решила переместить процесс из примера выше по адресу 16 KB. Что ей нужно
   сделать?
3. Процесс занимает 4 KB кода и кучи, 2 KB стека, а между ними 10 KB пустоты.
   Сколько физической памяти займёт он при base-and-bounds?

Ответы. (1) 0 → 49152; 4095 → 53247; 4096 — нарушение, `4096 < 4096` ложно.
(2) Остановить процесс, скопировать его 4 KB по новому адресу и записать в его PCB
base = 16384; при следующем запуске в регистр попадёт новое значение. Программа
ничего не заметит. (3) Все 16 KB: пустота резервируется вместе с остальным.

## Что спрашивают на собесе

- **В чём идея base-and-bounds и какая формула трансляции?** Ждут: к виртуальному
  адресу прибавляется base (`PA = VA + base`), а bounds задаёт размер куска и
  проверяется на каждом доступе для защиты.
- **Кто делает трансляцию — ОС или процессор?** Сам перевод и проверку границ — MMU
  (железо), на каждое обращение к памяти. ОС лишь задаёт значения base/bounds и
  обрабатывает нарушения. Если сказать, что трансляцией на каждый доступ занимается
  ОС — это красный флаг (было бы дико медленно).
- **Что происходит при выходе за bounds?** Железо генерирует exception, управление
  уходит в ОС, та обычно убивает процесс сигналом SIGSEGV (segmentation fault).
- **Что такое внутренняя фрагментация и почему base-and-bounds ей страдает?** Память
  выделяется одним непрерывным куском под всё адресное пространство, включая пустоту
  между кучей и стеком — она занимает RAM, хотя не используется.
- **Зачем base/bounds сохраняются при переключении контекста?** Это часть состояния
  процесса (лежит в PCB). У каждого процесса свой base, поэтому одинаковые
  виртуальные адреса разных процессов ведут в разные физические места — основа
  изоляции.
- **Чем сегментация лучше base-and-bounds?** Даёт несколько пар base/bounds (на код,
  кучу, стек), не резервируя пустоту между ними, и убирает основную внутреннюю
  фрагментацию.

## Вопросы для самопроверки

**Квиз «Трансляция адресов: base-and-bounds»**

1. Процесс имеет base = 32 KB (32768) и bounds = 16 KB (16384). По какому физическому адресу пойдёт обращение к виртуальному адресу 200?
   - Ответ: 32968
   - Пояснение: Формула трансляции: PA = VA + base. Сначала проверка границы: 200 < 16384 — ок. Затем 200 + 32768 = 32968. Виртуальный адрес 200 физически попадает в ячейку 32968.
2. Кто и когда выполняет проверку «VA < bounds» и сложение «VA + base»?
   - ОС один раз при загрузке программы
   - ОС на каждое обращение к памяти
   - Железо (MMU) на каждое обращение к памяти (верно)
   - Компилятор во время сборки программы
   - Пояснение: Это происходит на КАЖДЫЙ доступ к памяти, а их миллиарды в секунду. Поэтому делает тупое и быстрое железо (MMU), а не ОС. ОС лишь задаёт значения base/bounds и разбирает нарушения.
3. Что произойдёт, если процесс с bounds = 16 KB обратится к виртуальному адресу 20000?
   - MMU всё равно посчитает PA = 20000 + base
   - MMU сгенерирует исключение, ОС убьёт процесс (SIGSEGV) (верно)
   - Адрес обнулится и чтение пойдёт с начала куска
   - Процесс автоматически получит больше памяти
   - Пояснение: 20000 >= 16384, то есть адрес вне выделенного куска. MMU не делает трансляцию, а генерирует аппаратное прерывание; управление уходит в ОС, которая обычно посылает SIGSEGV — это и есть segmentation fault.
4. Почему base-and-bounds приводит к внутренней фрагментации?
   - Под процесс резервируется один сплошной кусок, включая неиспользуемую пустоту между кучей и стеком (верно)
   - Каждый доступ к памяти требует двух регистров
   - ОС слишком медленно переключает контекст
   - Память дробится на мелкие свободные кусочки между процессами
   - Пояснение: Вся память процесса выделяется одним непрерывным куском размера bounds. Большая пустота между кучей и стеком тоже попадает в этот кусок и физически занимает RAM, хотя данными не заполнена — это внутренняя фрагментация.
5. Что из перечисленного делает именно ОС (а не железо)?
   - Складывает VA + base на каждом доступе
   - Сохраняет и восстанавливает base/bounds при переключении контекста (верно)
   - Сравнивает VA с bounds на каждом доступе
   - Находит свободный кусок RAM при создании процесса (верно)
   - Убивает процесс при выходе за границу (верно)
   - Пояснение: ОС работает на редких событиях: поиск памяти при создании процесса, смена base/bounds при переключении контекста, реакция на нарушение (kill). А сложение и сравнение на каждом доступе — это работа MMU (железа).
6. Почему у двух процессов один и тот же виртуальный адрес 100 не конфликтует в физической RAM?
   - ОС запрещает им использовать одинаковые адреса
   - Виртуальные адреса на самом деле всегда разные
   - Железо хранит общую таблицу всех адресов сразу
   - У каждого процесса свой base, поэтому VA 100 транслируется в разные PA (верно)
   - Пояснение: База у процессов разная. VA 100 у процесса с base=32K даёт PA 32868, а у процесса с base=16K — PA 16484. Одинаковые виртуальные адреса ведут в разные физические места, а bounds не даёт залезть к соседу — это основа изоляции.
