小程子找Bug之for循环的初始化表达类型

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”,先上代码:

NOISE_HCB:

begin

result:=29;

exit;

end;

没错一个CASE语句,看起来没有一点问题,但是这个选项过于诡异,因为经过追踪发现这个值是一个关键值,不可能直接退出。到这一步需要找到移植前的源码进行对比,就是这么幸运,随手在必应上搜索就找到了源码,不过不是C++而是VC++,话不多说,看代码:

#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。出错的代码如下:

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循环时只关注初始表达式的类型,无论结束表达式是否同类都会格式化成同类去处理。

演示程序代码如下:

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的解决:

实际上解决方案有以下几种:

  1. 改for循环为while。这种效果最好,因为不用减1;
  2. 改for循环的初始表达式类型为smallint有符号数值。不现实,因为很多自定义类,不可能一个个改;
  3. for循环前加一行条件判断,非0才可以进行循环;

作为一个狠猿,本着为难自己的原则,必须要用最难搞的方法,所以果断选择第三种,而这个Bug是贯穿整个程序的,所以修改也用了很长时间。

图3:修改成功的程序

总结:

  1. 当发现Bug时一定要在第一时间彻底解决,不要尝试给Bug打补丁,这是一种愚蠢的形为,只会让问题变的更加复杂!
  2. 在处理移植代码时一定要注意IDE的差异,避免掉入“逻辑错误”的陷阱。
  3. 最重要的一点,Bug不会随着时间的流逝而消失,与其假装Bug不存在倒不如静下心来Debug。
; MASM专用版(无任何NASM语法) stack segment stack dw 100h dup(0) stack ends data segment ; 32个测试数据(含正负) BLOCK dw -1, 2, -3, 4, -5, 6, 7, 8, 9, 10 dw 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 dw 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 dw 31, 32 ; 打印提示字符串 prompt db '处理后前5个结果:$' data ends code segment assume cs:code, ds:data, ss:stack start: ; 初始化数据段 mov ax, data mov ds, ax ; 1. 处理数据:负数求补,正数不变 mov si, offset BLOCK ; MASM必须用offset取偏移 mov cx, 32 ; 32个数循环 process_loop: mov ax, [si] ; 取当前数 or ax, ax ; 判断正负(SF标志) jns next ; 非负则跳过 neg ax ; 负数求补 mov [si], ax ; 存回原地址 next: add si, 2 ; 双字节,偏移+2 loop process_loop ; 2. 打印前5个结果 mov si, offset BLOCK mov cx, 5 ; 打印提示字符串(DOS中断09h) mov ah, 09h lea dx, prompt ; MASM用lea取字符串偏移 int 21h print_loop: mov ax, [si] push cx ; 保护寄存器 push si call print_dec ; 调用打印程序 ; 打印空格分隔 mov ah, 02h mov dl, ' ' int 21h pop si ; 恢复寄存器 pop cx add si, 2 loop print_loop ; 3. 程序退出(DOS中断4Ch) mov ah, 4Ch int 21h ; 程序:打印AX中的16位有符号十进制数 ; 输入:AX = 待打印数 | 输出:控制台打印 print_dec proc near ; MASM声明近程程序 push bx push cx push dx mov bx, 10 xor cx, cx ; 处理负数 test ax, ax jns positive neg ax push ax mov ah, 02h mov dl, '-' int 21h pop ax positive: ; 分解数字到栈 xor dx, dx div bx push dx inc cx cmp ax, 0 jne positive ; 打印栈中数字 print_digit: pop dx add dl, 30h ; 转为ASCII(等价于add dl,'0') mov ah, 02h int 21h loop print_digit ; 恢复寄存器 pop dx pop cx pop bx ret print_dec endp ; MASM结束程序声明 code ends end start ; MASM程序结束
12-07
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值