GNU C扩展

要研究LINUX内核,C语言是基础中的基础,但是LINUX并不是完全的标准C,而是对标准C做了很多扩展,这些扩展特性对于我们分析内核有着很重要的作用,下面做些总结性的工作。

一:柔性数组(flexible array) 
柔性数组也称为零长度数组,或者零长度数组。这种数组通常在结构体当中出现,它本身并不占用空间,但是有了柔性数组意味着结构体的尺寸充满了变数。
例如以下例子:
struct  usb_interface_cache {
        unsigned  num_altsetting;  // number of alternate settings
        struct  kref  ref;
         /* variablee-length array of alternate settings for this           
             interface, stored usb_host_interface altsetting[0] */
         struct usb_host_interface  altsetting[0];
};
定义了包含柔性数组的结构,可以在随后的实例化过程中通过具体所需来动态地改变结构体的大小,例如:
struct  usb_interface_cache *p = /
           malloc(sizeof(struct usb_interface_cache) + datasize);
其中,datasize就是你需要扩展的内存大小,可以用来存放结构体数据,此时对这部分内存的引用,就可以用以下方式:
p->altsetting[ i ];
这样,就能通过标识符altsetting来访问紧邻结构体成员之后的内存了。用这种方法可以根据具体情况方便地扩展结构体的大小。


二、标号元素
在标准C里,数组或者结构变量的初始化值必须以固定的顺序出现,而在GCC中,通过制定索引或者结构域名,则允许初始化值以任意顺序出现。
指定数组索引的方法是在初始化值前面写“[INDEX]=”,还可以使用“[FIRST … LAST]=”的形式指定一个范围。比如:
int  array[20] = {[2] = 100, [10 … 19] = 200};
对于结构体初始化,比如:
struct  person  Bill = {
                  .name = “Bill Gates”;
                  .age = 50;
                  .wealth’s world ranking = 1;
};
将结构Bill的元素name初始化为”Bill Gates”, 元素age初始化为50,依此类推。
使用这种形式,当结构体的定义变化导致元素的偏移位置改变时,仍然可以确保已知元素的正确性。对于未出现在初始化中的元素,其初值为0。



三、case范围
在GCC中,你可以在case标记后面指定一个连续值,例如:
case  low … high:
这种写法等价于把每个值独立成一个个case标记的情况:
case  low:
case  low+1:
… …
case  high:
这个特性对于要写连续的ASCII码值的时候特别有用:
case  ‘A’ … ‘Z’ :
注意:在 “…” 的左右两边一定要有空格,否则编将有词法错误。


四、语句表达式(statement-embedded expression)
GCC 把包含在括号中的复合语句看作是一个表达式,称为语句表达式,它允许在一个表达式内使用循环、跳转、局部变量甚至函数调用,并可以出现在任何允许表达式出现的地方。
位于括号中的复合语句的最后一句必须是一个以分号结尾的表达式。它的值将成为这个语句表达式的值。
例如,以下是一个语句表达式:
({int y = foo();   int z;
   if(y > 0)  z = y;   else  z = -y;
   z;})

语句表达式(statement-embedded expression)
计算极值通常被定义为:
#define   MAX(x, y)    (((x)>(y)) ? (x) : (y)) 
#define   MIN(x, y)    (((x)<(y)) ? (x) : (y))
但是其中的x和y可能会分别被计算两次。当参数x和y带有副作用时,将会产生错误的结果。而内核则使用语句表达式将其定义为:
#define  MAX(x, y)  ({ typeof(x)  _x = x;  typeof(y)  _y = y;
                                    void (&_x == &_y);
                                    (((_x)<(_y)) ? (_x) : (_y))     })
其中,typeof关键字用来获取变量的类型。中间的void (&_x == &_y);用来判断x和y的类型是否相同的(如果相同则没有反应,如果不相同则他们的地址类型是不同的,会有警告)。


五、变参宏
在ISO C99里,一个宏可以被声明为带可变的参数个数,就像函数一样。语法如下:
#define  debug(format, …)  fprintf(stderr, format, __VA_ARGS__)
这里的 “…” 代表变参,在引用宏debug的地方它代表着零个或多个相应的标识符,包括逗号。这些标识符将会替换__VA_ARGS__。(但是这样的宏不能处理零变参的情况,否则编译不会通过,因为零变参的时候会多一个逗号)

GCC 支持变参宏,并且提供另一种词法来定义它,即可赋予变参名称,就像普通参数一样:
#define  debug(format, args…)  fprintf(stderr, format, args)
这种用法与上面所述的ISO C形式的宏定义完全一样,只是看起来更具阅读性。(args跟后面的三个点可以连在一起,也可以用空格分开,当然这个宏同样不支持零变参的情形,原因同上)

除了前面提到的可以为变参命名之外,GNU 预处理器CPP对ISO C的变参宏还进行了进一步的扩展,使之能处理零变参个数的情况。举例来说,以下这个语句在ISO C编译器中编译时是错的:
debug(“A message”)
在ISO C中不允许省略所有的变参,因为在这个字符串之后缺少了一个逗号”,”。
GNU预处理器CPP允许你省略全部的变参,方法是在变参前加上黏贴符“##”:
#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(那个转义换行符'/n'不在讨论范围内),依次分析是:
第一:在字符串中直接出现的“宏参数”实际上并不会被当成参数,而是一个普通的字符n;第二:如果要解决第一个问题,那就要在字符串当中使用一个井号 # 来黏贴宏参数;第三,不在字符串当中要黏贴宏参数,则需要两个井号 ## 来黏贴;第四,不在字符串中,如果直接出现宏参数,则预处理器将进行宏展开。


六、局部标签
GCC允许你在任何内嵌代码块中声明局部标签,所谓的局部标签跟普通的标签用法一样(用在goto语句或者被获取地址),只不过你只能在声明它的代码块中使用。
局部标签的声明如下:
__label__  label;       或者
__label__  label1, label1, … ;
局部标签声明只是定义了标签的名字,但是并没有定义标签本身,它本身必须像普通标签那样在语句内嵌表达式内部使用局部标签。
另外要注意的是,局部标签的声明必须在代码块的起始位置(即位于任何其他声明和语句之前)
在复杂的宏定义中,局部标签显得尤为有用。如果一个宏包含有内嵌循环,goto语句可以方便地跳出它们。

然而,拥有整个函数作用域的普通标签在这里不能被使用,因为该宏可能会在一个函数中被展开若干次,那样的话同样的一个标签就会被重复定义。局部标签就是用来避免这种情况的,例如:#define SEARCH(value, array, target) 
do {
      __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:;
} 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;
})
局部标签在内嵌函数(如果有)中也是可见的。 (小细节:注意到我们用语句表达式的时候,在最后的found局部标签后面有个语句 value;  而在do ... while循环中并无出现,原因是语句表达式的值取决于最后的表达式,而do ... while 循环中的found局部标签仅仅用来跳出循环)

 

七、attribute机制
在GNU C中, 允许声明函数、变量和类型的特殊属性,以便指示编译器进行特定方面的优化和更仔细的代码检查。使用方式为在声明后面加上: __attribute__(( ATTRIBUTE ))。关键字__attribute__用来在声明的时候指定一个特定的属性。该关键字后面紧跟两对圆括号,括号里指定具体的属性,多个说明之间以逗号分隔。

GCC支持很多属性,常用的有以下这些:

1、noreturn
该属性用于函数,表示该函数从不返回。它能够让编译器生成较为优化的代码,消除不必要的警告。例如:

int for_testing(int);
void myexit(void) __attribute__ ((noreturn));
int main(void)
{
    int i = 1;
    for_testing(i);
    return 0;
}
int for_testing(int n)
{
    if(n > 0)
        myexit( );
    else
        return 100;
}
void myexit(void)
{
    exit(0);
}

在这个例子中,如果函数myexit不用 __attribute__ ((noreturn))修饰的话,编译的时候会有警告信息,因为函数for_testing是又返回类型的,但是如果进到了if分支语句的话,就有可能执行exit函数而再也不返回了。
标准C库函数中有很多函数用到了这个属性,比如最熟悉的有exit() 和 abort(),这两个函数从来不返回。
extern void exit(int)   __attribute__((noreturn));
extern void abort(void) __attribute__((noreturn));


2、format
其格式是:format (archetype, string-index, first-to-check)。
属性format用于函数,表示该函数使用printf、scanf、strftime或者strfmon风格的参数,并可以让编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。
archetype指定哪种风格( printf , scanf , strftime , gnu_printf , gnu_scanf , gnu_strftime或者 strfmon。 你也可以用  __printf__ , __scanf__ , __strftime__  或者 __strfmon__),string-index指定传入函数的第几个参数是格式化字符串,first-to-check指定从函数的第几个参数开始是变参。
比如:
    static inline int printk(const char *s, ...) __attribute__ ((format (printf, 1, 2)));
这是include/kernel.h中对printk()函数的声明,其中printk的第1个参数是格式化字符串,从第2个参数开始根据格式化字符串检查变参。


3、unused
属性unused用于函数和变量,表示该函数或者变量可能并不使用,这个属性能够避免编译器产生警告信息。比如:
int maybe __attribute__((unused));

void __attribute__((unused)) maybe(void)
{
    printf("whatever");
}

4、section("section-name")
通常,编译器会把生成的代码放在 .text 代码段中,但是有时候你可能需要一些额外的段,或者你需要把你的函数放在特定的段中。属性section允许你的这些要求。例如:
    extern void foobar (void)__attribute__((section("bar")));
编译器将会把函数foobar放在bar这个段里面。

再比如:
++++ include/linux/init.h
#define  __init  __attribute__((section(".init.text")))
#define  __initdata  __attribute__((section(".init.data")))
#define  __exitdata  __attribute__((section(".exit.data")))
#define  __exit_call  __attribute_used__ __attribute__((section(".exitcall.exit")))
通常编译器将函数放在.text段中,静态变量放在.data或者.bss段里面,而使用section属性,可以让编译器将函数或变量放在指定的段中。因此上面对 __init 的定义便表示将 __init 修饰的代码放在 .init.text 段里面。(放在init段里面的代码只会在初始化的时候被执行一遍,因此该段的代码可以在执行之后释放)。


5、aligned(ALIGNMENT)
属性aligned用于变量、结构或者联合,设定一个指定大小的对齐格式,以字节为单位。但是要注意,这个属性的设定只能用来增加对齐地址而不能减少,例如:
char c __attribute__((aligned(16))); //让变量 c 以16字节对齐。
int i __attribute__((aligned(2))); //该aligned属性想要设定变量 i 以2字节对齐,但由于 i 需要4字节对齐。因此该声明无效。
再如:
struct ohci_hcca
{
#define NUM_INTS 32
    __hc32 ini_table [NUM_INTS];

    __hc32 frame_no;
    __hc32 done_head;
u8 reserved_for_hc [116];
u8 what[4];
} __attribute__((aligned(256)));
这表示结构体 ohci_hcca 以256字节对齐。

对于结构体的大小而言,网上书上都有一些比较含糊其辞的解释,今天我再啰嗦两句,彻底解决这个问题。一个结构体的大小取决于里面的成员的排列顺序,以及需要最大对齐方式的那个成员。怎么理解呢? 请看下面这几个例子:
struct A
{
    short s1, s2;
    char c;
};


sizeof(struct A) = 6。因为A成员中最大的对齐成员是short类型,2字节对齐,因此成员c后面需要补1个零。因此总共占用6个字节。

struct B
{
    int i;
    char c;
};



sizeof(struct B) = 8。因为B成员中最大的对齐成员是int类型,4字节对齐,因此成员c后面需要补3个零。因此总共占用8个字节。

struct C
{
    double lf;
    char c;
};


sizeof(struct C) = 12。因为C成员中最大的对齐成员是double类型,4字节对齐,因此成员c后面需要补3个零。因此总共占用8个字节。这里一定要注意,一个变量的最小地址对齐格式跟它所占的字节大小不要混淆,一个变量所占的字节的大小指的是变量的尺寸大小,而一个变量的最小地址对齐格式,指的是一个变量的起始地址的值至少是某个数的整数倍,比如int型的最小对齐格式是4个字节,因此int型变量的起始地址至少是4的倍数(可以是8的倍数,16的倍数等)。

再澄清一下,一个结构体里面有很多成员,每个成员都有自己的“最小对齐格式”,那么对于整个结构体来说呢?它的“最小对齐格式”又是什么呢?那就取决于成员中最大的那个“最小对齐格式”,这样说明白了吗? 再来看一个复杂点的例子:

struct D{
    int i;
    char c __attribute__((aligned(256)));
    short s;
};


这时,sizeof (struct D) = 512。由于结构体D中包含的成员c被aligned属性限定为最小256字节对齐,因此整个结构体也要256字节对齐,因此 i 的起始地址a是一个256的倍数,另外,成员c的起始地址也必须是256的倍数,因此 i 与 c 之间空了很多零,另外s两字节对齐,之后统统补零,因为要256对齐,整个结构体占512字节。
如果我们变换一下成员的相对位置,则就不一样了:
struct E
{
    char c __attribute__((aligned(256)));
    int i;
    short s;};

同样的,地址a和地址b都是256的地址,因为虽然成员 c 只占一个字节,但是它的最小对其格式是要256字节对齐,这也决定了整个结构体的最小对齐格式,因此s之后一直补零,一直补满256个字节。
6、packed属性packed用于变量和类型,当用于修饰变量(包括结构体成员)时表示使用最小可能的地址对齐格式(如果此时该变量被aligned属性所修饰,那么它的最小地址对齐格式就是aligned所指定的那个值),当用于修饰枚举、结构体或者联合类型时表示使该类型内的所有成员都具有可能的最小的地址对齐格式(就相当于一次性修饰每一个成员)。
例如:
struct A
{
    char c;
    int i __attribute__((packed));
    short s;
};


被packed修饰的变量 i 会以1个字节对齐,因此整个结构体占8个字节。(否则占12字节)

struct B
{
    char c;
    int i;
    short s;
}__attribute__((packed));
被packed修饰的结构体B会让它里面的所有的成员都具有尽可能小的地址对齐格式,因此整个结构体将会占用 1+4+2 = 7 个字节。

struct C
{
    int i __attribute__((aligned(256)));
    char c;
    short s;
}__attribute__((packed));
被packed修饰的结构体C同样会让它里面的所有的成员都具有尽可能小的地址对齐格式(记住!这代表 i 的起始地址至少是256 的倍数,但是别忘了它只占4个字节,c成员被放置在 i 之后的第4个字节处),但如果结构体C没有packed修饰,成员s的地址将会是偶数,但是有packed修饰的这个结构体中,s的地址将会是紧邻c之后的奇数. 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值