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

时间:2011-09-02

  在信息工程中指针是一个用来指示一个内存地址的计算机语言的变量或中央处理器(CPU)中寄存器(Register)。指针一般出现在比较近机器语言的语言,如汇编语言或C语言。面向对象的语言如Java一般避免用指针。指针一般指向一个函数或一个变量。在使用一个指针时,一个程序既可以直接使用这个指针所储存的内存地址,又可以使用这个地址里储存的变量或函数的值。指针赋予了C编程的灵活性;结构体使得C程序整齐而紧凑;联合体在某些要求注重效率的场合有精彩的表现,这三个要素是C语言的精华。

  然而,精华并不意味着完美,C语言在赋予程序员足够灵活性的同时,也给了程序员很多犯错误的机会。C语言是一种计算机程序设计语言。它既具有语言的特点,又具有汇编语言的特点。它可以作为工作系统设计语言,编写系统应用程序,也可以作为应用程序设计语言,编写不依赖计算机硬件的应用程序。因此,它的应用范围广泛,不仅仅是在软件开发上,而且各类科研都需要用到C语言,具体应用比如单片机以及嵌入式系统开发。C语言发展如此迅速,而且成为的语言之一,主要因为它具有强大的功能。许多着名的系统软件,如DBASE Ⅳ都是由C 语言编写的。用C 语言加上一些汇编语言子程序,就更能显示C 语言的优势了,像PC- DOS 、WORDSTAR等就是用这种方法编写的。

  在此。部分介绍《MISRA—C:2004》中与指针相关的部分规则,第二部分讲解结构体和联合体的操作规范。

  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付的整型来参与运算。

  中央处理器(英文Central Processing Unit,CPU)是一台计算机的运算和控制。CPU、内部存储器和输入/输出设备是电子计算机三大部件。电脑中所有操作都由CPU负责读取指令,对指令译码并执行指令的部件。其功能主要是解释计算机指令以及处理计算机软件中的数据。所谓的计算机的可编程性主要是指对CPU的编程。 CPU由运算器、控制器和寄存器及实现它们之间联系的数据、控制及状态的总线构成。差不多所有的CPU的运作原理可分为四个阶段:提取(Fetch)、解码(Decode)、执行(Execute)和写回(Writeback)。如果CPU允许各种数据对象存放在任意的存储单元,则以上转换没有问题。

  规则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属性,编译器的优化可能导致程序员预先设计的硬件时序操作失。

  1.2  指针的运算

  由于互联网用户的急剧增加及应用范围的不断扩大,一个以制定互联网相关标准及推广应用为目的的互联网用户协会ISCO应用而生,它标志着互联网开始真正向商用过渡。ISOC(国际互联网协会)是一个非政府、非营利性的行业性国际组织,总部及秘书处设在美国弗吉尼亚州莱斯顿地区(Reston)并在美国华盛顿和瑞士日内瓦设有办事处。协会的目标是保证互联网的开放发展并为全人类服务。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语言的一个重要元素,所以MISRA—C主张禁止使用联合体的同时,也为效率和资源要求比较苛刻的情况开了一扇门,程序员在明确联合体各个实现细节的前提下,在万不得已的时候,仍可谨慎使用联合体,在不同体系的CPU间移植程序的时候要注意做相应的修改。

  此外,《MISRA—C:2004》中也对结构体和联合体的编程风格作了限定。

  规则18 1:所有结构体和联合体的定义必须保证完整性。

  由于涉及ISOC中类型定义完整性等概念,碍于篇幅的原因,此处就不再赘述,读者可以参阅《MISRA-C:2004》一书和ISOC标准以了解更多信息,完善自己的编程风格。

  3  小  结

  总而言之,对于C程序中为灵活的指针、结构体和联合体,程序员不仅仅要关注其定义和操作的一般方法,更要注重实现的细节。由于指针、联合体等的功能性错误一般都可以逃过编译器的检查。



  
上一篇:浅谈国产龙芯GS32I在小系统的硬件应用
下一篇:浅谈学习嵌入式系统实验教学的应用

免责声明: 凡注明来源本网的所有作品,均为本网合法拥有版权或有权使用的作品,欢迎转载,注明出处。非本网作品均来自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

相关技术资料