现在位置: 首页 > 计算机组成原理 > 正文

程序和数据为什么能放在同一块内存里

上一讲我们看到了冯·诺依曼架构的全貌——五大组件和三条总线。

这一讲,我们聚焦它最革命性的思想:把程序指令当作一种特殊的数据,和数据一起存放在同一块内存中


生活化类比:乐谱与音乐播放器

想象两个不同的音乐设备:

设备 A:八音盒

一个发条驱动的八音盒,内部有一根布满凸点的金属滚筒。每个凸点拨动一根钢片发出一个音符。

要换一首曲子?你得拆开八音盒,换上另一根凸点排列不同的滚筒。滚筒本身就是「程序」和「演奏器」的物理融合体。

设备 B:智能手机上的音乐 App

你的手机里存着上百首 MP3 文件。点哪首就播放哪首。

MP3 文件(程序/数据)和播放器 App(执行器)是分离的——换个歌只需换个文件,不用换手机。

八音盒就是冯·诺依曼之前的计算机:程序和硬件绑定在一起,换任务等于换机器。

智能手机就是冯·诺依曼之后的计算机:程序是存储器中的可替换内容,硬件是通用的执行平台。

对比维度八音盒(外部程序控制)智能手机(存储程序)
程序存储方式固化在机械结构中以数据形式存在存储器中
更换曲目拆机换滚筒点一下屏幕即可
硬件是否通用一只八音盒 = 一首曲一台手机 = 无限首曲
灵活性几乎为零极高

冯·诺依曼做的就是这一件事:把「曲谱」从硬件里抽出来,变成可以随意替换的「数据文件」。从此,硬件是硬件,软件是软件。


历史背景:从「外部程序控制」到「存储程序」

2.1 冯·诺依曼之前的计算机

在 1940 年代早期,已经有一些电子计算机投入使用,但它们有一个共同的问题:

ENIAC(1946 年)——世界上第一台通用电子计算机,运算速度惊人,但每次换一个计算任务,工程师需要花几天时间手动插拔数百根电缆和设置数千个开关来「重新编程」。

Harvard Mark I(1944 年)——使用打孔纸带输入程序,但程序存储器(纸带读取器)和数据存储器是物理上分离的两个器件,CPU 不能像访问数据那样访问程序指令。

这些机器的共同缺点:

  • 更改程序极度费时——需要人工调整硬件连接或更换物理介质
  • 程序指令无法被程序自身修改——这限制了高级编程技术(如编译器、链接器)的出现
  • 硬件设计复杂——每种任务可能需要不同的硬件配置

2.2 冯·诺依曼的突破

1945 年,冯·诺依曼在《关于 EDVAC 的报告草案》中提出了一个大胆的想法:

把程序指令编码成二进制数字,和数据一起存放在同一个可寻址的存储器中。

这个想法带来了三个决定性优势:

  1. 硬件统一化:CPU 不需要知道某个内存位置存的是指令还是数据——它只管按地址读写。这让硬件设计大大简化。
  2. 软件独立存在:程序变成了可以存储、复制、传输的「文件」。这就是「软件」概念的诞生。
  3. 程序可自我修改:程序可以像操作数据一样修改自身或其他程序的指令。这为汇编器、编译器、操作系统等划时代的技术奠定了基础。

原理详解:内存中指令与数据的共存

3.1 内存条示意图:谁是指令?谁是数据?

下面这张图展示了一段内存区域的快照。每个格子是一个存储单元,有唯一的地址编号。

请观察——指令和数据在物理上完全一样,都是 0 和 1。区分它们的唯一方式是谁在什么时候访问了它。

主存储器 (RAM) 地址 存储内容 类型 实际含义 0x0000 10110001 00000101 [ 指令 ] LOAD A, 5 0x0001 00000000 00000101 [ 数据 ] 立即数 5 0x0002 10110010 00000011 [ 指令 ] LOAD B, 3 0x0003 00000000 00000011 [ 数据 ] 立即数 3 0x0004 00100000 00000001 [ 指令 ] ADD A, B 0x0005 00110000 01100100 [ 指令 ] STORE 100 0x0064 00000000 00001000 [ 数据 ] 计算结果: 8 图例说明: 蓝色高亮 = 指令单元(操作码 + 操作数) 橙色高亮 = 数据单元(立即数、变量值、计算结果) 说明:指令和数据在物理存储上完全一样,都是二进制位,只有 CPU 取指令时的 PC 指针决定了它是指令还是数据。

3.2 关键问题:CPU 如何区分指令和数据?

从上面的示意图可以看出一个关键事实:指令和数据在物理存储层面完全无法区分——它们都是二进制位串。

那么 CPU 怎么知道该把一个内存单元当作指令还是数据?

答案很简单:看访问时机和访问者。

  • 取指令阶段:CPU 的 程序计数器 (PC) 提供地址,控制器发出「读指令」控制信号。此时从内存读出的内容被当作「指令」送入指令寄存器进行译码。
  • 执行阶段:CPU 根据译码结果计算出操作数的地址(可能来自某个寄存器或指令中的立即数),控制器发出「读数据」控制信号。此时从内存读出的内容被当作「数据」送入 ALU 参与运算。

一句话总结:同一个内存地址,PC 指向它的时候它是「指令」,操作数地址指向它的时候它就是「数据」。这就是「存储程序」的精髓——指令和数据在物理上不可区分,在逻辑上靠访问上下文来区分。


交互演示:自己动手查看内存中的指令和数据

下面的 Python 代码创建了一个可以交互探索的内存系统——你可以写入指令或数据,然后查看内存中任何地址的内容,并判断它是指令还是数据。

实例

"""
内存中指令与数据的混合存储演示(runoob 演示)
展示「存储程序」思想:指令和数据在同一块内存中无法从存储内容上区分
"""


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=""):
        """向内存写入一条指令(占用两个单元:操作码 + 操作数)"""
        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"如果 CPU 的 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,在地址 200 (数据区) 也写 5
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("例如,程序可以把地址 6 处的指令 STORE 100 改成 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 交互演示:点击内存格子查看内容(runoob)

下面是一个 64 字节的内存区域模拟。蓝色格子代表指令橙色格子代表数据,灰色为空闲区域。悬停任意格子查看其存储的原始值,点击格子查看详细说明——包括该地址存的究竟是「指令」还是「数据」,以及它的实际含义。

指令 (Instruction) 数据 (Data) 空闲 (Empty)
点击上方内存格子,查看该地址存储的内容类型和含义

与哈佛架构的对比

冯·诺依曼架构并不是唯一的计算机架构。它的主要「竞争对手」是哈佛架构(Harvard Architecture)

对比维度冯·诺依曼架构哈佛架构
存储器单一存储器,指令和数据共用两个独立存储器,指令和数据分开
总线一组地址/数据总线,指令和数据分时复用两组独立的总线,可同时访问指令和数据
优点硬件设计简单,存储器利用率高,软件灵活可同时取指令和取数据,速度更快(流水线友好)
缺点取指令和取数据可能冲突(冯·诺依曼瓶颈)硬件更复杂,指令存储和数据存储不能动态分配
典型应用PC、服务器等通用计算机嵌入式 MCU、DSP 数字信号处理器

现代 CPU 在实际实现中往往对两种架构做了融合:对外呈现冯·诺依曼架构的统一内存模型(方便编程),内部在 L1 Cache 层面使用分离的指令缓存和数据缓存(借鉴哈佛架构的速度优势)。这就是所谓的「改进型哈佛架构」。