嵌入式软件中的断言应该分成三个级别,而不是仅有启用和关闭两种。1、启用断言并打印可读信息;2、启用断言并打印代码地址、3、关闭断言。
原文:http://blog.csdn.net/zoomdy/article/details/46289867
mingdu.zheng at gmail dot com
矛盾
断言可以帮助开发人员在软件开发过程中比较容易地发现软件缺陷。典型的做法是在开发过程中启用断言,而在最终发布时关闭断言。
对于嵌入式软件而言,可能在软件开发的后期就不得不关闭断言,因为嵌入式系统的内存资源是非常有限的,而启用断言会占用相当可观的ROM资源,当嵌入式软件开发进行到后期时,所有的功能代码集成以后这些功能代码基本上用光了ROM资源,已经没有ROM资源可以启用断言(除非在开发时使用的是比最终发布的硬件具有更多ROM资源的硬件)。
还有人建议永远都不要关闭断言,即使是发布的版本也不要关闭断言。对于C/C++这样的仅有极少运行时检查特性的语言而言,断言还是一种简单有效的运行时检查机制。另外,少量的软件缺陷可能仍然残留在系统中,断言仍然可以检测出这些残留的缺陷并报告。发布时关闭断言,当发现问题时重新启用断言进行检查,这样的做法对于时间相关的代码而言可能是不可重现的,因为启用断言后程序的执行时间是与关闭断言时不相同的。这是建议总是保留断言的另一个原因,为了执行时间上的一致性。
一方面,嵌入式系统的ROM资源是有限的,增加ROM资源意味着增加成本;另一方面,断言应该尽可能地保留以便能检测错误。嵌入式系统开发过程总是充斥着矛盾。
解决办法
解决这个矛盾的办法是减少断言所占用的ROM资源量。断言宏通常会打印一些可读信息,例如产生断言的文件名和行号以及断言条件等,这些可读信息可以非常快速地帮助开发人员定位发生断言的位置。这些可读信息基本上是字符串常量,占用大量ROM资源的正是这些字符串常量。如果用代码地址来代替这些可读字符串,那么就可以节省大量的ROM资源,代价是开发人员必须通过一些工具(例如addr2line)来定位产生问题的代码,而不是直观地看到问题所在之处。
由此可见,断言应该分成三个级别,而不是仅有启用和关闭两种。
- 启用断言并打印可读信息,可读信息可以帮助开发人员快速定位问题,因此有条件打印可读信息的情况下还是应该打印可读信息,这可以提高开发效率的。
- 启动断言并打印代码地址,在发布版本中以及ROM资源十分紧张的情况下使用这种方式,一方面可以继续检测错误,另一方面尽可能节省资源。
- 关闭断言。
调试专用断言
在调试阶段,希望断言失败时可以立即进入调试模式,源代码调试器将当前代码停留在断言失败处。为了达到这个目的,可以在断言实现中加入断点指令,一旦断言失败就会执行到断点指令,继而进入调试模式。
断言定义示例
//#define ZMD_ASSERT_EN_FILE_LINE
//#define ZMD_ASSERT_EN_PC
#define ZMD_ASSERT_EN_BKPT
//#define ZMD_ASSERT_EN
#if defined(ZMD_ASSERT_EN_FILE_LINE)
#define ZMD_ASSERT(c) \
if(!(c)) { \
zmd_diag_print_file_line("ASSERT", __FILE__, __LINE__, #c); \
for(;;) {} \
}
#elif defined(ZMD_ASSERT_EN_PC)
__attribute__( ( always_inline ) ) static inline uint32_t __get_PC(void)
{
uint32_t result;
asm volatile ("MOV %0, pc" : "=r" (result) );
return(result);
}
#define ZMD_ASSERT(c) \
if(!(c)) { \
zmd_diag_print_pc(__get_PC()); \
for(;;) {} \
}
#elif defined(ZMD_ASSERT_EN_BKPT)
#define ZMD_ASSERT_BKPT() __asm volatile ("bkpt 0")
#define ZMD_ASSERT(c) \
if(!(c)) { \
ZMD_ASSERT_BKPT(); \
for(;;) {} \
}
#elif defined(ZMD_ASSERT_EN)
#define ZMD_ASSERT(c) \
if(!(c)) { \
for(;;) {} \
}
#else
#define ZMD_ASSERT(c)
#endif