跨越数据类型的重重陷阱
扫描二维码
随时随地手机看文章
数据类型是编程语言中最基本的构成元素,但却是最易被忽略的一环,程序员愿意把几乎100%的精力都花在算法研究、程序流控制等大环节上,却很少在数据类型问题上反复斟酌。
细节决定成败,一个螺丝钉的失误可能导致一个飞行器的毁灭,一个数据类型的错误同样可以让庞大的软件系统崩溃。
MISRA—c中关于数据类型的规则主要分为两个方面。一是数据类型相关的编程风格;二是不同数据类型之间的转换,后者是重点。这里介绍MISRA_C关于数据类型的部分规则,更多的规则请参考《MISRA-C:2OO4)》一书。
下文中凡是未加特殊说明的都是强制(required)规则.个别推荐(advisory)规则加了“推荐”标识。
在展开论述之前,先看两个问题,读者可以带着疑问阅读完本章内容。
问题1:执行以下程序,result_8的值是多少?
ulnt8_t porI=0x5a;
uint8一t resuh_8;
result_8=(~port)>>4;
/*注:uint8_t表示8位无符号整型*/
问题2:执行以下程序,d的值是多少?
uintl6_t a=10;
uin|16_t b=6553l;
uint32_t c=0;
uint32_t d;
d=a+b+c;
/*注:uintl6_t表示16位无符号整型,uint32_t表示32位无符号整型*/
1 数据类型相关的编程风格
规则6.3(推荐):必须用typedef显式标识出各数据
类型的长度和符号特性,避免直接使用标准数据类型。
例如,一个32位的整数系统,可定义如下:
typedef char chat_t;
typedef sigrled char int8_t;
typedef signed short intl6_t;
typedef signed int int32_t;
typedef signed long int64_t;
typedef unsitgned chat uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned 1ong uint64_t;
之所以用intl6_t和uint32_t等代替signed short和unsigned int等标准数据类型标识符,是由于不同的编译器对标准数据类型的长度定义是不一样的。比如说一个16位系统,很可能就把short和int都定义成16位,long定义成32位,这与上文32位系统中标准数据类型的长度就不一致。用intl6_t和uint_32等标识符来定义变量,一方面增加了程序的可读性,使得程序员本人或其他读者都能对程序中数据的具体信息胸有成竹;另一方面也有助于程序在不同系统之间的移植,节省开发时间,减少隐患。规则7 1:不得使用八进制常数(O除外)或八进制转义符。
思考如下数组:
code[1]=109;
code[2]=100;
code[3]=O52
code[4]=O71;
/*注:八进制常数须在最高位加O*/
code[3]的实际值是42(十进制),code[4]的实际值是57(十进制);但估计很多读者会把code[3]认成是52(十进制),code[4]认成是7l(十进制)。
八进制数在C程序中使用的频率远小于十进制数和十六进制数,为了保证程序的可读性和安全性,程序员不允许使用八进制数以及八进制转义符。
2 数据类型转换
如果程序员对数据类型的转换有很清晰的认识,并且在必要的地方做了正确的显式强制转换,那程序是安全的。但有时由于程序员的疏忽,或者是过于相信编译器的“智慧”程度,导致表达式中有很多隐式转换(即没有显式地强制转换),而这些隐式数据类型转换很可能就构成致命的漏洞。MISRA—C中数据类型转换规则的着眼点,即是避免有漏洞的隐式数据转换。
在介绍MISRA—C关于数据类型转换的部分规则之前,先介绍整型操作数的“平衡(balance)”原则。所谓整型操作数“平衡”原则,即对于隐式表达式,编译器会按照既定规则对操作数进行位数扩充,其中int和unsiglled int在整型表达式“平衡”过程中占重要地位。
下面分析一个简单的隐式整型表达式c=a+b(假设a的存储位数不大于b的存储位数),编译器是这样来处理这个表达式的:
如果b是短整型(即位数少于int,比如char、short等)或者整型(int或unsigned int),那a也是短整型或者整型,执行“+”运算之前,a和b都将被扩充为整型(int或者unsigned int),然后相加的结果赋给c(如果c不是int或者unsigned int类型,则这个赋值操作也会包含隐式的扩充或截断操作)。
如果b是长整型(存储位数多于int),则a会被扩充为与b相当的长整型,再执行“+”运算,所得结果赋给c(可能包含隐式的扩充或截断操作)。
绝大部分的操作符用于整型运算的时候,都遵循上述“平衡”原则,比如:算术操作符、位操作符和关系运算符。
但逻辑操作符不遵循上述“平衡”原则。此外左移(<<)和右移(>>)运算符也不遵循“平衡”原则,只和移位操作符左边的整型操作数相关。假设一个8位的短整型变量值为Oxf5(十六进制),则右移4位所得结果是O xof(十六进制)。
明确了上述背景后,下面来关注本文一开始提出的“问题1”(代码参见前文)。绝大部分拥有嵌人式C程序开发经验的人都明白这段代码的原意是将port的值取反后右移4位赋值给result_8(在用I/O口控制共阳的LED时经常这么做),程序员期望的结果显然是resuIt_8=0xof。然而,由于整型的“平衡”原则,在16位编译器中,~port的值是Oxffa5;在32位编译器中,~pott的值是Oxffffffa5。无论哪种情况,最后结果(右移4位后赋值给result_8的时候有一个截断操作)都是resuIt_8=Oxfa,而非程序员预期的result_8=OxOf。
倘若将最后一行代码改成result一8=((uin8_t)(~port))>>4,则result_8可取得预期的值。
针对以上情况,MISRA-c提出了相应规则。
规则10.5:如果位操作符~和移位操作符<<(或>>)联合作用于unsigned char或者unsigned short类型的操作数时,中间运算步骤的结果必须立刻显式强制转换为预期的短整型数据类型。
为了加深对“平衡”原则的理解,再来分析一下“问题2”。
如果用一个32位的编译器来编译这段程序,最终结果是d=6554l,程序员“幸运地”得到了预期的结果。如果是16位的编译器,得到的结果却是d=5。
由于“+”运算是左结合的,所以d=a+b+c等效于d=(a+b)+c,即先执行a+b,所得的和再与c相加.最后结果赋值给d。问题就出在a+b这个中间步骤中。由于a和b都是16位整型(注意编译器也是16位的),故而a+b的结果也是16位整型,则a+b的值是Ox0005(有溢出);再扩充为32位整型Ox00000005和c相加赋值给d,d=5,这并非程序员预期的结果。
所以,在16位编译器中,问题2的那段代码很可能导致严重错误。当然,如果程序员用()指定了运算优先级的话,即最后一行代码写成d=a+(b+c),也可以避免上述溢出错误,然而,这终究不是治本的办法。只有明确每一个操作数的实际数据类型,才能保障代码的安全性。
MISRA-C中对于表达式中存在隐式数据类型转换的情况作了严格的限制。
规则10.1:以下情况下,整型表达式中不允许出现隐式数据类型转换。
①整型操作数不是被扩充为更多位数的同符号整数;
②表达式是复杂表达式;
③表达式不是常数表达式,且是函数的参数;
④表达式不是常数表达式,且是函数的返回表达式。。
规则10.2:以下情况下,浮点数表达式中不允许出现隐式数据类型转换。
①浮点型操作数不是被扩充为更多位数的同符号浮点数;
②表达式是复杂表达式;
③表达式是函数的参数;
④表达式是函数的返回表达式。
整型表达式规则和浮点数表达式规则基本类似,只是浮点数表达式规则更为苛刻一些,对浮点型的常数也作了严格的限定。
这两条规则中,出现了“复杂表达式”的概念。请注意,MISRA—C中“复杂表达式”的概念和其他介绍C编程规范书籍中“复杂表达式”的概念是不一样的。在MISRA-C中,非“复杂表达式”基本只限制在常数表达式或者函数的返回值。为了明确上述规则中关于“复杂表达式”和“返回表达式”的概念,此处举一例子。定义一个函数uintl6_t foo(void),函数体如下:
uintl6_t foo(void){
return(a+b+c);
函数体中最后一句return(a+b+c)中的a+b+c是返回表达式。倘若在C程序的其他地方有a=foo()这样的语句,则用的是foo()函数的返回值。在MISRA-c中,的资源,完成了采用USB接口技术的热敏打印机的开发,并对打印头作了充分的保护。通过采用相应的算法实现这个赋值表达式不是“复杂表达式”。
至于表达式作为函数参数等情况,碍于篇幅的原因,此处就不再详细展开了。
权衡一下利弊,在涉及到数据类型转换的时候,与其花很大力气去区分一个隐式表达式是否在MISRA—C规则的“黑名单”中,还不如用强制转换符显式地标识出每个操作数的实际数据类型,这是最为稳妥的方法。总而言之,MISRA—C关于数据类型转换规则的中心意思,是要求程序员明确任意一个操作数的实际数据类型。
3 小 结
作为一名优秀程序员,第一步就是以严谨的态度对待程序中的每一个数据,明白任何一个数据操作的关键,从而能写出最清晰易懂而又安全的代码。MISRA—C关于数据类型的规则可保障程序员在迈出这一步的时候不会摔倒。