Bug是每个程序猿都不愿意遇到的异常代码,但是无论愿意与否,在每个项目中肯定会有bug存在,这是一种很正常的情况,尤其是在代码量很大的时候。就我自己而言,遇到Bug然后Debug然后放弃最后再回来重新Debug......,因为心中总惦记着Bug产生的原因以及解决方案,这种砰然心动的感觉就是滴八哥(DeBug)。
问题初现:
此次Bug来源于一个C++移植的代码,是很久前做一个特殊视频方案时用到的,当时由于时间比较紧张所以在网上找到了一个国外程序猿移植的同名Delphi项目(暂且命名为A项目),所以直接拿来使用。结果在使用中发现很多问题,对代码作了大量修改,在使用中发现在某些情况下存在异常的情况,由于时间原因没有定位Bug,只是通过外部加代码"曲线救国"的方式暂时解决了。问题虽然解决了,但是滴八哥的种子已经在程序猿的心中种下,就等条件满足生根发芽......
问题复现:
前几日在处理另一个特殊的RTP推流的视频修复案例时,再次用到了A项目,结果在调试时发现了之前一直存在的老Bug,而且这次外部代码无效了(不仅如此,为解决问题的外部代码又带来了新的问题)。此事充分证明,Bug不会随着时间的流逝而消失,反而"曲线救国"的代码会带来更多的不确定性。
所以当发现Bug的时候一定要从根源上解决问题,而不是给这个Bug打上新的"补丁"!
滴八哥 (DeBug):
经过43200秒的不懈努力终于定位到了bug,"talk is cheap, show me the code",先上代码:
Delphi
NOISE_HCB:
begin
result:=29;
exit;
end;
没错一个CASE语句,看起来没有一点问题,但是这个选项过于诡异,因为经过追踪发现这个值是一个关键值,不可能直接退出。到这一步需要找到移植前的源码进行对比,就是这么幸运,随手在必应上搜索就找到了源码,不过不是C++而是VC++,话不多说,看代码:
cpp
#ifndef DRM
if (noise_flag)
{
noise_pcm_flag = 0;
t = (int16_t)getbits(ld, 9
) - 256;
} else {
t = scale_factor(ld);
t -= 60;
}
noise_energy += t;
isc->factors[g][sfb] = noise_energy;
......
#else
return 29;
#endif
可以看到源码中也存在返回29,不过是在"DRM"这个宏定义存在的情况下(即#else之后),而#ifndef则是指宏定义没有被定义时执行,所以很明显DRM是否被定义重要,经过确认DRM并不存在(猜测可能是作者开发前期为了方便调试设置的宏)。接下来是就是把这些代码移植过来了,量不大,很快完成,完成后做了测试,结果发现了另一个贯穿整个程序的Bug。出错的代码如下:
Delphi
for filt:= 0 to tns.n_filt[w]-1 do //for循环的初始化表达式类型出错
begin
......
end;
看代码的话是看不出任何问题的,一个标准的for循环,出问题的地方是for循环的初始化表达式类型,也就是变量filt是byte类,有人要说byte类没啥问题,循环照样跑,问题是for循环的结束表达式tns.n_filt[w]也是byte数组,当tns.n_filt[w]值为0时会触发一个逻辑异常,byte类取值范围是0-255,而0-1=-1,-1格式化成byte类对应的就是255,这就导致本来不作循环的代码进行了错误的循环。
问题的根源还是数字类的"有符号"和"无符号"导致的,在delphi中一般for循环使用的是integer整数型(有符号),这种类型是可以正常识别负号的;对应的byte、dword都属于无符号类型,其不识别负号。
个人估计第1个Bug可能是移植代码的作者一时疏忽导致的,毕竟代码量不少出错也正常;而第2个Bug就是硬伤了,没有考虑到DELPHI IDE下数值为负数时出现"逻辑错误"的基本问题,所以这个作者大概率是一位"根正苗红"的C++程序猿(因为这个问题C++上压根不会出现)。
有符号 数**---问题的根源:**
作为二进制的产物,电脑无论操作系统还是底层硬件压根是没有负数的。而负数是完全为了把人类的数学知识延伸到电脑而人为设置的。
比如8位的无符号数取值范围是0(00)-255(FF),而8位有符号数的取值范围是-128(80)~127(7F)。这样就解决了负数的问题,可以看到高位为1的统一加负号。

图1:同样的HEX值FF有符号的取值为-1而无符号则为255
Delphi 的for循环演示:
代码的演示是最有效的,直接先看运行结果。

这可以看到同样的for循环只有初始表达式为INTEGER的整数型运行正常,其它dword和byte在结束表达式为-1的情况下进行了错误的循环。从另一方面也能看到DELPHI IDE在处理for循环时只关注初始表达式的类型,无论结束表达式是否同类都会格式化成同类去处理。
演示程序代码如下:
Delphi
procedure TForm2.btn_openClick(Sender: TObject);
var
loop1_INT:integer;
loop1_Byte:byte;
End_INT:Integer;
loop1_DWORD:DWORD;
End_Byte:byte;
str1:string;
cnt1,loop1_int64:Int64;
begin
cnt1:=0;
End_Byte:=0; loop1_INT := 0;
for loop1_INT := 0 to End_Byte-1 do
begin
Inc(cnt1);
end;
str1:='['+FormatdateTime('ddddd',now)+' '+FormatDateTime('tt',Now())+']'+
'初始化表达式类型:'+'INTEGER'+','+
'结束表达式类型:'+'Byte'+';循环次数:'+IntToStr(cnt1);
form2.mmo1.Lines.Add(str1);
loop1_DWORD :=end_byte-1;
cnt1:=0;
End_Byte:=0; loop1_dword := 0;
for loop1_dword := 0 to End_Byte-1 do
begin
Inc(cnt1);
end;
str1:='['+FormatdateTime('ddddd',now)+' '+FormatDateTime('tt',Now())+']'+
'初始化表达式类型:'+'Dword'+','+
'结束表达式类型:'+'Byte'+';循环次数:'+IntToStr(cnt1);
form2.mmo1.Lines.Add(str1);
//初始化和结束类型都为byte
loop1_Byte :=end_byte-1;
cnt1:=0;
End_Byte:=0; loop1_Byte := 0;
for loop1_Byte := 0 to End_Byte-1 do
begin
Inc(cnt1);
end;
str1:='['+FormatdateTime('ddddd',now)+' '+FormatDateTime('tt',Now())+']'+
'初始化表达式类型:'+'byte'+','+
'结束表达式类型:'+'Byte'+';循环次数:'+IntToStr(cnt1);
form2.mmo1.Lines.Add(str1);
end_byte:=0;
loop1_int64 :=end_byte-1;
loop1_int :=end_byte-1;
loop1_dword :=end_byte-1;
loop1_byte :=end_byte-1;
str1:='['+FormatdateTime('ddddd',now)+' '+FormatDateTime('tt',Now())+']'+
'byte类(0-1)时不同变量的取值结果:'+#$0d+#$0a+
'变量取值(int64):'+IntToStr(loop1_int64)+','+#$0d+#$0a+
'变量取值(int):'+IntToStr(loop1_int)+','+#$0d+#$0a+
'变量取值(dword):'+IntToStr(loop1_dword)+','+#$0d+#$0a+
'变量取值(byte):'+IntToStr(loop1_byte)+','+#$0d+#$0a;
form2.mmo1.Lines.Add(str1);
end;
Bug 的解决:
实际上解决方案有以下几种:
- 改for循环为while。这种效果最好,因为不用减1;
- 改for循环的初始表达式类型为smallint有符号数值。不现实,因为很多自定义类,不可能一个个改;
- for循环前加一行条件判断,非0才可以进行循环;
作为一个狠猿,本着为难自己的原则,必须要用最难搞的方法,所以果断选择第三种,而这个Bug是贯穿整个程序的,所以修改也用了很长时间。

图3:修改成功的程序
总结:
- 当发现Bug时一定要在第一时间彻底解决,不要尝试给Bug打补丁,这是一种愚蠢的形为,只会让问题变的更加复杂!
- 在处理移植代码时一定要注意IDE的差异,避免掉入"逻辑错误"的陷阱。
- 最重要的一点,Bug不会随着时间的流逝而消失,与其假装Bug不存在倒不如静下心来Debug。