Почему программы и данные можно размещать в одной и той же памяти

В предыдущей лекции мы увидели полную картину архитектуры фон Неймана — пять основных компонентов и три шины.

В этой лекции мы сосредоточимся на её самой революционной идее:рассматривать команды программы как особый вид данных и хранить их вместе с данными в одной и той же памяти。


Бытовая аналогия: ноты и музыкальные плееры

Представьте два разных музыкальных устройства:

Устройство A: Музыкальная шкатулка

Музыкальная шкатулка с пружинным приводом содержит металлический цилиндр, покрытый крошечными выступами. Каждый выступ щиплет стальной зуб гребёнки, извлекая ноту.

Хотите сменить мелодию? Придётся разобрать музыкальную шкатулку и заменить цилиндр на другой, выступы которого расположены иначе. Сам цилиндр — это физическое слияние «программы» и «исполнителя».

Устройство B: Музыкальное приложение на смартфоне

Ваш телефон хранит сотни MP3-файлов. Коснитесь одного — и он заиграет.

MP3-файл (программа/данные) и приложение-плеер (исполнитель) разделены — чтобы сменить песню, достаточно заменить файл, а не телефон.

Музыкальная шкатулка — это компьютер до фон Неймана: программа и аппаратное обеспечение связаны воедино, и смена задачи означает смену машины.

Смартфон — это пост-фон-неймановский компьютер: программы — это заменяемое содержимое в памяти, а аппаратное обеспечение — универсальная платформа для выполнения.

Критерий сравненияМузыкальная шкатулка (внешнее программное управление)Смартфон (хранимая программа)
Подход на основе хранимой программыЗафиксировано в механической структуреХранится в памяти в виде данных
Сменить трекРазобрать и заменить барабанПросто коснуться экрана
Универсально ли аппаратное обеспечение?Одна музыкальная шкатулка = одна мелодияОдин телефон = неограниченное количество мелодий
ГибкостьПочти нольчрезвычайно высокий

То, что сделал фон Нейман, было именно этим: он извлёк «партитуру» из аппаратного обеспечения и превратил её в «файл данных», который можно было свободно заменять. С тех пор аппаратное обеспечение оставалось аппаратным обеспечением, а программное — программным.


Исторический контекст: от «внешнего управления программой» к «хранимой программе»

2.1 Компьютеры до фон Неймана

В начале 1940-х годов некоторые электронные компьютеры уже использовались, но у всех них была одна общая проблема:

ENIAC (1946)—Первый в мире универсальный электронный компьютер, поразительно быстрый, но всякий раз, когда вычислительная задача менялась, инженерам приходилось тратить несколько днейна ручное подключение и отключение сотен кабелей и установку тысяч переключателейЧтобы «перепрограммировать».

Harvard Mark I (1944)—Программы вводились с помощью перфорированной бумажной ленты, но хранилище программ (считыватель ленты) и хранилище данных были двумя физически раздельными устройствами; центральный процессор не мог получать доступ к командам программы так же, как к данным.

Общие недостатки этих машин:

  • Смена программ была крайне трудоёмкой — требовалось вручную перекоммутировать аппаратуру или заменять физические носители.
  • Команды программы не могли изменяться самой программой — это ограничивало появление продвинутых методов программирования (таких как компиляторы и компоновщики).
  • Проектирование аппаратного обеспечения было сложным — разные задачи могли требовать разных аппаратных конфигураций.

2.2 Прорыв фон Неймана

В 1945 году фон Нейман предложил смелую идею в «Первом проекте отчёта об EDVAC»:

Кодировать команды программы в двоичные числа и хранить их вместе с данными в одной и той же адресуемой памяти.

Эта идея дала три решающих преимущества:

  1. Унификация аппаратного обеспечения: Процессору не нужно знать, находится ли в данной ячейке памяти команда или данные — он просто читает и записывает по адресу. Это чрезвычайно упростило проектирование аппаратного обеспечения.
  2. Программное обеспечение существует независимо: Программы стали «файлами», которые можно было хранить, копировать и передавать. Так родилось понятие «программное обеспечение».
  3. Программы могут изменять сами себя: Программа может изменять свои собственные команды или команды других программ так же, как она работает с данными. Это заложило основу для эпохальных технологий, таких как ассемблеры, компиляторы и операционные системы.

Подробный принцип: сосуществование команд и данных в памяти

3.1 Схема модуля памяти: что является командой? что является данными?

На рисунке ниже показан снимок области памяти. Каждая ячейка — это единица хранения с уникальным номером адреса.

Обратите внимание—Инструкции и данные физически абсолютно одинаковы; это всё 0 и 1.Единственный способ отличить их — кто обращается к ним и когда.

Instruction color gradient Data color gradient Memory module background Main memory (RAM)header Address Stored content Type Actual meaningData row Address 0: instruction LOAD A 5 0x0000 10110001 00000101 [ Instruction ] LOAD A, 5Address 2: Data 5 (value of A) 0x0001 00000000 00000101 [ Data ] Immediate value 5Address 3: Instruction LOAD B 3 0x0002 10110010 00000011 [ Instruction ] LOAD B, 3Address 5: Data 3 (value of B) 0x0003 00000000 00000011 [ Data ] Immediate value 3Address 4: Instruction ADD A B 0x0004 00100000 00000001 [ Instruction ] ADD A, BAddress 6: Instruction STORE A 0x0005 00110000 01100100 [ Instruction ] STORE 100Address 100: data storage result 0x0064 00000000 00001000 [ Data ] Calculation result: 8Legend area Legend: Blue highlight = instruction unit (opcode + operand) Orange highlight = data unit (immediate values, variable values, calculation results) Note: Instructions and data are exactly the same in physical storage; they are all binary bits. Only the PC pointer when the CPU fetches an instruction determines whether it is an instruction or data.

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-байтовой области памяти.Синие ячейки обозначают инструкции,Оранжевые ячейки обозначают данные, серые области свободны. Наведите курсор на любую ячейку, чтобы просмотреть её необработанное сохранённое значение,Щёлкните ячейкупросмотреть подробное объяснение — включая то, действительно ли по адресу хранится «команда» или «данные», а также его фактическое значение.

Команда Данные Свободно (Пусто)
Ячейки сетки, динамически создаваемые JS
Нажмите на ячейку памяти выше, чтобы увидеть тип и значение содержимого, хранящегося по этому адресу

Сравнение с Гарвардской архитектурой

Архитектура фон Неймана — не единственная компьютерная архитектура. Её главный «конкурент» — Гарвардская архитектура.Гарвардская архитектура。

Критерий сравненияАрхитектура фон НейманаГарвардская архитектура
ПамятьЕдиная память, команды и данные совместно используютДве независимые памяти, команды и данные разделены
шинаОдин набор шин адреса/данных; команды и данные совместно используют шину с временным разделениемДва независимых набора шин; доступ к командам и данным может осуществляться одновременно
ПреимуществаПростая аппаратная конструкция, высокая утилизация памяти, гибкое программное обеспечениеВыборка команд и выборка данных могут происходить одновременно, быстрее (удобно для конвейера)
НедостаткиВыборка команд и выборка данных могут конфликтовать (узкое место фон Неймана)Более сложная аппаратная часть; память для команд и память для данных нельзя динамически распределять
Типичные области примененияКомпьютеры общего назначения, такие как ПК, серверы и т. д.Встраиваемые MCU, DSP — цифровые сигнальные процессоры

В реальных реализациях современные CPU часто объединяют обе архитектуры: внешне они представляют единую модель памяти архитектуры фон Неймана (для удобства программирования), а внутри используют отдельные кэши команд и данных на уровне L1 Cache (заимствуя скоростные преимущества Гарвардской архитектуры). Это так называемая «модифицированная Гарвардская архитектура».

Другие расширения