C语言和内存
1.程序的运行
对cpu来说,内存只是一个存放指令和数据的地方,具体的运算在cpu内完成。
1.寄存器(Register)
是CPU内部非常小、非常快速的存储部件,它的容量很有限,对于32位的CPU,每个寄存器一般能存储32位(4个字节)的数据,对于64位的CPU,每个寄存器一般能存储64位(8个字节)的数据。为了完成各种复杂的功能,现代CPU都内置了几十个甚至上百个的寄存器,嵌入式系统功能单一,寄存器数量较少。
寄存器在程序的执行过程中至关重要,不可或缺,它们可以用来完成数学运算、控制循环次数、控制程序的执行流程、标记CPU运行状态等。
2.缓存
虽然内存的读取速度已经很快了,但是和CPU比起来,还是有很大差距的,不是一个数量级的,如果每次都从内存中读取数据,会严重拖慢CPU的运行速度,CPU经常处于等待状态,无事可做。在CPU内部设置一个缓存,可以将使用频繁的数据暂时读取到缓存,需要同一地址上的数据时,就不用大老远地再去访问内存,直接从缓存中读取即可。
2.虚拟内存(内存地址都是假的)
1.虚拟地址
把程序给出的地址看做是一种虚拟地址(Virtual Address),然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。这样,只要我们能够妥善地控制这个虚拟地址到物理地址的映射过程,就可以保证程序每次运行时都可以使用相同的地址。
除了在编程时可以使用固定的内存地址,给程序员带来方便外,使用虚拟地址还能够使不同程序的地址空间相互隔离,提高内存使用效率。
使用了虚拟地址后,程序A和程序B虽然都可以访问同一个地址,但它们对应的物理地址是不同的,无论如何操作,都不会修改对方的内存。
2.提高内存使用效率
使用虚拟地址后,操作系统会更多地介入到内存管理工作中,这使得控制内存权限成为可能。例如,我们希望保存数据的内存没有执行权限,保存代码的内存没有修改权限,操作系统占用的内存普通程序没有读取权限等。
3.内存对齐,提高寻址效率
将一个数据尽量放在一个步长之内,避免跨步长存储,这称为内存对齐。
在32位编译模式下,默认以4字节对齐;在64位编译模式下,默认以8字节对齐。
内存对齐由编译程序实现,编译时我们可以设置对齐长度。
4.内存分页机制
现代计算机都使用分页(Paging)的方式对虚拟地址空间和物理地址空间进行分割和映射,以减小换入换出的粒度,提高程序运行效率。
分页(Paging)的思想是指把地址空间人为地分成大小相等(并且固定)的若干份,这样的一份称为一页,就像一本书由很多页面组成,每个页面的大小相等。如此,就能够以页为单位对内存进行换入换出:
- 当程序运行时,只需要将必要的数据从磁盘读取到内存,暂时用不到的数据先留在磁盘中,什么时候用到什么时候读取。
- 当物理内存不足时,只需要将原来程序的部分数据写入磁盘,腾出足够的空间即可,不用把整个程序都写入磁盘。
页的大小由硬件决定,或硬件支持多种,由操作系统选择一种大小,只能选一种。
内存分页由操作系统决定管理。
5.Linux下c语言程序的内存布局
内核空间和用户空间
操作系统会默认将高地址的1G或2G空间分配给内核,剩下的内存空间是用户空间
Linux下32位环境的用户空间内存分布情况:
程序代码区:主要存放二进制代码。不可修改,有执行权限
常量区:存放一般的常量,有读取权限,没有修改权限。所以数据不能修改
全局数据区:存放全局变量,静态变量等,有读写权限
堆区:由程序员分配和释放,malloc(),calloc()等函数操作的内存
栈区:存放函数的参数,局部变量值
一个程序的代码区,常量区,全局数据区在程序加载到内存的时候就分配好了,大小固定。
函数被调用时,会将参数、局部变量、返回地址等与函数相关的信息压入栈中,函数执行结束后,这些信息都将被销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部,因为它们的内存不在了。
常量区、全局数据区、栈上的内存由系统自动分配和释放,不能由程序员控制。程序员唯一能控制的内存区域就是堆(Heap):它是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分,在这片空间中,程序可以申请一块内存,并自由地使用(放入任何数据)。堆内存在程序主动释放之前会一直存在,不随函数的结束而失效。在函数内部产生的数据只要放到堆中,就可以在函数外部使用。
6.用户模式和内核模式
内核空间存放的是操作系统内核代码和数据,是被所有程序共享的,在程序中修改内核空间中的数据不仅会影响操作系统本身的稳定性,还会影响其他程序,这是非常危险的行为,所以操作系统禁止用户程序直接访问内核空间。
要想访问内核空间,必须借助操作系统提供的 API 函数,执行内核提供的代码,让内核自己来访问,这样才能保证内核空间的数据不会被随意修改,才能保证操作系统本身和其他程序的稳定性。
用户程序调用系统 API 函数称为系统调用(System Call);发生系统调用时会暂停用户程序,转而执行内核代码(内核也是程序),访问内核空间,这称为内核模式(Kernel Mode)。
用户空间保存的是应用程序的代码和数据,是程序私有的,其他程序一般无法访问。当执行应用程序自己的代码时,称为用户模式(User Mode)。
7.栈的概念以及栈溢出
栈的概念:
栈内存由系统自动分配和释放:发生函数调用时就为函数运行时用到的数据分配内存,函数调用结束后就将之前分配的内存全部销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部。
在计算机中,栈可以理解为一个特殊的容器,用户可以将数据依次放入栈中,然后再将数据按照相反的顺序从栈中取出。也就是说,先放入的数据最后才能取出,而最后放入的数据必须先取出。这称为先进后出(First In Last Out)原则。
栈的大小以及栈溢出
对每个程序来说,栈能使用的内存是有限的,一般是1M~8M,在程序编译时已经决定,运行期间不能改变,如果程序使用的栈超出最大值,就会发生栈溢出错误。
一个程序可能包含多个线程,每个线程都有自己的栈,所以严格来说,最大值是针对线程来说的。
栈的大小是编译器决定的,我们可以通过对编译器设置来调整栈的大小。
栈溢出攻击原理
C语言不会对数组溢出做检测,数组溢出导致覆盖了函数返回地址的例子,我们将这样的错误称为“栈溢出错误”。
8.c语言动态内存分配
在进程的地址空间中,代码区、常量区、全局数据区的内存在程序启动时就已经分配好了,它们大小固定,不能由程序员分配和释放,只能等到程序运行结束由操作系统回收。这称为静态内存分配。