内存访问错误学习笔记Word文件下载.docx
- 文档编号:5836297
- 上传时间:2023-05-05
- 格式:DOCX
- 页数:15
- 大小:21.91KB
内存访问错误学习笔记Word文件下载.docx
《内存访问错误学习笔记Word文件下载.docx》由会员分享,可在线阅读,更多相关《内存访问错误学习笔记Word文件下载.docx(15页珍藏版)》请在冰点文库上搜索。
另外一种是写越界,又叫缓冲区溢出。
所写入的数据对别人来说是随机的,它也会产生不可预料的后果。
内存越界访问造成的后果非常严重,是程序稳定性的致命威胁之一。
更麻烦的是,它造成的后果是随机的,表现出来的症状和时机也是随机的,让BUG的现象和本质看似没有什么联系,这给BUG的定位带来极大的困难。
一些工具可以够帮助检查内存越界访问的问题,但也不能太依赖于工具。
内存越界访问通常是动态出现的,即依赖于测试数据,在极端的情况下才会出现,除非精心设计测试数据,工具也无能为力。
工具本身也有一些限制,甚至在一些大型项目中,工具变得完全不可用。
比较保险的方法还是在编程是就小心,特别是对于外部传入的参数要仔细检查。
3.
野指针。
野指针是指那些你已经释放掉的内存指针。
当你调用free(p)时,你真正清楚这个动作背后的内容吗?
你会说p指向的内存被释放了。
没错,p本身有变化吗?
答案是p本身没有变化。
它指向的内存仍然是有效的,你继续读写p指向的内存,没有人能拦得住你。
释放掉的内存会被内存管理器重新分配,此时,野指针指向的内存已经被赋予新的意义。
对野指针指向内存的访问,无论是有意还是无意的,都为此会付出巨大代价,因为它造成的后果,如同越界访问一样是不可预料的。
释放内存后立即把对应指针置为空值,这是避免野指针常用的方法。
这个方法简单有效,只是要注意,当然指针是从函数外层传入的时,在函数内把指针置为空值,对外层的指针没有影响。
比如,你在析构函数里把this指针置为空值,没有任何效果,这时应该在函数外层把指针置为空值。
4.
访问空指针。
空指针在C/C++中占有特殊的地址,通常用来判断一个指针的有效性。
空指针一般定义为0。
现代操作系统都会保留从0开始的一块内存,至于这块内存有多大,视不同的操作系统而定。
一旦程序试图访问这块内存,系统就会触发一个异常。
操作系统为什么要保留一块内存,而不是仅仅保留一个字节的内存呢?
原因是:
一般内存管理都是按页进行管理的,无法单纯保留一个字节,至少要保留一个页面。
保留一块内存也有额外的好处,可以检查诸如p=NULL;
p[1]之类的内存错误。
在一些嵌入式系统(如arm7)中,从0开始的一块内存是用来安装中断向量的,没有MMU的保护,直接访问这块内存好像不会引发异常。
不过这块内存是代码段的,不是程序中有效的变量地址,所以用空指针来判断指针的有效性仍然可行。
在访问指针指向的内存时,在确保指针不是空指针。
访问空指针指向的内存,通常会导致程度崩溃,或者不可预料的错误。
5.
引用未初始化的变量。
未初始化变量的内容是随机的(像VC一类的编译器会把它们初始化为固定值,如0xcc),使用这些数据会造成不可预料的后果,调试这样的BUG也是非常困难的。
对于态度严谨的程度员来说,防止这类BUG非常容易。
在声明变量时就对它进行初始化,是一个编程的好习惯。
另外也要重视编译器的警告信息,发现有引用未初始化的变量,立即修改过来。
6.
不清楚指针运算。
对于一些新手来说,指针常常让他们犯糊涂。
比如int*p=…;
p+1等于(size_t)p+1吗
老手自然清楚,新手可能就搞不清了。
事实上,p+n
等于
(size_t)p+n*sizeof(*p)
指针是C/C++中最有力的武器,功能非常强大,无论是变量指针还是函数指针,都应该掌握都非常熟练。
只要有不确定的地方,马上写个小程序验证一下。
对每一个细节都了然于胸,在编程时会省下不少时间。
7.
结构的成员顺序变化引发的错误。
在初始化一个结构时,老手可能很少像新手那样老老实实的,一个成员一个成员的为结构初始化,而是采用快捷方式,如:
Struct
s
{
int
l;
char*
p;
};
main(int
argc,
argv[])
struct
s
s1
={4,
"
abcd"
return
0;
}
以上这种方式是非常危险的,原因在于你对结构的内存布局作了假设。
如果这个结构是第三方提供的,他很可能调整结构中成员的相对位置。
而这样的调整往往不会在文档中说明,你自然很少去关注。
如果调整的两个成员具有相同数据类型,编译时不会有任何警告,而程序的逻辑上可能相距十万八千里了。
正确的初始化方法应该是(当然,一个成员一个成员的初始化也行):
={.l=4,.p
=
s2
={l:
4,
p:
8.
结构的大小变化引发的错误。
我们看看下面这个例子:
base
n;
base
b;
m;
在OOP中,我们可以认为第二个结构继承了第一结构,这有什么问题吗?
当然没有,这是C语言中实现继承的基本手法。
现在假设第一个结构是第三方提供的,第二个结构是你自己的。
第三方提供的库是以DLL方式分发的,DLL最大好处在于可以独立替换。
但随着软件的进化,问题可能就来了。
当第三方在第一个结构中增加了一个新的成员intk;
,编译好后把DLL给你,你直接给了客户了。
程序加载时不会有任何问题,在运行逻辑可能完全改变!
原因是两个结构的内存布局重叠了。
解决这类错误的唯一办法就是全部重新相关的代码。
解决这类错误的唯一办法就是重新编译全部代码。
由此看来,DLL并不见得可以动态替换,如果你想了解更多相关内容,建议阅读《COM本质论》。
9.
分配/释放不配对。
大家都知道malloc要和free配对使用,new要和delete/delete[]配对使用,重载了类new操作,应该同时重载类的delete/delete[]操作。
这些都是书上反复强调过的,除非当时晕了头,一般不会犯这样的低级错误。
而有时候我们却被蒙在鼓里,两个代码看起来都是调用的free函数,实际上却调用了不同的实现。
比如在Win32下,调试版与发布版,单线程与多线程是不同的运行时库,不同的运行时库使用的是不同的内存管理器。
一不小心链接错了库,那你就麻烦了。
程序可能动则崩溃,原因在于在一个内存管理器中分配的内存,在另外一个内存管理器中释放时出现了问题。
10.
返回指向临时变量的指针
大家都知道,栈里面的变量都是临时的。
当前函数执行完成时,相关的临时变量和参数都被清除了。
不能把指向这些临时变量的指针返回给调用者,这样的指针指向的数据是随机的,会给程序造成不可预料的后果。
下面是个错误的例子:
char*get_str(void)
char
str[]={"
str;
char*argv[])
p
=get_str();
printf("
%s/n"
p);
下面这个例子没有问题,大家知道为什么吗?
get_str(void)
str
={"
get_str();
11.
试图修改常量
在函数参数前加上const修饰符,只是给编译器做类型检查用的,编译器禁止修改这样的变量。
但这并不是强制的,你完全可以用强制类型转换绕过去,一般也不会出什么错。
而全局常量和字符串,用强制类型转换绕过去,运行时仍然会出错。
原因在于它们是是放在.rodata里面的,而.rodata内存页面是不能修改的。
试图对它们修改,会引发内存错误。
下面这个程序在运行时会出错:
;
*p
'
1'
12.
误解传值与传引用
在C/C++中,参数默认传递方式是传值的,即在参数入栈时被拷贝一份。
在函数里修改这些参数,不会影响外面的调用者。
如:
#include<
stdlib.h>
stdio.h>
voidget_str(char*p)
p=malloc(sizeof("
));
strcpy(p,"
);
return;
intmain(intargc,char*argv[])
char*p=NULL;
get_str(p);
p=%p/n"
p);
return0;
在main函数里,p的值仍然是空值。
13.
重名符号。
无论是函数名还是变量名,如果在不同的作用范围内重名,自然没有问题。
但如果两个符号的作用域有交集,如全局变量和局部变量,全局变量与全局变量之间,重名的现象一定要坚决避免。
gcc有一些隐式规则来决定处理同名变量的方式,编译时可能没有任何警告和错误,但结果通常并非你所期望的。
下面例子编译时就没有警告:
t.c
#include
<
count
=0;
get_count(void)
count;
main.c
externintget_count(void);
intcount;
count=10;
get_count=%d/n"
get_count());
如果把main.c中的intcount;
修改为intcount=0;
,gcc就会编辑出错,说multipledefinitionof`count'
。
它的隐式规则比较奇妙吧,所以还是不要依赖它为好。
14.
栈溢出。
我们在前面关于堆栈的一节讲过,在PC上,普通线程的栈空间也有十几M,通常够用了,定义大一点的临时变量不会有什么问题。
而在一些嵌入式中,线程的栈空间可能只5K大小,甚至小到只有256个字节。
在这样的平台中,栈溢出是最常用的错误之一。
在编程时应该清楚自己平台的限制,避免栈溢出的可能。
15.
误用sizeof。
尽管C/C++通常是按值传递参数,而数组则是例外,在传递数组参数时,数组退化为指针(即按引用传递),用sizeof是无法取得数组的大小的。
从下面这个例子可以看出:
void
test(char
str[20])
%s:
size=%d/n"
__func__,
sizeof(str));
}
str[20]
={0};
test(str);
[root@localhostmm]#./t.exe
test:
size=4
main:
size=20
16.
字节对齐。
字节对齐主要目的是提高内存访问的效率。
但在有的平台(如arm7)上,就不光是效率问题了,如果不对齐,得到的数据是错误的。
所幸的是,大多数情况下,编译会保证全局变量和临时变量按正确的方式对齐。
内存管理器会保证动态内存按正确的方式对齐。
要注意的是,在不同类型的变量之间转换时要小心,如把char*强制转换为int*时,要格外小心。
另外,字节对齐也会造成结构大小的变化,在程序内部用sizeof来取得结构的大小,这就足够了。
若数据要在不同的机器间传递时,在通信协议中要规定对齐的方式,避免对齐方式不一致引发的问题。
17.
字节顺序。
字节顺序历来是设计跨平台软件时头疼的问题。
字节顺序是关于数据在物理内存中的布局的问题,最常见的字节顺序有两种:
大端模式与小端模式。
大端模式是高位字节数据存放在低地址处,低位字节数据存放在高地址处。
小端模式指低位字节数据存放在内存低地址处,高位字节数据存放在内存高地址处;
比如longn=0x11223344。
模式
第1个字节
第2个字节
第3个字节
第4个字节
大端模式
0x11
0x22
0x33
0x44
小端模式
在普通软件中,字节顺序问题并不引人注目。
而在开发与网络通信和数据交换有关的软件时,字节顺序问题就要特殊注意了。
18.
多线程共享变量没有用valotile修饰。
在关于全局内存的一节中,我们讲了valotile的作用,它告诉编译器,不要把变量优化到寄存器中。
在开发多线程并发的软件时,如果这些线程共享一些全局变量,这些全局变量最好用valotile修饰。
这样可以避免因为编译器优化而引起的错误,这样的错误非常难查。
可能还有其它一些内存相关错误,一时想不全面,这里算是抛砖引玉吧,希望各位高手补充。
~~end~~
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 内存 访问 错误 学习 笔记