11.1 什么是内建函数

内建函数,顾名思义,就是编译器内部实现的函数。这些函数跟关键字一样,可以直接使用,无须像标准库函数那样,要 #include 对应的头文件才能使用。

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

  • 用来处理变长参数列表;
  • 用来处理程序运行异常;
  • 程序的编译优化、性能优化;
  • 查看函数运行中的底层信息、堆栈信息等;
  • C 标准库函数的内建版本。

因为内建函数是编译器内部定义,主要由编译器相关的工具和程序调用,所以这些函数并没有文档说明,而且变动而频繁。对于程序开发者来说,不建议使用这些函数。

但有些函数,对于我们了解程序运行的底层信息、编译优化很有帮助,而且在 Linux 内核中也经常使用这些函数,所以还是很有必要去了解 Linux 内核中常用的一些内建函数。

11.2 常用内建函数

__builtin_return_address(LEVEL)

这个函数用来返回当前函数或调用者的返回地址。函数的参数 LEVEl 表示函数调用链中的不同层次的函数,各个值代表的意义如下。

  • 0:返回当前函数的返回地址;
  • 1:返回当前函数调用者的返回地址;
  • 2:返回当前函数调用者的调用者的返回地址;
  • ……

我们接下来写一个测试程序。

void f(void)
{
int *p;
p = __builtin_return_address(0);
printf("f return address: %p\n",p);
p = __builtin_return_address(1);;
printf("func return address: %p\n",p);
p = __builtin_return_address(2);;
printf("main return address: %p\n",p);
printf("\n");
}
void func(void)
{
int *p;
p = __builtin_return_address(0);
printf("func return address: %p\n",p);
p = __builtin_return_address(1);;
printf("main return address: %p\n",p);
printf("\n");
f();
}

int main(void)
{
int *p;
p = __builtin_return_address(0);
printf("main return address: %p\n",p);
printf("\n");
func();
printf("goodbye!\n");
return 0;
}

C 语言函数在调用过程中,会将当前函数的返回地址、寄存器等现场信息保存在堆栈中,然后才会跳到被调用函数中去执行。当被调用函数执行结束后,根据保存在堆栈中的返回地址,就可以直接返回到原来的函数中继续执行。

在这个程序中,main() 函数调用 func() 函数,在 main() 函数跳转到 func() 函数执行之前,会将程序正在运行的当前语句的下一条语句(如下代码所示)的地址保存到堆栈中,然后才去执行 func(); 这条语句,跳到 func() 函数去执行。func() 执行完毕后,如何返回到 main() 函数呢?很简单,将保存到堆栈中的返回地址赋值给 PC 指针,就可以直接返回到 main() 函数,继续往下执行了。

printf("goodbye!\n");

每一层函数调用,都会将当前函数的下一条指令地址,即返回地址压入堆栈保存。各层函数调用就构成 了一个函数调用链。在各层函数内部,我们使用内建函数就可以打印这个调用链上各个函数的返回地址。程序的运行结果如下。

main return address:0040124B

func return address:004013C3
main return address:0040124B

f return address:00401385
func return address:004013C3
main return address:0040124B

__builtin_frame_address(LEVEL)

在函数调用过程中,还有一个“栈帧”的概念。函数每调用一次,都会将当前函数的现场(返回地址、寄存器等)保存在栈中,每一层函数调用都会将各自的现场信息都保存在各自的栈中。这个栈也就是当前函数的栈帧,每一个栈帧有起始地址和结束地址,表示当前函数的堆栈信息。多层函数调用就会有多个栈帧,每个栈帧里会保存上一层栈帧的起始地址,这样各个栈帧就形成了一个调用链。很多调试器、GDB、包括我们的这个内建函数,其实都是通过回溯函数栈帧调用链来获取函数底层的各种信息的。比如,返回地址 i、调用关系等。在 ARM 系统中,使用 FP 和 SP 这两个寄存器,分别指向当前函数栈帧的起始地址和结束地址。当函数继续调用或者返回,这两个寄存器的值也会发生变化,总是指向当前函数栈帧的起始地址和结束地址。

我们可以通过内建函数 __builtinframeaddress(LEVEL),查看函数的栈帧地址。

  • 0:查看当前函数的栈帧地址
  • 1:查看当前函数调用者的栈帧地址
  • ……

写一个程序,打印当前函数的栈帧地址。

void func(void)
{
int *p;
p = __builtin_frame_address(0);
printf("func frame:%p\n",p);
p = __builtin_frame_address(1);
printf("main frame:%p\n",p);
}

int main(void)
{
int *p;
p = __builtin_frame_address(0);
printf("main frame:%p\n",p);
printf("\n");
func();
return 0;
}

程序运行结果如下。

main frame:0028FF48

func frame:0028FF28
main frame:0028FF48

11.3 C 标准库的内建函数

在 GNU C 编译器内部,实现了一些和 C 标准库函数类似的内建函数。这些函数跟 C 标准库函数功能相似,函数名也相同,只是在前面加了一个前缀 __builtin。如果你不想使用 C 库函数,也可以加个前缀,直接使用对应的内建函数。

常见的标准库函数如下:

  • 内存相关的函数:memcpy 、memset、memcmp
  • 数学函数:log、cos、abs、exp
  • 字符串处理函数:strcat、strcmp、strcpy、strlen
  • 打印函数:printf、scanf、putchar、puts

接下来我们写个小程序,使用与 C 标准库对应的内建函数。

int main(void)
{
char a[100];
__builtin_memcpy(a,"hello world!",20);
__builtin_puts(a);

return 0;
}

程序运行结果如下。

hello world!

通过运行结果我们看到,使用与 C 标准库对应的内建函数,同样也能实现字符串的复制和打印,实现 C 标准库函数的功能。

11.4 内建函数:__builtin_constant_p(n)

编译器内部还有一些内建函数,主要用来编译优化、性能优化,如 __builtinconstantp(n) 函数。该函数主要用来判断参数 n 在编译时是否为常量,是常量的话,函数返回1;否则函数返回0。该函数常用于宏定义中,用于编译优化。一个宏定义,根据宏的参数是常量还是变量,可能实现的方法不一样。在内核中经常看到这样的宏。

#define _dma_cache_sync(addr, sz, dir)        \
do { \
if (__builtin_constant_p(dir)) \
__inline_dma_cache_sync(addr, sz, dir); \
else \
__arc_dma_cache_sync(addr, sz, dir); \
} \
while (0);

很多计算或者操作在参数为常数时可能有更优化的实现,在这个宏定义中,我们实现了两个版本。根据参数是否为常数,我们可以灵活选用不同的版本。

11.5 内建函数:__builtin_expect(exp,c)

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

参数 c 跟这个函数的返回值无关,无论 c 为何值,函数的返回值都是 exp。

int main(void)
{
int a;
a = __builtin_expect(3,1);
printf("a = %d\n",a);

a = __builtin_expect(3,10);
printf("a = %d\n",a);

a = __builtin_expect(3,100);
printf("a = %d\n",a);
return 0;
}

程序运行结果如下。

a = 3
a = 3
a = 3

这个函数的主要用途就是编译器的分支预测优化。现代 CPU 内部,都有 cache 这个缓存器件。CPU 的运行速度很高,而外部 RAM 的速度相对来说就低了不少,所以当 CPU 从内存 RAM 读写数据时就会有一定的性能瓶颈。为了提高程序执行效率,CPU 都会通过 cache 这个 CPU 内部缓冲区来缓存一定的指令或数据。CPU 读写内存 RAM 中的数据时,会先到 cache 里面去看看能不能找到。找到的话就直接进行读写;找不到的话,cache 会重新缓存一部分内存数据进来。CPU 读写 cache 的速度远远大于内存 RAM,所以通过这种方式,可以提高系统的性能。

那 cache 如何缓存内存数据呢?简单来说,就是依据空间相近原则。比如 CPU 正在执行一条指令,那么下一个指令周期,CPU 就会大概率执行当前指令的下一条指令。如果此时 cache 将下面几条指令都缓存到 cache 里面,下一个指令周期 CPU 就可以直接到 cache 里取指、翻译、执行,从而使运算效率大大提高。

但有时候也会出现意外。比如程序在执行过程中遇到函数调用、if 分支、goto 跳转等程序结构,会跳到其它地址执行,那么缓存到 cache 中的指令就不是 CPU 要获取的指令。此时,我们就说 cache 没有命中,cache 会重新缓存正确的指令代码给 CPU 读取,这就是 cache 工作的基本流程。

有了这个理论基础,我们在编写程序时,遇到 if/switch 这种选择分支的程序结构,可以将大概率发生的分支写在前面,这样程序运行时,因为大概率发生,所以大部分时间就不需要跳转,程序就相当于一个顺序结构,从而提高 cache 的命中率。内核中已经实现一些相关的宏,如 likely 和 unlikely,用来提醒程序员优化程序。

11.6 内核中的 likely 和 unlikely

Linux 内核中,使用 __builtin_expect 内建函数,定义了两个宏。

#define likely(x) __builtin_expect(!!(x),1)
#define unlikely(x) __builtin_expect(!!(x),0)

这两个宏的主要作用,就是告诉编译器:某一个分支发生的概率很高,或者说很低,基本不可能发生。编译器就根据这个提示信息,就会去做一些分值预测的编译优化。在这两个宏定义有一个细节,就是对宏的参数 x 做两次取非操作,这是为了将参数 x 转换为布尔类型,然后与 1 和 0 作比较,告诉编译器 x 为真或为假的可能性很高。

我们接下来举个例子,让大家感受下,使用这两个宏后,编译器在分支预测上的一些编译变化。

//expect.c
int main(void)
{
int a;
scanf("%d",&a);
if( a==)
{
printf("%d",);
printf("%d",);
printf("\n");
}
else
{
printf("%d",);
printf("%d",);
printf("\n");
}
return ;
}

在这个程序中,根据我们输入变量 a 的值,程序会执行不同的分支代码。我们接着对这个程序反汇编,生成对应的汇编代码。

$ arm-linux-gnueabi-gcc  expect.c
$ arm-linux-gnueabi-objdump -D a.out
<main>:
: e92d4800 push {fp, lr}
1055c: e28db004 add fp, sp, #
: e24dd008 sub sp, sp, #
: e59f308c ldr r3, [pc, #]
: e5933000 ldr r3, [r3]
1056c: e50b3008 str r3, [fp, #-]
: e24b300c sub r3, fp, #
: e1a01003 mov r1, r3
: e59f007c ldr r0, [pc, #]
1057c: ebffffa5 bl <__isoc99_scanf@plt>
: e51b300c ldr r3, [fp, #-]
: e3530000 cmp r3, #
: 1a000008 bne 105b0 <main+0x58>
1058c: e3a01001 mov r1, #
: e59f0068 ldr r0, [pc, #]
: ebffff90 bl 103dc <printf@plt>
: e3a01002 mov r1, #
1059c: e59f005c ldr r0, [pc, #]
105a0: ebffff8d bl 103dc <printf@plt>
105a4: e3a0000a mov r0, #
105a8: ebffff97 bl 1040c <putchar@plt>
105ac: ea000007 b 105d0 <main+0x78>
105b0: e3a01005 mov r1, #
105b4: e59f0044 ldr r0, [pc, #]
105b8: ebffff87 bl 103dc <printf@plt>
105bc: e3a01006 mov r1, #
105c0: e59f0038 ldr r0, [pc, #]
105c4: ebffff84 bl 103dc <printf@plt>

观察 main 函数的反汇编代码,我们看到:汇编代码的结构就是基于我们的 if/else 分支先后顺序,依次生成对应的汇编代码(看 10588:bne 105b0 跳转)。我们接着改一下代码,使用 unlikely 修饰 if 分支,告诉编译器,这个 if 分支小概率发生,或者说不可能发生。

//expect.c
int main(void)
{
int a;
scanf("%d",&a);
if( unlikely(a==) )
{
printf("%d",);
printf("%d",);
printf("\n");
}
else
{
printf("%d",);
printf("%d",);
printf("\n");
}
return ;
}

对这个程序添加 -O2 优化参数编译,并对生成的可执行文件 a.out 反汇编。

$ arm-linux-gnueabi-gcc -O2 expect.c
$ arm-linux-gnueabi-objdump -D a.out
  <main>:
: e92d4010 push {r4, lr}
1043c: e59f4080 ldr r4, [pc, #]
: e24dd008 sub sp, sp, #
: e5943000 ldr r3, [r4]
: e1a0100d mov r1, sp
1044c: e59f0074 ldr r0, [pc, #]
: e58d3004 str r3, [sp, #]
: ebfffff1 bl <__isoc99_scanf@plt>
: e59d3000 ldr r3, [sp]
1045c: e3530000 cmp r3, #
: 0a000010 beq 104a8 <main+0x70>
: e3a02005 mov r2, #
: e59f105c ldr r1, [pc, #]
1046c: e3a00001 mov r0, #
: ebffffe7 bl <__printf_chk@plt>
: e3a02006 mov r2, #
: e59f104c ldr r1, [pc, #]
1047c: e3a00001 mov r0, #
: ebffffe3 bl <__printf_chk@plt>
: e3a0000a mov r0, #
: ebffffde bl <putchar@plt>
1048c: e59d2004 ldr r2, [sp, #]
: e5943000 ldr r3, [r4]
: e3a00000 mov r0, #
: e1520003 cmp r2, r3
1049c: 1a000007 bne 104c0 <main+0x88>
104a0: e28dd008 add sp, sp, #
104a4: e8bd8010 pop {r4, pc}
104a8: e3a02001 mov r2, #
104ac: e59f1018 ldr r1, [pc, #]
104b0: e1a00002 mov r0, r2
104b4: ebffffd6 bl <__printf_chk@plt>
104b8: e3a02002 mov r2, #
104bc: eaffffed b <main+0x40>

我们对 if 分支条件表达式使用 unlikely 修饰,告诉编译器这个分支小概率发生。在编译器开启优化编译条件下,通过生成的反汇编代码(10460:beq 104a8),我们可以看到,编译器将小概率发生的 if 分支汇编代码放在了后面,将 else 分支的汇编代码放在了前面,这样就确保了程序在执行时,大部分时间都不需要跳转,直接按顺序执行下面大概率发生的分支代码。

在 Linux 内核中,你会发现很多地方使用 likely 和 unlikely 宏修饰,此时你应该知道它们的用途了吧。

嵌入式C语言自我修养 11:有一种函数,叫内建函数的更多相关文章

  1. 嵌入式C语言自我修养 10:内联函数探究

    10.1 属性声明:noinline & always_inline 这一节,接着讲 __atttribute__ 属性声明,__atttribute__ 可以说是 GNU C 最大的特色.我 ...

  2. 嵌入式C语言自我修养 13:C语言习题测试

    13.1 总结 前面12节的课程,主要针对 Linux 内核中 GNU C 扩展的一些常用 C 语言语法进行了分析.GNU C 的这些扩展语法,主要用来完善 C 语言标准和编译优化.而通过 C 标准的 ...

  3. 嵌入式C语言自我修养 06:U-boot镜像自拷贝分析:section属性

    6.1 GNU C 的扩展关键字:attribute GNU C 增加一个 __atttribute__ 关键字用来声明一个函数.变量或类型的特殊属性.声明这个特殊属性有什么用呢?主要用途就是指导编译 ...

  4. 嵌入式C语言自我修养 04:Linux 内核第一宏:container_of

    4.1 typeof 关键字 ANSI C 定义了 sizeof 关键字,用来获取一个变量或数据类型在内存中所占的存储字节数.GNU C 扩展了一个关键字 typeof,用来获取一个变量或表达式的类型 ...

  5. 嵌入式C语言自我修养 03:宏构造利器:语句表达式

    3.1 基础复习:表达式.语句和代码块 表达式 表达式和语句是 C 语言中的基础概念.什么是表达式呢?表达式就是由一系列操作符和操作数构成的式子.操作符可以是 C 语言标准规定的各种算术运算符.逻辑运 ...

  6. 嵌入式C语言自我修养 12:有一种宏,叫可变参数宏

    12.1 什么是可变参数宏 在上面的教程中,我们学会了变参函数的定义和使用,基本套路就是使用 va_list.va_start.va_end 等宏,去解析那些可变参数列表我们找到这些参数的存储地址后, ...

  7. 嵌入式C语言自我修养 01:Linux 内核中的GNU C语言语法扩展

    1.1 Linux 内核驱动中的奇怪语法 大家在看一些 GNU 开源软件,或者阅读 Linux 内核.驱动源码时会发现,在 Linux 内核源码中,有大量的 C 程序看起来“怪怪的”.说它是C语言吧, ...

  8. 嵌入式C语言自我修养 02:Linux 内核驱动中的指定初始化

    2.1 什么是指定初始化 在标准 C 中,当我们定义并初始化一个数组时,常用方法如下: ] = {,,,,,,,,}; 按照这种固定的顺序,我们可以依次给 a[0] 和 a[8] 赋值.因为没有对 a ...

  9. 嵌入式C语言自我修养 05:零长度数组

    5.1 什么是零长度数组 顾名思义,零长度数组就是长度为0的数组. ANSI C 标准规定:定义一个数组时,数组的长度必须是一个常数,即数组的长度在编译的时候是确定的.在ANSI C 中定义一个数组的 ...

随机推荐

  1. SSH 无法启动的原因分析及解决方法

    简介 Secure Shell(缩写为 SSH),由 IETF 的网络工作小组(Network Working Group)所制定:SSH 为一项创建在应用层和传输层基础上的安全协议,为计算机上的 S ...

  2. Mysql进阶-day2

    Mysql cmake方式安装 1.卸载原有mysql下载MySQL安装包 [root@localhost ~]# yum remove mysql* -y [root@localhost ~]# m ...

  3. July 28th 2017 Week 30th Friday

    If equal affection cannot be, let the more loving be me. 如果没有相等的爱,那就让我爱多一点吧. There is seldom equal a ...

  4. Chapter 5 Order Inversion Pattern

    5.1 Introdution The main focus of this chapter is to discuss the order inversion (OI) pattern, which ...

  5. Linux基础入门 - 1

    第一节 Linux简介 Linux 就是一个操作系统,也就是系统调用和内核那两层,Linux 本身只是操作系统的内核(内核是使其他程序能够运行的基础.它实现了多任务和硬件管理,用户或者系统管理员交互运 ...

  6. 【RabbitMQ】3、工作队列模式(work模式)

    上一篇博客的作为rabbitMQ的入门程序,也是简单队列模式,一个生产者,一个消费者,今天这篇博客介绍work模式,一个生产者,多个消费者,下面的例子模拟两个消费者的情况. 图示:         一 ...

  7. CString char BSTR 转换

     关于字符集不一的历史原因,可以参考: UNICODE与ANSI的区别 以下是网上转载的资料.我将辅以自己的实例,说明并总结关系. 一.CString, int, string, char*之间的转换 ...

  8. HTML5旋转立方体

    http://42.121.104.41/templets/default/test1.htm 须要源代码的留言邮箱哈~

  9. 「NOIP2018 保卫王国」

    题目 强制选点我们可以把那个点权搞成\(-inf\),强制不选我们搞成\(inf\),之后就真的成为动态\(dp\)的板子题了 由于不想像板子那样再写一个最大独立集的方程,之后利用最小点覆盖=总点权- ...

  10. Django实战(一)之简单Demo

    菜鸟教程上Django安装可供参考: 参考链接: http://www.runoob.com/django/django-install.html 菜鸟教程上如果不行的话,下面博客网址可以供参考 Li ...