Почему программы и данные можно размещать в одной и той же памяти
В предыдущей лекции мы увидели полную картину архитектуры фон Неймана — пять основных компонентов и три шины.
В этой лекции мы сосредоточимся на её самой революционной идее:рассматривать команды программы как особый вид данных и хранить их вместе с данными в одной и той же памяти。
Бытовая аналогия: ноты и музыкальные плееры
Представьте два разных музыкальных устройства:
Устройство A: Музыкальная шкатулка
Музыкальная шкатулка с пружинным приводом содержит металлический цилиндр, покрытый крошечными выступами. Каждый выступ щиплет стальной зуб гребёнки, извлекая ноту.
Хотите сменить мелодию? Придётся разобрать музыкальную шкатулку и заменить цилиндр на другой, выступы которого расположены иначе. Сам цилиндр — это физическое слияние «программы» и «исполнителя».
Устройство B: Музыкальное приложение на смартфоне
Ваш телефон хранит сотни MP3-файлов. Коснитесь одного — и он заиграет.
MP3-файл (программа/данные) и приложение-плеер (исполнитель) разделены — чтобы сменить песню, достаточно заменить файл, а не телефон.
Музыкальная шкатулка — это компьютер до фон Неймана: программа и аппаратное обеспечение связаны воедино, и смена задачи означает смену машины.
Смартфон — это пост-фон-неймановский компьютер: программы — это заменяемое содержимое в памяти, а аппаратное обеспечение — универсальная платформа для выполнения.
| Критерий сравнения | Музыкальная шкатулка (внешнее программное управление) | Смартфон (хранимая программа) |
|---|---|---|
| Подход на основе хранимой программы | Зафиксировано в механической структуре | Хранится в памяти в виде данных |
| Сменить трек | Разобрать и заменить барабан | Просто коснуться экрана |
| Универсально ли аппаратное обеспечение? | Одна музыкальная шкатулка = одна мелодия | Один телефон = неограниченное количество мелодий |
| Гибкость | Почти ноль | чрезвычайно высокий |
То, что сделал фон Нейман, было именно этим: он извлёк «партитуру» из аппаратного обеспечения и превратил её в «файл данных», который можно было свободно заменять. С тех пор аппаратное обеспечение оставалось аппаратным обеспечением, а программное — программным.
Исторический контекст: от «внешнего управления программой» к «хранимой программе»
2.1 Компьютеры до фон Неймана
В начале 1940-х годов некоторые электронные компьютеры уже использовались, но у всех них была одна общая проблема:
ENIAC (1946)—Первый в мире универсальный электронный компьютер, поразительно быстрый, но всякий раз, когда вычислительная задача менялась, инженерам приходилось тратить несколько днейна ручное подключение и отключение сотен кабелей и установку тысяч переключателейЧтобы «перепрограммировать».
Harvard Mark I (1944)—Программы вводились с помощью перфорированной бумажной ленты, но хранилище программ (считыватель ленты) и хранилище данных были двумя физически раздельными устройствами; центральный процессор не мог получать доступ к командам программы так же, как к данным.
Общие недостатки этих машин:
- Смена программ была крайне трудоёмкой — требовалось вручную перекоммутировать аппаратуру или заменять физические носители.
- Команды программы не могли изменяться самой программой — это ограничивало появление продвинутых методов программирования (таких как компиляторы и компоновщики).
- Проектирование аппаратного обеспечения было сложным — разные задачи могли требовать разных аппаратных конфигураций.
2.2 Прорыв фон Неймана
В 1945 году фон Нейман предложил смелую идею в «Первом проекте отчёта об EDVAC»:
Кодировать команды программы в двоичные числа и хранить их вместе с данными в одной и той же адресуемой памяти.
Эта идея дала три решающих преимущества:
- Унификация аппаратного обеспечения: Процессору не нужно знать, находится ли в данной ячейке памяти команда или данные — он просто читает и записывает по адресу. Это чрезвычайно упростило проектирование аппаратного обеспечения.
- Программное обеспечение существует независимо: Программы стали «файлами», которые можно было хранить, копировать и передавать. Так родилось понятие «программное обеспечение».
- Программы могут изменять сами себя: Программа может изменять свои собственные команды или команды других программ так же, как она работает с данными. Это заложило основу для эпохальных технологий, таких как ассемблеры, компиляторы и операционные системы.
Подробный принцип: сосуществование команд и данных в памяти
3.1 Схема модуля памяти: что является командой? что является данными?
На рисунке ниже показан снимок области памяти. Каждая ячейка — это единица хранения с уникальным номером адреса.
Обратите внимание—Инструкции и данные физически абсолютно одинаковы; это всё 0 и 1.Единственный способ отличить их — кто обращается к ним и когда.
3.2 Ключевой вопрос: как процессор отличает команды от данных?
Из приведённой выше диаграммы виден ключевой факт: команды и данные неразличимы на уровне физического хранения.Совершенно неразличимы—Все они являются двоичными битовыми строками.
Так как же процессор узнаёт, следует ли рассматривать ячейку памяти как команду или как данные?
Ответ прост:Зависит от момента доступа и от того, кто выполняет доступ.
- Этап выборки команды: у процессорасчётчик команд (PC)предоставляет адрес, и блок управления посылает управляющий сигнал «чтение команды». В этот момент содержимое, прочитанное из памяти, рассматривается как «команда» и отправляется в регистр команд для декодирования.
- Этап выполнения: Процессор вычисляет адрес операнда на основе результата декодирования (который может поступать из регистра или быть непосредственным значением в команде), и контроллер выдаёт управляющий сигнал «чтение данных». В этот момент содержимое, прочитанное из памяти, отправляется в ALU как «данные» для участия в вычислении.
Одним предложением: для одного и того же адреса памяти, когда на него указывает PC, это «команда»; когда на него указывает адрес операнда, это «данные». В этом суть «хранимой программы» — команды и данные физически неразличимы и логически различаются контекстом доступа.
Интерактивная демонстрация: исследуйте команды и данные в памяти самостоятельно
Приведённый ниже код на Python создаёт систему памяти, которую можно исследовать интерактивно — вы можете записывать команды или данные, а затем просматривать содержимое любого адреса в памяти и определять, является ли оно командой или данными.
Пример
Демонстрация смешанного хранения команд и данных в памяти (пример демонстрации)
Демонстрация концепции «хранимая программа»: команды и данные в одной и той же памяти невозможно различить по их содержимому.
"""
class MemoryInspector:
Инспектор памяти — наглядно показывает сосуществование команд и данных в памяти
def __init__(self, size=256):
# Инициализация памяти: каждая ячейка хранит целое значение (имитация 8-бит)
self.memory = [0] * size
# Массив тегов: хранит тип каждого адреса — 'instruction' или 'data'
# Примечание: этот маркер не существует в реальном оборудовании! Он здесь только для удобства демонстрации.
self.type_tags = ['empty'] * size
# Запись пояснений к мнемоникам для каждого адреса (также для удобства демонстрации)
self.annotations = [''] * size
def write_instruction(self, address, opcode, operand, annotation=""):
"""Записать инструкцию в память (занимает две единицы: opcode + operand)"""
self.memory[address] = opcode
self.type_tags[address] = 'instruction'
self.annotations[address] = annotation
self.memory[address + 1] = operand
self.type_tags[address + 1] = 'instruction'
self.annotations[address + 1] = f" └ Операнд: {operand}"
def write_data(self, address, value, annotation=""):
"""Записать единицу данных в память"""
self.memory[address] = value
self.type_tags[address] = 'data'
self.annotations[address] = annotation
def inspect(self, start=0, end=16):
"""Просмотреть содержимое памяти в указанном диапазоне"""
print("=" * 68)
print(f'{Адрес':>6} | {'Хранимое значение':>8} | {'Двоичное':>10} | {'Тип':>12} | Смысл)
print("-" * 68)
for addr in range(start, min(end, len(self.memory))):
val = self.memory[addr]
binary = format(val, '08b') # 8-битное двоичное представление
tag = self.type_tags[addr]
anno = self.annotations[addr]
# Используйте маркеры, чтобы различать цвета отображения (в терминале нет цвета, используйте вместо этого символы)
marker = "[I]" if tag == 'instruction' else \
"[D]" if tag == 'data' else \
"[ ]"
if val != 0 or tag != 'empty':
print(f" {addr:4d} | {marker} {val:5d} | {binary} | {tag:>12} | {anno}")
print("=" * 68)
print([I] = Инструкция [D] = Данные [ ] = Свободно\n")
def compare_bytes(self, addr1, addr2):
"""
Сравните необработанные байты двух адресов — вы обнаружите, что на двоичном уровне они неразличимы
"""
v1 = self.memory[addr1]
v2 = self.memory[addr2]
b1 = format(v1, '08b')
b2 = format(v2, '08b')
print(f"\n--- Сравнение адреса {addr1} и адреса {addr2} ---")
print(fАдрес {addr1}: Двоичное представление {b1} (Тип: {self.type_tags[addr1]}))
print(fАдрес {addr2}: Двоичное представление {b2} (Тип: {self.type_tags[addr2]}))
if b1 == b2 and self.type_tags[addr1] != self.type_tags[addr2]:
print(f>>> Ключевое наблюдение: двоичное содержимое этих двух адресов в точности совпадает ({b1})!)
print(f>>> Но их типы различаются: адрес {addr1} — {self.type_tags[addr1]},)
print(f>>> Адрес {addr2} — {self.type_tags[addr2]}.)
print(f>>> Это показывает, что — имея только двоичное содержимое, невозможно отличить инструкции от данных!)
else:
print(f"Содержимое этих двух адресов различается.")
def simulate_instruction_data_ambiguity(self, addr):
"""
Продемонстрировать, что одно и то же двоичное значение интерпретируется по-разному в разных контекстах
"""
val = self.memory[addr]
binary = format(val, '08b')
print(f"\n--- Демонстрация неоднозначности адреса {addr} ---")
print(fСохранённое двоичное значение: {binary} (десятичное: {val}))
print(f"")
print(f"Если PC процессора указывает сюда (этап выборки инструкции):")
print(f" -> этот байт интерпретируется как 'код операции инструкции'")
print(f" -> Например, {binary} может представлять инструкцию 'LOAD'")
print(f"")
print(f"Если ALU обращается сюда через адрес операнда (этап исполнения):")
print(f" -> Этот байт интерпретируется как 'данные'")
print(f-> Например, десятичное {val} — это просто обычное числовое значение)
print(f"")
print(f"Одна и та же последовательность битов, прочитанная в разных ролях, имеет совершенно разные значения!")
# ================================================================
# Демонстрация 1: Создание гибридного хранилища в памяти
# ================================================================
print(>>> Демонстрация 1: Смешивание инструкций и данных в памяти\n")
mem = MemoryInspector()
# Запись последовательности инструкций
mem.write_instruction(0, 0xB1, 5, "LOAD A, 5")
mem.write_instruction(2, 0xB2, 3, "LOAD B, 3")
mem.write_instruction(4, 0x20, 1, "ADD A, B")
mem.write_instruction(6, 0x30, 100, "STORE 100")
# Запись данных
mem.write_data(100, 8, Результат вычисления: 5 + 3 = 8)
# Просмотр обзора памяти
mem.inspect(0, 8)
mem.inspect(100, 102)
# ================================================================
# Демо 2: Неоднозначность инструкции/данных для одного и того же значения
# ================================================================
print(>>> Демо 2: Одно и то же двоичное значение может быть инструкцией или данными\n")
# Намеренно записать значение 5 по адресу (в область инструкций), а также записать 5 по адресу 200 (в область данных)
mem.write_instruction(10, 5, 0, "Это 5 — код операции")
mem.write_data(200, 5, "Это 5 — значение данных")
mem.compare_bytes(10, 200)
# Демонстрация неоднозначности
mem.simulate_instruction_data_ambiguity(10)
# ================================================================
# Демо 3: Концепция самомодифицирующегося кода (продвинутая тема)
# ================================================================
print(">>> Демо 3: Концепция самомодифицирующихся программ\n")
print("Поскольку инструкции и данные находятся в одной и той же памяти, программа может изменять собственные инструкции!")
print(Именно эту возможность обеспечивает архитектура с хранимой программой.)
print("")
print(Например, программа может изменить инструкцию STORE 100 по адресу 6 на STORE 200:)
print(f"Перед изменением, адрес 6: {mem.memory[6]} (код операции)")
print(f"Перед изменением, адрес 7: {mem.memory[7]} (операнд)")
# Программа изменяет себя: изменить операнд STORE с 100 на 200
mem.memory[7] = 200
mem.type_tags[7] = 'data'
mem.annotations[7] = Изменено самой программой: операнд 100 -> 200
print(fПосле изменения, адрес 7: {mem.memory[7]} (операнд изменился))
print("")
mem.inspect(6, 8)
print(>>> Эта возможность лежит в основе таких технологий, как компиляторы, динамические компоновщики и виртуальные машины.)
Запустите эту программу, и вы ясно увидите: инструкции и данные — это всего лишь разные "роли" в одном и том же блоке памяти. Они используют один и тот же механизм хранения и адресации — в этом заключается самая тонкая элегантность в самом сердце архитектуры фон Неймана.
3.3 Интерактивная демонстрация: щёлкните по ячейке памяти, чтобы просмотреть её содержимое (пример)
Ниже представлена симуляция 64-байтовой области памяти.Синие ячейки обозначают инструкции,Оранжевые ячейки обозначают данные, серые области свободны. Наведите курсор на любую ячейку, чтобы просмотреть её необработанное сохранённое значение,Щёлкните ячейкупросмотреть подробное объяснение — включая то, действительно ли по адресу хранится «команда» или «данные», а также его фактическое значение.
Сравнение с Гарвардской архитектурой
Архитектура фон Неймана — не единственная компьютерная архитектура. Её главный «конкурент» — Гарвардская архитектура.Гарвардская архитектура。
| Критерий сравнения | Архитектура фон Неймана | Гарвардская архитектура |
|---|---|---|
| Память | Единая память, команды и данные совместно используют | Две независимые памяти, команды и данные разделены |
| шина | Один набор шин адреса/данных; команды и данные совместно используют шину с временным разделением | Два независимых набора шин; доступ к командам и данным может осуществляться одновременно |
| Преимущества | Простая аппаратная конструкция, высокая утилизация памяти, гибкое программное обеспечение | Выборка команд и выборка данных могут происходить одновременно, быстрее (удобно для конвейера) |
| Недостатки | Выборка команд и выборка данных могут конфликтовать (узкое место фон Неймана) | Более сложная аппаратная часть; память для команд и память для данных нельзя динамически распределять |
| Типичные области применения | Компьютеры общего назначения, такие как ПК, серверы и т. д. | Встраиваемые MCU, DSP — цифровые сигнальные процессоры |
Другие расширенияВ реальных реализациях современные CPU часто объединяют обе архитектуры: внешне они представляют единую модель памяти архитектуры фон Неймана (для удобства программирования), а внутри используют отдельные кэши команд и данных на уровне L1 Cache (заимствуя скоростные преимущества Гарвардской архитектуры). Это так называемая «модифицированная Гарвардская архитектура».