« 温故而知新 | 返回首页 | C# 版的 Lua »

内存异常排查

今天开电话会议,帮助合作方排查 C++ 开发的程序崩溃的原因。

现象是这样的:

有一个 C++ 对象,其中有一个 vector ,里面存放了大量对象指针。这个 C++ 对象在构造完毕后,没有任何改写操作,全部是只读的。

在某个退出环境中,C++ 对象被析构,析构函数需要调用 vector 中所有对象指针的删除方法。这时,这个长度大约为 100 多的 vector 第 95 个指针是错的。比它应该的指针位置偏移了一到三字节(多次运行现象不一,出错几率也不大)。

整个数组只有这一个指针错误。因为这个错误,引发了程序崩溃。

从电话中的描述,我推断:

这不太可能是由错误的调用此 C++ 对象的方法改写 vector 的内容导致的,因为这个对象全部是读方法,并加了 const 修饰。且,这个数组保存的是对象地址。一个由正常的内存分配器分配出来的地址都是对齐的,但此处错误的被改写为奇数。

这个错误也不太可能是由内存越界写造成的,因为在这一长片内存中,只有一个指针异常。

最大的可能是:某个对象被删除,但其指针还在使用造成的,所谓悬空指针问题。

那么,导致是这个出问题的对象指针悬空,还是另一个对象指针悬空影响的呢?

最大的可能是,这个出问题对象所占据的内存空间,曾经被别的对象使用过。但是前任使用者释放了内存,却在某处保留了指针。然后出问题的对象复用了这块地址。

做这个判断是因为,如果出问题的对象本身是悬空指针,那么后来者占用了它的内存的话,应该成片的改写。

从现象可以推断,内存异常一定是对原有数据进行递减造成的,而不是简单的覆盖了一个新的值。

从退出时出错可以判断,这个 C++ 对象在有效生命期间很有可能一直是正常的,否则指针错误很可能很早就暴露出来了。

那么,我认为最大的可能性就是:有另一个 C++ 采用了引用计数的机制。这个对象曾经引用到 0 而被正确的析构并释放掉了。但某种原因在另一个地方还保留了针对它的 raw 指针。

这个被释放掉的对象的内存被后来的出问题的这个 C++ 对象复用,一系列的退出析构操作导致了一系列的对象析构函数的调用。那个悬空的 raw 指针被调用了。这个指针指向的对象没有虚表,所以它的析构函数可以正确的执行。引用计数这个量存在的位置恰巧在后来出问题的 C++ 对象的中间。

减引用的函数把这个位置的值,也就是另一个对象的指针作为数值减了 1 ,发现不到 0 就跳过了。接下来的析构操作到这个位置时,访问了不正确的指针。这里期盼的指针引用的对象有虚表,偏移的差别导致跳转到虚析构函数出错。

从电话里可以获得的信息有限,我的推断只能到这里了。

为了进一步的盘查错误,我建议:把所有的对引用计数的操作,包括标准库中的智能指针的代码,都加上 assert 判断。断言所有的引用计数的值都应该在 0 到 10000 之间。

我并不是说智能指针的实现会有问题,尤其是采用标准库的实现一定没有问题。

但是,当一个对象本身被释放后,它的(悬空)指针还可以被调用析构函数却是一个隐患。悬空指针调用的对象中如果有一个智能指针的话,那么这个智能指针的析构函数依旧是会做递减操作的。


顺便吐槽 C++ 。

我现在看到的这个 C++ 项目,如果用一个标准的 windows 下的 malloc ,也就是一个性能比较低下的内存管理器,性能简直不能接受。你必须换一个非常优秀的内存管理器才能正常工作。这样的依赖内存分配器的性能,在我见过的 C 开发的项目中几乎不可能存在。

这是因为 C++ 的项目多半层次混乱。我说的混乱,不一定指开发逻辑层次上的混乱,而是假借高性能之名,看起来在源代码层次把软件的层次分清楚了,但是在二进制层面却是混杂在一起的。

一个小小的内存管理模块,就穿插于最底层到最上层。

一个层次分明的系统,在物理上就应该是相互隔离的,这种隔离,仅仅存在于人阅读的源代码层是绝对不够的。这就好比 OS 管理下的应用进程,它绝对不依赖应用进程的程序的工作正常,不依赖应用进程准确的申请和释放资源。而是当应用进程结束后,干净的回收它申请过的所有东西。

小到同一进程下的软件,也理所当然要按这个思路进行才对。C++ 不强调这一点,反而鼓励程序员生产出庞大的软件,并美其名曰:信任程序员。但却把用 C++ 的程序员引向一条邪路。信任程序员和放任程序员是两码事。

Comments

云风大佬你的思考模式对我很有启发!

不错,,,

为什么不用硬件内存值改变断点呢?

这里水很深

感谢楼主分享

http://jinshanduba.org.cn/

感谢博主分享

http://jinshanduba.org.cn/

感谢博主分享

http://jinshanduba.org.cn/

感谢博主分享

http://jinshanduba.org.cn/

释放操作循环导致栈溢出,不谢

这个问题貌似我们曾经也遇到过,很蛋疼,当时代码也写的有点混乱,很难查,最后导致整个毁弃了,没人在敢用那个框架了

c++应该不会没落。相对于其它编程语言,它最强大处在于它的编译时。

猜测一下,是不是可能这样的情形,代码其它的地方对这个指针加了std::auto_ptr保护,退出的时候,需调用release的地方没有调用,导致指针被意外的删除了。

都一样。看个人优势和选择。

选不选c++和团队背景有关系,毕竟c++学习周期长不可能强迫所有人学

但c/c++03的用户还是有必要关心下c++最近的动态,原因是c++11写起来更轻松并且功能强大的库越来越多了。

现在只要适当跟住c++11/boost的步伐,就很少需要造车轮,系统长期维护的成本大幅降低,完全可以抵消版本升级的成本。

举几个例子

1. c++11有了异步函数之后,message queue实现变得非常容易,可以看下herb sutter今年1月4号给的讲座,给出了block mutex的各种non-block替代方案。整个实现一页ppt就写完了,很有启发性。

2. 今年2月4号即将发布的boost1.5.3会加入coroutine,使得c++下的coroutine变为现实。

3. 如果是windows平台,vs2010/2012还自带了asynchronous agents libary实现了actor model。

上面三个库写几个例子试试吧,用起来很容易。

可以和lua,zeromq比较下,相信c++还是很有吸引力的

javascript的对象模型实际上设计的比lua不合理(它的int, bool 都是对象),变量不明显地声明local, 这是造成不能更快的原因,而众所周知的,luajit不稳定至今还没得到广泛使用

其实这些例子并非是说孰能孰不能,c++有门槛,多少没理解好c++的人在使用,这是问题所在,而c++又降低了另外一块门槛,减轻了对对象管理,资源管理的负担.

同理,像是你那样使用c的人也需要大量积累,门槛估计不低,

每种语言的都有其使用价值,从个人而言,c++的使用价值很大,在未来10年内,这种趋势不太可能改变

C++的内存问题大都是由于RAII引起的,RAII在背后做了一些不太明显的事情。
C中可以自定义malloc和free方法,在里面做很多的内存检测,C++却很难做到这点。
我在每个分配的内存块前后都用额外的空间藏了效验码,这样可以检测内存的越界问题。
https://bitbucket.org/chunquedong/fancpp

@hyde

事实上, 你必须说服所有人, 包括你用的库的开发人员, 都按你觉得好的方式用 C++ 才成立.

而且等 C++2x 出现时(如果能有的话) 你会发现 C++1x 就是渣, 所有人都应该迁移到 C++2x 上去.

@lichking

v8 远不如 luajit 快.

问题解决了吗?

对了,还有世界上最好的编译器架构llvm

我也吐槽一下,世界上最快的脚本编译器(v8)是c++完成的,用了大量奇巧的模板技巧

C++03里allocator(state-less)是有大问题,一直被C/C++用户诟病,这点必须承认。
但C++是在不断进化的,C++11新引入了scoped_allocator来解决内存分配的问题

于是现在stl中使用memory arena已经轻松很多了,虽然语法上还是罗嗦了点,但是在实现上已不构成障碍。Bjarne Stroustrup的C++11 faq里给了4种内存分配的例子,其中一种可以做到物理上的完全隔离。比如vector<string>里所有内存都分配在指定的memory arena里,包括vector分配的和string分配的。这在C++03里是完全做不到的。

vs2012已经实现了这一特性,相信gcc应该更早就支持了。

-----------------------------

不要让vector<T*>拥有对象的所有权,这不是异常安全的,一但抛出异常立即造成内存泄露。(vector不会删除T*所指对象)
应该用vector<unique_ptr<T> >,需要主动删除时用reset,这样才符合RAII要求。

现代C++全部资源都可以用RAII管理,且没有任何性能损失。

不像C/lua/java,现代C++产生内存/资源泄露的可能几乎为零

总之以上的scoped_allocator + RAII完全可以构建清晰的内存布局
用的好heap和stack几乎没区别
------------------------------
有机会还是学习一下C++11吧,8年过去时代不同了

C++11才是真正的duck typing,auto用到飞起

lua动态语言实在太不安全,没有强静态类型检查来帮助消除语法/类型bug,做大项目就是死,调运行时才出现的bug太老命伤财了

设计和语言是两个层次的问题,在这里说C++语言带来了层次不分明,这个论点就是层次不分明的!

C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do, it blows away your whole leg. -- Bjarne Stroustrup, developer of the C++ programming language

1、资源不仅仅是内存。
对很小的玩具项目,可能不用管内存就很好了;但正常的项目,内存之外还有多种多样的资源。所以清晰的语言比“有保姆的语言”更好。

所以,好多年了,当潮水退去的时候,c的使用率又回到了第一。

2、同意你说的 隔离 。当然,我觉得你应该没有精确表达出自己的意思,然后别人也没理解对……

我觉得你可能是想说这句话:

Being abstract is something profoundly different from being vague… The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise. — Edsger Dijkstra

抽象和模糊完全地不同,抽象的目的并不是把事情变模糊,而去创建一个新的语义层,在那里是绝对精确的描述。 — Edsger Dijkstra


偶来东施效颦一把: 设计和流水帐完全的不同。设计是面向要做的事情,把每步要做什么搞清楚;然后,创建一个个语义层,让下一步通过、且只能通过相对应的语义层,去管理上一步完成工作。
而流水帐,是写一大堆的class,然后以为它就是抽象。但不管你把class的层次分的有多清楚、class间的关系定义的有多精确,只要没有创建出完备、精确的语义层,它就还是流水帐。

迷迷糊糊看了个大概,对象本身被删除了,但是vector中的指针没有被清除?

“有另一个 C++ 采用了引用计数的机制。这个对象曾经引用到 0 而被正确的析构并释放掉了。但某种原因在另一个地方还保留了针对它的 raw 指针。”后文又说,raw指针指向对象的析构函数调用,改变了引用计数值。这里raw指针指向的是一个什么样的对象?

to Anonymous:

你介绍的工具我基本都用过,这些工具都主要是在库级别增强了检测,只能查出一部分问题。Dr.Memory/Valgrind则是另外一种工具,原理是对每个汇编指令都会检测地址合法性。所以能差多大部分内存问题。

to tana :

在windows下,微软提供的排查内存错误的工具,其实不少。windbg,gflags,debugdiag等等。微软还提供了一个叫application verifier的工具,可以帮助开发人员在开发阶段检查内存使用等等。

看完了整篇文章, 发现java和c#早就解决了所有的这些问题.
C++的没落不是没道理的, 未来C/C++的地位迟早会落到现在汇编的地位.

内存问题在windows下面使用Dr.Memory(类似于valgrind,性能甚至更高,3d游戏都可以勉强跑),linux下面则是valgrind,可以解决大部分内存问题,包括越界读写,野指针,malloc/free问题,值得一试。程序方面可以对容易出问题的地方采用handle机制替代指针机制减小犯错和查错的成本。

不知道云风对内存问题调试分析以及防范内存问题还有什么经验是否可以写出来,感觉很多C/C++程序员面对内存问题这个坎都过不去。

你的意思是不是说这个C++项目的设计有问题, 以至于性能比较低下的内存管理器会拖累性能?为什么在C语言项目里,这个问题可以就不容易出现?

学习中,那个指针太无辜了.

既然如此,为什么要用?

C++的错误之处就在于它想为应用程序服务,但是又保留了指针。

使用MALLOC_CHECK_环境变量进行检测。
http://www.360doc.com/content/09/0311/10/36491_2775021.shtml

这样的问题能不能用valgrind之类工具检查呢?我经常用,就是速度慢。

在一般意义上的“物理上就应该是相互隔离”是premature optimization。实际需求是,若需要分隔,总能有花费足够小的成本来达到这一点,而不是彻头彻尾地假设和强制。无论如何,C++在这点上确实比较弱,但也远非放任。这点以及程序边界的抽象不足的有两个共同原因——一是对于传统的核心用户来说不太需要,至少没能形成普遍的习惯;二是历史包袱太重,想要根本地改变,对旧的系统的维护很要命,工作量也不切实际。
与其指望语言能怎样通过“强调”克服质量差的程序,还不如把培训和教育搞得靠谱点。毕竟,再好的语言也阻止不了用户写出渣程序。

二进制层面——指的是运行时环境?

有道理,,,,,,,

Post a comment

非这个主题相关的留言请到:留言本