善意提醒
2011-11-29
汇编还是不能丢啊
借此机会又再次温习了汇编语言,大学时候学的 8086 / 8088 汇编指令虽然早已过时,但思想还算没忘。那些关于现在的大学念了白念的说法,我觉得还是需要探讨。
之前用 tds2dbg 完成的工作也有价值,帮我把代码段确定了。C++ 代码能定位到就这一两句,那么再来对照汇编指令就比较好理解了。
另外,今天还学习到了 LEA 指令的相关知识,顺便网摘一篇对我有点帮助的博文:
http://www.cnitblog.com/textbox/articles/51912.html
掌握了更好的 BCB 程序调试手段
上次查过,有个办法是使用 map2dbg,把 map 文件转换成 dbg 文件,这样 windbg 也能够加载符号文件用于调试。不过 map 里面只有函数名称,没有代码的行号,所以调试起来还是不是很方便。如果断点是在一个长长的函数里面,而且没有嵌套调用什么函数,那么对于汇编功底不深的我也是一样的郁闷。
今天下了点决心要解决这个问题,否则调试效率太低了。
感谢万能的 Google,这次我知道了有个开源项目叫 tds2dbg。用法和 map2dbg 类似,生成的 dbg 文件的确可用,而且在正确的行号上指出了我遇到的问题。这样,Crash 就不再是一个问题了。
值得注意的是:BCB 编译选项中,必须打开 Compiler->Debugging->Line Number Information,以及 Linker 里面的 Create debug information。否则生成的 dbg 文件没法用。
2011-11-14
遇到 Bug 就像踩了地雷
平心而论,这段代码写得不怎么样。估计这位同事当时已经心不在焉了,只想早早结束了事吧。代码中充满了他此前做过的一个 BCB 项目的风格,主要的功能是用 C 风格的代码来实现(例如文件读写等),而工程又是一个 VC6 的 Win32 DLL,里面写了自己的类等等。我看了之后第一感觉就是想推了重写,好不容易才压制住这种想法。
当时我给自己安排了两天来接收并改造这些代码,后来拖长到了三四天。其实现在想想还不如重构。给我的代码功能测试都通不过,里面 Bug一大堆。我这人又是有一点代码洁癖的家伙,遇到空格缩进什么的都忍不住花时间去调。最后终于弄好了,真有点不想再去碰了,虽然我知道值得改进的地方还很多。
结果今天就碰到了一个藏在其中的 Bug。
这个日志模块被设计为保存加密日志,目的嘛……是为了防止用户抵赖。加密就不能全文加密,否则每来一条日志都得解密再加密,量稍微一大就死翘翘了。差分加密是必须的。设计的时候是对每一条日志的内容进行加密,只留下必要的信息放在每一条的头部以便索引。从性能和安全性方面权衡,当时选择了 AES 算法。
代码交到我手上时,之前那个同事大概已经从网上找了一段 AES 算法封装好了。既然能实现加密解密的功能,我也就没往里面细看。谁知道这玩意儿是线程不安全的。今天专拣双核机器报错,从 fseek 到 fopen 查了一圈,终于怀疑到这个 AES 算法上。浪费了约两个小时时间。
我估计我有时间可能真得把它给重写了,里面指不定还有什么问题。
2011-10-24
VC 和 BCB 那点事——DLL 导出函数的结构体参数
LONDATEEX __stdcall GetDataDate( USHORT sMarket);
BOOL __stdcall GetDataDate( USHORT sMarket, LONDATEEX* pDate);
2011-10-21
VC 和 BCB 那点事——虚函数的顺序问题
class A
{
……
virtual FunA() = 0;
virtual FunB() = 0;
……
};
class A
{
……
virtual FunB(int J) = 0;
virtual FunA() = 0;
virtual FunB() = 0;
……
};
class A
{
……
virtual FunB(int J) = 0;
virtual FunB() = 0;
virtual FunA() = 0;
……
};
2011-10-08
都是负数惹的祸?
十一长假之后第一天上班,继续调试那位还在婚假当中的同事留下的代码。上午查到的一个 Bug 让我相当无语。不过鉴于自己如果思想在开小差的话也很可能会犯类似错误,故记录下来警醒自己。
看看这段代码:
#define ST_ERR -1
……
BYTE result = ST_ERR;
……
if (result == ST_ERR)
return errmsg;
事实上,这个if语句根本就没得到执行。即使 result 在赋值了 ST_ERR 之后从未改变,代码在执行时仍然跳过了判断,继续执行下面的代码。也就是说,result 明明赋值了 ST_ERR,但却不等于 ST_ERR。
难道遇到鬼了,result 中间被篡改了?溢出过?
好吧,其实 result 的确不是 -1,而是 255。看看 BYTE 的定义就清楚了。BYTE 并不是 C/C++ 内建的数据类型。在 Win32 平台上,它通常都被定义为 unsigned char。
到这里就明白了,unsigned 当然不可能有负数。而 ST_ERR 因为用了宏定义,当做常量使用的时候并不会自动转成非负的 unsigned 形式,这样当然不可能相等了。
说穿了,不是负数惹的祸,而是「宏」这个东西惹的祸。如果用 const 常量,因为常量的定义上有类型信息存在,就不会有这个问题了。
当然,这里无意评价宏与 const 常量的好与坏,单纯指出问题而已。目前这个公司的代码,宏用得不少,难保会有菜鸟程序员出这种错,隐患啊!
2011-10-04
初始化,真的很重要
刚花了大半天的时间解决掉一个 Bug,再次证明了变量初始化的重要性。照例,还是先来看一小段代码:
map<string, TEOBJ> telist; // 全局对象 …… TEOBJ spte(3); telist["debug"] = spte;
很简单吧?按道理这点代码应该没有什么问题。不过在最后一句中却抛出了异常,发生了崩溃。负责这部分代码的人昨天就请了假回家了,节后还有婚假,一走要近二十天,只好自己动手找原因。
这里的 TEOBJ,是一个自己定义的类。在我的这个案例中,它是一个底层库里面定义的数据对象类,用在了很多地方。基于这个前提,我首先判断这个 Bug 的故障点在赋值号的左边。
可是左边也是个 std::map 的相当标准的用法,STL 出错的可能性恐怕比我们自己的底层库更低,而且这个用法我以前也用过很多,没有出过什么问题。怀疑的眼光于是又回到了赋值号的右边。
那么到底是哪一边呢?我也拿不准了,于是排除法上马。通过几轮替换,我终于把问题基本定性为「由 TEOBJ 引起,但需要在赋值到 map 的 [] 运算符时产生」。
既然说到赋值,那么就该看看 TEOBJ 的相关声明了:
class TEOBJ {
public:
int GroupID;
private:
int GroupCode;
public:
TEOBJ::TEOBJ() {};
TEOBJ::TEOBJ(int nGroupID) {
GroupID = nGroupID;
GroupCode = TDataStore::Groups[GroupID]->Code;
}
TEOBJ::TEOBJ(const TEOBJ& r) {
GroupID = r.GroupID;
GroupCode = TDataStore::Groups[GroupID]->Code;
}
const TEOBJ& TEOBJ::operator=(const TEOBJ& r) {
GroupID = r.GroupID;
GroupCode = TDataStore::Groups[GroupID]->Code;
}
};
这个 TEOBJ 重载了拷贝构造函数和赋值运算符,在其中通过查询一个数据字典确保 GroupCode 有准确的取值。
首先,赋值运算符重载这里的返回值有点小问题。const 引用类型的返回值与习惯上的 non-const 引用类型的返回值有所区别。不过这个最多造成编译时语法检查上的问题,不至于引起指针错误。
其次,就是这个 TDataStore::Groups[GroupID] 的数组的使用值得怀疑了。
实际案例比这个情况要复杂一些,而且我对于 BCB 的使用还不算熟悉,调试基本靠 OutputDebugString。问题就诡异在,往 TEOBJ::operator=() 里面只要任意加一行 OutputDebugString,故障就消失了。搞得好像是野指针一样。
我在此浪费了很多时间,走了不少弯路。后来,一咬牙用上了 windbg,配合 map2dbg,好歹是得到了 CallStack。但是 map2dbg 之后得到的符号名称又和我以前看到的有些不同,于是又错判了位置。
最后,我终于在 windbg + 排除法的协助下找到了问题。问题根本不在赋值运算符重载函数中。尽管这里代码中明显是用到了它,但它并没有出问题。抛出异常的是拷贝构造函数。这个就要从 map::operator[] 的实现说起了。
Borland C++ Builder 5 里面的 std::map,其实现代码在 map.h 里面,是一个 inline 函数。挖出来一看,倒也很简单:
mapped_type& operator[] (const key_type& k) {
value_type tmp(k,T());
return (*((insert(tmp)).first)).second;
}
可以看到,这里先生成了一个临时变量(其实本例中就是 pair<string, TEOBJ>)。在生成这个临时变量的时候,采用了无参数的 TEOBJ 构造函数。此时 TEOBJ::GroupID 还是没有初始化的,在 Release 版本中它可能是任意值(TEOBJ 所在的底层库就是 Release 编译)。
实际使用时,TEOBJ 都通过 TEOBJ::TEOBJ(int nGroupID) 来初始化,因此不会产生这种未被正确初始化的实例。但在 map::operator[] 中,由于是「先插入空值,再传出引用用于赋值」,这个时候就使得拷贝构造函数被调用,于是产生了类似的访问野指针的效果,导致故障的出现。
通过在拷贝构造函数中用 OutputDebugString 输出 GroupID 的取值,确认了这一点。
至此,真相大白。总结经验教训如下:
- 变量的初始化很重要,一个也不能放松,不能因为眼下不初始化也不会出问题,就放松警惕。做底层库的人尤其应该重视——你的代码不只是你一个人在用!
- 数组的下标,或者说指针的偏移量,这种变量与指针基本上是同样的性质。要提防野指针,它们也要算上。
- 调试手段无所谓牛刀不牛刀,好用就用,不要因为感觉问题小、简单,就先用笨办法尝试,直到一筹莫展了之后才考虑别的办法。如果我一开始就用 windbg,时间上可能会节省更多一些。
在 STL 容器中删对象一定要小心
先来看一段代码:
typedef map<string, string> Dict;
Dict eventmap;
for (Dict::iterator pIter = eventmap.begin(); pIter != eventmap.end(); ++pIter) {
if (pIter->second == "no")
eventmap.erase(pIter);
}
熟悉 STL 的人都知道,这段代码是错的。出问题的可能性不是 100%,但是相当大,而且可能会是那种不一定能稳定重现的问题,往往会搞得人很恍惚。
自己很早以前就吃过这个亏,那时候还不太会用 STL。当定位到这一段代码后,凭直觉也觉得如果这样删除对象,那删除之后的 pIter 是不是正确的?可能很难讲。毕竟学过「数据结构」,大概能猜到各种容器里面的信息是如何组织的。
昨天又看见同事在这样写代码。可能他对 STL 也不熟。至少我目前对于 erase 这个单词已经产生了足够的警戒,一旦要用的时候就会想起吃过的亏来。
正确的代码应该是:
typedef map<string, string> Dict;
Dict eventmap;
for (Dict::iterator pIter = eventmap.begin(); pIter != eventmap.end();) {
if (pIter->second == "no")
pIter = eventmap.erase(pIter);
else
++pIter;
}
另外,昨天发现 Borland C++ Builder 提供的 STL 和 VC 有点区别,map::erase 没有返回值,因此不支持这种写法。那么还有另一种办法:
eventmap.erase(pIter++);
这样就可以了。相比之下,这种写法可能适应性更强一些。
2011-03-29
CWebBrowser2 打开 PDF 后退出时崩溃问题的解决
- http://forums.adobe.com/message/2373918(链接已失效):贴子中的人也是一筹莫展了快一年,最后按下面的办法解决了;
- http://stackoverflow.com/questions/1359616/axacropdflib-at-closing-problem-c:简单地说,的确是没关干净。加上一句 CoFreeUnusedLibraries() 就万事大吉了。