当前位置: 首页 > article >正文

嵌入式C语言学习——8:GNU扩展

目录

C语言的历史

早期的背景

B语言的出现

C语言的诞生

C语言的推广与标准化

C语言与其他语言的关系

C语言的现代发展

初始化方法

表达式,语句和代码块

表达式

1. 表达式的组成

2. 表达式的特性

3. 表达式与语句的区别

语句

1. 语句的类型

2. 语句的执行顺序

3. 语句与表达式的关系

4. 语句的分号

代码块

1. 代码块的作用

2. 代码块与作用域

3. 代码块的嵌套

语句表达式

MAX宏为例子,表达宏中的表达式嵌套

container_of

2. 遍历链表

零长度数组

__attribute__,section...

__attribute__ 的基本用法

section 属性

1. 用法

2. 应用场景

aligned 属性

1. 用法

packed 属性

format 属性

1. 用法

2. 参数解释

3. format 属性在 printf 中的使用

4. 应用场景

weak 属性

1. 用法

2. 弱符号的链接行为

3. 使用 weak 属性

4. 覆盖弱符号

5. 应用场景

alias 属性

1. 用法

2. 应用场景

3. 使用 alias

noinline 属性

1. 用法

2. 应用场景

3. 使用 noinline

always_inline 属性

1. 用法

2. 应用场景

3. 使用 always_inline

内建函数

__builtin_return_address

用法

示例

使用场景

注意事项

__builtin_frame_address

1. 用法

示例

使用场景

注意事项

_builtin_return_address 与 __builtin_frame_address 的对比

__builtin_clz(x)

__builtin_ctz(x)

__builtin_popcount(x)

__builtin_fma(x, y, z)

内存相关的内建函数

__builtin_alloca(size)

__builtin_memcpy(dest, src, size)

__builtin_memmove(dest, src, size)

__builtin_assume_aligned(ptr, alignment)

控制流相关的内建函数

__builtin_expect(expression, value)

__builtin_unreachable()

__builtin_abort()

类型相关的内建函数

1. __builtin_types_compatible_p(type1, type2)

2. __builtin_constant_p(expression)

3. __builtin_object_size(ptr, mode)

硬件相关的内建函数

1. __builtin_prefetch(ptr, rw, locality)

2. __builtin_bswap32(x) 和 __builtin_bswap64(x)

Reference


C语言的历史

C语言的发展历史可以追溯到20世纪60年代末,当时计算机科学正在飞速发展,各种编程语言纷纷出现。C语言是由肯·汤普逊(Ken Thompson)和丹尼斯·里奇(Dennis Ritchie)等人共同开发的,它是从早期的B语言演变而来的,并逐渐成为一种广泛应用的编程语言。C语言的产生不仅深刻影响了计算机科学的发展,还对许多后来的编程语言产生了深远的影响。

早期的背景

在C语言诞生之前,计算机编程语言经历了几个重要的发展阶段。最早期的编程语言,如机器语言和汇编语言,直接与计算机的硬件架构紧密相关,需要程序员手动编写指令,这种方式不仅繁琐,而且容易出错。为了提高开发效率,人们开始开发高级编程语言。20世纪50年代,出现了如Fortran、Lisp和ALGOL等语言,它们为程序员提供了更简洁、更抽象的编程方式。

其中,ALGOL(算法语言)是一种重要的语言,它为后来的许多编程语言,包括C语言,提供了许多语言设计理念。ALGOL强调程序的结构化和控制结构的清晰性,这对C语言的设计产生了重要影响。

B语言的出现

C语言的前身是B语言。B语言由肯·汤普逊在1969年开发,最初是为了在贝尔实验室的PDP-7计算机上编写操作系统而设计的。B语言基于ALGOL的语法结构,并结合了早期的BCPL语言的特点。B语言的设计初衷是让程序员能够以更高效的方式编写低级的操作系统代码。

尽管B语言具有一些优点,但它也存在局限性。由于B语言的类型系统较为简陋,程序员在编写复杂程序时常常遇到困难。例如,B语言不支持数据类型的概念,程序员只能使用整数和指针。随着计算机硬件的进步和需求的增加,B语言的不足之处逐渐显现,尤其是在编写大型、复杂的程序时。

C语言的诞生

1972年,丹尼斯·里奇(Dennis Ritchie)在贝尔实验室的工作中开始了C语言的开发。C语言的设计是基于B语言的基础上进行改进的,目的是为了解决B语言在功能和效率上的一些不足。C语言的初步版本是在贝尔实验室的PDP-11计算机上编写的,C语言最初被用来开发UNIX操作系统。

C语言的最大特点之一是引入了强大的类型系统,支持整数、字符、数组、结构体等基本数据类型的操作。C语言的语法结构简洁明了,并提供了指针、函数和条件语句等强大的功能,使得程序员能够编写高效、灵活的程序。与B语言不同,C语言的设计不仅支持低级编程(如操作硬件和编写操作系统),同时也能用于更高级的应用程序开发。

C语言的另一大创新是对结构化编程的支持。结构化编程强调程序应该由清晰的控制结构(如条件语句、循环语句)和模块化的函数组成,而不是依赖于跳转语句(如GOTO)。这种结构化的编程思想影响了许多后来的编程语言。

C语言的推广与标准化

C语言的设计迅速得到广泛的应用。首先,C语言被用来开发UNIX操作系统,UNIX的成功极大推动了C语言的普及。到1970年代末,C语言已经成为许多计算机科学实验室和企业的标准编程语言。

随着C语言的广泛应用,出现了不同的C语言实现和方言,这导致了兼容性问题。为了统一C语言的标准,国际标准化组织(ISO)和其他相关组织开始着手制定C语言的标准。1983年,ISO正式发布了C语言的第一个标准——ANSI C标准,这一标准规范了C语言的语法、语义和库函数,为C语言的应用提供了统一的基础。

ANSI C标准的发布,使得C语言的编译器可以更加兼容不同的计算机平台,推动了C语言的进一步发展。C语言的标准化还促使了大量的工具和库的开发,使得C语言不仅限于操作系统开发,还广泛应用于嵌入式系统、图形处理、数据库等领域。

C语言与其他语言的关系

C语言在设计上受到多种语言的影响,尤其是ALGOL、B语言和BCPL。C语言在结构化编程的设计上继承了ALGOL的理念,而B语言则是C语言的直接前身。C语言通过简洁的语法和强大的功能,弥补了B语言的不足,成为一种适用于更广泛应用的编程语言。

C语言不仅对操作系统开发产生了深远的影响,它的设计理念还对许多后来的编程语言产生了重要影响。例如,C++语言是在C语言的基础上发展起来的,它在C语言的基础上增加了面向对象编程的特性。其他现代编程语言,如Java、C#等,也借鉴了C语言的语法和结构。尽管这些语言在很多方面做了扩展和改进,但它们都受到了C语言的深刻影响。

C语言的现代发展

虽然C语言已经诞生了几十年,但它依然是一种非常流行和广泛应用的编程语言。C语言的简单、灵活、效率高等优点,使得它仍然在操作系统、嵌入式系统、网络编程等领域占据重要地位。尽管许多新的编程语言已经出现,但C语言依然保持着强大的生命力,并且有着庞大的开发者社区和丰富的资源。

随着计算机硬件的不断发展,C语言也在不断发展。新的C语言标准(如C99和C11)增加了许多新特性,如对多线程编程的支持、更强大的库函数等。这些新特性进一步提升了C语言在现代开发中的适用性和效率。

初始化方法

在C99中,支持这样的数组初始化:

#include <stdio.h>
int main()
{
    int arr[10] = {[1] = 10, [3] = 30};
    for(int i = 0; i < 10; i++)
        printf("%d ", arr[i]);   
}

在GNU C中责任是很早就支持这样的拓展了。甚至如果我们想要给予范围扩展的时候,可以使用 ... 符号

#include <stdio.h>
int main()
{
    int arr[10] = {[1 ... 3] = 10, [5 ... 7] = 30};
    for(int i = 0; i < 10; i++)
        printf("%d ", arr[i]);   
}

不难懂,这就是说数组的a[1], a[2], a[3]初始化成10,[5], [6], [7]初始化成30.这个意思。当然这种...的特性可以用到我们的switch case语句:

#include <stdio.h>
​
void test (char code)
{
    switch (code){
        case '0' ... '9':
            printf("code in 0~9\n");
            break;
        case 'a' ... 'f':
            printf("code in a~f\n");
            break;
        case 'A' ... 'F':
            printf("code in A~F\n");
            break;
        default:
            printf("no right code\n");
    }
}   

测试一下可以。

对于结构体的初始化,GNU C也有更强的扩展。比如说对于ISO C,结构体的初始化比较严格,只可以使用严格对应的顺序初始化对应的成员。这点在GNU得到了消除:

struct student st = {
    .name   = "Charliechen",
    .age    = 20
};

这一点会在我们的驱动开发中十分的常见!

表达式,语句和代码块

表达式

表达式是一个或多个操作数(如常量、变量、函数调用)通过运算符进行组合后,最终计算出一个值的结构。简单来说,表达式代表了某个操作或计算,它的核心功能是生成结果。可以说,表达式是“做事”的部分,它通过计算、转换或比较等操作产生一个值。

1. 表达式的组成

表达式由操作数和运算符组成。操作数可以是常量、变量、函数调用等,而运算符则用于指定操作的类型,如加法、减法、乘法、除法、比较等。常见的表达式类型包括:

  • 算术表达式:例如 a + b,其目的是对操作数进行数学运算。

  • 逻辑表达式:例如 x > y && z == 0,用于逻辑判断,返回布尔值(真或假)。

  • 关系表达式:例如 a == b,用于比较两个操作数的大小或相等性。

  • 赋值表达式:例如 a = 5,将值赋给变量。

  • 函数调用表达式:例如 sin(x),调用一个函数并返回其计算结果。

2. 表达式的特性
  • :表达式的最终目标是计算出一个值。无论是算术运算的结果,还是函数返回的值,表达式的求值会产生一个具体的值。

  • 副作用:一些表达式除了计算值外,还可能带有副作用。例如,赋值表达式 a = 10 会改变变量 a 的值,或者自增表达式 i++ 会改变变量 i 的值。

  • 可嵌套性:表达式可以嵌套在其他表达式中。例如 (a + b) * c,其中 (a + b) 是一个子表达式,它的结果将作为另一个表达式的一部分参与运算。

3. 表达式与语句的区别

尽管表达式可以像语句一样出现在程序中,但它的主要目的是计算值。而语句则是程序执行的基本单元,它通常用于控制程序的流程或执行某些操作。例如,a = 5 是一个赋值表达式,但 a = 5; 是一个赋值语句,表示这行代码执行了赋值操作。可以说,所有的表达式都是语句,但并非所有语句都是表达式。

语句

语句是程序中执行的基本指令,它描述了程序的行为。每个语句通常都会完成某个特定的动作,比如计算值、改变变量的值、控制程序流程等。语句是程序控制流的核心,它决定了程序从开始到结束的执行顺序。

1. 语句的类型

语句的种类繁多,按功能可以分为以下几类:

  • 表达式语句:通过计算表达式来产生副作用(如赋值、函数调用)。例如,x = 10; 就是一个表达式语句,它通过赋值操作改变了 x 的值。

  • 控制语句:用于控制程序的执行流程,包括条件语句、循环语句等。例如,if 语句、while 循环、for 循环都是控制语句。

  • 跳转语句:用于改变程序的执行顺序。例如,breakcontinuereturngoto 都是跳转语句,它们可以使程序跳出循环或函数,或者跳转到指定位置执行代码。

  • 声明语句:用于定义变量或常量的类型并进行初始化。例如,int a = 5; 是一个声明语句,它声明了一个 int 类型的变量 a 并赋值为 5。

2. 语句的执行顺序

语句通常是按照从上到下、从左到右的顺序依次执行的。这个顺序由程序员编写的代码结构决定。然而,控制语句和跳转语句可以打乱这一顺序:

  • 条件语句:如 ifswitch,根据条件的真假决定执行哪一部分代码。

  • 循环语句:如 forwhiledo-while,重复执行一段代码直到满足某个条件为止。

  • 跳转语句:如 breakcontinuereturn,改变程序的执行流。

3. 语句与表达式的关系

语句是一个较为广泛的概念,其中包含了表达式语句。简单来说,所有表达式(如 a = b + 2)可以作为语句使用,而语句可能包含表达式。表达式本身会计算出一个值并可能引发副作用,而语句则是程序行为的核心部分,它可能不产生值,但完成某种特定的操作。

4. 语句的分号

在许多编程语言中(如C、C++、Java等),语句以分号 ; 结尾。分号用来区分不同的语句,以便程序能够按照正确的顺序执行。例如,在C语言中,int a = 5; 是一个声明语句,a = a + 1; 是一个赋值语句,这两行代码通过分号分隔,程序将依次执行它们。

代码块

代码块是一组被大括号 {} 包围的语句集合。代码块通常用于将多个语句组合在一起,形成一个逻辑上的整体。代码块可以在许多编程语言中作为一个结构体使用,以便进行条件判断、循环或函数定义等操作。

1. 代码块的作用

代码块的主要作用是组织和控制多个语句的执行。它可以用于以下几种情况:

  • 条件语句:例如,在 ifelse 中,代码块将决定在某个条件成立时需要执行哪些语句。

  • 循环语句:在 forwhiledo-while 循环中,代码块指定循环体内的多个语句。

  • 函数定义:函数内部的所有代码实际上都位于一个代码块中,它们共同完成函数的逻辑。

2. 代码块与作用域

在许多编程语言中,代码块不仅是语句的容器,还是作用域的界限。例如,在一个函数中,代码块内定义的变量只在该代码块内有效,外部无法访问。在 iffor 等语句块中,变量的生命周期和作用范围也通常局限于该代码块。

3. 代码块的嵌套

代码块可以嵌套在其他代码块中。比如,if 语句中的条件判断和 else 分支可以包含多个其他代码块;而循环语句中的代码块可能又包含更多的条件判断语句或其他循环。嵌套代码块使得程序的结构更加灵活和复杂,同时也为程序的逻辑提供了更多层次的控制。

语句表达式

GNU允许对一个表达式内部做内嵌语句扩展,允许我们在一个表达式的内部使用各种骚操作,比如说:

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

也就是说,这一套嵌套的返回值总是等于最后一个表达式的值。同样的,我们可以在内部使用goto等,跳转到外面的程序流。

MAX宏为例子,表达宏中的表达式嵌套

#define max(x, y) ({
    typeof(x) _x = x;
    typeof(y) _y = y;
    (void)(&_x == &_y);
    _x ? _y ? _x : _y;
})

typeof是GNU C扩展,尽管在现代的MSVC中也有typeof这个关键字。实际上就是取出来变量的类型。

    (void)(&_x == &_y);

这个是用来确保警告我们的比较类型需要是同一个类型,其次是消除编译器警告(使用void)。

container_of

container_of 是一个在 GNU C 语言编程中常见的宏,通常用于在嵌入式编程、内核开发(尤其是在 Linux 内核中)和其他低级编程环境中,帮助程序员实现从结构体成员指针反向查找其所属结构体的操作。这个宏在很多情况下是非常有用的,尤其是在处理链表、队列等数据结构时,它可以通过成员指针来获取整个包含该成员的结构体指针,从而便于实现复杂的数据结构操作。

在 C 语言中,通常会定义结构体(struct)来组织数据。结构体成员通常不是孤立存在的,而是作为某个更大的数据结构的一部分。例如,链表节点就是结构体的一部分,它包含一个数据元素和指向下一个节点的指针。当我们在遍历链表时,往往需要通过当前节点的某个成员(如指向下一个节点的指针)来访问整个结构体。这时,container_of 宏就显得尤为重要。

例如,假设我们有一个链表节点的结构体,其中包含一个指向下一个节点的指针,定义如下:

struct list_node {
    int data;
    struct list_node *next;
};

在遍历链表时,假设我们通过指向 next 成员的指针来访问当前节点。此时,我们如何从 next 成员指针获取整个 list_node 结构体的指针呢?这正是 container_of 宏发挥作用的地方。

container_of 宏的主要作用是通过结构体成员的地址来反向获取包含该成员的整个结构体的地址。这是一个非常典型的“逆向查找”操作,因为我们通常可以通过结构体指针来访问成员,但通过成员指针来访问结构体则需要知道如何定位结构体的开始位置。

具体来说,container_of 宏的原型通常如下所示:

#define container_of(ptr, type, member) ({                       \
    const typeof(((type *)0)->member) *__mptr = (ptr);           \
    (type *)((char *)__mptr - offsetof(type, member));           \
})

要理解 container_of 宏的工作原理,首先需要掌握几个概念:

  1. 结构体成员的偏移量:每个结构体成员都有一个内存偏移量,它指示成员在整个结构体中的位置。通过 offsetof(type, member) 可以获得结构体成员相对于结构体起始位置的偏移量。这个偏移量是编译器在编译时计算的。

  2. 指针运算:C 语言允许进行指针运算。通过 ptr - offset 可以反向获取原始的结构体指针,假设你知道成员的内存偏移量。

container_of 宏的实现分为两个步骤:

  • 首先,通过 typeof(((type *)0)->member) 获取成员的类型。这个步骤确保了我们获取的成员类型与给定指针 ptr 所指向的类型一致。

  • 然后,(char *)__mptr - offsetof(type, member) 计算出 ptr 相对于整个结构体起始位置的偏移量,最终通过类型转换将该地址转换为指向整个结构体的指针。

让我们通过一个具体的例子来理解 container_of 宏是如何工作的。假设我们有一个链表节点结构体,定义如下:

#include <stdio.h>
​
struct list_node {
    int data;
    struct list_node *next;
};
​
#define container_of(ptr, type, member) ({                          \
    const typeof(((type *)0)->member) *__mptr = (ptr);              \
    (type *)((char *)__mptr - offsetof(type, member));              \
})
​
int main() {
    struct list_node node;
    node.data = 10;
    node.next = NULL;
​
    // 假设我们有指向 `node.next` 的指针
    struct list_node *next_ptr = node.next;
​
    // 使用 container_of 获取指向整个结构体的指针
    struct list_node *node_ptr = container_of(next_ptr, struct list_node, next);
​
    printf("Node data: %d\n", node_ptr->data);  // 输出 10
​
    return 0;
}

在这个例子中,container_of(next_ptr, struct list_node, next) 的作用是通过 next_ptr 获取指向整个 struct list_node 的指针。container_of 宏通过 next 成员的偏移量反向计算出 node 结构体的地址。这个操作的本质是基于指针运算和内存布局。

container_of 宏自己就经常在内核中出现,比如说经常用于实现链表操作。内核中的链表节点通常是以结构体形式定义的,链表中的每个节点包含一个指向下一个节点的指针(next),而节点中的数据可能是另一个结构体。通过 container_of 宏,内核能够从链表的成员(如 next)反向获取整个节点结构体,这对高效地操作链表至关重要。

以下是一个简单的 Linux 内核链表节点结构体示例,展示了如何使用 container_of 宏:

struct list_head {
    struct list_head *next, *prev;
};
​
struct my_struct {
    int data;
    struct list_head list;
};
​
// 假设我们有指向 list_head 的指针 head_ptr
struct list_head *head_ptr = &some_node->list;
​
// 使用 container_of 获取指向 my_struct 结构体的指针
struct my_struct *my_ptr = container_of(head_ptr, struct my_struct, list);
2. 遍历链表

在链表的遍历过程中,我们经常从链表节点的 next 成员开始,通过指针遍历整个链表。如果节点结构体包含多个成员,使用 container_of 可以让我们方便地从指向成员的指针反向获取整个节点。这样,我们可以在遍历过程中对节点进行进一步的处理。

struct list_node *current;
for (current = head; current != NULL; current = current->next) {
    struct my_struct *container = container_of(current, struct my_struct, list);
    // 对 container 进行处理
}

当然,对上面讨论的内容感兴趣的话,可以进一步参考博客:Linux2.6.24-内核数据结构之list_head | DE-LAB (dessera.github.io)

零长度数组

零长度数组!很有趣的小玩意。这个玩意实际上并不占据空间大小,如果不对它做特殊操作的话,也是用不了的。那啥是特殊操作呢?答案放到结构体里扩展它:

struct Variable_Type{
    int length;
    int arr[0];
}

之后:

struct Variable_Type buf = (struct Variable_Type*)malloc(sizeof(struct Variable_Type) + 5*sizeof(int));

后面的5个int大小的空间实际上就是分配给了arr。也就是说它使得我们可以让结构体变得变长。但是值得注意的是为了防止结构体成员重排,总是需要将变长成员放到最后,而且必须只有一个变长成员。

__attribute__section...

在 GNU C 中,__attribute__ 是一个非常强大的扩展,允许程序员指定有关变量、函数、类型和其他实体的特性。这些属性提供了更细粒度的控制,以便进行优化、内存对齐、节省内存空间等。GNU C 的 __attribute__ 关键字允许我们使用一系列的声明属性,例如 sectionalignedpacked 等,这些属性可以帮助程序员更有效地控制程序的行为,尤其是在嘔容器、性能优化和底层硬件编程中。

__attribute__ 的基本用法

__attribute__ 是一个 GCC 的扩展,用来附加到变量、函数、类型等声明的后面。它的基本形式如下:

type __attribute__((attribute-list)) variable;

attribute-list 是一个或多个属性的逗号分隔的列表。我们将详细探讨以下几个常用的属性:sectionalignedpacked

section 属性

section 属性用于将变量或函数放置在特定的内存段(section)中。在嵌入式系统或操作系统开发中,程序员可能需要将某些变量或函数放置到特定的内存区域,以便硬件访问或优化内存使用。使用 section 属性,程序员可以指定这些变量或函数应当放入的特定段。

1. 用法
int my_var __attribute__((section(".my_section"))) = 42;

这个例子将 my_var 变量放入名为 .my_section 的自定义段中。通常,链接器会根据不同的段类型来处理程序的内存布局。

2. 应用场景
  • 嵌入式系统:某些硬件平台可能要求特定的变量或数据放置在特定的内存区域(例如,启动代码、设备寄存器、RAM 等)。section 属性可以帮助将数据段按要求组织到不同的内存区域。

  • 优化性能:通过将热点数据和函数放置到不同的段,可以优化内存访问,从而提高程序的性能。

aligned 属性

aligned 属性用于指定一个变量的对齐方式。对齐是指数据在内存中存储的地址必须是某个特定值的倍数,通常是数据类型大小的倍数。对齐要求是硬件架构上性能优化的一部分,不同的数据类型和平台对内存对齐有不同的要求。

1. 用法
int my_var __attribute__((aligned(16))) = 42;

这个例子表示 my_var 变量必须按照 16 字节边界对齐。对齐的值通常是 2 的幂,例如 81632 等。

  • 性能优化:在许多处理器上,数据访问的效率受到对齐的影响。未对齐的数据访问可能会导致性能下降,甚至硬件错误。使用 aligned 属性可以确保数据按最佳方式对齐,从而提高性能。

  • 嵌入式开发:在嵌入式系统中,硬件可能要求某些数据结构具有特定的对齐方式。aligned 属性可确保数据按照硬件要求的方式对齐,以便正常工作。

在许多平台上,编译器会根据数据类型的大小自动选择对齐边界。例如,int 类型通常是 4 字节对齐,double 类型通常是 8 字节对齐。但是,有时开发者需要显式指定对齐要求,特别是在处理结构体或与硬件交互时。

packed 属性

packed 属性用于告诉编译器尽可能紧凑地存储结构体或联合体的成员,以避免由于对齐要求引入的填充字节。在某些场景中,程序员需要将结构体的内存布局紧凑化,以减少内存占用,例如在网络协议、文件格式或与硬件寄存器交互时。

struct __attribute__((packed)) my_struct {
    char c;
    int i;
};

在这个例子中,my_struct 结构体的成员将被紧凑地存储,不会有任何填充字节。通常情况下,编译器会在结构体成员之间插入填充字节,以确保数据对齐。但是,使用 packed 属性后,编译器将省略这些填充字节。

一般而言,我们的packed关键字的身影可以在这里被看到:

  • 嵌入式系统:在嵌入式开发中,内存通常是有限的,因此需要减少结构体的内存占用。packed 属性可以减少内存使用,尤其在处理硬件寄存器映射时尤为重要。

  • 文件格式:一些文件格式要求数据紧凑地存储,避免内存或存储空间浪费。在这种情况下,packed 属性帮助优化内存布局。

  • 网络协议:某些网络协议规定了数据包格式,要求数据结构的内存布局与网络字节流格式一致。在这些情况下,packed 可以确保数据正确对齐并减少额外填充字节。

使用 packed 属性时,程序员必须小心,因为结构体的紧凑布局可能会导致性能下降。许多现代处理器对内存对齐有优化,如果禁用对齐,可能会降低访问速度,甚至导致硬件异常。因此,在使用 packed 属性时,要权衡内存占用与性能之间的关系。

__attribute__ 是 GCC 编译器提供的扩展,它并不仅限于 sectionalignedpacked。GNU C 还支持许多其他属性,下面列出一些常用的属性:

  • noreturn:用于指示函数永远不会返回。这对于优化和分析工具有帮助。

    void my_function() __attribute__((noreturn));
  • unused:指示一个变量或函数未使用。这个属性可以避免编译器发出未使用变量的警告。

    int my_var __attribute__((unused));
  • deprecated:标记函数或变量为已废弃,编译器将生成警告。

    void my_function() __attribute__((deprecated));
  • constructor / destructor:这些属性用于指定函数在程序启动时或退出时自动调用,通常用于初始化和清理操作。

    void init() __attribute__((constructor));
    void cleanup() __attribute__((destructor));
  • alignedpacked 的组合:在一些情况下,可以同时使用 alignedpacked 属性。这样可以确保结构体按某个对齐值紧凑地存储。

    struct my_struct {
        char c;
        int i;
    } __attribute__((packed, aligned(4)));

formatweak__attribute__ 关键字支持的两个非常有用的扩展属性,它们提供了额外的控制和优化能力,特别是在函数声明、格式检查以及符号链接和库管理方面。理解这两个属性的用途,可以帮助程序员编写更加高效和灵活的 C 代码。

format 属性

format 属性用于确保函数的格式化字符串参数(如 printfscanf 函数)符合预期的格式。这种类型的检查有助于捕捉格式字符串和传递给它的参数之间的不匹配,避免潜在的运行时错误,尤其是对于复杂的字符串处理函数。format 属性能够静态地验证格式字符串的正确性,减少程序中的潜在错误和提高程序的健壮性。

1. 用法

format 属性用于指定某个函数作为格式化函数,类似于标准库中的 printfsprintfscanf 等函数。它会验证传入的格式字符串是否与相应的参数类型匹配。

int my_printf(const char *fmt, ...) __attribute__((format(printf, 1, 2)));

这个例子中,my_printf 函数被标记为一个格式化函数,其中 12 分别表示格式字符串和其后的第一个变量参数的位置(参数的索引从 1 开始)。GCC 会在编译时检查 fmt 参数(格式字符串)和后续参数的类型是否匹配。具体来说,1 表示 fmt 参数是格式字符串,2 表示后面的参数应与格式字符串中的占位符匹配。

2. 参数解释
  • format(printf, n, m)

    属性要求:

    • n 是格式化字符串参数的位置(一般是 1),

    • m 是第一个可变参数的位置(通常是 2)。

    如果

    printf

    函数的格式字符串参数和其他参数之间的类型不匹配,GCC 编译器会发出警告。

3. format 属性在 printf 中的使用
#include <stdio.h>
​
void my_printf(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
​
void my_printf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);
}
​
int main() {
    my_printf("Hello, %s!\n", "world");  // 正确
    my_printf("Value: %d\n", 42);         // 正确
    my_printf("Error: %d\n", "wrong");   // 错误,类型不匹配,编译器会警告
    return 0;
}

在上面的代码中,my_printf 函数被标记为格式化函数。当调用 my_printf 时,如果格式字符串中的占位符与传入的参数类型不匹配,编译器会根据 format 属性给出警告。例如,在最后一行,%d 占位符期望一个整数,但传入的是一个字符串 "wrong",编译器将提示类型不匹配。

4. 应用场景
  • 错误检测format 属性帮助程序员在编译时就发现格式字符串与参数之间的不匹配,避免了潜在的运行时错误。

  • 代码审查:在团队开发中,使用 format 属性可以帮助代码审查人员快速检测格式化函数调用是否符合规范。

weak 属性

weak 属性用于指定一个符号(函数或变量)为弱符号。弱符号通常用于提供一个默认的定义,而让其他地方的强符号定义覆盖它。在链接时,如果没有找到强符号的定义,弱符号将被使用。这在实现某些扩展或动态功能时非常有用,特别是在库和操作系统内核的开发中。

1. 用法

weak 属性通常用于函数或全局变量声明。如果没有显式的强定义,链接器会使用弱定义。

void my_function(void) __attribute__((weak));

这个例子中,my_function 是一个弱符号,这意味着如果链接器找不到 my_function 的强定义(例如,在其他文件中),它将使用这个弱定义。弱符号常常作为某些库的默认实现,允许用户覆盖这些实现。

2. 弱符号的链接行为

在链接时,弱符号的优先级较低。如果链接器在某个目标文件中找到了强符号(即没有标记为 weak 的符号),则强符号会覆盖弱符号。如果没有强符号,则使用弱符号。

3. 使用 weak 属性
#include <stdio.h>
​
void my_function(void) __attribute__((weak));
​
void my_function(void) {
    printf("Default weak implementation\n");
}
​
int main() {
    my_function();  // 调用弱定义的 my_function
    return 0;
}

在上述例子中,my_function 是一个弱符号。如果没有其他地方提供强定义,链接器会使用 my_function 的弱定义(即输出 "Default weak implementation")。如果有另一个地方提供了强定义,那个强定义将覆盖弱定义。

4. 覆盖弱符号
#include <stdio.h>
​
void my_function(void) __attribute__((weak));
​
void my_function(void) {
    printf("Default weak implementation\n");
}
​
void my_function(void) {
    printf("Strong definition\n");
}
​
int main() {
    my_function();  // 调用强定义的 my_function
    return 0;
}

在这个例子中,my_function 有两个定义:一个是弱符号,另一个是强符号。由于强符号会覆盖弱符号,最终输出将是 "Strong definition"。

5. 应用场景
  • 库开发:在库中,可以使用弱符号提供默认的实现。这允许用户或应用程序提供自己的实现,以替换库中的默认行为。

    比如,在一些标准库中,某些系统调用或错误处理函数会以弱符号提供,允许应用程序在需要时提供自己的实现。

  • 系统级编程:在操作系统内核开发中,弱符号通常用于提供默认行为,而让某些模块覆盖这些默认行为。例如,内核中的某些硬件驱动可能提供默认的实现,而用户可以提供具体的硬件实现来替换这些默认实现。

  • 动态链接:通过使用弱符号,可以实现动态链接的功能。如果程序在运行时没有找到符号的强定义,它可以选择回退到弱定义的实现。

alias 属性

alias 属性用于为一个函数或变量创建别名。它允许一个符号作为另一个符号的“别名”进行使用。也就是说,当程序调用一个符号时,实际上调用的是另一个符号的实现。这个属性在动态链接和库开发中非常有用,特别是当你需要为现有函数创建多个名称时。

1. 用法
void my_function(void) __attribute__((alias("real_function")));

在这个例子中,my_function 会作为 real_function 的别名。也就是说,调用 my_function 实际上是调用 real_function

2. 应用场景
  • 函数重命名:如果你需要将一个函数在不同的代码库中暴露为不同的名字,可以使用 alias 属性。这样,库中的调用者可以通过不同的名字来访问相同的函数。

  • 动态链接:有时,程序中的多个模块或版本之间需要共享相同的符号。通过为一个函数创建多个别名,可以确保符号兼容性。

3. 使用 alias
#include <stdio.h>
​
void real_function(void) {
    printf("This is the real function\n");
}
​
void my_function(void) __attribute__((alias("real_function")));
​
int main() {
    my_function();  // 这将调用 real_function
    return 0;
}

在上面的代码中,调用 my_function() 会跳转到 real_function(),因为 my_functionreal_function 的别名。

下面的两个则是针对内联函数的,对于一些短小精悍的函数,我们可以选择直接将代码贴到我们的调用处而不是压栈弹栈保护恢复现场。那太低能了。

noinline 属性

noinline 属性告诉编译器不要将某个函数进行内联优化。内联是编译器的优化技术,通常会将函数的代码嵌入到函数调用的地方,从而减少函数调用的开销。然而,在某些情况下,程序员希望禁用内联,可能是为了调试,或者避免内联导致的代码膨胀。

1. 用法
void my_function(void) __attribute__((noinline));

这会告诉编译器不要对 my_function 进行内联优化。即使在编译器认为它适合内联时,它也不会进行内联操作。

2. 应用场景
  • 调试:在调试过程中,禁用内联可以确保函数调用栈的可见性。内联函数可能使得堆栈信息变得难以跟踪,因此禁用内联有助于调试。

  • 避免代码膨胀:有时候,内联函数会导致代码膨胀,尤其是当函数非常大或者在多个地方被调用时。禁用内联可以减少这种代码膨胀。

  • 性能问题:在某些情况下,内联可能会导致程序的性能下降(如由于缓存未命中或内存带宽问题)。禁用内联可以避免这种情况。

3. 使用 noinline
#include <stdio.h>
​
void my_function(void) __attribute__((noinline));
​
void my_function(void) {
    printf("This function is not inlined\n");
}
​
int main() {
    my_function();  // 编译器不会内联这个函数
    return 0;
}

在这个例子中,尽管 my_function 可能是一个非常简单的函数,编译器也不会对其进行内联。

always_inline 属性

always_inline 属性告诉编译器始终对函数进行内联优化,即使编译器认为内联并不合适,也会强制内联。这对于要求高性能的代码(如嵌入式系统、实时应用程序等)非常有用,可以确保函数的调用不会引入额外的开销。

1. 用法
void my_function(void) __attribute__((always_inline));

这会告诉编译器尽可能地对 my_function 进行内联,即使编译器认为内联可能导致性能下降,编译器也会强制内联。

2. 应用场景
  • 高性能优化:对于那些调用频繁且开销较大的小函数,强制内联可以减少函数调用的开销,提升性能。例如,在嵌入式系统或实时系统中,小函数的内联优化可能对性能有显著影响。

  • 避免函数调用开销:某些小函数的调用可能引入不可忽视的开销,特别是在性能敏感的代码中。通过 always_inline,可以避免这些开销。

  • 硬件交互:在嵌入式开发中,某些硬件操作可能需要紧密的优化,强制内联这些操作可以提高效率。

3. 使用 always_inline
#include <stdio.h>
​
void my_function(void) __attribute__((always_inline));
​
inline void my_function(void) {
    printf("This function is always inlined\n");
}
​
int main() {
    my_function();  // 编译器将始终内联这个函数
    return 0;
}

在这个例子中,my_function 被标记为 always_inline,编译器会尽可能地将它内联,即使它很简单,编译器也不会忽略这个要求。

内建函数

GNU C 提供了许多内建函数(built-in functions),这些函数是由 GCC 编译器提供的,不需要额外的库支持,可以直接在代码中调用。内建函数通常用于优化性能、提供对特定硬件的支持、以及增强语言功能的表达能力。它们可以减少编译器生成的代码量,避免常规函数调用的开销,从而提高程序的执行效率。

也就是说不用自己手搓,直接调用编译器提供的函数就直接梭哈完事了。因此,我们的来看看有哪些内建函数

__builtin_return_address

__builtin_return_address 函数返回当前函数调用的返回地址,即当当前函数执行完毕后,程序将跳转到的地址。返回地址通常是函数的调用指令的地址,它指定了程序执行流程的下一个位置。

用法
void* __builtin_return_address(unsigned int level);
  • level 参数:表示要获取的返回地址的级别。level = 0 获取当前函数的返回地址,level = 1 获取调用当前函数的函数的返回地址,依此类推。通常,level = 0 就是当前函数的返回地址,level = 1 是调用当前函数的函数的返回地址,level = 2 是调用那个函数的函数的返回地址,以此类推。

  • 返回值:返回一个 void* 类型的指针,表示返回地址。如果指定的级别没有有效的返回地址,返回 NULL

示例
#include <stdio.h>
​
void foo() {
    void* return_addr = __builtin_return_address(0);  // 获取当前函数的返回地址
    printf("Return address of foo: %p\n", return_addr);
}
​
int main() {
    foo();
    return 0;
}

在这个例子中,__builtin_return_address(0) 返回 foo 函数的返回地址。调用 foo 后,return_addr 将保存 foo 函数返回的地址(即 main 函数执行完后将返回到的位置)。

使用场景
  • 调试和栈分析__builtin_return_address 可以用于调试工具中,分析函数调用栈,特别是在栈溢出检测、栈跟踪等操作中。它帮助程序员了解当前执行点以及如何返回到调用点。

  • 异常处理:在异常处理框架中,有时需要知道函数的返回地址,以便能够正确地恢复到异常发生前的位置。可以通过 __builtin_return_address 获取这些信息。

  • 函数调用追踪:在编写函数调用追踪工具时,可以使用该内建函数捕获和记录每个函数的返回地址。

注意事项
  • __builtin_return_address 依赖于编译器生成的栈帧结构。在某些情况下,编译器优化(如内联、尾调用优化)可能使得这个函数无法正确返回预期的地址。

  • 对于某些高级编译器优化(例如栈帧重用或栈拆分),返回地址的有效性可能会受到影响,因此使用时需要小心。

__builtin_frame_address

__builtin_frame_address 返回当前函数的栈帧的起始地址。栈帧是一个函数调用时分配的内存区域,用于存储函数的局部变量、参数、返回地址等信息。该内建函数可以用来访问调用栈的信息,尤其是在调试和异常处理等场景中,查看栈帧结构和栈内存布局非常有用。

1. 用法
void* __builtin_frame_address(unsigned int level);
  • level 参数:与 __builtin_return_address 类似,level 参数表示栈帧的级别。level = 0 返回当前函数的栈帧地址,level = 1 返回调用当前函数的函数的栈帧地址,以此类推。

  • 返回值:返回一个 void* 类型的指针,指向栈帧的地址。如果指定的级别没有有效的栈帧,返回 NULL

示例
#include <stdio.h>
​
void foo() {
    void* frame_addr = __builtin_frame_address(0);  // 获取当前函数的栈帧地址
    printf("Frame address of foo: %p\n", frame_addr);
}
​
int main() {
    foo();
    return 0;
}

在这个例子中,__builtin_frame_address(0) 获取 foo 函数的栈帧地址。当 foo 被调用时,frame_addr 将保存 foo 函数栈帧的起始地址。

使用场景
  • 调试与栈分析__builtin_frame_address 对于栈跟踪和调试工具至关重要。它能够帮助调试工具访问函数的栈帧,识别局部变量、返回地址等。特别是在栈溢出或栈损坏的情况下,获取栈帧地址有助于诊断问题。

  • 内存分析:在底层编程或嵌入式开发中,可能需要直接访问栈帧,以便进行手动内存管理或检查栈内存的完整性。

  • 异常处理:类似于 __builtin_return_address__builtin_frame_address 可以在异常处理框架中用于捕获和分析栈信息。通过访问栈帧,可以在异常处理期间恢复到适当的位置。

  • 栈保护:某些低级应用可能需要直接操作栈帧,例如实现自定义的栈保护机制或栈溢出检测。在这种情况下,访问栈帧地址非常有用。

注意事项
  • __builtin_frame_address 依赖于编译器生成的栈帧。在优化级别较高的情况下,编译器可能会对栈帧进行修改(例如,尾调用优化可能会消除一些栈帧),使得该函数无法正确返回栈帧地址。

  • __builtin_return_address 类似,__builtin_frame_address 在某些编译器优化下可能无法提供准确的信息。

_builtin_return_address__builtin_frame_address 的对比
特性__builtin_return_address__builtin_frame_address
返回值返回函数的返回地址(调用函数的下一条指令)返回当前函数的栈帧地址(栈帧的起始位置)
参数level 指定栈深度,0 为当前函数level 指定栈深度,0 为当前函数
用途获取函数调用的返回地址获取当前函数的栈帧地址
使用场景调试、栈跟踪、异常处理、函数调用追踪调试、栈溢出检测、栈保护、栈分析
可能受影响的编译器优化内联、尾调用优化等可能导致返回地址不准确高级编译器优化(例如尾调用优化)可能会影响栈帧的有效性

__builtin_clz(x)

返回整数 x 的二进制表示中前导 0 的个数。

  • 用途:高效计算整数的位数,对于位运算优化特别有用。

int count = __builtin_clz(16);  // 结果是 27,因为 16 的二进制是 10000,前导 0 的个数是 27(假设是 32 位系统)
__builtin_ctz(x)

返回整数 x 的二进制表示中最右边 1 的位置(从 0 开始计数)。

  • 用途:快速查找二进制数中最低位的 1,广泛应用于位运算优化,如快速算术运算、哈希算法等。

int pos = __builtin_ctz(16);  // 结果是 4,因为 16 的二进制是 10000,最低位的 1 位于第 4 位
__builtin_popcount(x)

返回 x 的二进制表示中 1 的个数。

  • 用途:快速计算数字的 Hamming 权重(1 的个数),广泛应用于图论、哈希表等场景。

int count = __builtin_popcount(15);  // 结果是 4,因为 15 的二进制是 1111,有 4 个 1
__builtin_fma(x, y, z)

计算浮点数 x * y + z 的值。

  • 用途:可以避免浮点乘法和加法的精度损失,通常用于高性能计算中的数学运算。

double result = __builtin_fma(2.0, 3.0, 4.0);  // 结果是 10.0
内存相关的内建函数

GNU C 还提供了一些优化内存操作的内建函数,特别是与内存分配、复制、对齐和访问相关的操作。

__builtin_alloca(size)

分配一块动态大小的栈内存,类似于 alloca() 函数,但由 GCC 提供支持。

  • 用途:在栈上分配内存,避免使用堆分配,可以提高性能,但栈上分配的内存在函数返回时会自动释放。

void* ptr = __builtin_alloca(100);  // 分配 100 字节的栈内存
__builtin_memcpy(dest, src, size)

通常实现为更高效的内存复制操作,编译器会优化 memcpy,使其在内存布局优化时更加高效。

__builtin_memcpy(dest, src, n);  // 优化版本的 memcpy 函数
__builtin_memmove(dest, src, size)

memmove 相同,但使用内建函数进行优化,避免不必要的临时缓冲区。

__builtin_memmove(dest, src, n);  // 优化版本的 memmove 函数
__builtin_assume_aligned(ptr, alignment)

告诉编译器 ptr 按照 alignment 对齐。这通常用于优化内存访问,使得编译器能够生成更高效的代码,尤其是在对内存进行频繁访问时。

int* ptr = __builtin_assume_aligned(src, 16);  // 假设 src 是 16 字节对齐的
控制流相关的内建函数

这些函数帮助编译器进行控制流的优化,尤其是在程序运行时跳转、分支预测等方面。

__builtin_expect(expression, value)

告诉编译器某个条件表达式 expression 更可能为 value。这有助于编译器进行分支预测优化,减少不太可能执行的分支的开销。

  • 用途:优化条件判断和分支预测。

if (__builtin_expect(x == 0, 1)) {
    // 这里假设 x == 0 更可能成立
}
__builtin_unreachable()

指示编译器某段代码永远不会执行到。这通常用于一些不可能发生的代码路径(如 switch 语句中的默认分支)。

  • 用途:优化死代码检测和分支预测。

switch (x) {
    case 1:
        // 做某事
        break;
    default:
        __builtin_unreachable();  // 永远不会执行到这里
}
__builtin_abort()

立即终止程序执行,类似于 abort()。此函数可用于中止异常、断言失败或程序无法继续执行的情况下。

__builtin_abort();  // 直接终止程序
类型相关的内建函数

这些函数与类型的操作和检查有关,常用于类型兼容性检查、常量表达式的识别等。

1. __builtin_types_compatible_p(type1, type2)

检查 type1type2 是否兼容。如果两个类型完全相同或可以通过类型转换兼容,则返回 1,否则返回 0。

int result = __builtin_types_compatible_p(int, long);  // 返回 0,因为 int 和 long 不兼容
int result = __builtin_types_compatible_p(int, int);   // 返回 1,因为它们相同
2. __builtin_constant_p(expression)

检查表达式 expression 是否为常量表达式。如果表达式在编译时是常量,则返回非零值,否则返回 0。

int x = 10;
if (__builtin_constant_p(x)) {
    // x 是常量,可以进行编译时优化
}
3. __builtin_object_size(ptr, mode)

返回 ptr 指向对象的大小。mode 参数可以控制返回值的计算方式,0 表示计算大小,1 表示返回不包含指针的对象大小。

size_t size = __builtin_object_size(ptr, 0);  // 获取 ptr 指向的对象大小
硬件相关的内建函数

GNU C 还提供了一些针对硬件访问和优化的内建函数,尤其在嵌入式编程、操作系统开发中非常有用。

1. __builtin_prefetch(ptr, rw, locality)

告诉编译器预取内存块,提前将数据加载到缓存中。rw 参数表示访问类型(0 表示只读,1 表示写),locality 表示数据的局部性(越高预取的优先级越高)。

__builtin_prefetch(arr, 0, 3);  // 预取 arr 数组的数据
2. __builtin_bswap32(x)__builtin_bswap64(x)

将 32 位或 64 位整数的字节顺序进行交换。这在处理网络字节序或跨平台数据传输时非常有用。

unsigned int x = 0x12345678;
unsigned int y = __builtin_bswap32(x);  // 结果是 0x78563412

Reference

Other Builtins (Using the GNU Compiler Collection (GCC))

Linux2.6.24-内核数据结构之list_head | DE-LAB (dessera.github.io)


http://www.kler.cn/a/417941.html

相关文章:

  • TiDB 无统计信息时执行计划如何生成
  • 触觉智能亮相OpenHarmony人才生态大会2024
  • Django Auth的基本使用
  • Python中字符串和正则表达式
  • 排序学习整理(1)
  • JS听到了双生花的回响
  • vue.js学习(day 14)
  • 从缓存到分布式缓存的那些事
  • 游戏引擎学习第27天
  • Python 在Excel中插入、修改、提取和删除超链接
  • Vivo手机投屏到Windows笔记本电脑,支持多台手机投屏、共享音频!
  • 【linux学习指南】详解Linux进程信号保存
  • Python `def` 函数中使用 `yield` 和 `return` 的区别
  • git安装与配置与相关命令
  • Matlab搜索路径添加不上
  • 人脸识别API解锁智能生活、C++人脸识别接口软文
  • Apache SeaTunnel 自定义连接器适配华为大数据平台集成组件ClickHouse
  • FPGA存在的意义:为什么adc连续采样需要fpga来做,而不会直接用iic来实现
  • sentinel使用手册
  • 基于java注解实现websocket详解
  • 如何更好地设计SaaS系统架构
  • MATLAB期末复习笔记(上)
  • 基于Java Springboot 求职招聘平台
  • 爬虫框架快速入门——Scrapy
  • QT 实现组织树状图
  • flutter底部导航栏中间按钮凸起,导航栏中间部分凹陷效果