计算机内存探秘:物理存储器、地址空间与内存地址

Written by

in

文章目录
  • 首先,我们来谈谈物理存储器 (Physical Storage)。顾名思义,这是指计算机硬件中实际存在的、用于存储数据的芯片或设备。最常见的物理存储器包括: 主内存 (Main Memory / RAM): 插在主板上的内存条,是CPU主要的工作区域。 显卡显存 (VRAM): 位于显卡上的专用高速存储器,用于存储图形数据和纹理。 各种适配器上的 ROM 或 RAM: 例如,网卡、声卡等设备上也可能有存储固件(ROM)或少量用于缓冲数据的RAM。 这些物理存储器是分散在计算机系统中的独立硬件单元。它们各自有自己的控制器,以及访问其内部数据的机制。
  • 每个物理存储设备都有其内部的存储地址空间 (Storage Address Space)。这指的是该设备内部用来标识其存储单元(通常是字节)的地址范围。 例如,一个 8GB 的内存条,它内部可能有从地址 0 到 8GB-1 的存储单元。一块显卡的 4GB 显存,它内部也有从地址 0 到 4GB-1 的存储单元。一个设备上的 ROM 可能有从地址 0 到 ROM 大小-1 的地址。 你可以把这想象成不同的建筑物,每栋建筑物里的房间都有从 1 开始编号。建筑物 A 的 1 号房间和建筑物 B 的 1 号房间是完全不同的两个地方。每个物理设备就是一栋“建筑物”,它的内部地址空间就是这栋建筑物里“房间”的编号范围。 问题来了:CPU 如何统一管理和访问这些分散在不同物理设备、拥有各自独立地址空间的存储单元呢?CPU 不能直接说“请给我建筑物 B 的 1 号房间的东西”。
  • 为了让 CPU 和软件能够方便地访问和管理这些分散的物理存储资源,操作系统和硬件(特别是内存管理单元 MMU)会将这些物理设备的地址映射 (Mapping) 到一个统一的、线性的地址空间中。这个统一的地址空间,就是我们通常在讨论计算机系统时所说的内存地址空间,或者更精确地说,是线性地址空间 (Linear Address Space),在现代操作系统中,它往往是虚拟地址空间 (Virtual Address Space) 的一部分。 这个过程可以理解为:系统为所有的物理存储资源(包括 RAM、显存、各种设备的寄存器和内存等)编制了一个统一的“地图”。地图上的每一个地址都对应着某个物理设备上的某个具体的存储单元。 例如,在一个 32 位系统中,这个统一的线性地址空间通常是从地址 0x00000000 到 0xFFFFFFFF,总共 2^32 = 4GB 的地址范围。系统会将 4GB 物理内存的地址范围映射到这个线性地址空间的一部分,将显存映射到另一部分,将设备 ROM/RAM 映射到再一部分,等等。 这样一来,CPU 只需要使用这个统一的线性地址(例如 0x80001234),系统硬件就会负责将这个线性地址翻译成对应的物理设备上的物理地址(例如“显卡显存上的地址 0x101234”),从而完成数据的访问。 内存地址 (Memory Address) 这个概念,在程序开发者的视角看来,通常指的就是在这个统一的线性/虚拟地址空间中的地址。当我们在 C/C++ 中使用指针获取变量地址时,获取到的就是这个线性/虚拟地址空间中的地址。 将内存抽象成字节数组: 从软件(特别是操作系统和应用程序)的角度看,这个统一的内存地址空间可以被抽象成一个巨大的、一维的字节数组 (Byte Array)。这个数组的每一个“格子”就是一个字节(8 bits),并且都有一个唯一的、从 0 开始的编号,这个编号就是该字节的内存地址。 地址 0x00000000 对应第一个字节。 地址 0x00000001 对应第二个字节。 … 地址 0xFFFFFFFF 对应最后一个字节(在 32 位系统中)。 不同类型的数据,如 char (1 字节), int (通常 4 字节), float (通常 4 字节), double (通常 8 字节),以及更复杂的结构体和数组,它们在内存中会占据连续的若干个字节空间。一个变量的地址通常指的就是它所占用的第一个字节的地址。
  • 通过一个简单的 C 语言程序,我们可以直观地看到变量在程序所感知的这个“内存地址空间”中是如何被分配地址的。 #include <stdio.h> // 包含标准输入输出库,用于使用 printf 函数 #include <stddef.h> // 包含 stddef.h 以使用 size_t 类型 int main() { // 声明不同类型的变量 char my_char = ‘A’; // 字符类型,通常占 1 字节 int my_int = 12345; // 整型,通常占 4 字节 float my_float = 3.14f; // 浮点型,通常占 4 字节 double my_double = 2.71828; // 双精度浮点型,通常占 8 字节 // 声明一个数组 int my_array[5] = {10, 20, 30, 40, 50}; // 包含 5 个整型的数组 // 声明一个结构体 struct Point { int x; int y; }; struct Point p = {100, 200}; // 结构体变量 // 声明一个函数 (实际上是获取函数的入口地址) // 注意:函数地址通常在代码段,与数据段/栈段的地址在不同的内存区域 void (*print_msg)(void) = main; // 获取 main 函数的地址 (示例) // 实际调用函数指针的例子: // print_msg(); // 这会尝试再次执行 main 函数,可能会导致栈溢出或其他问题,不建议在实际代码中这样做! // 这里的目的是演示如何获取函数地址 // 打印变量的值、地址和占用的字节数 // %p 用于打印指针的值 (地址),需要转换为 (void*) 类型以保证跨平台兼容性 // sizeof 运算符用于获取变量或类型占用的字节数 printf(“变量 my_char:n”); printf(” 值: %cn”, my_char); printf(” 地址: %pn”, (void*)&my_char); printf(” 占用字节数: %zun”, sizeof(my_char)); // %zu 用于 size_t 类型 printf(“n变量 my_int:n”); printf(” 值: %dn”, my_int); printf(” 地址: %pn”, (void*)&my_int); printf(” 占用字节数: %zun”, sizeof(my_int)); printf(“n变量 my_float:n”); printf(” 值: %fn”, my_float); printf(” 地址: %pn”, (void*)&my_float); printf(” 占用字节数: %zun”, sizeof(my_float)); printf(“n变量 my_double:n”); printf(” 值: %lfn”, my_double); // %lf 用于 double 类型 printf(” 地址: %pn”, (void*)&my_double); printf(” 占用字节数: %zun”, sizeof(my_double)); printf(“n数组 my_array:n”); // 数组名本身通常代表数组第一个元素的地址 printf(” 数组首地址: %pn”, (void*)my_array); printf(” 第一个元素 my_array[0] 的地址: %pn”, (void*)&my_array[0]); printf(” 第二个元素 my_array[1] 的地址: %pn”, (void*)&my_array[1]); printf(” 占用总字节数: %zun”, sizeof(my_array)); printf(” 每个元素占字节数: %zun”, sizeof(my_array[0])); // 注意观察相邻元素地址之间的差异,它等于元素的大小 (这里是 sizeof(int)) printf(“n结构体 p:n”); printf(” 结构体首地址: %pn”, (void*)&p); printf(” 成员 p.x 的地址: %pn”, (void*)&p.x); printf(” 成员 p.y 的地址: %pn”, (void*)&p.y); printf(” 占用总字节数: %zun”, sizeof(p)); // 注意成员地址与结构体首地址的关系 printf(“n函数 main 的地址:n”); printf(” 地址: %pn”, (void*)print_msg); // 打印函数指针的值 return 0; // 程序正常结束 } 编译和运行: 将上述代码保存为 address_example.c 文件。 打开终端或命令提示符。 使用 C 编译器(如 GCC)编译代码: gcc address_example.c -o address_example 运行生成的可执行文件: ./address_example 运行结果示例: 请注意,输出的内存地址是示例值,具体数值在您的系统上运行或每次运行时都可能不同,因为操作系统会动态分配内存,并且涉及到虚拟内存地址。关键在于观察地址的相对关系和不同类型占用的字节数。 变量 my_char: 值: A 地址: 0x7ffd7533f0b7 占用字节数: 1 变量 my_int: 值: 12345 地址: 0x7ffd7533f0b0 占用字节数: 4 变量 my_float: 值: 3.140000 地址: 0x7ffd7533f0ac 占用字节数: 4 变量 my_double: 值: 2.718280 地址: 0x7ffd7533f0a0 占用字节数: 8 数组 my_array: 数组首地址: 0x7ffd7533f080 第一个元素 my_array[0] 的地址: 0x7ffd7533f080 第二个元素 my_array[1] 的地址: 0x7ffd7533f084 占用总字节数: 20 每个元素占字节数: 4 结构体 p: 结构体首地址: 0x7ffd7533f078 成员 p.x 的地址: 0x7ffd7533f078 成员 p.y 的地址: 0x7ffd7533f07c 占用总字节数: 8 函数 main 的地址: 地址: 0x563e556c4179 结果分析: 每个变量都被分配了一个唯一的地址。这些地址是程序在运行时看到的线性/虚拟地址。 sizeof 运算符显示了不同数据类型占用的字节数,这决定了它们在内存中占据的空间大小。 对于数组 my_array,数组名 my_array 的地址与第一个元素 &my_array[0] 的地址相同。相邻元素 &my_array[0] 和 &my_array[1] 的地址相差 4 个字节 (0x7ffd7533f084 – 0x7ffd7533f080 = 0x4),正好是一个 int 类型的大小,这印证了数组元素是连续存储的。 对于结构体 p,结构体的首地址就是其第一个成员 &p.x 的地址。第二个成员 &p.y 的地址紧随其后(或者根据编译器的对齐策略有微小的间隔),地址相差 4 个字节 (0x7ffd7533f07c – 0x7ffd7533f078 = 0x4),正好是 p.x (int) 的大小,这说明结构体成员也是按顺序存储的。结构体的总大小是其成员大小的总和(加上可能的对齐填充)。 函数 main 也有一个地址,这是函数代码在内存中的起始位置。函数的地址通常位于内存的不同区域(代码段)与变量(数据段/栈段)的地址区分开来。 这个例子清晰地展示了程序如何看待内存——一个拥有连续地址的字节序列,各种数据类型根据其大小占据其中的一部分。操作系统和硬件在底层默默地将这些程序可见的地址翻译成物理设备上的实际地址。 到此这篇关于计算机内存探秘:物理存储器、地址空间与内存地址的文章就介绍到这了,更多相关计算机内存:物理存储器、地址空间与内存地址内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客! 您可能感兴趣的文章: 电脑提速内存篇
  • 目录
    • 1. 物理存储器:硬件层面的“仓库”
    • 2. 存储地址空间 (Per-Device): 各自为政的地址范围
    • 3. 统一的视图:内存地址空间与线性地址
    • 4. 代码示例:窥探程序眼中的内存地址

    对于初学者来说,计算机的“内存”概念有时会让人感到困惑。我们知道程序和数据放在内存里运行,也听说过“内存地址”这个词,但它到底代表什么?物理内存条、显卡显存、主板上的ROM…这些都是存储器,它们是如何被统一管理的?

    本文将带你探索计算机存储器的不同层面,理解物理存储器、存储地址空间以及程序所感知的“内存地址”之间的关系。

    首先,我们来谈谈物理存储器 (Physical Storage)。顾名思义,这是指计算机硬件中实际存在的、用于存储数据的芯片或设备。最常见的物理存储器包括:

    • 主内存 (Main Memory / RAM): 插在主板上的内存条,是CPU主要的工作区域。
    • 显卡显存 (VRAM): 位于显卡上的专用高速存储器,用于存储图形数据和纹理。
    • 各种适配器上的 ROM 或 RAM: 例如,网卡、声卡等设备上也可能有存储固件(ROM)或少量用于缓冲数据的RAM。

    这些物理存储器是分散在计算机系统中的独立硬件单元。它们各自有自己的控制器,以及访问其内部数据的机制。

    每个物理存储设备都有其内部的存储地址空间 (Storage Address Space)。这指的是该设备内部用来标识其存储单元(通常是字节)的地址范围。

    例如,一个 8GB 的内存条,它内部可能有从地址 0 到 8GB-1 的存储单元。一块显卡的 4GB 显存,它内部也有从地址 0 到 4GB-1 的存储单元。一个设备上的 ROM 可能有从地址 0 到 ROM 大小-1 的地址。

    你可以把这想象成不同的建筑物,每栋建筑物里的房间都有从 1 开始编号。建筑物 A 的 1 号房间和建筑物 B 的 1 号房间是完全不同的两个地方。每个物理设备就是一栋“建筑物”,它的内部地址空间就是这栋建筑物里“房间”的编号范围。

    问题来了:CPU 如何统一管理和访问这些分散在不同物理设备、拥有各自独立地址空间的存储单元呢?CPU 不能直接说“请给我建筑物 B 的 1 号房间的东西”。

    为了让 CPU 和软件能够方便地访问和管理这些分散的物理存储资源,操作系统和硬件(特别是内存管理单元 MMU)会将这些物理设备的地址映射 (Mapping) 到一个统一的、线性的地址空间中。这个统一的地址空间,就是我们通常在讨论计算机系统时所说的内存地址空间,或者更精确地说,是线性地址空间 (Linear Address Space),在现代操作系统中,它往往是虚拟地址空间 (Virtual Address Space) 的一部分。

    这个过程可以理解为:系统为所有的物理存储资源(包括 RAM、显存、各种设备的寄存器和内存等)编制了一个统一的“地图”。地图上的每一个地址都对应着某个物理设备上的某个具体的存储单元。

    例如,在一个 32 位系统中,这个统一的线性地址空间通常是从地址 0x000000000xFFFFFFFF,总共 2^32 = 4GB 的地址范围。系统会将 4GB 物理内存的地址范围映射到这个线性地址空间的一部分,将显存映射到另一部分,将设备 ROM/RAM 映射到再一部分,等等。

    这样一来,CPU 只需要使用这个统一的线性地址(例如 0x80001234),系统硬件就会负责将这个线性地址翻译成对应的物理设备上的物理地址(例如“显卡显存上的地址 0x101234”),从而完成数据的访问。

    内存地址 (Memory Address) 这个概念,在程序开发者的视角看来,通常指的就是在这个统一的线性/虚拟地址空间中的地址。当我们在 C/C++ 中使用指针获取变量地址时,获取到的就是这个线性/虚拟地址空间中的地址。

    将内存抽象成字节数组:

    从软件(特别是操作系统和应用程序)的角度看,这个统一的内存地址空间可以被抽象成一个巨大的、一维的字节数组 (Byte Array)。这个数组的每一个“格子”就是一个字节(8 bits),并且都有一个唯一的、从 0 开始的编号,这个编号就是该字节的内存地址

    • 地址 0x00000000 对应第一个字节。
    • 地址 0x00000001 对应第二个字节。
    • 地址 0xFFFFFFFF 对应最后一个字节(在 32 位系统中)。

    不同类型的数据,如 char (1 字节), int (通常 4 字节), float (通常 4 字节), double (通常 8 字节),以及更复杂的结构体和数组,它们在内存中会占据连续的若干个字节空间。一个变量的地址通常指的就是它所占用的第一个字节的地址。

    通过一个简单的 C 语言程序,我们可以直观地看到变量在程序所感知的这个“内存地址空间”中是如何被分配地址的。

    #include <stdio.h> // 包含标准输入输出库,用于使用 printf 函数
    #include <stddef.h> // 包含 stddef.h 以使用 size_t 类型
    
    int main() {
        // 声明不同类型的变量
        char my_char = 'A';        // 字符类型,通常占 1 字节
        int my_int = 12345;        // 整型,通常占 4 字节
        float my_float = 3.14f;    // 浮点型,通常占 4 字节
        double my_double = 2.71828; // 双精度浮点型,通常占 8 字节
    
        // 声明一个数组
        int my_array[5] = {10, 20, 30, 40, 50}; // 包含 5 个整型的数组
    
        // 声明一个结构体
        struct Point {
            int x;
            int y;
        };
        struct Point p = {100, 200}; // 结构体变量
    
        // 声明一个函数 (实际上是获取函数的入口地址)
        // 注意:函数地址通常在代码段,与数据段/栈段的地址在不同的内存区域
        void (*print_msg)(void) = main; // 获取 main 函数的地址 (示例)
        // 实际调用函数指针的例子:
        // print_msg(); // 这会尝试再次执行 main 函数,可能会导致栈溢出或其他问题,不建议在实际代码中这样做!
        // 这里的目的是演示如何获取函数地址
    
        // 打印变量的值、地址和占用的字节数
        // %p 用于打印指针的值 (地址),需要转换为 (void*) 类型以保证跨平台兼容性
        // sizeof 运算符用于获取变量或类型占用的字节数
        printf("变量 my_char:n");
        printf("  值: %cn", my_char);
        printf("  地址: %pn", (void*)&my_char);
        printf("  占用字节数: %zun", sizeof(my_char)); // %zu 用于 size_t 类型
    
        printf("n变量 my_int:n");
        printf("  值: %dn", my_int);
        printf("  地址: %pn", (void*)&my_int);
        printf("  占用字节数: %zun", sizeof(my_int));
    
        printf("n变量 my_float:n");
        printf("  值: %fn", my_float);
        printf("  地址: %pn", (void*)&my_float);
        printf("  占用字节数: %zun", sizeof(my_float));
    
        printf("n变量 my_double:n");
        printf("  值: %lfn", my_double); // %lf 用于 double 类型
        printf("  地址: %pn", (void*)&my_double);
        printf("  占用字节数: %zun", sizeof(my_double));
    
        printf("n数组 my_array:n");
        // 数组名本身通常代表数组第一个元素的地址
        printf("  数组首地址: %pn", (void*)my_array);
        printf("  第一个元素 my_array[0] 的地址: %pn", (void*)&my_array[0]);
        printf("  第二个元素 my_array[1] 的地址: %pn", (void*)&my_array[1]);
        printf("  占用总字节数: %zun", sizeof(my_array));
        printf("  每个元素占字节数: %zun", sizeof(my_array[0]));
        // 注意观察相邻元素地址之间的差异,它等于元素的大小 (这里是 sizeof(int))
    
        printf("n结构体 p:n");
        printf("  结构体首地址: %pn", (void*)&p);
        printf("  成员 p.x 的地址: %pn", (void*)&p.x);
        printf("  成员 p.y 的地址: %pn", (void*)&p.y);
        printf("  占用总字节数: %zun", sizeof(p));
        // 注意成员地址与结构体首地址的关系
    
        printf("n函数 main 的地址:n");
        printf("  地址: %pn", (void*)print_msg); // 打印函数指针的值
    
        return 0; // 程序正常结束
    }

    编译和运行:

    1. 将上述代码保存为 address_example.c 文件。
    2. 打开终端或命令提示符。
    3. 使用 C 编译器(如 GCC)编译代码:
    gcc address_example.c -o address_example
    1. 运行生成的可执行文件:
    ./address_example

    运行结果示例:

    请注意,输出的内存地址是示例值,具体数值在您的系统上运行或每次运行时都可能不同,因为操作系统会动态分配内存,并且涉及到虚拟内存地址。关键在于观察地址的相对关系和不同类型占用的字节数

    变量 my_char:
      值: A
      地址: 0x7ffd7533f0b7
      占用字节数: 1
    
    变量 my_int:
      值: 12345
      地址: 0x7ffd7533f0b0
      占用字节数: 4
    
    变量 my_float:
      值: 3.140000
      地址: 0x7ffd7533f0ac
      占用字节数: 4
    
    变量 my_double:
      值: 2.718280
      地址: 0x7ffd7533f0a0
      占用字节数: 8
    
    数组 my_array:
      数组首地址: 0x7ffd7533f080
      第一个元素 my_array[0] 的地址: 0x7ffd7533f080
      第二个元素 my_array[1] 的地址: 0x7ffd7533f084
      占用总字节数: 20
      每个元素占字节数: 4
    
    结构体 p:
      结构体首地址: 0x7ffd7533f078
      成员 p.x 的地址: 0x7ffd7533f078
      成员 p.y 的地址: 0x7ffd7533f07c
      占用总字节数: 8
    
    函数 main 的地址:
      地址: 0x563e556c4179

    结果分析:

    1. 每个变量都被分配了一个唯一的地址。这些地址是程序在运行时看到的线性/虚拟地址
    2. sizeof 运算符显示了不同数据类型占用的字节数,这决定了它们在内存中占据的空间大小。
    3. 对于数组 my_array,数组名 my_array 的地址与第一个元素 &my_array[0] 的地址相同。相邻元素 &my_array[0] 和 &my_array[1] 的地址相差 4 个字节 (0x7ffd7533f084 - 0x7ffd7533f080 = 0x4),正好是一个 int 类型的大小,这印证了数组元素是连续存储的。
    4. 对于结构体 p,结构体的首地址就是其第一个成员 &p.x 的地址。第二个成员 &p.y 的地址紧随其后(或者根据编译器的对齐策略有微小的间隔),地址相差 4 个字节 (0x7ffd7533f07c - 0x7ffd7533f078 = 0x4),正好是 p.x (int) 的大小,这说明结构体成员也是按顺序存储的。结构体的总大小是其成员大小的总和(加上可能的对齐填充)。
    5. 函数 main 也有一个地址,这是函数代码在内存中的起始位置。函数的地址通常位于内存的不同区域(代码段)与变量(数据段/栈段)的地址区分开来。

    这个例子清晰地展示了程序如何看待内存——一个拥有连续地址的字节序列,各种数据类型根据其大小占据其中的一部分。操作系统和硬件在底层默默地将这些程序可见的地址翻译成物理设备上的实际地址。

    到此这篇关于计算机内存探秘:物理存储器、地址空间与内存地址的文章就介绍到这了,更多相关计算机内存:物理存储器、地址空间与内存地址内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!

    您可能感兴趣的文章:

    • 电脑提速内存篇

    站内搜索