【gcc编译优化系列】static与inline的区别与联系


作者:RT-Thread社区成员recan



1 问题来源



今天偶然留意到RT-Thread论坛的一个问题帖子,它的题目是RTT-VSCODE插件编译RTT工程与RTT Studio结果不符,这种编译问题是我最喜欢深扒的,于是我点进去了看看了。

得知,它的核心问题就是有一个类似这样定义的函数(为了简要说明问题,我精简了代码):

 1/* main.c */
2
3inline void test_func(int a, int b)
4
{
5    printf("%d, %d\n", a, b);
6}
7
8int main(int argc, const char *argv[])
9
{
10    /* do something */
11
12    /* call func */
13    test_func(12);
14
15    return 0;
16}


然后,问题就是 同一套工程代码在RT-Thread Studio上能够编译通过,但在VSCODE上却产生错误,这个错误居然是undefined reference to ‘test_func’


2 问题分析



看到undefined reference to ‘test_func’这个错误,熟悉C代码编译的都知道,这是一个典型的链接错误,也就是说错误发在链接阶段,链接错误的原因是找不到test_func函数的实现体

相信你一定也有许多问号?

test_func不是定义在main.c里面吗?

不就在main函数的上面吗?

怎么可能会发生链接错误呢?

我们平时写函数不就是这样写的吗?

难道这个inline作妖?


3 知识点分析



3.1 inline关键字是干嘛的?

准确来说,它是inline是一个C++关键字,在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。但是由于市面上的大部分C编译器都可以兼容部分C++的关键字和语法,所以我们也经常见到inline出现在C代码中。

3.2 inline与宏定义有什么区别?

1、宏定义发生在预编译处理阶段,它仅仅是做字符串的替换,没有任何的语法规则检查,比如类型不匹配,宏展开后的各种语法问题,的确让人比较头疼;

2、inline函数则是发生在编译阶段,有完整的语法检查,在Debug版本中也可以跟普通函数一样,正常打断点进行调试;

3、由于处理的阶段不一样,这就导致如果宏函数展开后仍然是一个函数调用的话,它是具有调用函数的开销,包括函数进栈出栈等等;而inline函数却仅仅是函数代码的拷贝替换,并不会发生函数调用的开销,在这一点上inline具有很高的执行效率。

3.3 inline函数与普通函数有什么区别?

正如上面提及的,普通函数的调用在汇编上有标准的 push 压实参指令,然后 call 指令调用函数,给函数开辟栈帧,函数运行完成,有函数退出栈帧的过程;而 inline 内联函数是在编译阶段,在函数的调用点将函数的代码展开,省略了函数栈帧开辟回退的调用开销,效率高。

3.4 static函数与普通函数有什么区别?

两者唯一的区别在于可见范围不一样:

  1. 不被static关键字修饰的函数,它在整个工程范围内,全局都可以调用,即其属性是global的;只要函数参与了编译,且最后链接的时候把函数的.o文件链接进去了,是不会报undefined reference to ‘xxx’的;

  2. 被static关键字修饰的函数,只能在其定义的C文件内可见,即其属性由global变成了local,这个时候如果有另一个C文件的函数想调用这个static的函数,那么对不起,最终链接阶段会报undefined reference to ‘xxx’错误的。


4 解决方案



回到前文的问题,该如何解决这个问题呢?我的想法,有两种解决思路:

4.1 放弃inline函数的优势,将inline函数修改为普通函数

这个方法很简单,无非就是去掉inline,做个姜维处理,把inline函数变成普通函数,自然编译链接就不会报错。但我想,既然写代码的原作者加了inline,肯定是希望用上inline的高效率的特性,所以去掉inline显然不是一个明智的选择。

4.2 对inline函数加上static修饰

这一个做法,就可以很聪明地把它的问题给解决了。一个函数被static和inline修饰,证明这个函数是一个静态的内联函数,它的可见范围依然是当前C文件,且同时具备inline函数的特性。


5 知其然且知其所以然



5.1 实践出真理

为了验证4.2的改法是否有效, 我在rt-thread/bsp/qemu-vexpress-a9中快速做个验证,只需要在applications/main.c里面添加上面的测试代码:

 1/* applications/main.c */
2static inline void test_func(int a, int b)
3
{
4  printf("%d, %d\n", a, b);
5}
6
7int main(void)
8
{
9    printf("hello rt-thread\n");
10
11    test_func(12);
12
13    return 0;
14}


特此说明下,我们的交叉编译链是:gcc-arm-none-eabi-5_4-2016q3/bin/arm-none-eabi-gcc

然后使用scons编译,果然编译成功了,运行rtthread.elf,功能一切正常。

而当我去掉static的时候,期望中的链接错误果然出现了。

1LINK rtthread.elf
2build/applications/main.o: In function `main':
3/home/recan/win_share_workspace/rt-thread-share/rt-thread/bsp/qemu-vexpress-a9/applications/main.c:253: undefined reference to `test_func'
4collect2: error: ld returned 1 exit status
5scons: *** [rtthread.elf] Error 1
6scons: building terminated because of errors.


为了做进一步验证,我在rtconfig.py里面的CFLAGS加了一个选项:-save-temps=obj;这个选项的作用就是在编译的过程中,把中间过程文件也同步输出,这里的中间文件有以下几个:

⚪ xxx.i 文件:这是预编译处理之后的文件,比如宏定义被展开之后是怎么样的,就可以看这个文件;

⚪ xxx.s 文件:这是由预编译处理后的xxx.i文件编译得到的汇编文件,里面描述的是汇编指令;

⚪ xxx.o 文件:这是最终对应单个C文件生成的目标文件,这个文件是最终参与链接成可执行文件的。

关于使用GCC编译C程序的完整过程这个话题,后面有时间,我再整理整理,分享出来,毕竟这个知识点,对于解决编译问题可是帮助非常大的。

5.2 实践结果分析

为了做对比,我把整个编译执行了两次,一次是加上static的,一次是不加static的;

5.2.1 .i文件对比

对比结果如下,使用的是linux下的diff命令

1diff ./build/applications/main.i.nostatic ./build/applications/main.i.static
24516c4516
3<             inline void test_func(int a, int b)
4---
5static inline void test_func(int a, int b) 
6


结果我们发现如我们期望一样,nostatic的进比static的少了一个static修饰符,其他都是一样的。

5.2.2 .s文件对比

.s文件使用文本对比工具,发现加了static的.s文件,里面有test_func的汇编实现代码,而不加的这个函数直接就被优化掉了,压根就找不到它的实现。

5.2.3 .o文件对比

由于.o文件已经不是可读的文本文件了,我们只能通过一些命令行工具来查看,这里推荐linux命令行下的nm工具,具体用途和方法可以使用man nm查看下。这里直接给出对比的命令行结果:

1nm -a ./build/applications/main.o.nostatic | grep test_func
2         U test_func
3
4nm -a ./build/applications/main.o.static | grep test_func  
5000002d8 t test_func 


OK,从中已经可以看到重要区别了:在不带static的版本中,main.c里定义的test_func函数被认为是一个外部函数(标识为U),而被static修饰的确实本地实现函数(标识为T)。
而标识为U的函数是需要外部去实现的,这也就解释了为何nostatic的版本会报undefined reference to 'test_func' 错误,因为压根就没有外部的谁去实现这个函数。

5.4 终极实验

5.4.1 补充测试代码

为了验证好这几个关键字的区别,以及为何加了inline为不内联,如何才能真正的内联,我补充了一下测试代码:

 1#include <stdio.h>
2
3#if 0
4/* only inline function : link error ! */
5inline void test_func(int a, int b)
6
{
7    printf("%d, %d\n", a, b);
8}
9#endif
10
11/* normal function: OK */
12void test_func1(int a, int b)
13
{
14    printf("%d, %d\n", a, b);
15}
16
17/* static function: OK */
18static void test_func2(int a, int b)
19
{
20    printf("%d, %d\n", a, b);
21}
22
23/* static inline function: OK, but no real inline */
24static inline void test_func3(int a, int b)
25
{
26    printf("%d, %d\n", a, b);
27}
28
29#define FORCE_FUNCTION  __attribute__((always_inline))
30
31/* static inline function: OK, it real inline. */
32FORCE_FUNCTION static inline void test_func4(int a, int b)
33
{
34    printf("%d, %d\n", a, b);
35}
36
37int main(int argc, const char *argv[])
38
{
39    printf("Hello world !\n");
40
41    /* call these functions with the same input praram */
42    //test_func(1, 2);
43    test_func1(12); // normal
44    test_func2(12); // static
45    test_func3(12); // static inline (real inline ?)
46    test_func4(12); // static inline (real inline ?)
47
48    return 0;
49}


5.4.2 编译验证

执行编译

1gcc main.c -save-temps=obj -Wall -o test_static -Wl,-Map=test_static.map


成功编译,运行也完全没有问题。


1./test_static 
2Hello world !
312
412
512
612


5.4.3 进阶分析

通过上面的章节,我们可以知道,我们应该重点分析.s文件和.o文件,因为.o文件不可读,我们用nm -a查看下:

1 nm -a test_static.o | grep test_func
20000000000000000 T test_func1
3000000000000002e t test_func2
4000000000000005c t test_func3


结果发现test_func4不在里面了,看样子是被真正inline了
我们打开.s文件确认下:


  1    .file   "main.c"
2    .text
3    .section    .rodata
4.LC0:
5    .string "%d, %d\n"
6    .text
7    .globl  test_func1
8    .type   test_func1, @function
9test_func1:
10.LFB0:
11    .cfi_startproc
12    endbr64
13    pushq   %rbp
14    .cfi_def_cfa_offset 16
15    .cfi_offset 6-16
16    movq    %rsp, %rbp
17    .cfi_def_cfa_register 6
18    subq    $16, %rsp
19    movl    %edi, -4(%rbp)
20    movl    %esi, -8(%rbp)
21    movl    -8(%rbp), %edx
22    movl    -4(%rbp), %eax
23    movl    %eax, %esi
24    leaq    .LC0(%rip), %rdi
25    movl    $0, %eax
26    call    printf@PLT
27    nop
28    leave
29    .cfi_def_cfa 78
30    ret
31    .cfi_endproc
32.LFE0:
33    .size   test_func1, .-test_func1
34    .type   test_func2, @function
35test_func2:
36.LFB1:
37    .cfi_startproc
38    endbr64
39    pushq   %rbp
40    .cfi_def_cfa_offset 16
41    .cfi_offset 6-16
42    movq    %rsp, %rbp
43    .cfi_def_cfa_register 6
44    subq    $16, %rsp
45    movl    %edi, -4(%rbp)
46    movl    %esi, -8(%rbp)
47    movl    -8(%rbp), %edx
48    movl    -4(%rbp), %eax
49    movl    %eax, %esi
50    leaq    .LC0(%rip), %rdi
51    movl    $0, %eax
52    call    printf@PLT
53    nop
54    leave
55    .cfi_def_cfa 78
56    ret
57    .cfi_endproc
58.LFE1:
59    .size   test_func2, .-test_func2
60    .type   test_func3, @function
61test_func3:
62.LFB2:
63    .cfi_startproc
64    pushq   %rbp
65    .cfi_def_cfa_offset 16
66    .cfi_offset 6-16
67    movq    %rsp, %rbp
68    .cfi_def_cfa_register 6
69    subq    $16, %rsp
70    movl    %edi, -4(%rbp)
71    movl    %esi, -8(%rbp)
72    movl    -8(%rbp), %edx
73    movl    -4(%rbp), %eax
74    movl    %eax, %esi
75    leaq    .LC0(%rip), %rdi
76    movl    $0, %eax
77    call    printf@PLT
78    nop
79    leave
80    .cfi_def_cfa 78
81    ret
82    .cfi_endproc
83.LFE2:
84    .size   test_func3, .-test_func3
85    .section    .rodata
86.LC1:
87    .string "Hello world !"
88    .text
89    .globl  main
90    .type   main, @function
91main:
92.LFB4:
93    .cfi_startproc
94    endbr64
95    pushq   %rbp
96    .cfi_def_cfa_offset 16
97    .cfi_offset 6-16
98    movq    %rsp, %rbp
99    .cfi_def_cfa_register 6
100    subq    $32, %rsp
101    movl    %edi, -20(%rbp)
102    movq    %rsi, -32(%rbp)
103    leaq    .LC1(%rip), %rdi
104    call    puts@PLT
105    movl    $2, %esi
106    movl    $1, %edi
107    call    test_func1
108    movl    $2, %esi
109    movl    $1, %edi
110    call    test_func2
111    movl    $2, %esi
112    movl    $1, %edi
113    call    test_func3
114    movl    $1-8(%rbp)
115    movl    $2-4(%rbp)
116    movl    -4(%rbp), %edx
117    movl    -8(%rbp), %eax
118    movl    %eax, %esi
119    leaq    .LC0(%rip), %rdi
120    movl    $0, %eax
121    call    printf@PLT
122    nop
123    movl    $0, %eax
124    leave
125    .cfi_def_cfa 78
126    ret
127    .cfi_endproc
128.LFE4:
129    .size   main, .-main
130    .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
131    .section    .note.GNU-stack,"",@progbits
132    .section    .note.gnu.property,"a"
133    .align 8
134    .long    1f - 0f
135    .long    4f - 1f
136    .long    5
1370:
138    .string  "GNU"
1391:
140    .align 8
141    .long    0xc0000002
142    .long    3f - 2f
1432:
144    .long    0x3
1453:
146    .align 8
1474:


从中,我们可以看到test_func1与test_func2的区别是test_func1是GLOBAL的,而test_func2是LOCAL的;而test_func2与test_func3却是完全一模一样;也就是说test_func3使用static inline压根就没有被内联


我们再找找test_func4,发现找不到了,到底是不是内联了?我们再看看main函数里面调用的部分:


 1main:
2.LFB4:
3    .cfi_startproc
4    endbr64
5    pushq   %rbp
6    .cfi_def_cfa_offset 16
7    .cfi_offset 6-16
8    movq    %rsp, %rbp
9    .cfi_def_cfa_register 6
10    subq    $32, %rsp
11    movl    %edi, -20(%rbp)
12    movq    %rsi, -32(%rbp)
13    leaq    .LC1(%rip), %rdi
14    call    puts@PLT
15
16    movl    $2, %esi
17    movl    $1, %edi
18    call    test_func1  //调用test_func1函数
19
20    movl    $2, %esi
21    movl    $1, %edi
22    call    test_func2  //调用test_func2函数
23
24    movl    $2, %esi
25    movl    $1, %edi
26    call    test_func3  //调用test_func3函数
27
28    movl    $1-8(%rbp)
29    movl    $2-4(%rbp)
30    movl    -4(%rbp), %edx
31    movl    -8(%rbp), %eax
32    movl    %eax, %esi
33    leaq    .LC0(%rip), %rdi
34    movl    $0, %eax
35    call    printf@PLT
36    nop
37    movl    $0, %eax
38    leave               //“调用”test_func4函数,使用了内联,直接拷贝了代码,并不是真的函数调用。
39
40
41    .cfi_def_cfa 78


哗,果然,这才是真正的内联,我们终于揭开了这个神秘的面纱。

5.4 实践经验总结

⚪ inline有利有弊,切记使用的时候,最好让它跟static一起使用,否则可能导致的问题超出你的想象。

⚪ 加了inline,不是你想内联,编译器就会一定会帮你内联,还得看代码的实现。

⚪ 如果要强制内联,还得加参数修饰,每个C编译器的方法还不一样,比如gcc的是使用__attribute__((always_inline))修饰定义的函数即可。


6 更多分享



本项目的所有测试代码和编译脚本,均可以在我的github仓库01workstation中找到,欢迎指正问题。

  


邀请你参加 2021 RT-Thread 开发者大会的七大理由

1、刷新RT-Thread最新技术动态和产业服务能力

2、聆听行业大咖分享,洞察产业趋势

4、丰富的技术和产品展示,前沿技术发展和应用

5、绝佳的实践机会:从MCU、AIOT、MPU、RISC-V、安全总有一个应用场景满足你

6、现场揭晓开发者专属纪念胸牌升级和新玩法

7、互动区体验掌握技术带来的魅力



立即长按识别下方二维码报名


   

你可以添加微信17775982065为好友,注明:公司+姓名,拉进RT-Thread官方微信交流群!



👇 阅读原文报名开发者大会

RTThread物联网操作系统 帮助您了解RT-Thread相关的资讯.
评论
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 108浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 115浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 195浏览
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 84浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 140浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 51浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 160浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 44浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 47浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 102浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦