嵌入式C语言自我修养:GNU C编译器扩展语法精讲

       在Linux内核的源码中,你会发现许多这样的“奇特”代码。它们看起来可能有点陌生,但它们实际上是C语言的一种扩展形式,这种扩展在C语言的标准教材中往往不会提及。这就是为什么你在阅读Linux驱动代码或内核源码时,可能会感到既熟悉又陌生。如

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

     本文将深入了解Linux内核或GNU开源软件中常用的一些C语言特殊语法扩展。通过学习这些编译器扩展特性,你将能够扫除这些C语言扩展语法带给你的阅读障碍,让你在Linux内核的世界中更加游刃有余。

目录

C语言标准和编译器

C语言标准的内容

C语言标准的发展过程

编译器对C语言标准的支持

编译器对C语言标准的扩展

指定初始化

指定初始化数组

指定初始化结构体成员

Linux内核驱动注册

指定初始化的好处

宏构造“利器”:语句表达式

表达式、语句和代码块

在宏定义中使用语句表达式

typeof与container_of宏

typeof关键字

Linux内核中的container_of宏

零长度数组

什么是零长度数组

linux内核中的零长度数组

指针与零长度数组

属性声明

GNU C编译器扩展关键字:__attribute__

aligned属性

结构体的对齐

packed属性

属性声明:section

U-boot镜像自复制分析

属性声明format

变参函数的格式检查

变参函数初体验

变参函数改进版

属性声明:weak

属性声明:alias

内联函数

属性声明:noinline

什么是内联函数

思考:内联函数为什么定义在头文件中

内建函数

常用的内建函数

__builtin_return_address()

__builtin_frame_address()

__builtin_constant_p(n)

__builtin_expect(exp,c)

Linux内核中的likely和unlikely

可变参数宏


C语言标准和编译器

C语言标准因为是在1989年发布的,所以人们一般称其为C89或C90标准,或者叫作ANSI C标准。

C语言标准的内容

C语言编程的一些语法惯例、约定规则,在C语言标准里:

● 定义各种关键字、数据类型。

● 定义各种运算规则、各种运算符的优先级和结合性。

● 数据类型转换。

● 变量的作用域。

● 函数原型、函数嵌套层数、函数参数个数限制。

● 标准库函数接口。

程序员开发程序时,按照这种标准规定的语法规则编写程序;编译器厂商开发编译

器工具时,也按照这种标准去解析、翻译程序。

C语言标准的发展过程

ANSI C统一了各大编译器厂商的不同标准,并对C语言的语法和特性做了一些扩展,在1989年发布的一个标准。这个标准一般也叫作C89/C90标准,也是目前各种编译器默认支持的C语言标准。

C99标准是ANSI在1999年基于C89标准发布的一个新标准。

C11标准是ANSI在2011年发布的最新C语言标准,C11标准修改了C语言标准的一些bug,增加了一些新特性。目前绝大多数编译器还不支持,暂时还用不到。

编译器对C语言标准的支持

目前对C99标准支持最好的是GNU C编译器。

编译器对C语言标准的扩展

不同编译器,出于开发环境、硬件平台、性能优化的需要,除了支持C语言标准,还会自己做一些扩展。

如GCC编译器也对C语言标准做了很多扩展。零长度数组,语句表达式,内建函数,__attribute__特殊属性声明.....这些新增的特性,C语言标准目前是不支持的,其他编译器也不支持。

指定初始化
指定初始化数组

C语言标准初始化数组

int a[10]={0,1,2,3,4,5,6,7,8};

a[9]默认为0.

int b[100]={[10]=1,[30]=2};

通过数组元素索引,我们可以直接给指定的数组元素赋值。在GNU C中,通过数组元素索引,可以直接给指定的几个元素赋值。

给数组中某一个索引范围的数组元素初始化

int main(void)
{int b[100] = {[10...30] = 1, [50...60] = 2};for(int i = 0; i < 100; i++){printf("%d", a[i]);if (i % 10 == 0)printf("\n");}return 0;
}

使用[10...30]表示一个索引范围,给a[10]到a[30]之间的20个数组元素赋值为1。

GNU C支持使用...表示范围扩展,这个特性不仅可以使用在数组初始化中,也可以使用在switch-case语句中。

#include <stdio.h>
int main(void)
{int i = 4;switch(i){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;}}return 0;
}
指定初始化结构体成员

在GNU C中我们可以通过结构域来指定初始化某个成员。

#include <stdio.h>
struct student {char name[20]; // 假设名字不超过19个字符int age;
};
int main(void) {struct student stu2 = {.name = "wanglitao", .age = 28};printf("%s:%d\n", stu2.name, stu2.age);return 0;
}

通过结构域名.name和.age,可以给结构体变量的某一个指定成员直接赋值。

Linux内核驱动注册

驱动程序中,我们经常使用file_operations这个结构体来注册我们开发的驱动,然后系统会以回调的方式来执行驱动实现的具体功能。

#include <linux/fs.h>
static const struct file_operations ab3100_otp_operations = {.open=ab3100_otp_open,.read=seq_read,.llseek=seq_lseek,.release=single_release,
};

flie_operations结构体中有很多函数,使用.指定可以减少代码量

#include <linux/fs.h>
struct file_operations {struct module *owner;loff_t (*llseek)(struct file *, loff_t, int);ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);ssize_t (*read_iter)(struct kiocb *, struct iov_iter *);ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);int (*iterate)(struct file *, struct dir_context *);unsigned int (*poll)(struct file *, struct poll_table_struct *);long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);long (*compat_ioctl)(struct file *, unsigned int, unsigned long);int (*mmap)(struct file *, struct vm_area_struct *);int (*open)(struct inode *, struct file *);int (*flush)(struct file *, fl_owner_t id);int (*release)(struct inode *, struct file *);int (*fsync)(struct file *, loff_t, loff_t, int datasync);int (*aio_fsync)(struct kiocb *, int datasync);int (*fasync)(int, struct file *, int);int (*lock)(struct file *, int, struct file_lock *);ssize_t (*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock)(struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifdef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endif
};
指定初始化的好处

当成百上千个文件都使用file_operations这个结构体类型来定

义变量并初始化时,如果采用C标准按照固定顺序赋值,当file_operations结构体类型发生变化时,如添加了一个成员、删除了一个成员、调整了成员顺序,那么使用该结构体类型定义变量的大量C文件都需要重新调整初始化顺序,牵一发而动全

身。我们通过指定初始化方式,就可以避免这个问题。无论file_operations结构体类型如何变化,添加成员也好、删除成员也好、调整成员顺序也好,都不会影响其他文件的使用。

宏构造“利器”:语句表达式
表达式、语句和代码块

在宏定义中使用语句表达式

使用简单的宏定义做文本替换后会因为括号出现优先级的问题,所以需要进行调整,如linux中min和max的宏定义如下:

#define min(x, y) ({ \typeof(x) _min1 = x; \typeof(y) _min2 = y; \(void) (&_min1 == &_min2); \_min1 < _min2 ? _min1 : _min2; })#define max(x, y) ({ \typeof(x) _max1 = x; \typeof(y) _max2 = y; \(void) (&_max1 == &_max2); \_max1 > _max2 ? _max1 : _max2; }

(void) 是一个类型转换,它将后面的表达式转换为 void 类型,这意味着表达式的结果被丢弃。而 &_min1 == &_min2 是一个比较两个变量地址是否相同的表达式,这个表达式的结果是一个布尔值,但通过将其转换为 void 类型,这个结果就被忽略了。void这一行代码的目的是让编译器认为 _min1_min2(或 _max1_max2)这两个变量被使用了,从而避免编译器发出未使用变量的警告。

typeof与container_of宏
typeof关键字

ANSI C定义了sizeof关键字,用来获取一个变量或数据类型在内存中所占的字节数。GNU C扩展了一个关键字typeof,用来获取一个变量或表达式的类型。typeof没有被纳入C标准,是GCC扩展的一个关键字。

int i ;
typeof(i)j= 20;
typeof(int *)a;
int f();
typeof(f())k;

变量i的类型为int,所以typeof(i)就等于int,typeof(i) j=20就相当于int j=20,typeof(int*) a;

相当于int*a,f()函数的返回值类型是int,所以typeof(f()) k;就相当于int k;

typeof(int*) y;

定义了一个类型为int*的变量 y,等价于int* y;

typeof (int) *y;

等价于int *y;

typeof(*x) y;

定义了一个变量 y,其类型是 指针x 指向的类型。如果 x 是一个指针,y 将是 x 所指向类型的变量。

typeof(int) y[4];

等价于 int y[4];

typeof ( typeof(char *)[4] ) y; 

等价于char *y[4];

typeof(int x[4]) y;

等价于int y[4];

Linux内核中的container_of宏
零长度数组

零长度数组、变长数组都是GNU C编译器支持的数组类型

什么是零长度数组

长度为0的数组

int a[0];

零长度数组有一个特点,就是不占用内存存储空间。零长度数组一般单独使用的机会很少,它常常作为结构 体的一个成员,构成一个变长结构体。

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

使用变长数组实现buffer。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct buffer {int len;int a[0]; // 一个零长度的数组,表示后面可以追加任意长度的数据
};
int main(void) {struct buffer *buf;buf = (struct buffer *)malloc(sizeof(struct buffer) + 20);if (buf == NULL) {// 内存分配失败的处理perror("malloc failed");return 1;}buf->len = 20;strcpy(buf->a, "hello zhaixue.cc!\n");puts(buf->a);free(buf);return 0;
}
linux内核中的零长度数组

网卡驱动中的套接字缓冲区(Socket Buffer),USB驱动中的usb request block.......

指针与零长度数组

为什么不使用指针来代替零长度数组?如果使用指针,指针本身占用存储空间不说,远远没有零长度数组用得巧妙:零长度数组不会对结构体定义造成冗余,而且使用起来很方便。

属性声明
GNU C编译器扩展关键字:__attribute__

GNU C增加了一个__attribute__关键字用来声明一个函数、变量或类型的特殊属性。

atttribute((ATTRIBUTE))

目前__attribute__支持十几种属性声明。

section,aligned,packed,format,weak,alias,noinline,always_inline...

aligned属性

aligned和packed用来显式指定一个变量的存储对齐方式。使用__atttribute__这个属性声明,可以告诉编译器,按照指定的边界对齐方式去给这个变量分配存储空间。

char c __attribute__((aligned(8))) = 4;

代码声明一个字符变量 c,使用 __attribute__((aligned(8))) 指定该变量的对齐方式,表示c应该被对齐到8字节的边界,c在内存中的地址应该是8的倍数。

aligned有一个参数,表示要按几字节对齐,使用时要注意,地址对齐的字节数必须是2的幂次方,否则编译就会出错。aligned只是建议编译器按照这种大小地址对齐,但不能超过编译器允许的最大值。

当定义一个变量时,编译器会按照默认的地址对齐方式,来给该变量分配一个存储空间地址。如果该变量是一个int型数据,那么编译器就会按4字节或4字节的整数倍地址对齐;如果该变量是一个short型数据,那么编译器就会按2字节或2字节的整数倍地址对齐;如果是一个char类型的变量,那么编译器就会按照1字节地址对齐。

地址对齐会造成一定的内存空洞,但是这种对齐设置可以简化CPU读取数据。一个32位的计算机系统,在CPU读取内存时,硬件设计上可能只支持4字节或4字节倍数对齐的地址访问,CPU每次向内存RAM读写数据时,一个周期可以读写4字节。如果我们把一个int型数据放在4字节对齐的地址上,那么CPU一次就可以把数据读写完毕;如果我们把一个int型数据放在一个非4字节对齐的地址上,那么CPU可能就要分两次才能把这个4字节大小的数据读写完毕。

结构体的对齐

char 1字节对齐,short 2 字节对齐,int 4字节对齐,结构体的整体对齐要按结构体所有成员中最大对齐字节数或其整数倍对齐。

struct data{char a;int b ;short c ;
}

1+3+4+2+2(结构体)=12

结构体成员按不同的顺序排放,可能会导致结构体的整体长度不一样。

struct data{char a;short b ;int c;
}

1+1(short 2字节对齐)+2+4=8

packed属性

packed属性则与之相反,一般用来减少地址对齐,指定变量或类型使用最可能小的地址对齐方式。

struct data{char a;short battribute ((packed));int c_attribute_((packed));
}

1+2+4=7

在ARM芯片中,每一个控制器的寄存器地址空间一般都是连续存在的。如果考虑数据对齐,则结构体内就可能有空洞,就和实际连续的寄存器地址不一致。使用packed可以避免这个问题,结构体的每个成员都紧挨着,依次分配存储地址,这样就避免了各个成员因地址对齐而造成的内存空洞。

在Linux内核源码中,经常看到aligned和packed一起使用,对一个变量或类型同时使用aligned和packed属性声明。这样避免了结构体内各成员因地址对齐产生内存空洞,又指定了整个结构体的对齐方式。

struct data {char a;short b;int c;
}_attribute_((packed,aligned(8)));
属性声明:section

可以使用__attribute__来声明一个section属性,section属性的主要作用是:在程序编译时,将一个函数或变量放到指定的段,放到指定的section中。

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

代码声明一个全局整型变量 global_val,并使用 __attribute__((section(".data"))) 指定该变量应该被放置在程序的哪个段中。

section(".data") 表示 global_val 应该被放置在名为 ".data" 的段中。全局变量默认被放置在 ".data" 段 或者在需要的情况下将其放置在其他段中。

一个可执行文件主要由代码段、数据段、BSS段构成。代码段主要存放编译生成的可执行指令代码,数据段和BSS段用来存放全局变量、未初始化的全局变量。代码段、数据段和BSS段构成了一个可执行文件的主要部分。

编译器在编译程序时,以源文件为单位,将一个个源文件编译生成一个个目标文件。在编译过程中,编译器都会按照这个默认规则,将函数、变量分别放在不同的section中,最后将各个section组成一个目标文件。编译过程结束后,链接器会将各个目标文件组装合并、重定位,生成一个可执行文件。

U-boot镜像自复制分析

U-boot在启动过程中,是如何将自身代码加载的RAM中的。U-boot的用途主要是加载Linux内核镜像到内存,给内核传递启动参数,然后引导Linux操作系统启动。

U-boot是怎样将自身代码从Flash复制到内存的呢?.....

属性声明format
变参函数的格式检查

GNU通过__attribute__扩展的format属性,来指定变参函数的参数格式检查。

#include <stdio.h>
#include <stdarg.h>
void myprintf(const char *format, ...) __attribute__((format(printf, 1, 2)));
void myprintf(const char *format, ...){va_list args;va_start(args, format);vprintf(format, args);va_end(args);
}
int main() {myprintf("%s %d %f\n", "The answer is", 42, 3.14);return 0;
}

va_list:定 义 在 编 译 器 头 文 件 stdarg.h 中,如typedef char * va_list(所以va_list是一个char指针)

va_start(fmt,args):根据参数args的地址,获取args后面参数的地址,并保存在fmt指针变量中。

va_end(args):释放args指针,将其赋值为NULL。

__attribute__((format(printf, 1, 2)))

myprintf 函数的参数应该按照 printf函数的参数格式来检查

格式字符串是第一个参数,从第二个参数开始的参数需要与格式字符串相匹配。

如果调用 myprintf 时提供的参数与格式字符串不匹配,编译器将会发出警告。

void LOG(int num,char *fmt,...) __attribute__((format(printf,2,3)));
变参函数初体验
#include <stdio.h>
void print_num(int count,...) {int *args = (int *) &count + 1;for (int i = 0; i < count; i++) {printf("*args: %d\n", *args);args++;}
}
int main(void) {print_num(5, 1, 2, 3, 4, 5);return 0;
}

在print_num()函数中,首先获取count参数地址,然后使用&count+1就可以获取下一个参数的地址,使用指针变量args保存这个地址,并依次访问下一个地址,就可以直接打印传进来的各个实参值。

变参函数改进版
#include <stdio.h>
#include <stdarg.h>
void print_num(int count, ...) {va_list args;va_start(args, count);for (int i = 0;i < count;i++) printf("%d\n", va_arg(args, int));va_end(args);
}
int main(void) {print_num(5, 1, 2, 3, 4, 5);return 0;
}

va_arg(args, int)相当于args指针加4后解引用(取出一个整数)。

实现不同等级的日志打印

#include <stdio.h>
#include <stdarg.h>
#define LOG_LEVEL_INFO  1
#define LOG_LEVEL_WARN  2
#define LOG_LEVEL_ERR   3
int current_log_level = LOG_LEVEL_INFO; // 默认日志等级为INFO
void log_info(const char *format, ...) {if (current_log_level <= LOG_LEVEL_INFO) {va_list args;va_start(args, format);printf("INFO: ");vprintf(format, args);va_end(args);}
}
void log_warn(const char *format, ...) {if (current_log_level <= LOG_LEVEL_WARN) {va_list args;va_start(args, format);printf("WARN: ");vprintf(format, args);va_end(args);}
}
void log_err(const char *format, ...) {if (current_log_level <= LOG_LEVEL_ERR) {va_list args;va_start(args, format);printf("ERROR: ");vprintf(format, args);va_end(args);}
}
int main() {log_info("This is an info message: %d", 42);log_warn("This is a warning message: %s", "Be careful!");log_err("This is an error message: %d", -1);return 0;
}
属性声明:weak

GNU C通过weak属性声明,可以将一个强符号转换为弱符号。程序中变量名/函数名,在编译器的眼里,就是一个符号而已。符号可以分为强符号和弱符号。

●强符号:函数名,初始化的全局变量名 ●弱符号:未初始化的全局变量名

对于相同的全局变量名、函数名

●强符号+强符号 ●强符号+弱符号 ●弱符号+弱符号。

强符号和弱符号主要用来解决在程序链接过程中,出现多个同名全局变量、同名函数的冲突问题。一般遵循下面3个规则:

不能同时存在两个强符号:两个同名的函数或全局变量,那么链接器在链接时就会报重定义错误。

强弱可以共处:可以同时定义一个初始化的全局变量和一个未初始化的全局变量。

体积大者胜出:当同名的符号都是弱符号时,那么编译器该选择哪个呢?谁的体积大,即谁在内存中的存储空间大,就选谁。

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

属性声明:alias

GNU C扩展了一个alias属性,用来给函数定义一个别名。

#include <stdio.h>
//定义 _f 函数
void _f(void) {printf("_f\n");
}
//为 _f 函数创建别名 f
void f(void) __attribute__ ((alias("_f")));
int main(void) {// 调用 f 函数,实际上调用的是 _f 函数f();return 0;
}
内联函数
属性声明:noinline

noinline和always_inline,两个属性的用途是告诉编译器,在编译时,对指定的函数内联展开或不展开。声明一个静态内联函数,使用 always inline 属性强制编译器内联。

static inline int func2() __attribute__((no_inline));
static inline int func2() __attribute__((always_inline));

使用inline声明一个内联函数,和使用关键字register声明一个寄存器变量一样,只是建议编译器在编译时内联展开。使用关键字register修饰一个变量,只是建议编译器在为变量分配存储空间时,将这个变量放到寄存器里,这会使程序的运行效

率更高。那么编译器会不会放呢?得视具体情况而定,编译器要根据寄存器资源是否紧张、这个变量的类型及是否频繁使用来做权衡。

当一个函数使用inline关键字修饰时,编译器在编译时一定会内联展开吗?也不一定。编译器也会根据实际情况,如函数体大小、函数体内是否有循环结构、是否有指针、是否有递归、函数调用是否频繁来做决定。使用noinline和always_inline对一个内联函数作显式属性声明后,编译器的编译行为就变得确定。

什么是内联函数

内联函数在调用的时候插入的调用的位置,省去了压栈和出栈的操作。与宏相比,内联函数有以下优势:

● 参数类型检查:内联函数虽然具有宏的展开特性,但其本质仍是函数,在编译过程中,编译器仍可以对其进行参数检查,而宏不具备这个功能。

● 便于调试:函数支持的调试功能有断点、单步等,内联函数同样支持。

● 返回值:内联函数有返回值,返回一个结果给调用者。这个优势是相对于ANSI C说的,因为现在宏也可以有返回值和类型了,如前面使用语句表达式定义的宏。

● 接口封装:有些内联函数可以用来封装一个接口,而宏不具备这个特性。

内联函数并不是完美无瑕的,也有一些缺点。内联函数会增大程序的体积,如果在一个文件中多次调用内联函数,多次展开,那么整个程序的体积就会变大,在一定程度上会降低程序的执行效率。函数的作用之一就是提高代码的复用性。我们将常用的一些代码或代码块封装成函数,进行模块化编程,可以减轻软件开发工作量。内联函数往往又降低了函数的复用性。编译器在对内联函数做展开时,除了检测用户定义的内联函数内部是否有指针、循环、递归,还会在函数执行效率和函数调用开销之间进行权衡。

当函数体积小,函数体内无指针赋值、递归、循环等语句,调用频繁,就可以使用static inline关键字修饰它。但编译器不一定会做内联展开,如果你想明确告诉编译器一定要展开,或者不展开,就可以使用noinline或always_inline对函数做一个属性声明。

思考:内联函数为什么定义在头文件中

内联函数为什么要定义在头文件中呢?因为它是一个内联函数,可以像宏一样使用,任何想使用这个内联函数的源文件,都不必亲自再去定义一遍,直接包含这个头文件,即可像宏一样使用。那么为什么还要用static修饰呢?因为我们使用inline定义的内联函数,编译器不一定会内联展开,那么当一个工程中多个文件都包含这个内联函数的定义时,编译时就有可能报重定义错误。使用static关键字修饰,则可以将这个函数的作用域限制在各自的文件内,避免重定义错误的发生。

内建函数

内建函数,就是编译器内部实现的函数。函数和关键字一样,可以直接调用,无须像标准库函数那样,要先声明后使用。

内建函数的函数命名,通常以__builtin开头。这些函数主要在编译器内部使用,主要是为编译器服务的。内建函数的主要用途如下。

●用来处理变长参数列表。

●用来处理程序运行异常、编译优化、性能优化。

●查看函数运行时的底层信息、堆栈信息等。

●实现C标准库的常用函数。

常用的内建函数
__builtin_return_address()

这个函数用来返回当前函数或调用者的返回地址。函数的参数LEVEL表示函数调用链中不同层级的函数。

● 0:获取当前函数的返回地址。

● 1:获取上一级函数的返回地址。

● 2:获取上二级函数的返回地址。

#include <stdio.h>
void f(void) {int *p;p = __builtin_return_address(0);printf("f return address: %p\n", (void*)p);p = __builtin_return_address(1);printf("func return address: %p\n", (void*)p);p = __builtin_return_address(2);printf("main return address: %p\n", (void*)p);printf("\n");
}
void func(void) {int *p;p = __builtin_return_address(0);printf("func return address: %p\n", (void*)p);p = __builtin_return_address(1);printf("main return address: %p\n", (void*)p);printf("\n");
}
int main(void) {int *p;p = __builtin_return_address(0);printf("main return address: %p\n", (void*)p);printf("\n");func();printf("goodbye!\n");return 0;
}
__builtin_frame_address()

函数每调用一次,都会将当前函数的现场(返回地址、寄存器、临时变量等)保存在栈中,每一层函数调用都会将各自的现场信息保存在各自的栈中。这个栈就是当前函数的栈帧,每一个栈帧都有起始地址和结束地址,多层函数调用就会有多个栈帧,每个栈帧都会保存上一层栈帧的起始地址,这样各个栈帧就形成了一个调用链。很多调试器其实都是通过回溯函数的栈帧调用链来获取函数底层的各种信息的,如返回地址、调用关系等。

通过内建函数__builtin_frame_address(LEVEL)查看函数的栈帧地址。

●0:查看当前函数的栈帧地址。

●1:查看上一级函数的栈帧地址。

#include <stdio.h>
void func(void) {int *p;p = __builtin_frame_address(0);printf("func frame: %p\n", (void*)p);p = __builtin_frame_address(1);printf("main frame: %p\n", (void*)p);
}
int main(void) {int *p;p = __builtin_frame_address(0);printf("main frame: %p\n", (void*)p);func();return 0;
}
__builtin_constant_p(n)

编译器内部还有一些内建函数主要用来编译优化、性能优化,如__builtin_constant_p(n) 函数。该函数主要用来判断参数n在编译时是否为常量。如果是常量,则函数返回1,否则函数返回0。

该函数常用于宏定义中,用来编译优化。一个宏定义,根据宏的参数是常量还

是变量,可能实现的方法不一样。

__builtin_expect(exp,c)

内建函数__builtin_expect()也常常用来编译优化,函数有2个参数,返回值就是其中一个参数,仍是exp。这个函数的意义主要是告诉编译器:参数exp的值为c的可能性很大,然后编译器可以根据这个提示信息,做一些分支预测上的代码优化。

那么Cache如何缓存内存数据呢?空间局部性。如CPU正在执行一条指令,那么在下一个时钟周期里,CPU一般会大概率执行当前指令的下一条指令。如果此时Cache将下面的几条指令都缓存到Cache里,则下一个时钟周期里,CPU就可以直接到Cache里取指、译指和执行,从而使运算效率大大提高。

如程序在执行过程中遇到函数调用、if 分支、goto跳转等程序结构,会跳到其他地方执行,原先缓存到Cache 里的指令不是CPU要执行的指令。此时,就说Cache没有命中, Cache会重新缓存正确的指令代码供CPU读取。

遇到if/switch这种选择 分支的程序结构,一般建议将大概率发生的分支写在前面。当程序运 行时,因为大概率发生,所以大部分时间就不需要跳转,程序就相当 于一个顺序结构。

Linux内核中的likely和unlikely

这两个宏的主要作用就是告诉编译器:某一个分支发生的概率很高,或者很低,基本不可能发生。

int main(void)
{int a; scanf("%d",&a); if (unlikely(a==0)) {{printf("%d", 1);printf("%d", 2);printf("\n");}else {printf("%d", 5); printf("%d", 6);printf("%d", 6); printf("\n");}}return 0;
}
可变参数宏

变参函数基本套路就是使用va_list、va_start、va_end等宏,去解析那些可变参数列表。只有GNU C标准支持这个功能,所以有时候我们也把这个可变参数宏看作GNU C标准的一个语法扩展。

#include <stdio.h>
#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#define DEBUG(...) printf(__VA_ARGS__)
int main(void) {LOG("Hello! I'm %s\n", "Wanglitao");DEBUG("Hello! I'm %s\n", "Wanglitao");return 0;
}

可变参数宏的实现形式其实和变参函数差不多:用...表示变参列表,变参列表由不确定的参数组成,各个参数之间用逗号隔开。可变参数宏使用C99标准新增加的一个__VA_ARGS__预定义标识符来表示前面的变参列表,而不是像变参函数一样,使用va_list、va_start、va_end这些宏去解析变参列表。预处理器在将宏展开时,会用变参列表替换掉宏定义中的所有__VA_ARGS__标识符。

标识符__VA_ARGS__前面加上了宏连接符##,这样做的好处是:当变参列表非空时,##的作用是连接fmt和变参列表,各个参数之间用逗号隔开,宏可以正常使用;当变参列表为空时,##还有一个特殊的用处,它会将固定参数fmt后面的逗号删除掉,这样宏就可以正常使用了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/429319.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

写一下线性表

如果你是c语言, "不会"c, 那么... 把iostream当成stdio.h 把cout当成printf, 不用管啥类型, 变量名字一给输出完事 把cin>>当成scanf, 变量名字一给输入完事 把endl当成\n, 换行. 哦对了, malloc已经不建议使用了, 现在使用new, 把new当作malloc, 把delete当…

【工具变量】科技金融试点城市DID数据集(2000-2023年)

时间跨度&#xff1a;2000-2023年数据范围&#xff1a;286个地级市包含指标&#xff1a; year city treat post DID&#xff08;treat*post&#xff09; 样例数据&#xff1a; 包含内容&#xff1a; 全部内容下载链接&#xff1a; 参考文献-pdf格式&#xff1a;https://…

【JVM】概述

前言 Java的技术体系主要由支撑Java程序运行的虚拟机、提供各开发领域接口支持的Java类库、Java编程语言及许许多多的第三方Java框架&#xff08;如Spring、MyBatis等&#xff09;构成。在国内&#xff0c;有关Java类库API、Java语言语法及第三方框架的技术资料和书籍非常丰富&…

Spring Boot蜗牛兼职网:全栈开发

第4章 系统设计 4.1 系统体系结构 蜗牛兼职网的结构图4-1所示&#xff1a; 图4-1 系统结构 登录系统结构图&#xff0c;如图4-2所示&#xff1a; 图4-2 登录结构图 蜗牛兼职网结构图&#xff0c;如图4-3所示。 图4-3 蜗牛兼职网结构图 4.2开发流程设计 系统流程的分析是通…

抖音短视频矩阵系统OEM源码开发注意事项,功能开发细节流程全揭秘

抖音短视频矩阵系统OEM源码开发注意事项,功能开发细节流程全揭秘 在当今数字化时代背景下&#xff0c;短视频产业正经历前所未有的快速发展。其中&#xff0c;抖音凭借其创新的算法及多元内容生态获得巨大成功&#xff0c;吸引了众多用户。对于意欲进入短视频领域的创业者而言&…

移动技术开发:ListView水果列表

1 实验名称 ListView水果列表 2 实验目的 掌握自定义ListView控件的实现方法 3 实验源代码 布局文件代码&#xff1a; activity_main.xml: <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.androi…

springboot注册和注入组件方式概览

IoC&#xff1a;Inversion of Control&#xff08;控制反转&#xff09; 控制&#xff1a;资源的控制权&#xff08;资源的创建、获取、销毁等&#xff09; 反转&#xff1a;和传统的方式不一样了 DI &#xff1a;Dependency Injection&#xff08;依赖注入&#xff09; 依赖&…

国人卖家可折叠无线充电器发起TRO专利维权,功能相同可能侵权

案件基本情况&#xff1a;起诉时间&#xff1a;2024-8-5案件号&#xff1a;2024-cv-22971原告&#xff1a;SHANGXING TECHNOLOG (SHENZHEN) CO., LTD原告律所&#xff1a;Rubio & Associates, P.A.起诉地&#xff1a;佛罗里达州南部法院涉案商标/版权&#xff1a;原告品牌简…

信息安全数学基础(19)同余式的基本概念及一次同余式

一、同余式概念 同余式是数论中的一个基本概念&#xff0c;用于描述两个数在除以某个数时所得的余数相同的情况。具体地&#xff0c;设m是一个正整数&#xff0c;a和b是两个整数&#xff0c;如果a和b除以m的余数相同&#xff0c;则称a和b模m同余&#xff0c;记作a≡b(mod m)。反…

计算机视觉的应用34-基于CV领域的人脸关键点特征智能提取的技术方法

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下计算机视觉的应用34-基于CV领域的人脸关键点特征智能提取的技术方法。本文主要探讨计算机视觉领域中人脸关键点特征智能提取的技术方法。详细介绍了基于卷积神经网络模型进行人脸关键点提取的过程&#xff0c;包括使…

Linux相关概念和重要知识点(5)(权限的修改、时间属性)

1.权限的修改 &#xff08;1&#xff09;提权行为 普通用户是会受到权限的限制&#xff0c;但root账户无视一切权限限制&#xff0c;因此当我们要获取更高的权限&#xff0c;有一种办法就是将自己变成root或者短暂拥有和root一样的权力。 普通用户 -> root &#xff1a;s…

人工智能有助于解决 IT/OT 集成安全挑战

思科的一项研究表明&#xff0c;信息技术 (IT) 和运营技术 (OT) 融合所带来的安全问题可以通过人工智能 (AI) 解决&#xff0c;尽管该技术也可能被恶意行为者利用。 该报告由思科和 Sapio Research 联合发布&#xff0c;对 17 个国家的 1,000 名行业专业人士进行了调查&#x…

硬件(驱动开发)

一、OSC基本架构&#xff08;片上系统&#xff09; OSC&#xff08;On-chip System Control&#xff0c;片上系统控制&#xff09;基本架构通常涉及片上系统中的各个组件如何进行协调与控制&#xff0c;以实现高效的处理、通信和管理。OSC架构在现代微处理器和系统单芯片&…

华为HarmonyOS地图服务 3 - 如何开启和展示“我的位置”?

一. 场景介绍 本章节将向您介绍如何开启和展示“我的位置”功能&#xff0c;“我的位置”指的是进入地图后点击“我的位置”显示当前位置点的功能。效果如下&#xff1a; 二. 接口说明 “我的位置”功能主要由MapComponentController的方法实现&#xff0c;更多接口及使用方法…

rocky Linux 9.4系统配置zabbix监控MySQL主从复制状态与配置钉钉告警

MySQL主从复制原理&#xff1a; 1. 主从复制的基本概念 主服务器&#xff08;Master&#xff09;&#xff1a;负责处理所有的写操作&#xff08;INSERT、UPDATE、DELETE&#xff09;&#xff0c;并将这些操作记录到二进制日志&#xff08;binary log&#xff09;中。 从服务器…

计算机网络(月考一知识点)

文章目录 计算机网络背诵默写版计算机网络知识点&#xff08;月考1版&#xff09; 计算机网络背诵默写版 为我自己留个印记&#xff0c;本来荧光笔画的是没记住的&#xff0c;但是后面用紫色的&#xff0c;结果扫描的时候就看不见了。 计算机网络知识点&#xff08;月考1版&a…

静态链表:实现、操作与性能优势【算法 16】

静态链表&#xff1a;实现、操作与性能优势 在算法和数据结构的探索中&#xff0c;链表作为一种基础且灵活的数据结构&#xff0c;广泛应用于各种场景。然而&#xff0c;在算法竞赛或需要高效内存管理的环境中&#xff0c;传统的动态链表可能会因为内存分配和释放的开销而影响性…

【H2O2|全栈】关于CSS(5)如何制作一个搜索网页的首页?

目录 CSS基础知识 前言 准备工作 简单网页的组成部分 案例 浏览器的窗口大小 划分主要部分 固定定位 头部导航&#xff08;左侧&#xff09; 头部导航&#xff08;右侧&#xff09; LOGO ​编辑搜索框 热搜标题 热搜内容 文字简介 资源 预告和回顾 后话 CSS…

Tomcat中BIO和NIO的区别(Tomcat)

BIO Tomcat中BIO的模型和理论很简单&#xff0c;例图如下 1.Acceptor线程死循环阻塞接收客户端的打过来的socket请求 2.接收到请求之后打包成一个SocketProcessor&#xff08;Runnable&#xff09;&#xff0c;扔到线程池中读取/写入数据 参数配置 1.Acceptor默认线程是1&#…

网络丢包定位记录(二)

网卡驱动丢包 查看&#xff1a;ifconfig eth1/eth0 等接口 1.RX errors: 表示总的收包的错误数量&#xff0c;还包括too-long-frames错误&#xff0c;Ring Buffer 溢出错误&#xff0c;crc 校验错误&#xff0c;帧同步错误&#xff0c;fifo overruns 以及 missed pkg 等等。 …