C语言的内存

cpu工作原理

#include <stdio.h>
#include <stdlib.h>
struct{
   
    int a;
    char b;
    int c;
}t={
    10, 'C', 20 };

int main(){
   
    printf("length: %d\n", sizeof(t));
    printf("&a: %X\n&b: %X\n&c: %X\n", &t.a, &t.b, &t.c);
    system("pause");
    return 0

      程序写好编译后保存在磁盘,然后加载到内存中运行的,一名合格的程序员必须了解内存,学习C语言更是要多了解些内存的知识点,C语言是一门偏向硬件的编程语言。

1、想理解清楚内存,先要弄清楚CPU的组成、工作原理和必要的一些相关概念
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

CPU总线

      习惯上人们把和CPU直接相关的局部总线叫做CPU总线或内部总线,而把和各种通用扩展槽相接的局部总线叫做系统总线或外部总线。

具体地,CPU总线一般指CPU与芯片组之间的公用连接线,又叫前端总线(FSB)。不管是内部总线还是外部总线,我们可以把它们理解成城市中的主干道和一般道路。

通常,总线可分为三类:数据总线,地址总线,控制总线,数据、地址和控制信号是分开传输的。三者配合起来实现CPU对数据和指令的读写操作。

寄存器和CPU指令

      寄存器(Register)是CPU内部非常小、非常快速的存储部件,它的容量很有限,对于32位的CPU,每个寄存器一般能存储32位(4个字节)的数据,对于64位的CPU,每个寄存器一般能存储64位(8个字节)的数据。

      为了完成各种复杂的功能,现代CPU都内置了几十个甚至上百个的寄存器,嵌入式系统功能单一,寄存器数较少。

我们经常听说多少位的CPU,即指的是寄存器能存储数据的位数,也是数据总线位数(总线的条数)。
现在个人电脑使用的CPU已经进入了64位时代,例如 Intel 的 Core i3、i5、i7 等。

      寄存器在程序的执行过程中至关重要,不可或缺,它们可以用来完成数学运算、控制循环次数、控制程序的执行流程、标记CPU运行状态等。

寄存器有很多种,例如:

  1. EIP(Extern Instruction Pointer )寄存器的值是下一条指令的地址,
    CPU执行完当前指令后,会根据 EIP 的值找到下一条指令,改变 EIP 的值,
    就会改变程序的执行流程,CPU中EIP就是我们上面说的程序计数器PC;
  2. CR3(Control Register )寄存器保存着当前进程页目录的物理地址,切换进程就会改变 CR3 的值;
  3. EBP(Extended Base Pointer)、ESP(Extended Stack Pointer) 寄存器用来指向栈的底部和顶部,
    函数调用会改变 EBP 和 ESP 的值。
  4. EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
  5. EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址。
  6. ECX 是计数器(counter), 是重复前缀指令和LOOP指令的内定计数器。
  7. EDX 则总是被用来放整数除法产生的余数。
  8. ESI/EDI分别叫做"源/目标索引寄存器"(source/destination inde因为在很多字符串操作指令中,ESI指向源串,EDI指向目标串.

CPU指令

要想让CPU工作,必须借助特定的指令,例如 add 用于加法运算,sub 用于除法运算,cmp 用于比较两个数的大小,这称为CPU的指令集(Instruction Set)。

我们的C语言代码最终也会编译成一条一条的CPU指令。
不同型号的CPU支持的指令集会有所差异,但绝大部分是相同的。

我们以C语言中的加法为例来演示CPU指令的使用。
假设有下面的C语言代码:

int a = 0X14, b = 0XAE, c;
c = a + b;

生成的CPU指令为(这是假的,不是真的指令,只是模拟CUP的处理过程):

mov  ptr[a], 0X14
mov  ptr[b], 0XAE
mov  eax, ptr[a]
add  eax, ptr[b]
mov  ptr[c], eax

总起来讲:第一二条指令给变量 a、b 赋值,
第三四条指令完成加法运算,
第五条指令将运算结果赋值给变量 c。
-----我们在程序里使用的内存地址是假的,是虚拟地址
在C语言中,指针变量的值就是一个内存地址,&运算符的作用也是取变量的内存地址,请看下面的代码:

#include <stdio.h>

int a = 1, b = 255;
int main(){
   
    int *pa = &a;
    printf("pa = %p, &b = %p\n", pa, &b);
    return 0;
}

我执行了两次,打印输出同样的地址。
在这里插入图片描述
      代码中的 a、b 是全局变量,它们的内存地址在链接时就已经决定了,以后再也不能改变,该程序无论在何时运行,结果都是一样的。
      那么问题来了,如果物理内存中的这两个地址被其他程序占用了怎么办,我们的程序岂不是无法运行了?

幸运的是,这些内存地址都是假的,不是真实的物理内存地址,而是虚拟地址。

      在程序运行时,需要使用真正的地址了,CPU会把虚拟地址转换成真正的内存的物理地址,而且每次程序运行时,操作系统都会重新安排虚拟地址和物理地址的对应关系,哪一段物理内存空闲就使用哪一段。
为什么要在程序与物理地址之间,加一个虚拟地址,不能让程序直接操作物理地址?

  1. 使用虚拟地址才能在编程中有一个确定地址,而物理地址不能确定
    我们知道编译完成后的程序是存放在硬盘上的,当运行的时候,需要将程序搬到内存当中去运行,如果直接使用物理地址的话,我们无法确定内存现在使用到哪里了,也就是说拷贝的实际内存地址每一次运行都是不确定的,比如:第一次执行a.out时候,内存当中一个进程都没有运行,所以搬移到内存地址是0x00000011,但是第二次的时候,内存已经有10个进程在运行了,那执行a.out的时候,内存地址就不一定了。

  2. 使用虚拟地址可以让不同程序的地址空间相互隔离
    如果所有程序都直接使用物理内存,那么程序所使用的地址空间不是相互隔离的。恶意程序可以很容易改写其他程序的内存数据,以达到破坏的目的;有些非恶意、但是有 Bug 的程序也可能会不小心修改其他程序的数据,导致其他程序崩溃。

      这对于需要安全稳定的计算机环境的用户来说是不能容忍的,用户希望他在使用计算机的时候,其中一个任务失败了,至少不会影响其他任务。

      使用了虚拟地址后,程序A和程序B虽然都可以访问同一个地址,但它们对应的物理地址是不同的,无论如何操作,都不会修改对方的内存。

  1. 提高内存使用效率
    使用虚拟地址后,操作系统会更多地介入到内存管理工作中,这使得控制内存权限成为可能。由操作系统更多的管理内存,当物理内存不够用,操作系统自动将不常用的数据转存到磁盘,用的时候在读回来,哪些内存在用,哪些内存没在用,OS可以动态判断,比我们程序员直接在程序里管理内存,更好,内存的使用率更高。

虚拟地址空间以及编译模式

      所谓虚拟地址空间,就是程序可以使用的虚拟地址的有效范围。
      虚拟地址和物理地址的映射关系由操作系统决定,相应地,虚拟地址空间的大小不仅由操作系统决定,还会受到编译模式的影响。重点讨论一下编译模式,要了解编译模式,还得从CPU来说起:

CPU的数据处理能力
      CPU是计算机的核心,决定了计算机的数据处理能力和寻址能力,也即决定了计算机的性能。
      CPU一次能处理的数据的大小由寄存器的位数和数据总线的宽度(有多少根数据总线)决定,我们通常所说的多少位的CPU,除了可以理解为寄存器的位数,也可以理解数据总线的宽度,通常情况下它们是相等的。

CPU实际支持多大的物理内存
      CPU支持的物理内存只是理论上的数据,实际应用中还会受到操作系统和其他条件的限制,例如,Win7 64位家庭版最大仅支持8GB或16GB的物理内存,Win7 64位专业版或企业版能够支持到192GB的物理内存。Win10 64位系统支持4G 8G 16G 32G 64G 128G 256G内存,理论上可以无限支持,但也要主板能支持才行。
编译模式
      为了兼容不同的平台,现代编译器大都提供两种编译模式:32位模式和64位模式。
32位编译模式
      在32位模式下,一个指针或地址占用4个字节的内存,共有32位,理论上能够访问的虚拟内存空间大小为 2^32 = 0X100000000 Bytes,即4GB,有效虚拟地址范围是 0 ~ 0XFFFFFFFF。

也就是说,对于32位的编译模式,不管实际物理内存有多大,程序能够访问的有效虚拟地址空间的范围就是0 ~ 0XFFFFFFFF,也即虚拟地址空间的大小是 4GB。换句话说,程序能够使用的最大内存为 4GB,跟物理内存没有关系。

如果程序需要的内存大于物理内存,或者内存中剩余的空间不足以容纳当前程序,那么操作系统会将内存中暂时用不到的一部分数据写入到磁盘,等需要的时候再读取回来,而我们的程序只管使用 4GB 的内存,不用关心硬件资源够不够。

如果物理内存大于 4GB,例如目前很多PC机都配备了8GB\16GB\32GB的内存,那么程序也无能为力,它只能够使用其中的 4GB。

64位编译模式
      在64位编译模式下,一个指针或地址占用8个字节的内存,共有64位,理论上能够访问的虚拟内存空间大小为 2^64。这是一个很大的值,几乎是无限的,就目前的技术来讲,不但物理内存不可能达到这么大,CPU的寻址能力也没有这么大,实现64位长的虚拟地址只会增加系统的复杂度和地址转换的成本,带不来任何好处,所以 Windows 和 Linux 都对虚拟地址进行了限制,仅使用虚拟地址的低48位(6个字节),总的虚拟地址空间大小为 2^48 = 256TB。
需要注意的是:
      1)32位的操作系统只能运行32位的程序,64位操作系统可以同时运行32位的程序和64位的程序。
      2)64位的CPU运行64位的程序才能发挥它的最大性能,运行32位的程序会白白浪费一部分资源。

      目前计算机可以说已经进入了64位的时代,之所以还要提供32位编译模式,是为了兼容一些老的硬件平台和操作系统,或者某些场合下32位的环境已经足够,使用64位环境会增大成本,例如嵌入式系统、单片机、工控等。

      这里所说的32位环境是指:32位的CPU + 32位的操作系统 + 32位的程序。

      另外需要说明的是,32位环境拥有非常经典的设计,易于理解,适合教学,课程里不特别说明默认都是基于32位来分析讲解相关内存的知识点。
下面代码是64位环境和32位环境下运行效果

#include <stdio.h>

int a = 1, b = 255;
int main(){
   
    int *pa = &a;
    printf("pa = %p, &b = %p\n,%d\n", pa, &b,sizeof(pa));
    return 0;
}
//执行与结果,分别有64位模式我32位模式编译执行
   gcc -g demo1.c -o demo1.exe     
   ./demo1
   pa = 0000000000403010, &b = 0000000000403014,8
//32位模式
    gcc -m32 -g demo1.c -o demo1.exe
    ./demo1
    pa = 00403004, &b = 00403008,4

内存对齐

       计算机内存是以字节(Byte)为单位划分的,理论上CPU可以访问任意编号的字节,但实际情况并非如此。

      CPU 通过地址来访问内存,通过地址在内存中定位要找的目标数据,我们叫寻址,CPU在寻址的时候它不是从0 1 2 3…挨着寻址的,有跳跃的步长的,这个步长和CPU的位数有关系!

       以32位的CPU为例,实际寻址的步长为4个字节,也就是只对地址为 4 的倍数的内存寻址,
例如 0、4、8、12、1000 等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qq_33406021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值