作者: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(1, 2);
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 知识点分析
准确来说,它是inline是一个C++关键字,在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。但是由于市面上的大部分C编译器都可以兼容部分C++的关键字和语法,所以我们也经常见到inline出现在C代码中。
1、宏定义发生在预编译处理阶段,它仅仅是做字符串的替换,没有任何的语法规则检查,比如类型不匹配,宏展开后的各种语法问题,的确让人比较头疼;
2、inline函数则是发生在编译阶段,有完整的语法检查,在Debug版本中也可以跟普通函数一样,正常打断点进行调试;
3、由于处理的阶段不一样,这就导致如果宏函数展开后仍然是一个函数调用的话,它是具有调用函数的开销,包括函数进栈出栈等等;而inline函数却仅仅是函数代码的拷贝替换,并不会发生函数调用的开销,在这一点上inline具有很高的执行效率。
正如上面提及的,普通函数的调用在汇编上有标准的 push 压实参指令,然后 call 指令调用函数,给函数开辟栈帧,函数运行完成,有函数退出栈帧的过程;而 inline 内联函数是在编译阶段,在函数的调用点将函数的代码展开,省略了函数栈帧开辟回退的调用开销,效率高。
两者唯一的区别在于可见范围不一样:
不被static关键字修饰的函数,它在整个工程范围内,全局都可以调用,即其属性是global的;只要函数参与了编译,且最后链接的时候把函数的.o文件链接进去了,是不会报undefined reference to ‘xxx’的;
被static关键字修饰的函数,只能在其定义的C文件内可见,即其属性由global变成了local,这个时候如果有另一个C文件的函数想调用这个static的函数,那么对不起,最终链接阶段会报undefined reference to ‘xxx’错误的。
4 解决方案
回到前文的问题,该如何解决这个问题呢?我的想法,有两种解决思路:
这个方法很简单,无非就是去掉inline,做个姜维处理,把inline函数变成普通函数,自然编译链接就不会报错。但我想,既然写代码的原作者加了inline,肯定是希望用上inline的高效率的特性,所以去掉inline显然不是一个明智的选择。
这一个做法,就可以很聪明地把它的问题给解决了。一个函数被static和inline修饰,证明这个函数是一个静态的内联函数,它的可见范围依然是当前C文件,且同时具备inline函数的特性。
5 知其然且知其所以然
为了验证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(1, 2);
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程序的完整过程这个话题,后面有时间,我再整理整理,分享出来,毕竟这个知识点,对于解决编译问题可是帮助非常大的。
为了做对比,我把整个编译执行了两次,一次是加上static的,一次是不加static的;
对比结果如下,使用的是linux下的diff命令
1diff ./build/applications/main.i.nostatic ./build/applications/main.i.static
24516c4516
3< inline void test_func(int a, int b)
4---
5> static inline void test_func(int a, int b)
6
结果我们发现如我们期望一样,nostatic的进比static的少了一个static修饰符,其他都是一样的。
.s文件使用文本对比工具,发现加了static的.s文件,里面有test_func的汇编实现代码,而不加的这个函数直接就被优化掉了,压根就找不到它的实现。
由于.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' 错误,因为压根就没有外部的谁去实现这个函数。
为了验证好这几个关键字的区别,以及为何加了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(1, 2); // normal
44 test_func2(1, 2); // static
45 test_func3(1, 2); // static inline (real inline ?)
46 test_func4(1, 2); // static inline (real inline ?)
47
48 return 0;
49}
执行编译
1gcc main.c -save-temps=obj -Wall -o test_static -Wl,-Map=test_static.map
成功编译,运行也完全没有问题。
1./test_static
2Hello world !
31, 2
41, 2
51, 2
61, 2
通过上面的章节,我们可以知道,我们应该重点分析.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 7, 8
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 7, 8
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 7, 8
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 7, 8
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 7, 8
哗,果然,这才是真正的内联,我们终于揭开了这个神秘的面纱。
⚪ 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官方微信交流群!
👇 阅读原文报名开发者大会