要研究LINUX内核,C语言是基础中的基础,但是LINUX并不是完全的标准C,而是对标准C做了很多扩展,这些扩展特性对于我们分析内核有着很重要的作用,下面做些总结性的工作。
({
int y=foo();
int z;
if(y>0)
z=y;
else
z=-y;
z;
})
这种特性在宏定义中尤为安全。(因为他们对操作数只进行一次赋值)。这里定义了一个安全的求最小值的宏,在标准C中,通常定义为:
#define max(a,b) ((a)>(b)?(a):(b))
在这种定义下,如果他们有副作用(如果a或b是自增或自建变量的话)的话,a和b将进行两次运算,会得到错误的结果。而且如果两个变量的类型不一致,同样也会导致错误。
在GNU C中,如果你知道操作数的类型(这里假定为整型),你可以这样来定义这个宏:
#define maxint(a,b) ({int __a=(a); int __b=(b); __a > __b ? __a : __b; })
或者将参数类型作为宏的一个参数传入进去:
/* 两个参数类型相同时 */
#define max__t(type,a,b) ({\
type __a=(a);\
type __b=(b);\
__a > __b ? __a : __b; })
当然,如果你不知道参数的具体类型,你也可以使用typeof或____auto__type运算符。
/* 两个参数类型不同时,会发出警告 */
#define max(a,b) ({\
const typeof(a) __a = (a);\
const typeof(b) __b = (b);\
(void)(&__a == &__b);\ //检查__a 和__b 的类型是否一致
__a > __b ? __a : __b; })
为什么要使用局部标签:在复杂的宏定义中,如果一个宏包含有内嵌循环,goto语句可以方便地跳出它们。然而,拥有整个函数作用域的普通标签在这里不能被使用,因为该宏可能会在一个函数中被展开若干次,那样的话同样的一个标签就会被重复定义。局部标签就是用来避免这种情况的
怎么使用局部标签:GCC允许在任何内嵌代码块中声明局部标签,所谓的局部标签跟普通的标签用法一样(用在goto语句或者被获取地址),只不过你只能在声明它的代码块中使用。
局部标签的声明:
__label__ label;
或者
__label__ label1, label2, ...;
局部标签声明只是定义了标签的名字,但是并没有定义标签本身,它本身必须像普通标签那样在语句内嵌表达式内部使用局部标签。
另外要注意的是,局部标签的声明必须在代码块的**起始位置**(即位于任何其他声明和语句之前)。
#define SEARCH(value, array, target)\
do {\
__label__ found;\ //声明局部标签“found”
typeof(target) _SEARCH_target = (target);\
typeof(*(array)) *_SEARCH_array = (array);\
int i, j, value;\
for (i = 0; i < max; i++)\
for (j = 0; j < max; j++)\
if (_SEARCH_array[j] == _SEARCH_target){\
(value) = i;\
goto found;\
}\
(value) = -1;\
found:;\ //跳出循环
}while (0)
当然,也可以用语句表达式改写这个宏定义:
#define SEARCH(value, array, target)\
({ __label__ found;\
typeof (target) _SEARCH_target = (target);\
typeof (*(array)) *_SEARCH_array = (array);\
int i, j, int value;\
for (i = 0; i < max; i++)\
for (j = 0; j < max; j++)\
if (_SEARCH_array[j] == _SEARCH_target){\
(value) = i;\
goto found; }\
(value) = -1;\
found:value;\ //语句表达式最后的返回值是value
})
在ISO C99里,一个宏可以被声明为带可变的参数个数,就像函数一样。语法如下:
#define debug(format,...) fprintf(stderr, format, __VA_ARGS__)
__VA_ARGS__
。GCC 支持变参宏,并且提供另一种词法来定义它,即可赋予变参名称,就像普通参数一样:
#define debug(format, args...) fprintf(stderr, format, args)
debug(“A message”)
。在ISO C中不允许省略所有的变参,因为在这个字符串之后多了一个逗号”,”。#define debug(format,...) fprintf(stderr, format, ##__VA_ARGS__)
或者
#define debug(format, args...) fprintf(stderr, format, ##args)
这样,当我们省略变参的时候黏贴符能自动清除前面多余的逗号。
另外,在宏里面,除了两个井号 ## 可以作为黏贴符之外,其实一个井号 # 也可以用来黏贴符号,但是它要被用在字符串当中,例如:
#define prt(n) printf("calculate i'n: i#n = %d, with parameter %d\n", i##n, n)
int i = 1;
int i8 = 800, i9 = 900;
prt(8);
prt(9);
执行的结果如下:
calculate i'n: i8 = 800, with parameter 8
calculate i'n: i9 = 900, with parameter 9
**总结:**在上面的例子中:
#define prt(n) printf("calculate i'n: " "i#n = %d\n", i##n, n)
在GCC中,你可以在case标记后面指定一个连续值,例如:
case low...high:
这种写法等价于把每个值独立成一个个case标记的情况:
case low:
case low+1:
...
case high:
这个特性对于要写连续的ASCII码值的时候特别有用:
switch (ch)
{
case '0' ... '9':
c -= '0';
break;
case 'a' ... 'f':
c -= 'a' - 10;
break;
case 'A' ... 'F':
c -= 'A' - 10;
break;
}
注意:在“…”的左右两边一定要有空格,否则编将有词法错误。
在标准C里,数组或者结构变量的初始化值必须以固定的顺序出现,而在GCC(GNU C)中,通过制定索引或者结构域名,则允许初始化值以任意顺序出现。
指定数组索引的方法是在初始化值前面写[INDEX]=
,还可以使用[FIRST … LAST]=
的形式指定一个范围。比如:
int array[20] = {[2] = 100, [10 ... 19] = 200};
对于结构体初始化:
struct file_operations ext2_file_operations =
{
llseek: generic_file_llseek,
read: generic_file_read,
write: generic_file_write,
ioctl: ext2_ioctl,
mmap: generic_file_mmap,
open: generic_file_open,
release: ext2_release_file,
fsync: ext2_sync_file,
};
/* 但在linux中,更倾向于使用如下方式*/
struct file_operations ext2_file_operations =
{
.llseek = generic_file_llseek,
.read = generic_file_read,
.write = generic_file_write,
.aio_read = generic_file_aio_read,
.aio_write = generic_file_aio_write,
...
.writev = generic_file_writev,
.sendfile = generic_file_sendfile,
};
GNU C 允许使用零长度数组,在定义变长对象的头结构时,这个特性非常有用。例如:
struct var_data
{
int len;
char data[0];
};
char data[0]仅仅意味着程序中通过 var_data 结构体实例的 data[index]成员可以访问 len 之后的第 index 个地址,它并没有为 data[]数组分配内存,因此 sizeof(struct var_data) = sizeof(int)。
假设 struct var_data 的数据域保存在 struct var_data 紧接着的内存区域,通过如下代码可以遍历这些数据:
struct var_data s;
...
for (i = 0; i < s.len; i++)
{
printf("%02x", s.data[i]);
}
__FUNCTION__
保存函数在源码中的名字,__PRETTY__FUNCTION__
保存带语言特色的名字。__PRETTY_FUNCTION__
包括函数返回类型等额外信息,Linux内核只使用了__FUNCTION__
。//fs/ext2/super.c
void ext2_update_dynamic_rev(struct super_block* sb)
{
struct ext2_super_block *es = EXT2_SB(sb)->s_es;
if(le32_to_cpu(es->s_rev_level)->EXT2_GOOD_OLD_REV)
return;
ext2_warning(sb, __FUNCTION__,\
"updating to rev %d becauseof new feature flag",\
"running e2fsck is recommended",\
EXT2_DYNAMIC_REV);}
__FUNCTION__
将被替换为函数名ext2_update_dynamic_rev
。__FUNCTION__
看起来类似于标准C中的__FILE__
,但实际上**__FUNCTION__
是被编译器替换的,而 __FILE__
是被预处理器替换**。__func__
宏,因此建议使用__func__
替代__FUNCTION__
。GNU C允许声明函数、变量和类型的特殊属性,以便进行手工的代码优化和定制代码检查的方法。
no return属性用于函数,表示该函数从不返回。这可以让编译器生成稍微优化的代码,最重要的是可以消除不必要的警告信息比如未初使化的变量。例如:
//include/linux/kernel.h
#define ATTRIB_NORET __attribute__((noreturn))
void do_exit(long error_code) ATTRIB_NORET;
format(ARCHETYPE,STRING-INDEX,FIRST-TO-CHECK)属性用于函数,表示该函数使用printf,scanf或strftime风格的参数,使用这类函数最容易犯的错误是格式串与参数不匹配,指定format属性可以让编译器根据格式串检查参数类型。例如:
//include/linux/kernel.h
asm linkage int printk(const char* fmt, ...) __attribute__((format(printf,1,2)));
unused属性用于函数和变量,表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息。
**__section__(“section-name”)**属性用于函数和变量,通常编译器将函数放在.text区,变量放在.data区或.bss区,使用section属性,可以让编译器将函数或变量放在指定的节中。例如:
//include/linux/init.h
#define __init __attribute__((__section__(".text.init")))
#define __exit __attribute__((unused,__section__(".text.exit")))
#define __init data__attribute__((__section__(".data.init")))
#define __exit data__attribute__((unused,__section__(".data.exit")))
#define __initsetup __attribute__((unused,__section__(".setup.init")))
#define __init_call __attribute__((unused,__section__(".initcall.init")))
#define __exit_call __attribute__((unused,__section__(".exitcall.exit")))
**aligned(ALIGNMENT)**属性用于变量、结构或联合类型,指定变量、结构域、结构或联合的对齐量,以字节为单位,例如:
//include/asm-i386/processor.h
struct i387_fxsave_struct{
unsigned short cwd;
...
}__attribute__((aligned(16)));
packed属性用于变量和类型,用于变量或结构域时表示使用最小可能的对齐,用于枚举、结构或联合类型时表示该类型使用最小的内存。例如:
//include/asm-i386/desc.h
struct Xgt_desc_struct{
unsigned short size;
unsigned long address;
}__attribute__((packed));
GNU C提供了大量的内建函数,其中很多是标准C库函数的内建版本,例如memcpy,它们与对应的C库函数功能相同,本文不讨论这类函数,其他内建函数的名字通常以__builtin开始。
内建函数__builtin_return_address(LEVEL)
返回当前函数或其调用者的返回地址,参数LEVEL指定调用栈的级数,如0表示当前函数的返回地址,1表示当前函数调用者的返回地址,依此类推。例如:
//kernel/sched.c
printk(KERN_ERR "schedule_timeout: wrong timeout value %lx from %p\n",\
timeout, __builtin_return_address(0));
内建函数__builtin_constant_p(EXP)
用于判断一个值是否为编译时常数,如果参数EXP的值是常数,函数返回1,否则返回0。
//include /asm-i386/bitops.h
/*检测第 1 个参数是否为编译时常数以确定采用参数版本还是非参数版本的代码*/
#define test_bit(nr,addr)\
(__builtin_constant_p(nr)?\
constant_test_bit( (nr),(addr) ):\
variable_test_bit( (nr),(addr) ) )
内建函数**__builtin_expect(EXP, C)
**用于为编译器提供分支预测信息,其返回值是整数表达式EXP的值,C的值必须是编译时常数。例如:
#include
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
//kernel/sched.c
if( unlikely(in_interrupt()) ){
printk("Scheduling ininterrupt\n");
BUG();
}
printk();BUG();
这段代码放在较远的位置,以保证经常执行的目标码更紧凑。-ansi-pedantic
即可,使用上述参数后,所有GNC C扩展语法部分将会有编译警报。