2012年7月16日星期一

谈谈内存越界读写的危险

谈谈内存越界读写的危险
  -- 记某一次低级而糟糕的程序犯错。


# 故事
这是发生在上周的事情了。当时就有想法,要写篇文章以表示‘铭记’。

想必,这个话题对于C/C++程序员一定很熟悉,都有过或多或少,或轻微或严重(惨痛)的经历吧。

最近正在开发的项目,正在内部开发人员自己测试阶段,突然爆出好多的程序崩溃,大概估摸了下这些崩溃的规律如下:

     1,程序崩溃不确定性但必崩,只要你多操作几次,或重启程序再来试试。
     2,这些崩溃基本都不一样,(从堆栈看来)分散在不同的几个模块;
     3,分析程序崩溃堆栈看到崩溃在正常的代码上(显然不可能),亦看不出错误代码地方了。
     4,由于有辅助程序可在程序崩溃时候帮助自动抓取程序堆栈。但是有时程序崩溃无堆栈或堆栈数据无效。
……

最后,辛苦地安分地回头好好Review最近提交的代码,(……由于这几次提交改动均无法自己做到完整的测试保证,只能自己简单的功能程序和代码Review……),涉及的代码量较大,所以花了不少时间才找到了错误所在,有地方内存越界读写了。虽然找到了错误并很快纠正过来了,但是那样的低级错误让吾情何以堪?也正是为了纪念这么一‘尴尬’,我才想整理一篇博文表示‘罪过’。前车之鉴,后车之师。


个人觉得,上面总结的规律四点不妨可作为“内存越界读写”这样错误的表象。若以后还是遇见这样情况,不如先怀疑怀疑你的内存被越界读写了,赶紧回头仔细Review代码吧,相信你已很快顺利找到错误地方……

# 示例
下面内容就简单描述一下如何导致程序乱蹦,即越界“乱踩”内存了!示例代码如下:

struct A {
     ...
};
struct B {
     ...
     A a;
     ...
};

// 错误程序如下:
void some_func(struct B* b_ptr) {
     ...
     memset(&(b_ptr->a), 0, sizeof(*b_ptr)); // Error: memory overwrite.
     ...
}

// 修正程序如下:
void some_func(struct B* b_ptr) {
     ...
     memset(&(b_ptr->a), 0, sizeof(b_ptr->a));    // 1. ok
     // memset(&(b_ptr->a), 0, sizeof(struct A)); // 2. or else, also well.
     ...
}

果然吧,错误的程序那么低级,So stupid,而找到了错误修正程序那么简单的!程序就恢复正常了。
我想当时写程序脑子犯晕了吧。好了,不再多说了。还是我以前的那么句话,“以后写程序时候头脑得时刻保持精神点,否则后果有你好受的。”

# 借鉴
最后顺便引用一段关于内存越界的描述吧,延伸看看,想想。
--
对Memory overwrite 和memory corruption有什么好办法哪?常见的,就是设置CPU的breakpoint,也就是对某一地址的写操作,dump出stack,然后找是哪个模块写坏的。或者可以把整个page设置成只读,在操作系统层面检查,然后dump。这些对只读的变量或者page还还用,但是对可读写的变量又该怎么办哪?如何区分合法的读写和非法的读写?
对于非法的读写,也有在语言层次进行控制的,比如c++/java/c#等面向对象的语言,就有对对象的保护。但是这些语言并没有排除全局变量,也就是说,在语言层次的控制并不彻底。基于语言的静态检查是个好办法,但是对大规模代码,检查也需要时间和精力。任何经过仔细review的代码都是好代码,但是,有哪些公司能够坚持严格的代码review哪?
对于第三方和legacy的代码,review也基本是不现实的,但是静态检查是第一道防线,一定要坚持。
在操作系统层面,没有对象的概念,导致在操作系统层面没法保护。或者说,操作系统层面的保护代价太大了。我觉得还是操作系统的设计思想没有体现保护,隔离的要求,传统操作系统在这方面,做的很不够。
新的操作系统应该能够在更细粒度上做到隔离,保护,但是性能,通信等等又如何解决?

-- 

还有,在stackoverflow上面关于内存越界的一段讨论,这里

(完)




没有评论:

发表评论