1.系统和平台熟悉
在一个新的平台上开发或者移植一款软件的时候,首先应该充分了解平台或者操作系统的各种属性,这些属性包括但不仅限于:
1)系统的任务调度,任务间的通信机制
任务调度包括是否是多任务实时操作系统,任务以何种方式存在的,如何添加和管理任务?任务间的优先级如何设置?任务间的优先级设置?任务堆栈?任务间的通信机制包括了解系统提供的通信机制,各种通信机制的优劣,通信细节等等。
案例1:
曾经在某一个平台上做一个软件,创建了多个任务,由于各个任务实现者不一样,一个漫不经心的错误造成了本该是高优先级的任务没有在优先级上体现出来,A,B,C,D4个任务,本来应当是A>B>C>D,结果设置成A>C>B>D,为此debug了一上午。
教训:
- 多方协作开发事先要把优先级定好,书面文档描述
- 写代码时一定要切忌马虎,有时候一些漫不经心的错误会无法估计地增加很多调试时间。即使是测试代码,也要严谨。养成写完代码检查的习惯!
案例2:
代码执行到一个函数突然跑飞,检查函数没有问题,大家都知道,跑飞在嵌入式系统中是很难跟踪定位问题了,只能根据经验不断做实验缩小疑点范围最后发现是该任务堆栈设置太小所致
案例3:
移植一款软件到某一款手机,代码加载上去发现开不了机原因:多个任务的堆栈开销大于系统提供的堆栈资源
2)系统的内存管理
系统采用的是动态内存机制还是静态内存机制,如果是内存分块机制,则需要了解块的划分标准和使用细节。如果是动态内存机制,则需要了解是否有动态内存泄漏的监测工具。在嵌入式系统中,资源是个很大的问题,因此,还需要了解系统总的内存资源是个什么样的情况,本软件或程序又可以使用多大的内存开销。BTW,资源的统计还包括定时器、内务、优先级等。
案例4:
代码执行到某个函数跑飞,该函数由于历史原因是一个嵌套比较深的函数,笔者不是特别熟悉,基本没有用单步,trace或者缩小范围的方法来定位问题。后来想到在熟悉平台的时候做过实验,当申请内存申请不到的情况下,会引起任务挂起。写了段测试代码,果然验证了这个猜测。
教训:
- 最好不要有多层嵌套的代码啦,非嵌套不可,拜托不要递归,以上两点恶心行为如果都具备了,那千万不要在里面分配动态内存,否则十有八九会搞死人
- 接触一个系统最好是能写个测试程序熟悉一下,不要迷信文档 或者技术支持的话文档里说内存分配失败不会阻塞会返回失败,多次找FAE确认,呵呵,最后还是实践是硬道理啊
案例5:
还是以上的项目,系统是用分块的方式来实现动态内存的,系统还不是特别成熟,分块也不科学,资源又不够用,本来许诺的资源开销满足不了,导致项目到中期要将某些模块的内存分配方式由动态改为静态,还好,一切顺利。
教训:
- 软件应该对内存的分配做出适配,提高可移植性,容易从一种方式很快地过渡到另一种方式。
- 开始开发前一定要了解系统现有的资源。
3)编译器选项。
- 有的平台会将char默认为有符号型,有的会默认为无符号型,如果你在代码中将其作为有符号型来使用,编译的时候必须注意这种编译选项,很多编译器都有这种编译选项。否则,程序可能走的流程会跟你的思路不一样。
- 高低位字节问题,有的编译器有这种选项,如果代码会收到高低位影响的话,那就要注意了,如果没有这个选项,也要理解编译器的高低位。确定是否要对代码做出适配。
4)调试手段
这个不用再说了吧,工欲善其事,必先利其器,后面我们再对调试手段做进一步的探讨
5)其它
可能系统有一些特殊的问题,比如奇偶地址对齐的问题,这个一方面要仔细的看文档,一方面要写测试程序做下测试,虽然要花点时间,但是决对是值得的。
2.调试方法和策略
1)断点
很多平台提供了断点调试的功能,这种调试方法比较直观,所以很多开发人员都过分依赖它。断点的功能主要包括设置断点,单步执行,更改PC指针,运行到光标处,查看及修改寄存器或者内存值(watch window,memory window),查看堆栈,查看汇编代码。有的还可以设置地址断点,条件断点,这在内存越界的调试的时候非常有用。
应该说,断点调试在开发调试流程中起着很大的作用,但是在断点调试中也应该多想办法来提高调试的效率,特别是嵌入式系统中,找到一个问题,编译下载可能是一件挺费时的事情,所以,一次下载要能够有计划的同时调试多个问题。
另外,断点调试笔者认为主要针对问题已经有疑点了,对根本还未发现问题或者问题不清晰,建议用其它的调试方法,以提高效率。还有,在有些情况下,可能不允许打断点调试(可能会破坏时序)
2)trace工具
很多平台通过PC的串口实现了调试信息的输出,这种调试工具需要在代码中调用相应的接口打印调试信息,所有调试效率的高低就取决于你对程序流程的把握上,如果你在流程关键处能够合理的输出调试信息,会大大提高你的调试效率,还有,trace消息一般是可以方便得进行保存,比较利于让异地的合作方分析问题。当然,trace工具也有他的缺点,首先他不如单步调试直观,当问题范围已经比较小的时候,他的效率可能不及单步,其次则是个比较严重的问题,那就是当你trace消息比较多的时候,容易丢消息,所以trace工具的可靠性就大打折扣。
3)输出内存
有的平台支持断点调试,又可以将某块内存的内容输出到PC上,这时就可以利用该工具进行调试在断点不太允许,trace又变得很不可靠的条件下,可以利用一块静态缓冲区来作为trace缓冲,将非常重要的trace消息写到该缓冲中,然后在需要看trace的时候打断点,将内存内容也就是trace信息输出到PC上来进行分析。
4)输出文件
有的平台支持将flash上的文件下载到PC上,此时可以利用这个特性来输出调试信息在断点不允许,trace不可靠,静态缓冲开不出的请况下,可以将trace消息写到文件中,然后通过相应的下载工具,将trace消息写入的文件下载到pc上,进行分析。
5)调试策略
- 重现bug
- 分析现象,找出bug出现的条件(每次必现或者是一定条件下出现),尽可能地推测及估计错误产生的原因,可能位置。
- 仔细地检查相关的代码,很多错误都可以从代码上直接看出来。
- 如果无法定位到问题,则采用一步步缩小范围的策略,将相关的代码或者程序一步步分离开来,简化程序和程序运行的环境及条件,一步步逼近疑点。
- 结合各种调试手段分析问题。
6)一些常见bug的分析(待补充)
a.程序跑飞的可能情况:堆栈溢出;非法地址操作;某些变量被不正常改写;有些平台的全局变量在声明时赋值就是全局变量,对这些变量重新赋值也会跑飞。
案例6:
移植程序,移植接口疏忽大意,导致相互嵌套,如A中调用了B,B中调用了C,C在某些情况下又调用到了A,且没有终止嵌套的条件,结果导致堆栈溢出,程序跑飞。
案例7:
测试代码:
{
sprintf(TestBuf,buf,"%d");
}
其中由于buf里面的数据是以16进制存的,而sprintf将其以10进制存成字符串,因此可能buf一个字节写到TestBuf中就变成了两个字节,导致了缓冲溢出。
b.内存泄漏的调试:
- 如果有监测工具,则利用调试工具,在加上必要的测试代码。
- 如果没有监测工具,自己书写一套内存分配监测的接口,调试的时候打开接口,运行是观察内存分配的情况,分析是否有泄漏出现。
3.其它
双方或者多方合作的项目,由于你根本无法预测对方的软件、代码可靠性,集成以后的环境又比较复杂,所以必须做好自己的单元测试和模块测试,有可能的话,你可以以一个最简单的方式模拟他方的行为或者程序,脱离他方的代码进行单独的调试,这在集成以后出现问题查起来的时候会对你相当有帮助。
注:本文来源于网络,具体不详。