GNU C扩展

163 阅读12分钟

总结

语法作用
指定初始化支持指定任意元素初始化,不再按照固定的顺序初始化
语句表达式在一个表达式里内嵌语句,使用局部变量、for 循环和 goto 跳转语句
零长度数组长度为0的数组,通过作为变长结构体的成员
关键字
typeof获取一个变量或表达式的类型
offsetof(TYPE, MEMBER)计算结构体某一成员在结构体内的偏移
container_of(ptr, type, member)根据结构体某一成员的地址,获取这个结构体的首地址
__atttribute__((ATTRIBUTE))声明一个函数、变量或类型的特殊属性,指导编译器在编译程序时进行特定方面的优化或代码检查
section将一个函数或变量放到指定的段
aligned指定一个变量或类型的对齐方式,一般用来增大变量的地址对齐
packed指定一个变量或类型的对齐方式,使用最可能小的地址对齐方式
format指定变参函数的参数格式检查
weak将一个强符号转换为弱符号,适用全局变量或全局函数
alias给函数定义一个别名
noinline指定的函数内联不展开
always_inline指定的函数内联展开

指定初始化

/* 初始化数组 */
int b[100] ={ [10] = 1, [11 ... 30] = 10, [50 ... 60] = 2, [90] = 2};

switch(i)
    {
        case 1:
            printf("1\n");
            break;
        case 2 ... 8:
            printf("%d\n",i);
            break;
        case 9:
            printf("9\n");
            break;
        default:
            printf("default!\n");
            break;
    }
    
/* 初始化结构体 */
void my_func(){ ... }

struct student{
    char name[32];
    int age;
    void (*func)();
};
    struct student stu2=
    {
        .name = get_name,
        .func = my_func,
        .age  = 18
    };

语句表达式

语句表达式最外面使用小括号()括起来,里面一对大括号{}包起来的是代码块,代码块里允许内嵌各种语句。语句的格式可以是 “表达式;”这种一般格式的语句,也可以是循环、跳转等语句。

({ 表达式1; 表达式2; 表达式3; })

语句表达式的值总等于最后一个表达式的值。

sum = 
    ({
        int s = 0;
        for( int i = 0; i < 10; i++)
            s = s + i;
            s;
    });

零长度数组

int len;
int a[len]; // 变长数组

int b[0];   // 零长度数组,sizeof(b) == 0

它不占用内存存储空间,仅是符号,当数组首地址使用。

它常常作为结构体的一个成员,构成一个变长结构体

struct buffer{
    int len;
    int a[0];
};

struct buffer *buf = (struct buffer *)malloc(sizeof(struct buffer)+ 20);
buf->len = 20;
strcpy(buf->a, "hello world!\n");

typeof关键字

  • 例子:
int i ;
typeof(i) j = 2; // int j = 2;

typeof(int *) a; // int *a;

int func();
typeof(f()) k; // int k;

#define MAX(x,y)             \
({                           \
    typeof(x) _x = x;        \
    typeof(x) _y = y;        \
    (void) (&_x == &_y);     /* warning:comparison of distinct pointer types lacks a cast */ \
    _x > _y ? _x : _y;       \
})

offsetof

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

container_of

#define  container_of(ptr, type, member) ({    \
     const typeof( ((type *)0)->member ) *__mptr = (ptr); \
     (type *)( (char *)__mptr - offsetof(type,member) ); \
})
  • 例子:
struct student
{
    char *name;
    int num;
    int age;
};
int main(void)
{
    struct student stu;
    struct student *p;
    
    p = container_of( &stu.num, struct student, num);
    return 0;
}

atttribute

声明属性:

section // 将一个函数或变量放到指定的段
aligned // 显式指定一个变量的存储边界对齐方式,一般用来增大变量的地址对齐
packed  // 显式指定一个变量的存储边界对齐方式,使用最可能小的地址对齐方式
format  // 指定变参函数的参数格式检查
weak    // 将一个强符号转换为弱符号
alias   // 给函数定义一个别名
noinline      // 指定的函数内联不展开
always_inline // 指定的函数内联展开
……

属性声明要紧挨着变量

aligned & packed

int uninit_val __attribute__((section(".data")));

char c1 __attribute__((aligned(8)) = 4;
char c2 __attribute__((packed,aligned(4)));
__attribute__((packed,aligned(4))) char c2 = 4;
char c2 = 4 __attribute__((packed,aligned(4)));  // error

// size: 8
struct data{
    char a;
    short b;
    int c ;
};

// size: 12
struct data{
    char a;
    short b __attribute__((aligned(4)));
    int c ;
};

// size: 16
struct data{
    char a;
    short b;
    int c ;
}__attribute__((aligned(16)));

// size: 7
struct data{
    char a;
    short b __attribute__((packed));
    int c __attribute__((packed));
};
struct data{
    char a;
    short b;
    int c ;
}__attribute__((packed));

// 避免了结构体内因地址对齐产生的内存空洞,又指定了整个结构体的对齐方式
struct data{
    char a;
    short b;
    int c ;
}__attribute__((packed, aligned(8))));

format

__attribute__(( format (archetype, string-index, first-to-check)))
void LOG(const char *fmt, ...)  __attribute__((format(printf,1,2)));
void LOG2(int num, char *fmt, ...)  __attribute__((format(printf,2,3)));

属性 format(printf,1,2) 有三个参数。第一个参数 printf 是告诉编译器,按照 printf 函数的检查标准来检查;第2个参数表示在 LOG 函数所有的参数列表中,格式字符串的位置索引;第3个参数是告诉编译器要检查的参数的起始位置。

LOG("I am li\n");
LOG("I am li, I have %d houses!\n", num);
LOG("I am li, I have %d houses! %d cars\n", num1, num2);
LOG(num, "I am li, I have %d houses! %d cars\n", num1, num2);

weak

//func.c
int a __attribute__((weak)) = 1;
void func(void)
{
    printf("func:a = %d\n", a);
}
//main.c
int a = 4;
void func(void);
int main(void)
{
    printf("main:a = %d\n", a);
    func();
    return 0;
}
运行结果:
main: a = 4
func: a = 4

//func.c
int a __attribute__((weak)) = 1;
void __attribute__((weak)) func(void)
{
    printf("func:a = %d\n", a);
}
//main.c
int a = 4;
void func(void)
{
    printf("I am a strong symbol!\n");
}
int main(void)
{
    printf("main:a = %d\n", a);
    func();
    return 0;
}
运行结果:
main: a = 4
func: I am a strong symbol!

弱符号的用途

当函数被声明为一个弱符号时:当链接器找不到这个函数的定义时,也不会报错。编译器会将这个函数名,即弱符号,设置为0或一个特殊的值。只有当程序运行时,调用到这个函数,跳转到0地址或一个特殊的地址才会报错。

//func.c
int a __attribute__((weak)) = 1;

//main.c
int a = 4;
void __attribute__((weak)) func(void);
int main(void)
{
    printf("main:a = %d\n", a);
    func();
    return 0;
}

运行结果:
main: a = 4
Segmentation fault (core dumped)

为了防止函数运行出错,我们可以在运行这个函数之前,先做一个判断,即看这个函数名的地址是不是0,然后再决定是否调用。这样就可以避免段错误了。

//func.c
int a __attribute__((weak)) = 1;

//main.c
int a = 4;
void __attribute__((weak)) func(void);
int main(void)
{
    printf("main:a = %d\n", a);
    if (func)
        func();
    return 0;
}
运行结果:
main: a = 4

弱符号的这个特性,在库函数中应用很广泛。比如你在开发一个库,基础的功能已经实现,有些高级的功能还没实现,那你可以将这些函数通过 weak 属性声明,转换为一个弱符号。通过这样设置,即使函数还没有定义,我们在应用程序中只要做一个非0的判断就可以了,并不影响我们程序的运行。等以后你发布新的库版本,实现了这些高级功能,应用程序也不需要任何修改,直接运行就可以调用这些高级功能。

弱符号还有一个好处,如果我们对库函数的实现不满意,我们可以自定义与库函数同名的函数,实现更好的功能。比如我们 C 标准库中定义的 gets() 函数,就存在漏洞,常常成为黑客堆栈溢出攻击的靶子。

int main(void)
{
    char a[10];
    gets(a);
    puts(a);
    return 0;   
}

C 标准定义的库函数 gets() 主要用于输入字符串,它的一个 Bug 就是使用回车符来判断用户输入结束标志。这样的设计很容易造成堆栈溢出。比如上面的程序,我们定义一个长度为10的字符数组用来存储用户输入的字符串,当我们输入一个长度大于10的字符串时,就会发生内存错误。

接着我们定义一个跟 gets() 相同类型的同名函数,并在 main 函数中直接调用,代码如下。

#include<stdio.h>

char * gets (char * str)
{
    printf("hello world!\n");
    return (char *)0;
}

int main(void)
{
    char a[10];
    gets(a);
    return 0;   
}
运行结果:
hello world!

通过运行结果,我们可以看到,虽然我们定义了跟 C 标准库函数同名的 gets() 函数,但编译是可以通过的。程序运行时调用 gets() 函数时,就会跳转到我们自定义的 gets() 函数中运行。

alias

void __f(void)
{
    printf("__f\n");
}

void f() __attribute__((alias("__f")));
int main(void)
{
    f();
    return 0;   
}

noinline & always_inline

内联函数

有些函数很小,而且调用频繁,调用开销大,算下来性价比不高。我们就可以将这个函数声明为内联函数。编译器在编译过程中遇到内联函数时,像宏一样,将内联函数直接在调用处展开。

好处:

  • 减少调用函数时的开销
  • 减少传参时可能引起的压栈出栈的开销。
  • 减少PC跳转时对流水线的破坏。

坏处:

  • 代码所占体积会更大。

编译器对内联函数的处理

inline关键字只是建议编译器将一个函数声明为内联函数,但编译器不一定会对这个内联函数展开处理。编译器也要进行评估,权衡展开和不展开的利弊。

一般来讲,判断对一个内联函数到底展不展开,从程序员的角度,主要考虑以下几个因素。

  • 函数体积小且调用频繁
  • 函数体内无递归、循环等语句
  • 函数本身当作一个函数指针在别处被引用
  • 函数和调用该函数调用者是否在同一文件内

当我们认为一个函数体积小,而且被大量频繁调用,应该做内联展开时,就可以使用 inline 关键字修饰它。但编译器会不会作内联展开,编译器也会有自己的权衡。如果你想告诉编译器一定要展开,或者不作展开,就可以使用 noinlinealways_inline 对函数作一个属性声明。

//inline.c
static inline 
__attribute__((always_inline))  int func(int a)
{
    return a+1;
}
​
static inline void print_num(int a)
{
    printf("%d\n",a);
}
int main(void)
{
    int i;
    i=func(3);
    print_num(10);
    return 0;
}

在这个程序中,我们分别定义两个内联函数 func() 和 print_num(),然后使用 always_inline 对 func() 函数进行属性声明。接下来,我们对生成的可执行文件 a.out 作反汇编处理,其汇编代码如下。

$ arm-linux-gnueabi-gcc -o a.out inline.c
$ arm-linux-gnueabi-objdump -D a.out 
00010438 <print_num>:
   10438:    e92d4800    push    {fp, lr}
   1043c:    e28db004    add fp, sp, #4
   10440:    e24dd008    sub sp, sp, #8
   10444:    e50b0008    str r0, [fp, #-8]
   10448:    e51b1008    ldr r1, [fp, #-8]
   1044c:    e59f000c    ldr r0, [pc, #12]
   10450:    ebffffa2    bl  102e0 <printf@plt>
   10454:    e1a00000    nop ; (mov r0, r0)
   10458:    e24bd004    sub sp, fp, #4
   1045c:    e8bd8800    pop {fp, pc}
   10460:    0001050c    andeq   r0, r1, ip, lsl #10
​
00010464 <main>:
   10464:    e92d4800    push    {fp, lr}
   10468:    e28db004    add fp, sp, #4
   1046c:    e24dd008    sub sp, sp, #8
   10470:    e3a03003    mov r3, #3
   10474:    e50b3008    str r3, [fp, #-8]
   10478:    e51b3008    ldr r3, [fp, #-8]
   1047c:    e2833001    add r3, r3, #1
   10480:    e50b300c    str r3, [fp, #-12]
   10484:    e3a0000a    mov r0, #10
   10488:    ebffffea    bl  10438 <print_num>
   1048c:    e3a03000    mov r3, #0
   10490:    e1a00003    mov r0, r3
   10494:    e24bd004    sub sp, fp, #4
   10498:    e8bd8800    pop {fp, pc}

通过反汇编代码可以看到,因为我们对 func() 函数作了 always_inline 属性声明,所以编译器在编译过程中,对于 main()函数调用 func(),会直接在调用处展开。

10470:    e3a03003    mov r3, #3
   10474:    e50b3008    str r3, [fp, #-8]
   10478:    e51b3008    ldr r3, [fp, #-8]
   1047c:    e2833001    add r3, r3, #1
   10480:    e50b300c    str r3, [fp, #-12]

而对于 print_num() 函数,虽然我们对其作了内联声明,但编译器并没有对其作内联展开,而是当作一个普通函数对待。还有一个注意的细节是,当编译器对内联函数作展开处理时,会直接在调用处展开内联函数的代码,不再给 func() 函数本身生成单独的汇编代码。这是因为其它调用该函数的位置都作了内联展开,没必要再去生成。在这个例子中,我们发现就没有给 func() 函数本身生成单独的汇编代码,编译器只给 print_num() 函数生成了独立的汇编代码。

static & inline

在 Linux 内核中,你会看到大量的内联函数定义在头文件中,而且常常使用 static 修饰。

内联函数为什么要定义在头文件中呢?

  • 因为它是一个内联函数,可以像宏一样使用,任何想使用这个内联函数的源文件,不必亲自再去定义一遍,直接包含这个头文件,即可像宏一样使用。

那为什么还要用 static 修饰呢?

  • 因为我们使用 inline 定义的内联函数,编译器不一定会内联展开,那么当多个文件都包含这个内联函数的定义时,编译时就有可能报重定义错误。而使用 static 修饰,可以将这个函数的作用域局限在各自本地文件内,避免了重定义错误。