要研究LINUX内核,C语言是基础中的基础,但是LINUX并不是完全的标准C,而是对标准C做了很多扩展,这些扩展特性对于我们分析内核有着很重要的作用,下面做些总结性的工作。
1 语句表达式
这种特性在宏定义中尤为安全。(因为他们对操作数只进行一次赋值)。这里定义了一个安全的求最小值的宏,在标准C中,通常定义为:
在这种定义下,如果他们有副作用(如果a或b是自增或自建变量的话)的话,a和b将进行两次运算,会得到错误的结果。而且如果两个变量的类型不一致,同样也会导致错误。
在GNU C中,如果你知道操作数的类型(这里假定为整型),你可以这样来定义这个宏:
或者将参数类型作为宏的一个参数传入进去:
当然,如果你不知道参数的具体类型,你也可以使用typeof或____auto__type运算符。
2 局部标签
- 为什么要使用局部标签:在复杂的宏定义中,如果一个宏包含有内嵌循环,goto语句可以方便地跳出它们。然而,拥有整个函数作用域的普通标签在这里不能被使用,因为该宏可能会在一个函数中被展开若干次,那样的话同样的一个标签就会被重复定义。局部标签就是用来避免这种情况的
- 怎么使用局部标签:GCC允许在任何内嵌代码块中声明局部标签,所谓的局部标签跟普通的标签用法一样(用在goto语句或者被获取地址),只不过你只能在声明它的代码块中使用。
- 局部标签的声明:
局部标签声明只是定义了标签的名字,但是并没有定义标签本身,它本身必须像普通标签那样在语句内嵌表达式内部使用局部标签。
另外要注意的是,局部标签的声明必须在代码块的**起始位置**(即位于任何其他声明和语句之前)。
- 举例说明:
当然,也可以用语句表达式改写这个宏定义:
- 注意:例子代码未必准确,仅展示局部标签的用法。我们用语句表达式的时候,在最后的found局部标签后面有个语句 value; 而在do … while循环中并无出现,原因是语句表达式的值取决于最后的表达式,而do … while 循环中的found局部标签仅仅用来跳出循环。
3 变参宏
标准C的变参宏
- 在ISO C99里,一个宏可以被声明为带可变的参数个数,就像函数一样。语法如下:
- 这里的 “…” 代表变参,在引用宏debug的地方它代表着零个或多个相应的标识符,包括逗号。这些标识符将会替换
__VA_ARGS__
。 - 但是这样的宏不能处理零变参的情况,否则编译不会通过,因为零变参的时候会多一个逗号。
GCC的变参宏
- GCC 支持变参宏,并且提供另一种词法来定义它,即可赋予变参名称,就像普通参数一样:
- 这种用法与上面所述的ISO C形式的宏定义完全一样,只是看起来更具阅读性。
- args跟后面的三个点可以连在一起,也可以用空格分开,当然这个宏同样不支持零变参的情形,原因同上。
GNU C的变参宏
- 除了前面提到的可以为变参命名之外,GNU 预处理器CPP对ISO C的变参宏还进行了进一步的扩展,使之能处理零变参个数的情况。
- 举例来说,以下这个语句在ISO C编译器中编译时是错的:
debug(“A message”)
。在ISO C中不允许省略所有的变参,因为在这个字符串之后多了一个逗号”,”。 - GNU预处理器CPP允许你省略全部的变参,方法是在变参前加上黏贴符“##”:
或者
这样,当我们省略变参的时候黏贴符能自动清除前面多余的逗号。
- 另外,在宏里面,除了两个井号 ## 可以作为黏贴符之外,其实一个井号 # 也可以用来黏贴符号,但是它要被用在字符串当中,例如:
执行的结果如下:
- 在字符串中,我们可以用一个井号来黏贴宏参数,就像上面我们看到的那样。其中字符串与黏贴字符之间的空格是可选的,预处理器会自动去掉多余的空格。
**总结:**在上面的例子中:
- 故意在四个地方都用到了标识符n(那个转义换行符’\n’不在讨论范围内),依次分析是:
- 在字符串中直接出现的“宏参数”实际上并不会被当成参数,而是一个普通的字符n;
- 如果要解决第一个问题,那就要在字符串当中使用一个井号 # 来黏贴宏参数;
- 不在字符串当中要黏贴宏参数,则需要两个井号 ## 来黏贴;
- 不在字符串中,如果直接出现宏参数,则预处理器将进行宏展开。
4 case范围
在GCC中,你可以在case标记后面指定一个连续值,例如:
这种写法等价于把每个值独立成一个个case标记的情况:
这个特性对于要写连续的ASCII码值的时候特别有用:
注意:在“…”的左右两边一定要有空格,否则编将有词法错误。
5. 标号元素
- 在标准C里,数组或者结构变量的初始化值必须以固定的顺序出现,而在GCC(GNU C)中,通过制定索引或者结构域名,则允许初始化值以任意顺序出现。
- 指定数组索引的方法是在初始化值前面写
[INDEX]=
,还可以使用[FIRST … LAST]=
的形式指定一个范围。比如:
对于结构体初始化:
- 使用这种形式,当结构体的定义变化导致元素的偏移位置改变时,仍然可以确保已知元素的正确性。对于未出现在初始化中的元素,其初值为0。
6. 零长度数组
GNU C 允许使用零长度数组,在定义变长对象的头结构时,这个特性非常有用。例如:
char data[0]仅仅意味着程序中通过 var_data 结构体实例的 data[index]成员可以访问 len 之后的第 index 个地址,它并没有为 data[]数组分配内存,因此 sizeof(struct var_data) = sizeof(int)。
假设 struct var_data 的数据域保存在 struct var_data 紧接着的内存区域,通过如下代码可以遍历这些数据:
7. 函数名
- GNU C中预定义两个标志符保存当前函数的名字,
__FUNCTION__
保存函数在源码中的名字,__PRETTY__FUNCTION__
保存带语言特色的名字。 - 在C函数中这两个名字是相同的。在C++函数中,
__PRETTY_FUNCTION__
包括函数返回类型等额外信息,Linux内核只使用了__FUNCTION__
。
- 这里
__FUNCTION__
将被替换为函数名ext2_update_dynamic_rev
。 - 虽然
__FUNCTION__
看起来类似于标准C中的__FILE__
,但实际上**__FUNCTION__
是被编译器替换的,而 __FILE__
是被预处理器替换**。 - 在C99中支持
__func__
宏,因此建议使用__func__
替代__FUNCTION__
。
8. 特殊属性声明
GNU C允许声明函数、变量和类型的特殊属性,以便进行手工的代码优化和定制代码检查的方法。
- no return属性用于函数,表示该函数从不返回。这可以让编译器生成稍微优化的代码,最重要的是可以消除不必要的警告信息比如未初使化的变量。例如:
- format(ARCHETYPE,STRING-INDEX,FIRST-TO-CHECK)属性用于函数,表示该函数使用printf,scanf或strftime风格的参数,使用这类函数最容易犯的错误是格式串与参数不匹配,指定format属性可以让编译器根据格式串检查参数类型。例如:
- 表示第一个参数是格式串,从第二个参数起,根据printf()函数的格式串规则检查参数。
- unused属性用于函数和变量,表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息。
- **__section__(“section-name”)**属性用于函数和变量,通常编译器将函数放在.text区,变量放在.data区或.bss区,使用section属性,可以让编译器将函数或变量放在指定的节中。例如:
- 连接器可以把相同节(section)的代码或数据安排在一起,Linux内核很喜欢使用这种技术,例如系统的初始化代码被安排在单独的一个节,在初始化结束后就可以释放这部分内存。
- **aligned(ALIGNMENT)**属性用于变量、结构或联合类型,指定变量、结构域、结构或联合的对齐量,以字节为单位,例如:
- 表示该结构类型的变量以16字节对齐。通常编译器会选择合适的对齐量,显示指定对齐通常是由于体系限制、优化等原因。
- packed属性用于变量和类型,用于变量或结构域时表示使用最小可能的对齐,用于枚举、结构或联合类型时表示该类型使用最小的内存。例如:
- 域address将紧接着size分配。属性packed的用途大多是定义硬件相关的结构,使元素之间没有因对齐而造成的空洞。
9. 内建函数
GNU C提供了大量的内建函数,其中很多是标准C库函数的内建版本,例如memcpy,它们与对应的C库函数功能相同,本文不讨论这类函数,其他内建函数的名字通常以__builtin开始。
- 内建函数
__builtin_return_address(LEVEL)
返回当前函数或其调用者的返回地址,参数LEVEL指定调用栈的级数,如0表示当前函数的返回地址,1表示当前函数调用者的返回地址,依此类推。例如:
- 内建函数
__builtin_constant_p(EXP)
用于判断一个值是否为编译时常数,如果参数EXP的值是常数,函数返回1,否则返回0。
- 很多计算或操作在参数为常数时有更优化的实现,在GNUC中用上面的方法可以根据参数是否为常数,只编译常数版本或非常数版本,这样既不失通用性,又能在参数是常数时编译出最优化的代码。
- 内建函数**
__builtin_expect(EXP, C)
**用于为编译器提供分支预测信息,其返回值是整数表达式EXP的值,C的值必须是编译时常数。例如:
- 这个内建函数的语义是EXP的预期值是C,编译器可以根据这个信息适当地重排语句块的顺序,使程序在预期的情况下有更高的执行效率。
- 上面的例子表示处于中断上下文是很少发生的,编译器可以将
printk();BUG();
这段代码放在较远的位置,以保证经常执行的目标码更紧凑。 - 若不想使用GNUC扩展,那么只需要在gcc参数后面加上
-ansi-pedantic
即可,使用上述参数后,所有GNC C扩展语法部分将会有编译警报。