嵌入式编程问题总结
嵌入式编程问题总结
嵌入式编程问题总结
前言
在嵌入式编程中,我们经常会遇到许多问题,应该及时总结和思考,尽量在编写代码的时候注意避免。因为很多问题可能造成难以复现,定位的BUG,对项目和后期维护都会花费很大的精力。
代码运算符优先级
同一行代码(将过长的代码分成不同物理行也算作一行)中,如果有多个运算符,建议按照预想的运算符执行顺序,将运算符加()。
尤其是!、&、^、|、&&、||、<<、>>、+、-,这些运算符的优先级容易搞混。比如之前优化代码的时候就发现 a = b << 8 + c这种代码,但实际上 + 比 << 优先级高,结果将是错误的。从上下文分析,实际代码应该是 a = (b << 8) + c。
++、–运算符如果不是必须的,不要作为宏的参数的一部分。
比如ip_ntohl宏,它展开后将是((((xx) << 24) & 0xff000000U) | (((xx) << 8) & 0x00ff0000U) | (((xx) >> 8) & 0x0000ff00U) | (((xx) >> 24) & 0x000000ffU)),传入的参数会出现4次,因此如果传入了++、–运算符,那么将进行4次++、–运算,结果可能就不是预期的值。
函数逻辑
if、else、for、while、do..while建议将其配套的代码段,加上{},并且单独放一行。
即使只有一行代码或者是空,也要加上{},这样即增加了可读性,同时在后续更改代码的时候也不容易出错。比如之前的代码,后续修改时非常容易出错
1 | if (write); |
函数返回值
- 函数的返回类型和实际代码要对应。
目前我们项目中代码有不少函数定义了返回值,但实际代码中不是每种情况都返回了值;甚至有时候没有任何return。有些函数的返回值,后续是在使用的,这样在用的地方就是个随机值。比如下面的函数(SVN中已经增加了else返回NULL)
1 | struct sh_author_attr *author_attr_record_get( int vty_num ) |
另外一个情况是,函数定义的是void类型,但函数中有return (value)这种情况。LLVM会对这个报错,但GCC/diab不会。
局部变量
- 函数中的局部变量,在使用之前必须要被赋值。
如果不赋值,或者赋值语句在一些复杂的条件下,可能就会造成使用的时候,是一个随机值。这一项会有编译器警告,修改代码时注意看警告。
数据类型
- 数据类型不要轻易混用
在我们目前使用的64位系统上,long和ptr已经变成64位长度,而int仍然是32位。在升级位64位系统时,这类混用,绝大部分已被发现和排除了。后续新增代码也要注意不要混用
- 结构体对齐
此问题同样是在系统升级为64位时出现最多。以前代码中,由于是32位系统,int/long/ptr都是32位长,因此在结构体中可以任意排列,最大对齐宽度是32位;但64位系统中,long/ptr变成64位,最大对齐宽度随之变化。因此,结构体中,必须要优先考虑long/ptr的位置
立即数
- 获取和使用长度的时候,尽量避免用立即数,而应使用sizeof或者宏。
此问题绝大部分情况下和结构体有关,如上面结构体对齐中所述。以前代码中有很多操作结构体相关的内存时,使用了立即数来指定结构体大小,造成在升级到64位系统时,出现了很多难以定位和查找的问题。
指针与函数
- C库函数中,对于内存操作的函数的使用,对于长度一定要确保在预期范围内。接收指针参数/返回指针的库函数,都要检查相应指针是否为空。
以strncpy/snprintf等函数替代strcpy/sprintf,保证操作字符串时长度受控。并且对于查找类的字符串库函数,要检查其返回值是否为空。同时在不同的操作系统上,memcpy/strlen等函数,传入NULL会直接导致任务STOP。
- 函数的参数中,如果有传入指针的情况,一定要传入对应类型的指针。除非完全明确传入不同类型的指针是正确的。
如下代码会导致传入的int类型参数a的下一个相邻的变量被修改。
1 | char *get_num(char *a, unsigned long *n) |
函数在调用的那一行之前,一定要有函数原型出现。
规范的做法是将函数原型声明放在对应的头文件中,在其它文件中包含该头文件即可,避免到处都是出现函数原型,而如果后面要改函数原型时容易改漏。
函数未声明目前出现了3种问题。
1). 返回指针的函数在64位系统上,指针将被截断,出现非法指针。进而造成任务stop.
2). 函数原型和实际使用时,参数个数、类型不匹配,传递了错误的参数。在SVN-17204版本更改就是这类问题。
3). 在64位的LLVM编译器上,将会导致编译错误。其情形是,在同一个源文件中,在100行调用函数foo,但在200行出现了函数体,而在100行之前没有foo的声明。这种情形,LLVM会报一个foo类型不匹配的错误。解决办法就是在100行之前,声明一下foo。
所以函数使用前声明,编译器都可以帮我们检查一些问题出来,并且在64位上也是迫切需要的。
操作系统
SMP多核系统中,共享资源,必须要做好保护和同步。
多核处理器,就是一个芯片封装下,有多个CPU核心。它们可以真正意义上实现同一时刻,多个任务在运行,类似于FPGA的并行执行。因此我们一定要改变过去在单核系统上的观念。在写代码时,尤其是在涉及到和其它任务有交互时,不能再依赖于任务间的优先级来实现共享资源的互斥保护。
凡是要与其它任务共享的资源,增/删/改/查,都要做好保护。增/删/改,都要当作原子操作,不能被新动作打断;并且在进行这三种操作期间,不能查询,否则可能得到脏数据。
在某个BUG的改动中,就是某个任务在共享的cmd_hotswap_transit_t结构体AVL树中增加节点时,正好有主备同步的主用任务hotswap_s查询到这个节点。由于增加动作尚未完成(只创建了节点的key,该节点的内容尚未填充),而主用任务就正好查找到该节点并开始使用,所以使用到的节点内容都是无效的,从而造成了死机。
后记
无论是小项目还是大项目,对于整个项目或者程序而言,编程的质量把控是必须的,嵌入式C也只能不断总结,不断避免BUG,而不是发现BUG再去解决它。从系统层面来看,越庞大的系统越容易出现稀奇古怪的问题,对于工程师来说,不仅只是满足功能,而更多我们需要考虑系统和架构上去规避。