摘要:在电力电网中大量使用的智能抄表终端,80%以上采用了Linux加ARM的体系架构,其软件开发均采用C/C++语言。在软件代码审查的单元测试阶段,如何结合Linux编程环境和智能抄表终端软件的特点,有针对性的进行代码审查,快速发现大量问题,为下游环节提供安全可靠的软件版本,本文根据大量的工作实践,系统的总结了在嵌入式Linux编程环境下,智能抄表终端软件的代码审查技巧。
关键词:Linux;代码审查;单元测试
1、引言
随着嵌入式硬件的飞速发展,嵌入式系统软件的规模日益增大,其结构也越来越复杂。嵌入式系统越来越多的被应用在工业控制、信息家电、移动通信、国防等对质量要求非常严格的领域,用于一些关键性的控制管理。因此,嵌入式系统中出现的问题往往会导致无可挽回、致命的错误,这使得对嵌入式系统软件代码的安全性,可靠性和稳定性的要求极高,在软件开发阶段,对软件代码进行审查,在早期发现并解决问题显得尤为重要。
嵌入式Linux系统的内核是用C语言编写,在Linux编程环境下,终端软件应用程序的开发使用的也是C/C++语言。现代的C/C++编译器都能在一定程度上发现代码存在的问题和缺陷,并对代码进行一定的优化。但是,大部分的编译器只能发现代码的一些基本语法问题,平衡代码的执行速度和大小。如果想要提高代码的安全性,可靠性和稳定性,并得到执行快速而又小的代码,仅仅依靠编译器是不可能的。目前,常用的代码审查方法和问题总结有很多,但是针对嵌入式代码的却很少,也几乎没有人做过系统全面的研究。
长沙威胜信息技术有限公司研发的系列智能抄表终端产品,均采用Linux+ARM架构,并使用C/C++语言进行应用程序开发。软件部分包括抄表模块,规约解析模块,数据管理模块,菜单显示模块,设备监控模块等多个模块,系统结构复杂,程序代码量大,如果在软件开发阶段,没有对代码质量严格把关,进行层层测试,产品进入现场后,将会暴露大量问题。实践也已经证明,从现场反馈回来的许多问题,都是由于在软件开发阶段,对代码缺乏严格高效的审查所造成的。如果在软件开发的早期,能够对代码审查进行严格的优化,这些问题都可以得到避免。为此,在产品的前期研发阶段和后期的维护阶段,开发人员编写或修改的每一条代码都必须经过代码测试人员的严格测试,包括动态测试和静态测试。本文根据大量工作实践经验,总结了基于嵌入式Linux的终端软件代码的审查技巧和方法。
2、嵌入式Linux终端软件代码的常见问题
2.1 运算符优先级问题
运算优先级问题是代码中常见的问题,它不涉及到语法,编译器无法识别程序员的真正意图,在软件后期维护中容易造成理解错误,例如:
case 2://分补
cctmp=c1;
if(cctmp&0x01==1)
……
break; ====>>>>如上,"==" 优先级比"&"高,结果导致表达式不符合期望。正确的应该是在前面的语句加上括号,如:if( ( cctmp&0x01 ) ==1):
2.2 内存泄露问题
代码审查过程中,发现仍然存在很多资源回收处理的问题,主要体现在以下几个方面:
2.2.1 函数返回时的return 问题
函数return时,需要进行资源回收处理,这里的return既包括错误处理分支return false,也包括函数执行完后的正常退出,资源释放包括关闭文件指针,释放内存空间,关闭数据库等等。在终端软件程序中存在这样的问题较多,示例参考2.2.2中的代码:
2.2.2 strdup使用问题
在使用strdup后,没有调用free()函数释放资源。strdup函数本身具有特殊性,在调用函数的时候,隐式的调用了malloc函数进行内存申请。所以,strdup函数的使用也应该和free函数配对使用。这样的函数实际中还会大量存在,我们在调用时,应该先明确函数功能描述,例如:
fname = strdup(*argv);
if (fname == NULL)
novm("+ua file name");
seteuid(getuid());
ufile = fopen(fname, "r"); seteuid(0);
if (ufile == NULL)
{
option_error("unable to open user login data file %s", fname);
return 0; ====>>>>规范编程要求,return false时,应该释放前面strdup函数获取的资源。
}
2.2.3 sqlite_exec()函数使用问题
使用sqlite_exec()函数后,没有判断返回值,对errmsg指针进行释放;使用sqlite_get_table()后,异常退出时,没有释放获取的table信息,由于在进行函数调用的时候,隐式的获取了资源,所以,在异常退出时,容易忽略对资源的回收处理,例如:
……
sprintf( sqlop, "select meter_addr from IMPORTANT_METER;" );
rc = sqlite3_get_table( sqldb , sqlop, &azResult , &nrow , &ncolumn , &zErrMsg );
if( rc != SQLITE_OK )
{
sqlite3_close( sqldb );
return false;
====>>>>ruturn false时,必须sqlite3_free_table( azResult )释放获取的table信息,避免内存泄露;同时,还需要调用sqlite3_free( zErrMsg ),释放zErrMsg信息,避免内存泄露
2.2.4 new内存后,异常返回时,没有delete 内存
在使用new申请动态内存后,如果遇到异常情况返回时,没有使用delete释放内存,之前该内存依然被占用,这个问题在程序运行的短时间内可能不会体现,但是如果该操作时在循环内执行,在长时间运行后,系统内存将被大量占用,可能导致系统运行缓慢甚至崩溃,例如:
void PrOAFN04::get_fntable(unsigned int fn)
{
FnJZ_table* m_pList =new FnJZ_table[MAXTABLE];
memset(m_pList,0x00,MAXTABLE*sizeof(FnJZ_table));
int m_num = 0;
CArchive m_Archive( STATINI, EXTREMUMID);
if(m_Archive.GetIdArch((unsigned char) fn, m_pList, m_num))
{
m_pFnJZ_table =m_pList;
}
====>>>>如上if判断,应该加上else分支,GetIdArch方法操作失败时,应该delete m_pList资源,不然就存在内存泄露
2.3 数组访问越界
数组访问越界问题是一个非常严重的问题,在程序运行时,它的表现是不定的,有可能程序会正常运行,有时候可能会导致系统突然崩溃,如果在程序开发阶段没有发现这个问题,一旦产品流入市场,它就会像一颗定时炸弹,随时可能造成无法估量的后果。数组越界主要表现在以下几个方面:指针变量地址访问越界、循环变量失控,导致越界、循环变量与数组元素数量没有关联,导致明显访问越界、代码编写时,拷贝引起的笔误导致数组访问越界等,例如:
bool CShmMemory::ShmGet( int CreatMode, int AttchMod )
{
……
char *p = (char * )pshmaddr;
if( *p != 1 && (IPC_CREAT & CreatMode) )
{
memset( p + 1, 0xFF, m_shm_size);
===>>>p指向共享内存的首地址,初始化时,如果从p+1开始,那么最多只能写(m_shm_size-1)个字节,这里的初始化赋值语句明显访问越界了
……
2.4 编程规范性问题
在进行软件开发前,首先需要制定企业的编程规范,然后根据编程规范,列出需要重点改进的编程规范性问题列表,程序员根据编程规范进行编码,代码测试人员在进行单元测试时,也要根据编程规范进行审查,这样可以大大提高代码的可读性和可移植性,并且降低风险和减少后期维护成本。
3、嵌入式Linux终端软件中的线程安全性问题
线程中隐藏的一个问题就是他们可能会调用非线程安全的库函数,这样可能会产生错误的结果,如果多个线程能够同时执行函数的多个活动请求而不会相互干扰,那么这个函数就是线程安全的(thread-safe)。POSIX规定了非线程安全的函数如下,这些函数一定要有一个以后缀_r表示对应的线程安全版本函数。(如图1)
既然上述函数不是线程安全函数的,那么我们就必须对代码中这样的函数调用进行专项审查,通过搜索,函数查找等方法进行逐项排查,确保多线程执行的程序代码中不存在非线程安全的函数调用。实际上这样的问题,在实际应用多次出现,并且问题的定位和解决困扰了开发人员很长时间,在嵌入式Linux终端软件代码审查时,我们需要汲取这样的经验教训。
4、代码审查工具的使用
4.1 使用改进后的Pc-Lint审查工具对整个软件工程进行全面审查
PC-Lint是一种C/C++软件代码静态分析工具,借助PC-Lint可以对整个软件工程的代码进行批量检测,根据输出的检查结果,分析代码中潜在的问题隐患。目前,在我们的代码审查工作中,已经大面积的在使用Pc-Lint,通过实际的工作,Pc-Lint还是存在一些不足,比如在检查之前要进行大批量的命令行输入,最后的审查结果以文本格式输出,没有进行分类汇总,针对这些问题,我们使用VBA编程技术,将Excel和Pc-Lint结合起来使用,收到了很好的效果。
我们将PC-Lint内嵌至Excel中,并设计了一个简单的操作界面,将参数设置,选项设置,待检查文件写入全部集中于一个操作界面中。在启动宏时,同时启动PC-Lint,可以避免大量的手动输入,并使检查结果自动分类,如图2所示:
在检查完成后,检查结果以电子表格的形式输出,并且已经进行了分类汇总,在输出的检查结果中,各类代码问题按照不同模块,不同文件排列出来,还给出了出现问题的行号,问题类型,以及详细的错误信息,这样一来,程序员可以快速定位到发生错误的地方,及时加以纠正,而且便于分类汇总,统计分析。
4.2 使用beyond compare工具对代码修改进行有针对性的审查
终端软件在日常开发并修改后,通过CVS代码管理系统提交到服务器,修改内容可以通过CVSTRAC网页工具进行查看,相关修改信息一目了然,这对于每日代码修改的审查提供了很大的方便,然而,借助专业代码比较工具beyond compare,可以对不同版本代码进行深入细致比对,并对修改部分进行重点审查,确保修改能够真正满足要求,如图3所示:
在beyond compare工具的比对窗口中,红色部分表示此次修改的内容,测试人员由此可以迅速的定位到代码的修改部分进行重点审查,大大的提高了效率,降低了工作量。
5、结语
本文结合多年来对嵌入式Linux软件代码的审查经验,总结了在代码审查过程中发现的一些常见问题,并针对终端软件代码多线程编程的特点,根据实践经验,对线程安全性问题进行了分析,最后介绍了根据实际需要进行了改进的Pc-Lint检查工具和beyond compare工具的使用,因此,本文对基于嵌入式Linux的抄表终端软件的代码审查具有一定的借鉴和指导意义。