当前位置: > 华清远见教育科技集团 > 嵌入式学习 > 讲师博文 > 内核宏函数MIN()详解
内核宏函数MIN()详解
时间:2016-12-12作者:华清远见

宏函数(或者叫带参宏)是一个功能代码段,它用宏来实现,跟普通函数不同,它在编译过程的预处理阶段被替换成相应的语句,从而减少了普通函数调用前的栈操作,提高了程序的运行效率,这一点在频繁调用某个函数的时候尤其突出。但是宏函数带来的安全问题也不容忽视,下面用一段代码来说明这个问题:

该程序是用宏函数求两数之间的小数并返回,编译运行,没有问题:

以上程序看似没有问题其实是漏洞百出,一下代码展现这些问题:

漏洞1:

讲表达式作为参数给函数传参:

程序结果本来应该是11,但是程序却打印出12:

为什么会这样呢?因为预处理器在处理宏函数时是将传入的表达式完全替换:

图中预处理之后的代码第857行“++x”被完整替换了两次,导致程序运行时x的值被自增了两次。

漏洞2:

比较两个不同类型的数据的时候.

编译运行,结果如下:

虽然将变量min的类型修改称float就可以解决这个问题,但是我们在未知参数的情况下无法得知该用什么类型的变量来接受其返回值,一个通用函数不应该随意出现这种错误。

那么该怎样编写宏函数才能更加安全呢?

我们来参考一下内核中的MIN函数,看内核是如何完成的

宏的定义如果未能在一行写完则需加“\”表示续行未完,其中需要注意在该行“\”后不可以出现任何字符。

为解决漏洞1中的问题,内核没有直接对传入的值比较,而是定义了两个变量_x_ 和_y_ 先接受两个传入参数X和Y的值,然后用_x_ 和 _y_这两个值去比较。如此就避免了表达式重复计算的问题。但是变量的定义需要类型,那么如何获知传入的参数类型呢?在GCC中提供了一个扩展关键字typeof,它可以用于获取传入参数的类型。另外既然是比较那么就不需要去修改原数据的值,为了避免误操作带来的危害,我们再加入一个const关键字来修饰新定义的两个变量_x_ 和_y_ 为只读。因此宏函数的5,6两行就是定义了两个常量(或叫只读变量)并将值初始化为参数X和Y的值, 这样即使传入两个表达式也将一并获取其值。

于是宏函数的后一行用三目运算将刚才定义的两个只读变量_x_ 和_y_ 的值进行比较并返回较大值。

但是为何第7行出现了一个表达式“(void)(&_x_ == &_y_)”,这是为了解决漏洞二中的参数类型不匹配问题,我们不希望MIN函数去比较两个类型不相同的数,以避免其带来的隐患。

首先(&_x_ == &_y_)意思是将新定义的两个变量的地址相比较是否相等,我们知道两个变量的地址肯定不相等,况且此语句比较的结果并没有对程序造成任何影响,但是为何还要比较呢?这是为了让编译器发现传入的参数类型不一致的时候给出警告。而方法就是让两不同类型的地址相比较(不同类型的地址不能运算)。后表达式前的(void)是一个强制类型转换,而除了地址之外void类型是不允许存在的,所以它的意思是告诉编译器去忽略表达式的结果。

后,为何要在一对“()”内加“{}”呢?这是因为小括号“()”内部应是一个表达式,不可以定义变量,因此“typeof(X) _x_ = (X)”语句将不能出现在”()”内部。而“{}”则是一个局部代码段的标志。

那么,既然加了“{}”为何还要在外面加“()”?

来看下面的代码:

代码中定义了两个相同名字的变量x,y,局部变量没有声明全部是定义,于是不能出现多次定义。但是结果呢?

为何没有出现重复定义的错误呢?关键在于第9行和第12行的一对{},函数内部的大括号内括的代码段是局部代码段,里面的变量作用域比函数内部的局部变量还要小(局部变量作用域是整个函数,而{}内的变量作用域仅在括号内部)。所以避免了局部变量重复定义的问题出现。但是相反在外部想要使用括号内部的变量该如何呢?

程序运行结果:

在括号外想使用内部的值,需在“{}”外加“()”,第9行代码的意思是将整个代码段“{}”的值复制给ret,而“{}”内部的后一条语句的值就是整个“{}”段的值。

同样,还可以写表达式或常量:

运行结果:

因此,宏函数MIN的完整定义为:

#define MIN(X,Y) ( \
         { \
                 const typeof(X) _x_ = (X); \
                 const typeof(Y) _y_ = (Y); \
                 (void) (&_x_ == &_y_); \
                 _x_ > _y_ ? _y_ : _x_ ; \
        })

发表评论
评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)