指针、结构体、联合体的安全规范

指针赋予了C编程最大的灵活性;结构体使得C程序整齐而紧凑;联合体在某些要求注重效率的场合有精彩的表现,这三个要素是C语言的精华。
然而,精华并不意味着完美,C语言在赋予程序员足够灵活性的同时,也给了程序员很多犯错误的机会。所以有必要关注指针、结构体和联合体的实现细节,从而保障程序的安全性。
在此.第一部分介绍《MISRA—C:2004》中与指针相关的部分规则,第二部分讲解结构体和联合体的操作规范。下文中凡是未加特殊说明的都是强制(required)规则,个别推荐(advisory)规则加了“推荐”标示。
1 指针的安全规范
《MISRA—C:2004》关于指针的规范主要分为三个部分:指针的类型转换规则、指针运算的规则和指针的有效性规则。
1.1 指针的类型转换
指针类型转换是个高风险的操作,所以应该尽量避免进行这个操作。MISRA—C对其中可能造成严重错误的情况作了严格的限定,选择其中两条作简要分析。
规则11.4(推荐):指向不同数据类型的指针之间不能相互转换。
思考如下程序:
uint8_t*pl;
uint32)_t*p2;
p2=(uint32_t*)pl;
/*注:uint8_t表示8位无符号整型,uint3_t表示32位无符号整型。*/
程序员希望将从p1单元开始的4个字节组成一个32付的整型来参与运算。
如果CPU允许各种数据对象存放在任意的存储单元,则以上转换没有问题。但某些CPU对某种(些)数据类型加强了对齐限制,要求这些数据对象占用一定的地址空间,比如某些字节寻址的CPU会要求32位(4字节)整型存放在4的整倍数地址上。在这个前提下.思考程序中的指针转换:假设pl一开始指向的是0x00O3单元(对uint8_t型的整型没有对齐要求),则执行最后一行强制转换后,p2到底指向哪个单元就无法预料了。
规则1 1.5:指针转换过程中不允许丢失指针的const、volatile属性。按如下定义指针:
uIntl6一t x;
uint16_t*const cpi=&x; /*const指针*/
uintl6_t*const *pcpi; /*指向const指针的指针*/
const uintl6_t* *ppci; /*指向const整型指针的指针*/
uIntl6_t* *ppi ;
const uint16_t *pci; /*指向const整型的指针*/
volatik uint16_t *pvi; /*指向volatile整型的指针*/
uintl6_t *pi;
则以下指针转换是允许的:
pl=cpi;
以下指针转换是不允许的:
pi=(umtl6_t*)pci;
pi=(uintl6_t*)pvil
ppi=(uintl6_t* *)pcpi;
ppi=(uintl6_I**)ppci+
以上非法指针类型转换将会丢失const或者volatile类型。丢失const属性,将有可能导致在对只读内容进行写操作时,编译器不会发出警告,编译器将不对具有volatile属性的变量作优化;丢失volatile属性,编译器的优化可能导致程序员预先设计的硬件时序操作失效,这样的错误很难发现。关于const和volatile关键字的详细作用,读者可参考ISOC获取更多信息。
1.2 指针的运算
ISOC标准中,对指向数组成员的指针运算(包括算术运算、比较等)做了规范定义,除此以外的指针运算属于未定义(undefined)范围,具体实现有赖于具体编译器,其安全性无法得到保障,MISRA—C中对指针运算的合法范围做了如下限定。
规则17.1:只有指向数组的指针才允许进行算术运算①。
规则17 2:只有指向同一个数组的两个指针才允许相减 ②。
规则17 3:只有指向同一个数组的两个指针才允许用>,>=,<,<=等关系运算符进行比较。
为了尽最大可能减少直接进行指针运算带来的隐患,尤其是程序动态运行时可能发生的数组越界等问题,MISRA—C对指针运算作了更为严格的规定。规则17 4:只允许用数组索引做指针运算。按如下方式定义数组和指针:
uint8_t a[10];
uint8_t *p;
则*(p+5)=O是不允许的.而p[5]=O则是允许的,尽管就这段程序而言,二者等价。
以下给出一段程序,读者可参照相应程序行的注释,细细品味上述规则的含义。
void my_fn(uInt*_t*p1.uint8_t p2[]){
①其实此处的算术运算仅限定于指针加减某个整数.比如ppoint=point一5.ppoint++等。0两个指针可指向不同的散组成员。
uint8_t index=0;
uint8_t *p3
uint8_t *p4;
*pl=O;
p1++; /*不允许,pl不是指向数组的指针*/
p1=p1+5;/*不允许,pl不是指向数组的指针*/
pl[5]=O; /*不允许,p1不是指向数组的指针*/
p3=&p1[5];/*不允许,pl不是指向数组的指针*/
p2[0]=O;
index++;
index=index+5:
p2[index]=0; /*允许*/
*(p2+index)=O; /*不允许*/
p4=&p2[5]; /*允许*/
}
1.3 指针的有效性
下面介绍《MISRA—C:2004》中关于指针有效性的规则。
规则17 6:动态分配对象的地址不允许在本对象消亡后传给另外一个对象。
这条规则的实际意义是不允许将栈对象的地址传给外部作用域的对象。
请看以下这段程序:
#include″stdi0.h″
char*getm(void){
char p[]=″hello world″;
return p;
intmain(){
char* str=NULL;
str=getm();
printf(str);
程序员希望最后的输出结果是″hello world″这个字符串,然而实际运行时,却出现乱码(具体内容依赖于编译环境)。
简单分析一下,由于chat p[]=″hell0 world″这条语句是在栈中分配空间存储″hell0 world″这个字符串,当函数getm()返回的时候,已分配的空间将会被释放(但内容并不会被销毁),而priM(str)涉及系统调用,有数据压栈,会修改从前分配给数组p[]存储空间的内容,导致程序无法得到预期的效果。
倘若将getm()函数体中的char p[]=″hell0 world″程序行改成char*q=″hello world″,则执行main( )的时候可以正确输出″hello world″,这是由于q指向的是静态数据区,而非栈中的某个单元。
所以,数组名是指针不假,但在实现细节上还是有很大的差异,程序员在使用指针的时候必须慎之又慎。
2 结构体、联合体的安全规范
规则18 4:不允许使用联合体。这是一个不太近情理的规定,在具体阐述为何《MIS—RA—C:2004》如此“痛恨”联合体之前,首先需要明确与联合体相关的细节:
①联合体的末尾有多少个填充单元?
②联合体中的各个成员如何对齐?
③多字节的数据类型高低字节如何排放顺序?
④如果包含位字段(bit—field),各位如何排放顺序?
针对细节3举个例子。
程序段2.1
typedef union{
uilat32_t word;
uint8_t bytes[4];
}word_msg_t;
unit32_t read_nasg(void){
word_rnsg_t tmp;
/*注:tmn bvte[O]对府干tmp.word的高8位,tmp byter[l]对应于
tmp.WOfO的次高8位,依次类推。*/
tmp.bytes[O]=read_byte();
tmp.bytes[1]=read_byte();
tmp.bytes[2]=read_byte();
tmp.bytes[3]=read_byte();
retlarn(trap.word);
}
以上代码格式在各种通信协议中使用的频率很高,接收端接收到的数据一般都以字节为单位存放,主控程序需要根据相应的协议将接收到的多个字节进行组合。为了实现相同的功能,《MISRA-C:2004》推荐了read_msg()函数的另外一种写法。
程序段2.2
uint32_trcad_msg(void){
uint32_t word;
Word=((unit32_t)read_byte())<<24;
word=word│(((unit32_t)read_byte())<<16);
word=word│(((unit32_t)read_byted_byte<<8);
word=word│(((unit32_t)read_byte());
return(word):
}
无论从程序的清晰程度还是执行效率来讲,程序段2.1都优于程序段2.2。然而,程序段2.1在Intel 80x86/Pentlurn体系(1ittle—endian,存储多字节整数的时候低字节存放在低地址,高字节存放在高地址)CPU中和在Mo—torola 68K体系(big—endian,存储多字节整数的时候高字节存放在低地址,低字节存放在高地址)cPu中的执行结果完全不一样。假设read_byte()函数返回的数据依次是0x01、0x02、0x03和0x04,则在Intel体系中,程序段2.1
中read_msg()函数的返回值是0x432l;在Motorola体系中,read_msg()的返回值是0x1234。
无论在Intel体系还是Motorola体系中,程序段2.2中read_msg()的返回值都是0x1 234。
以上是联合体中多字节整型字节排放顺序不定导致漏洞的一个例子。倘若不明确联合体末尾填充的细节,或者不清楚联合体成员的对齐方式,或者不注意联合体中位字段成员的位排列次序,都有可能导致错误。作为将安全性放在第一位的C标准,MlSRA—C禁止使用联合体并非不可理喻。
然而,联合体毕竟是C语言的一个重要元素,所以MISRA—C主张禁止使用联合体的同时,也为效率和资源要求比较苛刻的情况开了一扇门,程序员在明确联合体各个实现细节的前提下,在万不得已的时候,仍可谨慎使用联合体,在不同体系的CPU间移植程序的时候要注意做相应的修改。
此外,《MISRA—C:2004》中也对结构体和联合体的编程风格作了限定。
规则18 1:所有结构体和联合体的定义必须保证完整性。
由于涉及ISOC中类型定义完整性等概念,碍于篇幅的原因,此处就不再赘述,读者可以参阅《MISRA-C:2004》一书和ISOC标准以了解更多信息,完善自己的编程风格。
3 小 结
总而言之,对于C程序中最为灵活的指针、结构体和联合体,程序员不仅仅要关注其定义和操作的一般方法,更要注重实现的细节。由于指针、联合体等的功能性错误一般都可以逃过编译器的检查,所以稍有疏忽,就可能导致程序在运行的时候出现严重错误,程序员必须以严谨甚至苛刻的态度对待指针、结构体和联合体。

文章版权归西部工控xbgk所有,未经许可不得转载。