C语言学习笔记之内存精讲

主题:
简介:
参考:
作者:
时间:

14.1 存储在硬盘中的程序需要载入内存才能运行,CPU也只能从内存中读取数据和指令,对于CPU而言,memory仅仅存放instruction和data,不能在memory完成运算,任何计算都需要读取到CPU内部才能进行运算。
CPU——Memory——Disk
CPU:运算单元、寄存器、缓存
缓存:如果每次都从内存中读取数据,会严重拖慢CPU的运行速度,CPU经常处于等待状态,在CPU内部设置一次缓存,将使用频繁的数据暂时读取到缓存,需要同一地址上的数据时,直接存缓存中读取即可。缓存的容量是有限的,CPU只能从缓存中读取到部分数据,对于使用不频繁的数据,会从内存中读取。
注:如何定义频繁?那些数据留在缓存?那些不留在缓存?
寄存器:寄存器用来完成数学运算、控制循环次数、控制程序的执行流程、标记CPU的状态等。例如EIP寄存器的值是下一条指令的地址,CPU执行完当前指令后,会根据EIP的值找到下一条指令,改变EIP的值,就会改变程序的执行流程。
CR3寄存器保存当前进程页目录的物理地址,切换进程就会改变CR3的值。
EBP、ESP寄存器用来指向栈的底部和顶部,函数调用会改变EBP和ESP的值。

CPU指令集:
想要让CPU工作,需要借助特定的指令,例如add用于加法运算,sub用于除法,cmp用于比较两个数的大小,这称为CPU的指令集。
C语言代码最终会被编译成一条一条的CPU指令。
14.2虚拟内存
在C语言中,指针变量的值就是一个内存地址,&运算符的作用也是取变量的内存地址。

#include
#include
int a = 1, b = 2;
int main() {
	int *pa = &a;
	printf("pa=%#x,b=%#x\n",pa,&b);
	system("pause");
	return 0;
}

使用虚拟地址能够是得不同程序的地址空间相互隔离,提高内存使用效率。
14.3 虚拟地址空间以及编译模式
虚拟地址空间:
虚拟地址空间是程序可以使用的虚拟地址的有效范围。
虚拟地址和物理地址的映射关系由操作系统决定,因此虚拟地址空间的大小也有操作系统决定,同时还会受到编译模式的影响。
注:数据总线用于CPU和内存之间传输数据;地址总线用于在内存上定位数据。
编译模式:

14.4C语言内存对齐
CPU通过地址总线访问内存。
对于程序来说,一个变量最好位域一个寻址步长的范围内,这样一次就可以读取到变量的值;如果跨步长存储就需要读取两次,然后再拼凑数据,使得效率降低。
内存对齐:将一个数据尽量放在一个步长之内,避免跨步长存储。
为了提高存取效率,编译器会自动进行内存对齐,但是会浪费一些字节的空间。

从结构体成员看内存对齐的应用:

#include
#include

struct 
{
	int a;
	char b;
	int c;
}t = {1,'a',2};

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

}

注:对于全局变量release模式下会进行内存对齐;
对于局部变量不会进行内存对齐。
14.5内存分页机制完成虚拟地址的映射
虚拟地址与物理地址的映射:
作用:
地址隔离、程序可以使用固定的内存地址、内存使用效率较低

内存分页机制:
一般情况下,程序运行时,在某个时间段内,指示频繁的用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内都不会背用到。如果以整个程序为单位进行映射,会将暂时用不到的数据从磁盘读取到内存,导致过多的数据一次性写入磁盘或内存,会严重降低程序的运行效率。
计算机使用分页的方式对虚拟地址空间和物理地址空间进行分割和映射,以减小换入换出的粒度,提高程序运行效率。

分页思想:把地址空间人为地分成大小相等的若干份。每份的大小相等,这样就可以以页为单位对内存进行换入换出。
14.6分页机制的实现
操作系统使用分页机制来管理内存,使得程序拥有自己的地址空间,每当程序使用虚拟地址进行读写时,必须转换为实际的物理地址,才能够在内存上定位数据。
内存地址的转换是通过页表的机制完成的。
虚拟地址——页表转换——物理地址
注:中间层的思想
一级页表、两级页表、多级页表???
14.7 内存管理单元
CPU 虚拟地址 MMU 物理地址 物理内存
MMU通过页表完成虚拟地址到物理地址的映射们不会构建页表,页表的构建是操作系统的任务。
在程序加载到内存以及程序运行过程中,操作系统会不断更新程序对应的页表,并将页目录的物理地址保存到CR3寄存器,MMU向缓存中加载页表时,会根据CR3寄存器找到页目录,再找到页表,最终通过软硬件的结合完成内存映射。

对内存权限的控制:
MMU除了能够完成虚拟地址到物理地址的映射,还能够以对内存权限进行控制。
14.8 Linux下C语言程序的内存布局
C语言程序的内存在整个地址空间中是如何分布的?数据在哪里?代码在哪里?这样分布的原因是什么?

程序内存在地址空间中的分布情况称为内存模型。内存模型由操作系统构建,而且受编译模式的影响。
内核空间和用户空间:
操作系统内核使用的地址空间,应用程序无法直接访问这一段内存,这一部分内存地址称为内核空间。

Linux下64位环境的用户空间内存分布情况:
C语言学习笔记之内存精讲_第1张图片
code:存放函数体的二进制代码。一个C语言程序由多个函数构成,C原因程序的执行就是函数之间的相互调用。
constant:存放一般的常量、字符串常量等。(只读数据)这块内存只有读取权限,没有写入权限,因此他们的值在程序运行期间不能改变。
global data:存放全局变量、static变量。这块内存有读写权限,他们的值在程序运行期间可以任意改变。
heap:由程序员手动分配和释放,程序运行结束时由操作系统回收。malloc()、calloc()、free()等函数操作的就是这块内存
dll:用于在程序运行期间加载和卸载动态链接库。
stack:存放函数的参数值、局部变量的值、局部数组。

注:局部变量在栈区、全局变量在全局数据区、字符串常量在常量区
14.9 windows下C语言程序的内存布局
每个线程的栈都是独立的,一个进程有多少个线程就有多少个对应的栈。
14.10 用户模式和内核模式
简单的说一个可执行程序就是一个进程,进程最显著的特点就是拥有独立的地址空间。严格来说,程序是存储在磁盘上的一个文件,是指令和数据的集合,是一个静态的概念;进程是程序加载到内存运行后的一系列获得,是一个动态概念。
内核模式和用户模式:
用户空间保存的是应用程序的代码和数据,是程序私有的,其他程序一般无法访问。当执行应用程序自己的代码时,称为用户模式。
用户程序调用系统API函数称为System Call;发生系统调用时会暂停用户程序,转而执行内核代码,访问内核空间,称为内核模式。
为什么内核和用户程序要公用地址空间:
内核和用用户程序共享地址空间,发生系统调用时进行的是模式切换,模式切换仅仅需要寄存器进栈出栈,不会导致CPU中的数据缓存失效。
14.11栈的概念和栈溢出
栈:存放局部变量、函数参数、局部数组等作用范围在函数内部的数据,它的用途就是完成函数的调用。
栈内存由系统自动根据需求进行分配和释放,发生函数调用时就为函数运行时用到的数据分配内存,函数调用结束后就将之前分配的内存全部销毁,因此局部变量、函数参数、局部数组只在当前函数有效,不能传递到函数外部。
栈的概念:FILO
队列:FIFO
注:双栈实现队列
push和pop
从本质上讲,栈是一段连续的内存,需要同时记录栈底和栈顶,才能堆栈进行定位。现代计算机中,通常使用EBP寄存器指向栈底,ESP寄存器指向栈顶,随着数据的push和pop,ESP的值会不断变化,进栈是ESP值减小,出栈时ESP的值增大。
低地址和高地址???C语言学习笔记之内存精讲_第2张图片
栈的大小以及栈溢出:
一个程序包含多个线程,每个线程都有自己的栈,栈的最大值是针对线程来说的,而不是程序。
14.12函数在栈上是怎样的
函数的调用和栈是分不开的,没有栈就没有函数调用。

栈帧/活动记录:
当发生函数调用时,会将函数运行需要的信息全部压入栈中,这被称为栈帧或活动记录,Activate Record包含函数的返回地址、参数和局部变量、编译器自动生成的临时数据、一些需要保存的寄存器。
函数的返回地址:函数执行完成后从哪里开始继续执行后面的代码。
函数参数和变量:
编译器自动生成的临时数据:当函数返回值的长度较大,首先将返回值压入栈中,然后再交给函数调用者。当函数返回值的长度较小,现将函数返回值放入寄存器,然后再交给函数调用者。也就是说大于40个字节时,入栈,小于40个字节时,入寄存器。
一些需要保存的寄存器:之所以要保存寄存器的值,是为了在函数退出时能够恢复到函数调用之前的场景,继续执行上层函数。
注:当发生函数调用时,实参、返回地址、ebp寄存器首先入栈,然后再分配一块内存局部变量、返回值使用、最后将其他寄存器的值压入栈中。
14.13函数调用惯例Calling Convention
一个调用惯例一般规定以下两方面的内容:
函数参数的传递方式
函数参数的传递顺序
参数弹出方式
14.14函数push和pop的过程剖析

14.15栈溢出攻击的原理

#include
int main() {
	char str[10] = { 0 };
	gets(str);

	printf("str:%s\n",str);
	return 0;

}

14.16C语言动态内存分配
静态内存分配:在进程的地址空间中,程序代码区、常量区、全局数据区
的内存在程序启动时就已经分配完成,大小固定,不能由程序员分配和释放,只能等到程序运行结束由操作系统回收。
动态内存分配:堆区的内存在程序运行期间可以根据实际需求来分配和释放,不能在程序刚启动时就备足所有内存。
栈区:栈区内存由系统分配和释放,不受程序员控制;
堆区:堆区内存由程序员自行分配和释放;
动态内存分配函数:
malloc()、calloc()、realloc()、free()
void* malloc(size_t size);
void* calloc(size_t n ,size_t size);
void* realloc(void *ptr,size_t size);
内存释放函数:
void free(void *ptr);

#include
#include

#define N 5
#define N1 7
#define N2 3

int main() {
	int *ip;
	int *large_ip;
	int *small_ip;

	if ((ip=(int *)malloc(N*sizeof(int)))==NULL)
	{
		printf("memory allocated failed\n");
		exit(1);//异常退出
	}
	int i;
	for (int i = 0; i < N; i++)
	{
		ip[i] = 1;
		printf("ip[%d]=%d\t",i,ip[i]);
	}
	printf("\n");

	if ((large_ip=(int *)realloc(ip,N1*sizeof(int)))==NULL)
	{
		printf("memory allocated failed\n");
		exit(1);//异常退出
	}
	for (int i = N; i < N1; i++)
	{
		large_ip[i] = 9;
	}
	for (int i = 0; i < N1; i++)
	{
		ip[i] = 1;
		printf("large_ip[%d]=%d\t", i, large_ip[i]);
	}
	if ((small_ip = (int *)realloc(large_ip, N2 * sizeof(int))) == NULL)
	{
		printf("memory allocated failed\n");
		exit(1);//异常退出
	}
	for (int i = 0; i < N2; i++)
	{
		
		printf("small_ip[%d]=%d\t", i, small_ip[i]);
		printf("\n");
	}

	free(small_ip);
	small_ip = NULL;

	system("pause");
	return 0;

}

14.17内存池
malloc()函数在堆上如何进行内存分配:
(1)一种是把malloc函数的内存管理交给系统内核去做,既然内核管理着进程的地址空间,那么如果他提供一个系统调用,可以让malloc使用这个调用去申请内存,然而这种做法的性能较差,每次程序申请或者释放堆空间都要进行系统调用,系统调用的开销十分大,如果程序对堆的操作比较频繁,这样会影响程序的性能。
比较好的做法就是malloc向操作系统申请一块适当大小的堆空间,然后由malloc自己管理这块空间。
malloc和free的管理算法
void* malloc(size_t size)
内存块的结构类似于链表,他们之间通过指针链接在一起。
malloc和free的工作是对已有内存块进行拆分和合并,没有频繁的向操作系统申请内存,提高了内存分配效率。
另外,由于单线链表只能向一个方向搜索,在合并和拆分内存块时不方便,所以打分malloc实现都会在内存块中增加一个pre指针指向上一个内存块。
内存池:
为了减少系统调用、减少内存碎片,malloc的整体思想是先向操作系统申请一块大小适当的内存,然后自己管理,这就是内存池。
池化技术:
所谓池化技术,就是程序先向系统申请过量的资源,然后自己管理。
之所以要申请过量的资源,是因为每次申请资源都有巨大的开销,不如提前申请。
14.18野指针以及非法内存操作
野指针:如果一个指针指向的内存没有访问权限,或者指向一块已经释放掉的内存???,那么久无法对该指针进行操作,这样的指针称为野指针。
stack:存放函数的参数值、局部变量的值、局部数组。
规避野指针:
(1)当指针指向的内存被释放掉时,要将指针的值设置为NULL,因为free只是释放掉了内存,并未改变指针的值。???
(2)指针变量如果暂时不需要赋值,一定要初始化为NULL,因为任何指针变量刚被创建时不会自动称为NULL指针,它的缺省值是随机的。???
14.19内存泄漏
堆区内存:
栈区内存
内存分配和内存回收机制
内存泄漏:程序和内存失去了联系,在程序结束前无法再次进行操作。
(1)两次分配的内存使用同一指针,导致第一次分配的内存找不到了
(2)没有指针指向旧的分配内存
14.20 C语言变量的存储类别和生存期
变量有数据类型、存储类别(存储区域)
存储类别:变量在内存中存放的区域
存储类别:auto、extern、static、register
static:static声明的变量称为静态变量,无论全局还是局部,都存储在静态数据区。
静态数据区的数据在程序启动时就会初始化,直到程序运行结束;
对于代码中的局部静态变量,即使代码执行结束,也不会销毁。???
注:静态数据区的变量只能初始化一次,以后只能改变他的值,不能在初始化。

#include
#include
int main() {
	int r;
	for (int i = 1; i <= 100; i++)
	{
		r = sum(i);
		
	}
	printf("r=%d\n", r);
	system("pause");
	return 0;
}
int sum(int n) {
	static int result = 0;//静态局部变量存储在静态数据区,它的作用域仅限于定义它的代码块。
	result += n;
	return result;
}

register变量:
(1)局部静态变量不能定义为寄存器变量(一个变量只能是一种存储类别)
(2)只有较短的数据类型才适合定义为寄存器变量
(3)为寄存器变量分配寄存器是动态完成的,只有局部变量和形参才能定义为寄存器变量
(4)循环控制变量是使用频繁的变量

你可能感兴趣的:(C语言,c语言)