总结一下断言与防御式编程
嵌入式断言与防御式编程给你的代码装上保险丝欢迎关注微信公众号“边缘AI嵌入式”带你了解更多嵌入式加边缘AI的前沿技术和应用示例有一次现场出了个诡异的bug——设备运行几天后突然控制失灵。远程抓日志、看波形折腾了一周。最后发现原因是一个函数传入了NULL指针函数里直接解引用踩了野内存把一个全局状态变量覆盖了。后果在几小时后才表现出来表现形式跟根因隔了十万八千里。如果那个函数入口处有一行assert(ptr ! NULL)设备会在NULL传入的那一刻立刻停住或者至少打一条ERROR日志而不是悄无声息地继续跑在几小时后以一种匪夷所思的方式崩溃。防御式编程不是让你的代码不出bug——那不现实。它是让bug第一时间暴露在犯错的地方而不是传播到系统的另一端才爆炸。一、防御式编程的核心原则不要假设调用者会传入正确的参数。不要假设硬件会按预期工作。不要假设数据会在合法范围内。对每一个可能出错的前提条件做显式检查。这些检查是代码的保险丝——正常情况下它们无感存在异常时立刻熔断报警。二、assert的正确用法标准assert#includeassert.hvoidringbuf_init(ringbuf_t*rb,uint8_t*buf,uint32_tsize){assert(rb!NULL);assert(buf!NULL);assert(size0);assert((size(size-1))0);// 必须是2的幂// ...}标准assert在条件为假时调用abort()。在嵌入式里这通常意味着死循环或复位。嵌入式定制版assert标准assert的问题它只告诉你断言失败了不告诉你在哪个文件哪一行。而且release版通常被#define NDEBUG干掉了线上出问题时毫无帮助。自定义一个更好用的#defineASSERT(expr)do{\if(!(expr)){\assert_failed(__FILE__,__LINE__,#expr);\}\}while(0)voidassert_failed(constchar*file,intline,constchar*expr){// 关中断防止情况继续恶化__disable_irq();// 打印或存储断言信息log_error(ASSERT FAILED: %s:%d (%s),file,line,expr);// 存到Flash的特定区域下次上电可以读出来fault_record_save(file,line,expr);// 可以选择死循环等调试器连上来 / 复位 / 降级运行#ifdefDEBUG__BKPT(0);// 触发断点调试器会停在这里while(1);#elseNVIC_SystemReset();// release版直接复位#endif}assert该检查什么该assert的例子函数入口参数合法性指针非NULL、数组下标在范围内、枚举值合法不可能到达的代码路径switch-case的default、if-else的最后分支数据结构的不变式链表长度跟计数器一致、环形缓冲区的head-tail在合理范围初始化前置条件模块使用前已经初始化、时钟已经配好不该用assert的原因该用什么可预期的运行时错误用户输入了错误的参数返回错误码通信超时正常的异常情况超时重试机制外部设备读取失败硬件可能暂时故障错误处理降级assert是给不应该发生的情况准备的。如果某个条件是可能发生、但不希望发生的应该用错误码和错误处理来应对而不是assert。三、防御式编程的几个实用技巧技巧一哨兵值检测在关键数据结构前后放一个特殊值定期检查有没有被踩。#defineSTACK_CANARY0xDEADBEEFtypedefstruct{uint32_tcanary_head;// 哨兵头uint8_tdata[256];uint16_tlen;uint32_tcanary_tail;// 哨兵尾}protected_buf_t;voidcheck_integrity(protected_buf_t*buf){ASSERT(buf-canary_headSTACK_CANARY);ASSERT(buf-canary_tailSTACK_CANARY);}如果某个bug导致内存越界写入哨兵值会被破坏你能在第一时间发现。技巧二状态机的default分支switch(state){caseSTATE_IDLE:/* ... */break;caseSTATE_RUNNING:/* ... */break;caseSTATE_FAULT:/* ... */break;default:ASSERT(0);// 不应该到达这里stateSTATE_IDLE;// 如果release版assert被关掉了至少恢复到安全状态break;}技巧三函数返回值必查// 坏习惯hal_spi_init(SPI1,1000000);// 好习惯intrethal_spi_init(SPI1,1000000);ASSERT(ret0);尤其是硬件初始化函数——如果SPI初始化失败了你不知道后面所有的SPI通信都是在往黑洞里发数据。技巧四取值范围钳位// 防御式写法即使上游传了一个越界值也不会导致数组越界voidset_led(uint8_tled_id,uint8_tbrightness){if(led_idMAX_LED_COUNT){LOG_W(LED id越界: %d,led_id);return;}if(brightness100)brightness100;// 钳位不是报错led_table[led_id].brightnessbrightness;}四、release版的断言策略是否致命(数据结构损坏)严重但可恢复轻微(参数越界)断言失败当前是DEBUG版?触发断点调试器停在现场故障严重程度?记录故障信息到Flash系统复位记录日志尝试恢复到安全状态记录日志钳位处理后继续不同级别的断言应该有不同的处理策略。有人喜欢用三个宏来区分#defineASSERT_FATAL(expr)// 失败就复位#defineASSERT_ERROR(expr)// 失败就恢复到安全状态#defineASSERT_WARN(expr)// 失败就打日志继续跑五、防御式编程的比喻防御式编程就像高速公路上的护栏。正常开车的时候你根本注意不到护栏的存在。但万一你打了个盹方向偏了护栏会立刻把你挡回来——虽然车身会刮花但你不会冲下山崖。没有护栏的代码就像没有护栏的盘山公路一个小小的方向偏差在几秒几小时后变成了万丈深渊系统级崩溃。assert就是护栏。它的价值不在于正常时做了什么而在于异常时拦住了什么。每个函数入口加几行assert每个switch加个default每个返回值检查一下。代码量增加不到10%但你排查bug的时间可能减少90%。这笔账太划算了。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2445266.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!