深入理解操作系统(29)第十章:虚拟存储器(6)垃圾收集器+C程序中常见的与内存有关的错误(包括:访问非法内存/读写未初始化的指针/越界或数组缓冲区溢出/指针优先级操作的问题/传值和传指针/野指针)

1. 垃圾收集器的基本知识(略)

垃圾收集器(garbage collector)是一种动态存储分配器,它自动释放程序不再需要的己分配块。

这些块被称为垃圾(garbage),因此术语就称之为垃圾收集器。

自动回收堆存储的过程叫做垃圾收集(garbage collection)。
在一个支持垃圾收集的系统中,应用显式分配堆块,但是从不显示地释放它们。在C程序的上下文中,应用调用malloc,但是从不调用freee取而代之的是垃圾收集器定期识别垃圾块,并相应地调用free,将这些块放回到空闲链表中。

垃圾收集可以追溯到John McCarthy在20世纪年代早期在MIT开发的Lisp系统。它是诸如Java、ML、Perl和Mathemauca等现代语言系统的一个重要部分,而且它仍然是一个重要的研究领
域。有关文献描述了大量的垃圾收集方法,其数量令人吃惊。

我们的讨论局限于McCarthy独创的Mark&Sweep(标记&清除)算法,

这个算法很有趣,因为它可以建立在己存在的malloc包的基础之上,为c和c++程序提供垃圾收集。

图10.52

2. C程序中常见的与内存有关的错误

2.1 间接引用坏指针(访问非法内存,段错误)

在进程的虚拟地址空间中有较大的洞,没有映射到任何有意义的数据。

1. 如果我们试图间接引用一个指向这些洞的指针,那么操作系统就会以段异常终止我们的程序。
2. 而且,虚拟看储器的其些区域是只读的。试图写这些区域将造成以保护异常终止这个程序。

常见示例:

间接引用坏指针的一个常见示例是经典的scanf错误。

假设我们想要使用scanf从stdin读一个整数到一个变量。做这件事情正确的方法是传递给scanf一个格式串和变量的地址:

scanf("%d",&val)

然而,对于c程序员初学者而言(对有经验者也是如此!),很容易传递的内容.而不是它的地址:

scanf("%d",val)

在这种情况下,scanf将把val的内容解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序立即以异常终止。在最糟糕的情况下,的内容对应于虚拟存储器的某个合法的读/写区域,于是我们就盖了存储器,这通常会在相当以后造成灾难性的,令人困感的后果。

2.2 读未初始化的存储器(读写未初始化的指针)

虽然.bss存储器位置(诸如未初始化的全局c变量)总是被加载器初始化为零,但是对于堆存储器却并不是这样的,

一个常见的错误就是假设堆存储器被初始化为零:

例子:

int *y = (int *)malloc(1024 * sizeof(int));
res = y[0] + ……

在这个示例中,程序员不正确地假设向量Y被初始化为零。正确的实现方式是在malloc后memset一下,或者使用calloc。

2.3 允许栈缓冲区溢出(数组缓冲区溢出)

如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误(buffer overflow bug)。
例如,下面的函数就有缓冲区误,因为gets函数拷贝一个任意长度的串到缓冲区。为了纠正这个错误,我们必须使用fgets函数,
这个函数限制了输入串的大小:

char buf[32];
gets(buf);

修改:

if( fgets(buf, 32, fp)!=NULL ) {
	/* 从文件流fp向buf 写入内容 最多32个字符 */
	…………
	//printf("%s",buf);
}

2.4 假设指针和它们指向的对象都是相同大小的

/* 目的:创建一个 n*m 大小的数组 */
int i;
int **ppArr = (int **)malloc(n * sizeof(int));
for (i = 0; i < n; i++)
	ppArr = (int *)malloc(m * sizeof(int));

这里的目的是创建一个由个指针组成的数组,每个指针都指向一个包含m个int的数组。然而,因为程序员在第2行将sizeof(int*)写成了sizeof(int),代码实际创建的是一个int的数组。

这段代码只有在int和指向int的指针大小相同的机器上运行良好。
	32位下:int 和 int* 都是4字节
	64位下:int 4字节 int* 8字节

如果我们在像Alpha这样的机器上运行这段代码,其中指针大于int,那么for循环将写到超出数组末端的地方。因为这些字中的一个很可能是己分配块的边界标记脚部,所以我们可能不会发现这个错误,直到我们在这个程序的后面很久释放这个块时,此时,分配器中
的合并代码会戏剧性地失败,而没有任何明显的原因。

这是“在远处起作用(actionatdistance)”的一个阴险示例。
这类“在远处起作用”是与存储器有关的编程错误的典型情况,

2.5 造成错位错误(越界或者缓冲区溢出)

错位(Off-by-one)错误是另一种很常见的覆盖错误发生的原因:

/* 目的:创建一个 n*m 大小的数组 */
int i;
int **ppArr = (int **)malloc(n * sizeof(int));
for (i = 0; i <= n; i++)
	ppArr = (int *)malloc(m * sizeof(int));

这是前面一节中程序的另一个版本。区别就是for循环初始化的时候多加了一个等号,在这个过程中覆盖了数组后面的某个存储器。

2.6 引用指针,而不是它所指向的对象(指针优先级操作的问题)

如:

*pTr--; //操作

后置自减运算符和取值运算符优先级相同,所以从右向左结合,先进行–,后进行*
如果要对指针的值进行自减

(*pTr)--;

参考:
C语言-移位,位域,和运算符优先级

运算符优先级

1. 指针优先,单目运算符高于双目运算符
2. 算数> 移位 >位运算 如:
	1 << 3+2 && 7 等价于 (1 << (3+2)) && 7
3. 逻辑运算符最后计算
4. 位异或:同为0 异为1, 0^0 = 0
5. & :位与操作 1&2 = 0
6. && :逻辑与操作 1&&2=1

https://blog.csdn.net/lqy971966/article/details/99682879

2.7 误解指针运算(指针的算术操作是以它们指向的对象的大小为单位来进行的)

另一种常见的错误是忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的
而这种大小单位并不一定是字节。

例如,下面函数的目的是扫描一个int的数组,并返回一个指针,指向val的首次出现:

int *search(int *p, int va1)
{
	while((*p) && (*p != val))
		p += sizeof(int); //应该p++;
		
	return p;
}

然而,因为每次循环时,第2行都把指针加了4(一个整数的字节数),函数就不正确地扫描数组中每4个整数。
只需要:p++;即可

这个参考:

void test()
{
	//二维数组
	int a[2][3]={{0,1,2},{3,4,5}}; //两行三列
	int (*p)[3];//p是数组指针,指向含有三个元素的一维数组
	int *q[2];//指针数组,一个数组内存放两个指针变量
	
	p=a;
	q[0]=a[0];
	q[1]=a[1];
	
	printf("%d\n",a[1][2]);//5
	printf("%d\n",*(p[1]+2));//5
	printf("%d\n",*(*(p+1)+2));//5 指针的算术操作是以它们指向的对象的大小为单位来进行的
								   所以p+1指向第二行
	printf("%d\n",(*(p+1))[2]);//5
	printf("%d\n",p[1][2]);//5
	
	printf("%d\n",*(q[1]+2));//5
	printf("%d\n",*(*(q+1)+2));//5
	printf("%d\n",(*(q+1))[2]);//5
	printf("%d\n",q[1][2]);//5
}

参考:指针数组和数组指针
https://blog.csdn.net/lqy971966/article/details/105100796

2.8 引用不存在的变量(函数参数的传值和传指针)

没有太多经验的C程序员不理解栈的规则,有时会引用不再合法的本地变量

如下列所示:

int *stackref()
{
	int val;
	return &val;
}

编译错误:

hello.c: In function ‘stackref’:
hello.c:12:3: warning: function returns address of local variable [-Wreturn-local-addr]
	return &val;

这个函数返回一个指针(比如说是p),指向栈里的一个局部变量。然后弹出它的栈帧。
局部变量存储在栈区,在代码块执行前申请一片内存,执行完毕后,这块内存即被释放

1.参考:函数参数的传值和传指针(引用)(1)基本概念
https://blog.csdn.net/lqy971966/article/details/106011497

2.参考:函数参数的传值和传指针(2)扩展getmemery 问题
https://blog.csdn.net/zqixiao_09/article/details/50127249

3.扩展:字符数组和字符常量指针的区别

字符数组和字符常量指针
char a[] = "123";//1 字符数组
char b[] = "123";
char *p = "123"; //2 字符指针
char *q = "123";

https://blog.csdn.net/lqy971966/article/details/99599751

2.9 引用空闲堆块中的数据(free之后还能使用 野指针)

2.9.1 free内存之后的例子:

void test()
{
	int *p=NULL;
	p = malloc(1024);
	*p = 100;
	printf("%d\n",*p);
	free(p);
	printf("%d\n",*p);
	//p=NULL;
	*p=200;
	printf("%d\n",*p);
	
	return;
}

编译,执行都ok。

[root@localhost 10]# ./a.out 
100
100
200
[root@localhost 10]#

如果加上 p=NULL;则段错误

^
[root@localhost 10]# ./a.out 
100
Segmentation fault (core dumped)
[root@localhost 10]#

说明:

1. 不加NULL时,在free(p)之后还可打印,
	因为free只是让操作系统回收这块内存,但是该快内存上的值还未改变(有的系统会释放)
	所以它的值将是不确定的,可能是原值,也可能是乱码。(依据不同系统而言)
	
2. 不加NULL时,在free(p)之后也可赋值,
	因为p还是指向之前那块内存区域,但是它已经被回收了,会被其他程序使用,这样会造成值覆盖。
	
3. 加NULL时,再操作就非法访问了

参考:
https://blog.csdn.net/weixin_48344581/article/details/114995109

2.9.2 通俗易懂说明:

释放指针后的操作就好像我们租房子,租一段时间后退租了,但是我们还有房子的钥匙。
1. 退租后,如果房东没有把房子收拾,那么我们还可以进去查看我们留下的物品;
2. 退租后,如果房东没有把房子收拾,那么我们还可以进去放东西或者住一晚;
3. 退租后,如果房东把房子收拾,打扫干净了,并且换了锁,那么我们就进不去了

2.9.3 野指针:

一、野指针
   1、指针变量中的值是非法内存地址,进而形成野指针
   2、野指针不是NULL指针,是指向不可用内存地址的指针
   3、NULL指针并无危害,很好判断,也很好调试
   4、C语言中无法判断一个指针所保存的地址是否合法

二、野指针由来
  1、局部指针变量没有初始化
  2、指针所指向的变量在指针之前被销毁
  3、使用已经释放过的指针
  4、进行了错误指针运算
  5、进行了错误的强制类型转换

参考:C语言中的野指针问题
https://blog.csdn.net/liuchunjie11/article/details/80969689

2.10 引起存储器泄露(没有free导致的内存泄露)

malloc之后忘记free导致内存泄露

参考:linux 开源内存泄露检测工具: valgrind
https://blog.csdn.net/lqy971966/article/details/110563576

3. 一些关于虚拟存储器的关键概念(java诞生原因之一:虚拟存储器)

在这一章里,我们已经看到了虚拟存储器是如何工作的,系统如何用它来实现某些功能,例如加载程序、映射共享库以及为进程提供私有受保护的地址空间。我们还看到了许多应用程序正确或者不正确地使用虚拟有储器的方式。

3.1 一个关键的经验教训是,

即使虚拟存储器是由系统自动提供的,它也是一种有限的存储器资源,应用程序必须精明地管理它。

正如我们从对动态存储分配器的研究中学到的那样,管理拟存储器资源可能包括一些微妙的时间和空间的平衡另一个关键的经验教训是,在C程序中很容易犯与存储器有关的错误。坏的指针值、释放己经空闲了的块、不恰当的强制类型转换和指针运算,以及覆盖堆结构,这些只是可能给我们带来麻烦的许多方式中的一小部分。

3.2 java诞生原因之一:虚拟存储器

实际上,与存储器有关的错误很讨厌,这是导致Java产生的一个重要原因,
Java取消了取变量地址的能力,完全控制了动态存储分配器,从而严格控制了对虚拟存储器的访问。

4. 第十章总结

1. 虚拟存储器是对主存的一个抽象。

2. 支持虚拟存储器的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。
    处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。
	从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作。
	专门的硬件通过使用页表来翻译虚拟地址,而页表的内容是由操作系统提供的。

3. 虚拟存储器提供三个重要的功能。
	3.1 第一,它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。
		虚拟存储器缓存中的块叫做页。
		对磁盘上页的引用会触发缺页,缺页将控制转移到操作系统中的一个缺页处理程序。
		缺页处理程序将页面从磁盘拷贝到主存缓存,如果必要,将写回被驱逐的页。
	
	3.2 第二,虚拟存储器简化了存储器管理,
		进而又简化了链接、在进程间共享数据、进程的存储器分配,以及程序加载。
		(因为每个进程都有相同规格的4GB的虚拟地址空间)
		
	3.3 第三,最后,虚拟存储器通过在每条页表条目中加入保护位,从而了简化了存储器保护。
	
	
4. 地址翻译的过程必须和系统中任意硬件缓存的操作集成在一起。
	大多数页表条目位于LI高速缓存中,但是一个称为TLB的页表条目在芯片上的高速缓存,
	通常会消除访问在L1上的页表条目的开销。
	
5. 存储器映射
	现代系统通过将虚拟存储器组块(chunk)和磁盘上的文件组块关联起来,
	来初始化虚拟存储器组块,这个过程称为存储器映射。
	
	5.1 存储器映射为共享数据、创建新的进程以及加载程序,提供了一种高效的机制。
		
	5.2 应用可以使用mmap函数来手工地创建和删除虚拟地址空间的区域。
	
	5.3 然而,大多数程序依赖于动态存储器分配器,例如malloc,
		它管理虚拟地址空间区域内一个称为堆的区域。
		
	5.4 动态存储器分配器是一个有系统级感觉的应用级程序,它直接操作存储器,而无需类型系统的很多帮助。
	
	5.5 分配器有两种类型:显式分配器要求应用显式地释放它们的存储器块:
		隐式分配器(垃圾收集器)自动释放任何无用的和不可达的块!

6. 对于c程序员来说,管理和使用虚拟存储器是一件困难和容易出错的任务。
	常见的错误示例包括:
		1. 间接引用坏指针(访问非法地址)
		2. 读取未初始化的存储器(对未初始化的指针进行操作)
		3. 允许栈缓冲区溢出(数组缓冲区溢出)
		4. 假设指针和它们指向的对象大小相同
		5. 引用指针而不是它所指向的对象(指针运算符优先级问题)
		6. 误解指针运算
		7. 引用不存在的变量
		8. 引起存储器泄露
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值