在Linux内核的源码中,你会发现许多这样的"奇特"代码。它们看起来可能有点陌生,但它们实际上是C语言的一种扩展形式,这种扩展在C语言的标准教材中往往不会提及。这就是为什么你在阅读Linux驱动代码或内核源码时,可能会感到既熟悉又陌生。如
cpp
__attribute__((format(printf, 1, 2)))
int printk(const char *fmt, ...);
本文将深入了解Linux内核或GNU开源软件中常用的一些C语言特殊语法扩展。通过学习这些编译器扩展特性,你将能够扫除这些C语言扩展语法带给你的阅读障碍,让你在Linux内核的世界中更加游刃有余。
目录
[GNU C编译器扩展关键字:attribute](#GNU C编译器扩展关键字:attribute)
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语言标准初始化数组
cpp
int a[10]={0,1,2,3,4,5,6,7,8};
a[9]默认为0.
cpp
int b[100]={[10]=1,[30]=2};
通过数组元素索引,我们可以直接给指定的数组元素赋值。在GNU C中,通过数组元素索引,可以直接给指定的几个元素赋值。
给数组中某一个索引范围的数组元素初始化
cpp
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语句中。
cpp
#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中我们可以通过结构域来指定初始化某个成员。
cpp
#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这个结构体来注册我们开发的驱动,然后系统会以回调的方式来执行驱动实现的具体功能。
cpp
#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结构体中有很多函数,使用.指定可以减少代码量
cpp
#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_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
指定初始化的好处
当成百上千个文件都使用file_operations这个结构体类型来定
义变量并初始化时,如果采用C标准按照固定顺序赋值,当file_operations结构体类型发生变化时,如添加了一个成员、删除了一个成员、调整了成员顺序,那么使用该结构体类型定义变量的大量C文件都需要重新调整初始化顺序,牵一发而动全
身。我们通过指定初始化方式,就可以避免这个问题。无论file_operations结构体类型如何变化,添加成员也好、删除成员也好、调整成员顺序也好,都不会影响其他文件的使用。
宏构造"利器":语句表达式
表达式、语句和代码块
略
在宏定义中使用语句表达式
使用简单的宏定义做文本替换后会因为括号出现优先级的问题,所以需要进行调整,如linux中min和max的宏定义如下:
cpp
#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扩展的一个关键字。
cpp
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;
cpp
typeof(int*) y;
定义了一个类型为int*
的变量 y
,等价于int* y;
cpp
typeof (int) *y;
等价于int *y;
cpp
typeof(*x) y;
定义了一个变量 y
,其类型是 指针x
指向的类型。如果 x
是一个指针,y
将是 x
所指向类型的变量。
cpp
typeof(int) y[4];
等价于 int y[4];
cpp
typeof ( typeof(char *)[4] ) y;
等价于char *y[4];
cpp
typeof(int x[4]) y;
等价于int y[4];
Linux内核中的container_of宏
零长度数组
零长度数组、变长数组都是GNU C编译器支持的数组类型
什么是零长度数组
长度为0的数组
cpp
int a[0];
零长度数组有一个特点,就是不占用内存存储空间。零长度数组一般单独使用的机会很少,它常常作为结构 体的一个成员,构成一个变长结构体。
cpp
struct buffer{
int len;
int a[0];
}
使用变长数组实现buffer。
cpp
#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__关键字用来声明一个函数、变量或类型的特殊属性。
cpp
atttribute((ATTRIBUTE))
目前__attribute__支持十几种属性声明。
section,aligned,packed,format,weak,alias,noinline,always_inline...
aligned属性
aligned和packed用来显式指定一个变量的存储对齐方式。使用__atttribute__这个属性声明,可以告诉编译器,按照指定的边界对齐方式去给这个变量分配存储空间。
cpp
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字节对齐,结构体的整体对齐要按结构体所有成员中最大对齐字节数或其整数倍对齐。
cpp
struct data{
char a;
int b ;
short c ;
}
1+3+4+2+2(结构体)=12
结构体成员按不同的顺序排放,可能会导致结构体的整体长度不一样。
cpp
struct data{
char a;
short b ;
int c;
}
1+1(short 2字节对齐)+2+4=8
packed属性
packed属性则与之相反,一般用来减少地址对齐,指定变量或类型使用最可能小的地址对齐方式。
cpp
struct data{
char a;
short battribute ((packed));
int c_attribute_((packed));
}
1+2+4=7
在ARM芯片中,每一个控制器的寄存器地址空间一般都是连续存在的。如果考虑数据对齐,则结构体内就可能有空洞,就和实际连续的寄存器地址不一致。使用packed可以避免这个问题,结构体的每个成员都紧挨着,依次分配存储地址,这样就避免了各个成员因地址对齐而造成的内存空洞。
在Linux内核源码中,经常看到aligned和packed一起使用,对一个变量或类型同时使用aligned和packed属性声明。这样避免了结构体内各成员因地址对齐产生内存空洞,又指定了整个结构体的对齐方式。
cpp
struct data {
char a;
short b;
int c;
}_attribute_((packed,aligned(8)));
属性声明:section
可以使用__attribute__来声明一个section属性,section属性的主要作用是:在程序编译时,将一个函数或变量放到指定的段,放到指定的section中。
cpp
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属性,来指定变参函数的参数格式检查。
cpp
#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
时提供的参数与格式字符串不匹配,编译器将会发出警告。
cpp
void LOG(int num,char *fmt,...) __attribute__((format(printf,2,3)));
变参函数初体验
cpp
#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保存这个地址,并依次访问下一个地址,就可以直接打印传进来的各个实参值。
变参函数改进版
cpp
#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后解引用(取出一个整数)。
实现不同等级的日志打印
cpp
#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属性,用来给函数定义一个别名。
cpp
#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 属性强制编译器内联。
cpp
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:获取上二级函数的返回地址。
cpp
#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:查看上一级函数的栈帧地址。
cpp
#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
这两个宏的主要作用就是告诉编译器:某一个分支发生的概率很高,或者很低,基本不可能发生。
cpp
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标准的一个语法扩展。
cpp
#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后面的逗号删除掉,这样宏就可以正常使用了。