软件开发中的BUG案例
软件开发中的BUG案例
1 概述
众所周知,软件开发过程中BUG是难以避免的。但是⼀个训练有素的程序员却能将BUG的出现率尽可能的降低。本⽂档将BUG粗略地分为⼏个⼤类,以便于学习参考。
程序结构和处理逻辑类:包括程序的结构,算法的选择和实现等。
可移植性类:包括跨平台代码的移植、封装等。
可维护性类:包括诊断性代码、测试⽀持、注释、命名风格等。
其他问题:不好归类的BUG、实践技巧等。
2 程序结构和处理逻辑
2.1 ##
某Linux应⽤程序采⽤了DailyBuild,为了⾃动维护其构建版本号,我们将每⽇构建的版本号单独定义为:#define BUILDNO?“0001”
需要引⽤该版本号的地⽅采⽤了预编译操作符“##”:
#define VERSION?“8.0.”##BUILDNO””
#define VERSION_STR “8.0.”##BUILDNO” Special Release for RedHat Linux 8.0”
这在GCC 3.3之前⼯作得很好,可是换成了 GCC 3.3.1 后,出现了错误:
foo.c:127:33: pasting ""8.0."" and "BUILDNO" does not give a valid preprocessing token
解决的办法很简单,就是将“##”去掉。结尾的空串””也是多余的。操作符“##”的⽤途主要是⽤于宏展开时将参数保留为字符串形式,例如:
#define __CONCAT(x, y)?x##y
__CONCAT(foo, bar)
2.2 变量初始化
某系统⽀持UNIX命令⾏风格的命令,例如:SHOW SETTINGS等。其语法分析代码中使⽤了⼀个全局字符串数组,⽤于记录某些特殊的语法⽚断。可是该变量不是每次语法分析启动前都初始化的,导致以下现象发⽣了:
某个命令执⾏第⼀次没有问题,但连续执⾏4次就会导致系统内部的内存检查模块报告异常。
因为问题很容易重现,系统内部的内存检查机制⼯作正常,很快定位到相关的代码:
char ParseString[1024];
if (ParseString == NULL)?/* 注1 */
strcpy(ParseString, parse_segment);
else
strcat(ParseString, parse_segment);
原来是strcat每次都是盲⽬地往ParseString后⾯追加字符串,执⾏4次后内存越界了。解决的办法很简单,在语法分析初始化的地⽅给ParseString初始化:
ParseString[0] = '/0';
显然“注1”标记的代码也是不对的,⾄少应该改为:
if (ParseString[0] == '/0')
事实上,这部分的代码问题相当严重,已经违背了当初系统设计的基本原则,要解决其全部问题,只能重新进⾏设计和编码了。从客户反映来看,出⾃该部分的BUG也最多。
2.3 ⽇志信息不能及时写到⽂件
某系统采⽤标准输⼊输出函数进⾏诊断性的⽇志信息输出,在不将错误输出重定向到⽂件时,⽇志信息能够及时输出。例如:c:/foo>
LOG: FOO is starting up
LOG: Using data file located at c:/foo/data/db.dat
LOG: Client 1 connected
可是在将错误输出重定向到⽂件,并在UltraEdit中打开该⽂件时,不能及时看到⽇志信息。⼀天早上,
程序员⼩A突然⼤叫:“系统的⽇志信息没有了!”。资深程序员⽼K跑去⼀看,命令⾏使⽤⽅式正常,UltraEdit中打开的⽂件中有主进程打出的信息,并不是什么也没有。不过⼦进程的输出不见了。命令⾏:
C:/foo> 2>foo.log
error parse new
乍⼀看,似乎整个⽇志系统失效了。该系统是C/S结构,服务器会⽣成⼀些服务进程,⾸先怀疑是不是⼦进程的输出被丢弃了。经过代码分析,表明创建⼦进程时已经处理了各种句柄的继承,不应该存在问题。随意地连上了⼀个客户端,发了不少命令过去,再看看⽇志⽂件,主进程和⼦进程的⽇志记录⼀个也不少。⾄此,问题已经⽐较明确:应该是某些地⽅忘了调⽤fflush()了。到系统的log_trace()函数,补上fflush(),测试、回归测试,搞定。
2.4 共享数据的同步处理
某系统已经运⾏了⼀定的年份了。系统结构⽐较传统,是多进程结构。在UNIX下的表现不错,于是决定将其搬到Windows上来。移植当然是个⽐较⿇烦的过程,也遇到了不少问题。经过⼏个⽉的努⼒,系统已经可以在Windows上运⾏了。可是压⼒测试下,偶尔会出现⼀些错误信息:
“⽆效的系统配置参数”
在UNIX下,fork()进程时,⼦进程会⾃动继承⽗进程当前所有的全局变量和打开的句柄等。可是在Windows下,这些就不能完全照搬了。我们只好把某些全局变量通过⼀块专门的共享内存由⽗进程传递给⼦进程。这些全局变量就包含了⼏个全局的系统配置参数。怀疑出问题的地⽅⾃然就在这段代码了。
出现错误信息的时候,我们让系统Assert(false),程序就会⾃动弹出⼀个带有“Abort”、“Retry”、“Ignore”按钮的对话框(这其实是VC的调试功能)。点击“Retry”,即可追踪出错的进程了。看了半天,毫⽆头绪,不理解⽗进程传递来的东西怎么会突然变成了“⽆效参数”了呢?
⼀夜没睡好。早上起来突然醒悟了:⼀定是共享内存的同步没做好。⽗进程在填写完共享内存中的参数后,要等待⼦进程读完参数信息,才能再次fork()(Windows下其实是CreateProcess())下⼀个⼦进程。修改、测试、回归测试、压⼒测试,⼀切OK。
2.5 预编译宏:WIN32
某系统为第三⽅的开发者提供了⼀套函数库,采⽤的是Windows下的DLL。函数库的头⽂件foo.h写得⽆可挑剔:
#ifndef FOO_H
#define FOO_H
#ifndef DLLIMPORT
#ifdef WIN32
#define DLLIMPORT __declspec(dllimport)
#else
#define DLLIMPORT
#endif /* WIN32 */
#endif /* !DLLIMPORT */
#ifdef __cplusplus
extern "C" {
#endif
extern DLLIMPORT struct foo_struct?foo_var;
void bar(void);
#ifdef __cplusplus
}
#endif
#endif /* FOO_H */
系统内部测试、⽤户试⽤,反馈都很好。突然某⼀天,有个⽤户发邮件来了,“你们的系统怎么连变量都没有初始化?”。经过仔细询问,原来在他们的程序中发现变量foo_var的成员没有初始化,函数库⽆法使⽤。请⽤户将他的例⼦代码发送过来。嘿,这⽤户还挺⾼级的,没⽤VC⾃动⽣成的项⽬,⾃⼰写的Makefile,⽤NMAKE编译。打开Makefile,问题就在这⾥了:
PROJECT = test
CPP = cl
LINK = link
INCS = /I"C:/Program Files/Microsoft Visual Studio/VC98/Include"
INCS = $(INCS) /I"C:/Program Files/foo/include"
LIBS = /LIBPATH:"C:/Program Files/Microsoft Visual Studio/VC98/lib"
LIBS = $(LIBS) /LIBPATH:"C:/Program Files/foo/lib"
OBJS =
LINKLIB = kernel32.lib user32.lib
LINKLIB = $(LINKLIB) libfoo.lib
CPPOPT = $(INCS) /O2 /Ot /G6 /c
LINKOPT = $(LIBS) /SUBSYSTEM:CONSOLE /MACHINE:X86 $(LINKLIB) $(OBJS) /OUT:$(PROJECT).exe
$(PROJECT).exe: $(PROJECT).obj $(OBJS)
.c.obj:
$(CPP) $(CPPOPT) $<
.:
$(LINK) $(LINKOPT) $<
“/D WIN32”到哪⼉去了?没有了它,foo.h将不会导出DLL中的foo_var.
3 可移植性
3.1 signed char
类型char应该是C中使⽤的最多的数据类型了。可是你可曾想到,char类型并不是那么的简单易⽤,由它导致的BUG甚⾄会耗费了好多天的时间?在吃过⼀次苦头后,下⾯的⽂档对你应该有些助益:
MSDN:
The char type is used to store the integer value of a member of therepresentable character set. That integer value is the ASCII code corresponding to the specified character.
Microsoft Specific —>
Character values of type unsigned char have a range from 0 to 0xFF hexadecimal. A signed char has range 0x80 to 0x7F. These ranges translate to 0 to 255 decimal, and –128 to +127 decimal, respectively. The /J compiler option changes the default from signed to unsigned.
END Microsoft Specific
VC中char缺省对应为signed char,因此下⾯的语句是正确的:
char flag;
if (flag == -1)
可是其他的编译器未必如此(况且CL的开关“/J”还可以改变VC的缺省设置)。上述的代码显然是不可移植的。要么避免对char类型使⽤0x00-0x7F之外的值(例如:-1),要么改⽤signed char或int等。
4 可维护性
4.1 修改函数参数个数
某系统中划分了很多⼦系统,为了保证系统的层次清晰,各⼦系统严格定义了调⽤层次和对外的接⼝API。系统⼤量使⽤了C语⾔中常⽤的办法:静态函数,来避免不必要的外部函数。处于性能优化的考虑,某开发⼈员给现有的⼀个对外函数foo()增加了⼀个开关参数,指⽰其是否进⾏特别的优化处理。但是这个函数在⼏⼗个地⽅被调⽤,修改接⼝势必涉及所有这些地⽅。⽽需要将这个新参数设置为true的地⽅仅有⼀个⽂件bar.c,该⽂件包含两处对foo()的调⽤。因此我们采⽤下⾯的修改办法:
1. 将foo()改名为foo_ex();
2. 重写新的foo()为:
inline void foo(void)
{
foo_ex(false);
}
3. 将bar.c中对foo()的调⽤修改为foo_ex(true);
当然,这种⽅法的使⽤仅适合于某些特殊的场合:
1. 调⽤foo的地⽅太多,⽽⽤到foo_ex的地⽅很少且⽐较集中
2. ⼦系统对外的接⼝轻易不能变化
3. 其他⼦系统甚⾄应⽤程序的源代码不能得到
这种办法在Windows的WIN32 API升级中经常可以看到。⽐如:CreateWindowEx()、WaitForSingleObjectEx()等等。
5 其他问题
5.1 从EXE中导出函数和数据
开发动态连接库时,我们需要导出函数和数据。在Windows中开发DLL更为简单,VC、BCB等IDE都提供了⾮常好的向导。编译器对导⼊/导出函数和数据的⽀持⽐较类似,⼀般都是采⽤dllimport/dllexport这样的关键字来修饰函数和数据的定义。当然,DEF⽂件也是⽐较⽅便的。
其实EXE⽂件也可以导出函数和数据。只不过我们不能在其DEF⽂件中写上LIBRARY节。在EXE调⽤DLL,⽽被调⽤的DLL⼜想调⽤EXE中的函数和使⽤EXE中数据时,就需要采⽤这样的功能了。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。