软件检测实验室在建立软件测试体系或申请cnas/cma相关资质时,需要依据相关标准,使用有效的方法开展检验检测活动,GJB-8114是一部嵌入式软件安全测试相关的国家标准,本系列文章我们就针对GJB-8114《C/C++语言编程安全子集》的具体内容进行分析解读。
GJB-8114标准规则中一共有124条强制性规则,是按类分的,一共有13类,声明定义类、版面书写类、指针使用类、分支控制类、跳转控制类、运算处理类、函数调用类、语句使用类、循环控制类、类型转换类、初始化类、比较判断类以及名称、符号与变量使用类。本文我们先针对声明定义类进行解读。
声明定义类一共有23条,都是我们在日常的工作中写程序的时候容易出的一些瑕疵,不能说是错误,但是在规则里是不被允许的。很多人可能会说,它并不影响程序的实际运行,这样理解是片面的。所有这些规则都是航天型号软件在整个发展过程中出现的一些问题,经过总结归纳而来的。
R-1-1-1禁止通过宏定义改变关键字和基本类型含义
大家用过C语言的都知道,像字符类型定义等都被称为关键字,这些关键字通常不能把它当作一个基本类型,重新定义。
R-1-1-2禁止将其他标识宏定义为关键字和基本类型
比如说把一个int64定义成长整型,这就是违反规则的,标准的定义应该是把一个长整型定义成int64。
R-1-1-3用typedef自定义的类型禁止被重新定义
比如说我们把一个int的类型定义成了mytype,就不能再把一个float型或其他类型定义成mytype了。
R-1-1-4禁止重新定义C或C++关键字
本来在标准的C语言中是不支持重新定义的,但是在C++语言中支持重定义。实际上在大家在编写程序的过程中会发现,在C++中重新定义关键字是没有任何问题的,只不过咱们的GJB-8114规定不允许这样定义。
R-1-1-5禁止#define被重复定义
用#define定义一个宏,然后再定义这个宏为别的,这样就是重复定义,原则上是不允许的。在C语言以及C++语言现在的各个编译器中这样定义并不会出错,但是会出现一个问题,当你用到这个被定义的宏的时候,就会困惑,这个宏到底用的是哪一次定义的值?原则上是离它最近的那次定义的值,但是分不清。所以想重新定义这个宏的话,要用一个undef取消定义后再重新定义。
R-1-1-6函数中的#define和#undef必须配对使用
原则上来说是不允许单独使用#undef这个关键字的,大家可以根据上面举的这些例子看一下。实际上这些在预编译过程中就可以发现定义得是否正确,比如说示例2中的例子,#ifdef、 #undef,如果你不用#endif的话是通不过的。但是在早期的编译器中是不太容易发现的,所以这条规则也被提了出来。
R-1-1-7以函数形式定义的宏,参数和结果必须用括号括起来
比如说违背示例中展示的,我们定义了两个参数,这两个参数我们没有把它括起来,或者我们定义了一个参数,我们也没有将它括起来。这样会出现什么问题呢?在咱们的编译器中,是先把这个宏进行解析,替代。也就是用你定义的这个宏替代你写的表达式中相应的宏引用。
当你不用括号的时候,比如说你存的这个参数是一个表达式,这个表达式不是先计算一个,就会出现问题。通常大家可能也不太注意,这样用的时候,程序可能也没错,是因为你存参数的时候没有用表达式,当你用表达式的时候就会出问题了。
R-1-1-8结构、联合、枚举的定义必须定义标识名
比如像违背示例所示,我们定义一个结构,把这个结构体取了一个变量名。但是在早期,基本上都是这样做的。这种方式定义的结果是什么呢?对程序没有任何影响。但是按照现在的要求,这样做是不可以的,一定要把这个结构取一个名字,然后在后边声明这个结构为哪个变量。
结构、联合、枚举都是这样要求的,像违背示例中展示的例子,实际上你在程序中这样做了它也不会出问题,只不过你用遵循示例这样的结构重新声明一个变量的时候,就会出问题了。
R-1-1-9结构体定义中禁止含有无名结构体
在一个结构体中又包含一个结构体,里面这个结构体一定要有一个对应的变量名,这样你才能够引用这个结构体中的元素,如果没有变量名的话就不能引用这个元素。像违背示例中展示的,比如说我要用到这里的xs,它是哪一个元素下面对应的元素呢?遵循示例中就告诉你,它是Scoor结构元素下对应的一个元素。
R-1-1-10位定义的有符号整型变量位长必须大于1
由于符号位至少要占有1位,所以定义一个有符号的位变量的时候,至少要达到2位才能表示有符号的位变量是整的还是负的。
R-1-1-11位定义的整数型变量必须明确定义是有符号还是无符号
违背示例中所展示的定义方式,原则上来说是没有什么问题的,编译器并不会显示这是一个错误。但是为了明确表达这个数有没有负的可能性,所以说一定要在前面标识出,这个数是整型的,是带符号的,还是不带符号的。这是一个强制性要求,在程序中大多数情况不会出错,只有在极少数的情况下会出错。所以说,一般的情况下还是要按照这种规则定义一下。
R-1-1-12位定义的变量必须是同长度的类型且定义位禁止跨越类型的长度
比如说像违背示例中,定义了很多的串。不能跨越类型也就是说,一个定义不能由两个串(单位)组成。与此同时,不能定义一个位变量既在第一个八位当中,同时在其它八位中也含有它的一些位,也就是说不能跨一个字符。
R-1-1-13函数声明中必须对参数类型进行声明,并带有变量名
违背示例中所举的例子,在最早的C语言中是标准的定义,但是现在要求,凡是不带参数的要写上void,带参数的不能光写它的类型,同时要有一个它的名字。通常我们在写函数体中是肯定带这个名字的,但是在函数声明中,有些人为了简略,或者有些年龄比较大的人,学C语言比较早的人就会容易不带变量名去写声明。
这个在编译器里是直接通过的,没有任何问题,但是要求不允许这样做。这样做会引起什么后果呢?可以很负责任地说不会引起什么后果,这就是一个强制性的规则。
R-1-1-14函数声明必须与函数原型一致;一致性要求包括函数类型,参数类型和参数名
这个在我们的实际操作过程中,往往是先写了一个标准的函数,然后把函数定义的部分直接就复制到函数声明中去,这样就不会引起错误。但是有些人是先构造了一些函数,再具体写这些函数实现,这样往往就会导致函数的参数名前后不一致了。
比如违背示例中展示的,在实现中,用了一个length、一个width,但是在声明中声明了一个length一个b。实际上这个编译也不会有任何问题,但是要求是不允许的。同时这个例子中,它的声明和定义就不是一个函数,如果在写程序的过程中,如果出现这种情况,就会发现,链接提示找不到那个函数,它俩并不被认为是一个函数。
R-1-1-15函数中的参数必须使用类型声明
也就是说参数必须有一个类型,如果没有类型的话,实际上编译器是无法通过的。在咱们介绍的这么多规则当中,有一些实际上你想这么做都做不到,因为只要你这样做了,编译器就通不过,所以这些规则在以后的GJB-8114的修订过程中,就有可能就会把这些规则给废掉了,但是目前没有这种打算,这种规则依旧存在。
R-1-1-16外部声明的变量,类型必须与定义一致
这个也就是说你声明了一个外部的变量,它定义的类型在重新声明外部变量的时候,类型还必须是一致的。如果你的声明不一致,有些编译器检查得不太严格,也不会出问题,但是当你用的时候,就会发觉一些问题。比如说你用一个整型的,重新声明的时候声明为了长整型。
当然现在在PC机上,整型和长整型都是4个字节的,应该不会出什么问题,但是原来这种整型是2个字节,16位,长整型是4个字节,32位。当你用到重新声明中的长整型时,你对它进行赋值,赋的是32位,可你只占了16位的地址,就会把这个变量后面的地址中后面的16位也给赋值了,就会改写你的程序。有时候你还发现不了,这个是一个挺严重的错误。
R-1-1-17禁止在函数体内使用外部声明
这个是说我在一个函数体内声明外部的变量、函数,这个在大多数的编译器中都不会出问题,都能够通过,但是上原则上是不允许的,要像遵循示例中表示的那样,把这些外部声明专门放到函数体的外面。比遵循示例中更好的方式是,放到一个头文件中,在前面有一个#include头文件。
如果你总是按照违背示例中那样写的话,就会发现整个程序会很乱,不易于阅读,而且它对于一个函数的总行数也有很大的影响。
R-1-1-18数组定义禁止没有显式的边界限定
通常的时候我们做数组定义的时候,并不知道要定义多长,就不写我这个边界到底是多大,有多大就自动做成多大。在我们学习C语言或者C++语言的时候这种定义没有任何错误,实际上它是符合C语言的语法的,只不过是不符合GJB-8114的规则。
所以说有时候当我们把程序交给测评机构进行测评的时候,他们会发现违反了很多的规则,给你列出来都有哪些。你一看会觉得,我学的时候都是这样学的呀。实际上语法就是这样,可能你的语法是没问题的,但是却是违反规则的。
就是说必须显式地定义它的长度,不依靠机器或者编译器给它分配长度。但是这里面有一个例外,大家看看遵循示例中举的例子,比如说我们定义指针的数组,就可以定义成两个指向字符串指针的地址。显然没有对每一个字符串的长度没有做限定,对字符串的个数做了限定。
为什么支持这样的呢,我们原来好多做页面的时候要做菜单,菜单最好是这么定义,不能限定死了,尤其是我们还涉及到一些指针的动态分配,所以说这种情况是被允许的。当然我们现在很少有直接写菜单的了,所以严格来说,这样写也不是非常合适的,这里就是我们GJB-8114的举例中有些矛盾的地方。
R-1-1-19禁止使用extern 声明对变量初始化
一个变量初始化可以在声明中进行初始化,但是不允许在外部声明的时候对它进行初始化。也就是说不管在我声明的时候有没有初始化,但是在外部声明的时候都不能做初始化。
R-1-1-20用于数值计算的字符型(短型)变量必须明确定义是有符号还是无符号
在示例中,char是一个标准的类型定义,但是在我们的GJB-8114规则中规定。不允许char这种类型,只能有 unsigned的char或者signed的char。这样可以帮助大家通俗地理解char这个类型是存正数或者存正负数。
R-1-1-21禁止在#include 语句中使用绝对路径
这个一般老的程序员不会犯这种错误,新程序员挺容易犯这种错误的,在写路径的时候就直接指明了是什么目录,就像违背示例中展示的那样,实际上是不允许的。像遵循示例中那样写,好处是什么呢?像违背示例,比如说你的程序,在C盘上运行,可以寻找C盘的目录,如果在D盘上运行,它仍然要找C盘的目录。而遵循示例,会去找当前目录,也就是说我在哪运行就找哪个目录下的东西。所以,考虑到可移植性、可运行性等,禁止用绝对路径来表示。
R-1-1-22禁止头文件重复包含
像违背示例中展示的例子,对文件1做了定义,在文件2中包含了文件1,在实际用的时候,即包含了文件1,也包含了文件2,这样就是重复包含了。这种重复包含往往不容易发现,那怎么办呢?
对于头文件定义.h文件的写法有一个通常的写法,像遵循示例中展示的那样,如果没有定义头文件,我就把头文件定义一下,把内容写进去,在最后用个#endif。文件2中既包含了文件1,又包含了文件2,当我遇到文件2的时候,对它进行解析,发现文件1已经被定义了,直接就到最后了,就不会出现重复包含。
如果大家在头文件中定义的是一些变量,大家会发现在编译器中,违背示例那样是通不过了,如果没有定义变量,只是定了一些宏,那是可以通过的。
R-1-1-23函数参数表为空时,必须使用void 明确说明
以前我们学习C语言的时候,可能老师会说括号里面没有参数,可以不写参数直接用括号来代替了,现在规定,括号里面如果没有参数,必须用一个void 来说明,比如说你这个函数不希望它返回参数,也需要用void作为它的类型声明。实际上违反它并不会使程序出错,但是它是违反我们强制性规则的。
后面的文章会继续针对其他大类为大家展开介绍,欢迎继续关注。