常见的内存错误

好久没有更新博客了,一方面是最近一段时间很忙很忙,策划需求不明确,开会喷案子,修改再修改,最后形成定稿,不容易;另一方面是负责了比较重要的系统,需要花更多的时间去分析,整理方案,评估更好的方案和未考虑到的风险,还要不断的完善和修改策划丢过来的需求,然后写文档(会在周会上,可能拿出来分享,发现问题),编码,自测,联调,回归测试等等。这篇主要是总结阅读书籍:《深入理解计算机系统》和《C/C++安全编码》上的一些点和面,这些问题在工作中也常会碰见,虽然很少会发生那些错误。

一般内存错误都是因为读或写非法地址造成的,写比读严重很多,运气好点造成宕机早点发现问题,运气不好会修改不该修改的数据而不被发现,那样排查问题就比较困难。

1:间接引用坏指针

由于进程的虚拟地址空间中存在较大的洞,没有映射到任何有意义的数据,如果程序中解引用它,那么就会段错误:

比如语句scanf("%d", &val)被无意的写成了scanf("%d", val),编译的时候是不能发现这种隐晦的错误,那么scanf会把val的内容解释为一个地址,去写该地址,如果该地址不代表任何合法地址,那么试图写入就会段错误。

2:读写未初始化的变量

主要发生在申请堆内存或栈空间时,定义的变量一定要初始化,否则会读到上一次使用后脏的数据位,因为在释放栈空间或者堆内存时,并不会把对应的位模式清零,在使用该变量时在初始化它。

3:缓冲区溢出

当使用一些C库函数从源缓冲区赋值内容到目标缓冲区时,需要注意复制的长度,相反会多写后面的内存影响其他数据;也可能会乱写函数的返回地址和帧指针(32bit是ebp),那么会造成漏洞使得函数跳转到攻击者指定的地方[后期会专门写一篇来介绍缓冲区溢出的危害]。

4:越界

主要是没有正确设置好截止条件,造成写了不应该写的内存,一般引起越界的情况包括差一错误,没有正确以空字符结尾的字符数组等,比如

int32_t HandleArray()

{

    int32_t iArrList[5];

    for (int i = 0; i <= 5; ++i)

    {

          iArrList[i] = 100;

    }

}

上门这种情况可能会写乱ebp的内容,退栈时使得esp指向其他地方,或者当i <= 6时,把函数地址给覆盖了,会造成3情况中的问题,当然在栈上分配空间规则看编译器,也许不会出现那些问题;

这里有个例子,具体问题是来自一位实习生面试腾讯时面试官问的,具体是"有一个类指针,指向类实例化的对象,在这个对象程序的运行过程中,程序崩溃了,后来发现是这个类指针的虚函数表被破坏了,现在如何定位这个问题"[https://www.zhihu.com/question/43416744],下面有些不错的回答(看了笔者的描述,好像有些问题)。如果让我来分析:

class CBase

{

    public:

        virtual int foo() {...}

};

CBase * pBase =  new CBase; //暂不考虑引用

pBase->foo();  //1

这样类的每个实例,都会在实例的首或者尾部有个指针,具体多少位看机器的架构,指向某个地址,该指针被称为vptr,某地址所代表的是一个虚函数表,表里的每个项是虚函数的地址和其他类型信息;这个虚函数表存放在只读的.rodata区(read only data),受操作系统保护,那么,题中已经说明了该虚函数表【非对象的vptr内容被非法修改】被破坏了(笔者描述错误?不是对象的vptr?),导致程序宕机了。由于调用函数会被名字重整,故1处会被改写大概如下形式:(*pBase->vptr[1])(pBase),在没有调用前,就宕机了。

这里有两种可能,pBase的值是该对象的地址,pBase分配在栈上,对象分配在堆上[由于虚机制只能以引用或指针触发(不考虑如int *,void *等)],很可能因为越界导致pBase的内容被非法改写,如果此时pBase为非法地址,那么pBase->vptr会宕机,但不是笔者描述的虚函数表被破坏了;如果pBase没有被非法改写,而它所代表的对象的内容被非法改写了[由于发生在堆上,下面会分析此类问题的原因],那么pBase->vptr的值(地址)是有问题的,然后在这个有问题地址的偏移量(+sizeof(函数指针))获得有问题的值,去解引用,即调用函数,就会宕机。这两个问题都与虚函数表无关,因为还没有到那一步。

那么虚函数表被破坏,即.rodata被非法改写了,那么在调用前就应该由操作系统保护而触发宕机,而不是到在pBase->foo()时宕机。所以,这个问题要么是笔者写错了,要么是笔者没有理解面试官的提问,再要么是想面试者回答这个bug不是因为调用虚函数而引起的(已经宕机了,程序已经休息了)。

总结:数组越界错误很难排查,可能出现问题了,已经里错误代码很远了,也可能很难复现,比如在多线程中。

5:引用指针,而不是它所指向的对象

主要发生在没有弄清楚C语言操作符的优先级和结合性,就会错误的操作指针,而不是指针所指向的对象,比如 int * pInt = &iValue;

本意是想对pInt所指向的内容减一:(*pInt)--,却写成了*pInt--,由于这两个操作符的优先级相同,故从右向左结合,那么语句”*pInt--“等价与”*(pInt--)",后者表示对指针移动到pInt + sizeof(int)位置再解引用,那么就造成了内存非法访问,而不是修改最初的地址,所以当写代码时最好加上括号。


6:引用不存在的变量

如果返回指向函数局部变量地址的指针或引用时就会出现这个问题,比如:

int * GetWrongPointer()

{

    int iValue = 10;

    return & iValue;

}

由于iValue在函数调用时在栈上分配的,会有个地址,当函数返回栈回退时,该地址所代表的已经不是一个合法的变量了,虽然地址是有效的;如果该地址是个类实例,那么会调用析构函数,已经是残骸了,脏的位模式,一切的访问都是未定义的。如果该地址正好被其他函数的局部变量使用,那么就发生了非法访问。

7:引用已经释放的内存

在malloc,new获得的指针后,调用了相对应的free,delete后,没有把该指针赋值为NULL,free和delete本身不会做p =

NULL这件事。如果已经释放的内存重新分配,那么在使用p访问或者修改相应的内存,会引起难排查的bugs,如果没有分配,可能会破坏内存管理器所使用

的数据结构。

如 int * p = new int(100);

     delete p;   //最好再写一条 p = NULL

   int * q = new(10);

     *p = 200;   

即使用了已经释放的内存,p的值还是个有效地址,此时解引用p可能修改了q值的内存地址处的内容,造成非法访问了。


8:内存泄露

主要是那些有限资源,向系统申请用完后需要归还的资源,但没有归还。平时中指的较多的是堆内存,随着进程的运行,申请内存的次数越来越多,这个在初期较难发现,在最糟糕的情况下,会占用整个虚拟地址空间,导致进程无法正常工作。

对于内存泄露定位的问题,使用valgrind工具可以发现出来,或者code review,或者gdb pid,然后shell pmap pid,分析,我自己没有在工作中实践过。

http://blog.csdn.net/lw1a2/article/details/5598006

9:多次释放同一块内存

比如 int * p = new int;

        delete p;

        //more code

        delete p;

一方面会造成内存管理器的数据结构,另一方面造成可被利用的漏洞。


10:没有正确的检查分配失败

使用malloc分配内存,失败时返回NULL,用户代码需要检查,而new分配时,要么成功,要么跑出异常,所以下面用法:

int * p = new int;

if (p != NULL)

{

   //more code

}

是不正确的,应该这样使用: int * p = new(std::nothrow) int;在失败时返回NULL而不是个异常。

11:不正确的内存分配和释放配对使用

如用malloc分配的使用delete释放,用new分配的用free释放,用new[]分配的用delete释放等等,会引起一系列的问题。

malloc只申请内存不初始化,而new申请并调用构造函数,后者底层实现也是使用malloc来分配的,如果使用new申请而free释放就会造成内存泄露什么的,不会调用析构函数;在分配对象数组时,会多申请空间存放管理信息比如个数,为后来的delete []正确调用;如果申请一个对象而使用delete[],就可能破坏内存管理器的数据结构,破坏其他内存,导致对内存损坏。

还有重载operator new的问题



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • (JG-2014-08-20)(前半部分经过网上多篇文章对比整理)(后半部分根据ExceptionalCpp、C+...
    JasonGao阅读 5,592评论 2 23
  • C语言中内存分配 在任何程序设计环境及语言中,内存管理都十分重要。在目前的计算机系统或嵌入式系统中,内存资源仍然是...
    一生信仰阅读 1,142评论 0 2
  • const 引用 const 引用是指向 const 对象的引用:const int ival = 1024;co...
    rogerwu1228阅读 613评论 0 1
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,278评论 0 6
  • 学而第一共十六章 此篇论君子、孝悌、仁人、忠信、道国之法、主友之规,问政在乎行德,由礼贵于用和,无求安饱以好学,能...
    7望月阅读 1,632评论 0 0