1 問(wèn)題來(lái)源
今天偶然留意到RT-Thread論壇的一個(gè)問(wèn)題帖子,它的題目是RTT-VSCODE插件編譯RTT工程與RTT Studio結(jié)果不符,這種編譯問(wèn)題是我最喜歡深扒的,于是我點(diǎn)進(jìn)去看了看。
得知,它的核心問(wèn)題就是有一個(gè)類(lèi)似這樣定義的函數(shù)(為了簡(jiǎn)要說(shuō)明問(wèn)題,我精簡(jiǎn)了代碼):
/* main.c */
inline void test_func(int a, int b)
{
printf("%d, %d\n", a, b);
}
int main(int argc, const char *argv[])
{
/* do something */
/* call func */
test_func(1, 2);
return 0;
}
然后,問(wèn)題就是 同一套工程代碼在RT-Thread Studio上能夠編譯通過(guò),但在VSCODE上卻產(chǎn)生錯(cuò)誤,這個(gè)錯(cuò)誤居然是undefined reference to ‘test_func’。
2 問(wèn)題分析
看到undefined reference to ‘testfunc’這個(gè)錯(cuò)誤,熟悉C代碼編譯流程的都知道,這是一個(gè)典型的鏈接錯(cuò)誤,也就是說(shuō)錯(cuò)誤發(fā)在鏈接階段,鏈接錯(cuò)誤的原因是找不到testfunc函數(shù)的實(shí)現(xiàn)體。
相信你一定也有許多問(wèn)號(hào)??????
test_func不是定義在main.c里面嗎?????
不就在main函數(shù)的上面嗎??????
怎么可能會(huì)發(fā)生鏈接錯(cuò)誤呢??????
我們平時(shí)寫(xiě)函數(shù)不就是這樣寫(xiě)的嗎??????
難道這個(gè)inline作妖??????
3 知識(shí)點(diǎn)分析
3.1 inline關(guān)鍵字是干嘛的?
準(zhǔn)確來(lái)說(shuō),它這個(gè)inline是一個(gè)C++關(guān)鍵字,在函數(shù)聲明或定義中,函數(shù)返回類(lèi)型前加上關(guān)鍵字inline,即可以把函數(shù)指定為內(nèi)聯(lián)函數(shù)。但是由于市面上的大部分C編譯器都可以兼容部分C++的關(guān)鍵字和語(yǔ)法,所以我們也經(jīng)常見(jiàn)到inline出現(xiàn)在C代碼中。
3.2 inline與宏定義有什么區(qū)別?
- 宏定義發(fā)生在預(yù)編譯處理階段,它僅僅是做字符串的替換,沒(méi)有任何的語(yǔ)法規(guī)則檢查,比如類(lèi)型不匹配,宏展開(kāi)后的各種語(yǔ)法問(wèn)題,的確讓人比較頭疼;
- inline函數(shù)則是發(fā)生在編譯階段,有完整的語(yǔ)法檢查,在Debug版本中也可以跟普通函數(shù)一樣,正常打斷點(diǎn)進(jìn)行調(diào)試;
- 由于處理的階段不一樣,這就導(dǎo)致如果宏函數(shù)展開(kāi)后仍然是一個(gè)函數(shù)調(diào)用的話,它是具有調(diào)用函數(shù)的開(kāi)銷(xiāo),包括函數(shù)進(jìn)棧出棧等等;而inline函數(shù)卻僅僅是函數(shù)代碼的拷貝替換,并不會(huì)發(fā)生函數(shù)調(diào)用的開(kāi)銷(xiāo),在這一點(diǎn)上inline具有很高的執(zhí)行效率。
3.3 inline函數(shù)與普通函數(shù)有什么區(qū)別?
正如上面提及的,普通函數(shù)的調(diào)用在匯編上有標(biāo)準(zhǔn)的 push 壓實(shí)參指令,然后 call 指令調(diào)用函數(shù),給函數(shù)開(kāi)辟棧幀,函數(shù)運(yùn)行完成,有函數(shù)退出棧幀的過(guò)程;而 inline 內(nèi)聯(lián)函數(shù)是在編譯階段,在函數(shù)的調(diào)用點(diǎn)將函數(shù)的代碼展開(kāi),省略了函數(shù)棧幀開(kāi)辟回退的調(diào)用開(kāi)銷(xiāo),效率高。
3.4 static函數(shù)與普通函數(shù)有什么區(qū)別?
兩者唯一的區(qū)別在于可見(jiàn)范圍不一樣:
- 不被static關(guān)鍵字修飾的函數(shù),它在整個(gè)工程范圍內(nèi),全局都可以調(diào)用,即其屬性是global的;只要函數(shù)參與了編譯,且最后鏈接的時(shí)候把函數(shù)的.o文件鏈接進(jìn)去了,是不會(huì)報(bào)undefined reference to ‘xxx’的;
- 被static關(guān)鍵字修飾的函數(shù),只能在其定義的C文件內(nèi)可見(jiàn),即其屬性由global變成了local,這個(gè)時(shí)候如果有另一個(gè)C文件的函數(shù)想調(diào)用這個(gè)static的函數(shù),那么對(duì)不起,最終鏈接階段會(huì)報(bào)undefined reference to ‘xxx’錯(cuò)誤的。
4 解決方案
回到前文的問(wèn)題,該如何解決這個(gè)問(wèn)題呢?我的想法,有兩種解決思路:
4.1 放棄inline函數(shù)的優(yōu)勢(shì),將inline函數(shù)修改為普通函數(shù)
這個(gè)方法很簡(jiǎn)單,無(wú)非就是去掉inline,做個(gè)降維處理,把inline函數(shù)變成普通函數(shù),自然編譯鏈接就不會(huì)報(bào)錯(cuò)。但我想,既然寫(xiě)代碼的原作者加了inline,肯定是希望用上inline的高效率的特性,所以去掉inline顯然不是一個(gè)明智的選擇。
4.2 對(duì)inline函數(shù)加上static修飾
這一個(gè)做法,就可以很聰明地把它的問(wèn)題給解決了。一個(gè)函數(shù)被static和inline修飾,證明這個(gè)函數(shù)是一個(gè)靜態(tài)的內(nèi)聯(lián)函數(shù),它的可見(jiàn)范圍依然是當(dāng)前C文件,且同時(shí)具備inline函數(shù)的特性。
5 知其然且知其所以然
5.1 實(shí)踐出真理
為了驗(yàn)證4.2的改法是否有效, 我在rt-thread/bsp/qemu-vexpress-a9
中快速做個(gè)驗(yàn)證,只需要在applications/main.c里面添加下面的測(cè)試代碼:
/* applications/main.c */
static inline void test_func(int a, int b)
{
printf("%d, %d\n", a, b);
}
int main(void)
{
printf("hello rt-thread\n");
test_func(1, 2);
return 0;
}
特此說(shuō)明下,我使用的交叉編譯鏈?zhǔn)牵?strong>gcc-arm-none-eabi-5_4-2016q3/bin/arm-none-eabi-gcc
然后使用scons編譯,果然編譯成功了,運(yùn)行rtthread.elf,功能一切正常。
而當(dāng)我去掉static的時(shí)候,期望中的鏈接錯(cuò)誤果然出現(xiàn)了。
LINK rtthread.elf
build/applications/main.o: In function `main':
/home/recan/win_share_workspace/rt-thread-share/rt-thread/bsp/qemu-vexpress-a9/applications/main.c:253: undefined reference to `test_func'
collect2: error: ld returned 1 exit status
scons: *** [rtthread.elf] Error 1
scons: building terminated because of errors.
為了做進(jìn)一步驗(yàn)證,我在rtconfig.py里面的CFLAGS加了一個(gè)編譯選項(xiàng):-save-temps=obj;這個(gè)選項(xiàng)的作用就是在編譯的過(guò)程中,把中間過(guò)程文件也同步輸出,這里的中間文件有以下幾個(gè):
xxx.o 文件:這是最終對(duì)應(yīng)單個(gè)C文件生成的二進(jìn)制目標(biāo)文件,這個(gè)文件是最終參與鏈接成可執(zhí)行文件的。
xxx.s 文件:這是由預(yù)編譯處理后的xxx.i文件編譯得到的匯編文件,里面描述的是匯編指令;
xxx.i 文件:這是預(yù)編譯處理之后的文件,比如想宏定義被展開(kāi)之后是怎么樣的,就可以看這個(gè)文件;
關(guān)于使用GCC編譯C程序的完整過(guò)程這個(gè)話題,我已經(jīng)整理出來(lái)了,分享分享給大家,畢竟這個(gè)知識(shí)點(diǎn),對(duì)于解決編譯問(wèn)題可是幫助非常大的。
5.2 實(shí)踐結(jié)果分析
為了做對(duì)比,我把整個(gè)編譯執(zhí)行了兩次,一次是加上static的,一次是不加static的;
5.2.1 .i文件對(duì)比
對(duì)比結(jié)果如下,使用的是linux下的diff命令
diff ./build/applications/main.i.nostatic ./build/applications/main.i.static
4516c4516
< inline void test_func(int a, int b)
---
> static inline void test_func(int a, int b)
結(jié)果我們發(fā)現(xiàn)如我們期望一樣,nostatic的僅比static的少了一個(gè)static修飾符,其他都是一樣的。
5.2.2 .s文件對(duì)比
.s文件使用文本對(duì)比工具,發(fā)現(xiàn)加了static的.s文件,里面有test_func的匯編實(shí)現(xiàn)代碼,而不加的這個(gè)函數(shù)直接就被優(yōu)化掉了,壓根就找不到它的實(shí)現(xiàn)。
5.2.3 .o文件對(duì)比
由于.o文件已經(jīng)不是可讀的文本文件了,我們只能通過(guò)一些命令行工具來(lái)查看,這里推薦linux命令行下的nm工具,具體用途和方法可以使用man nm
查看下。這里直接給出對(duì)比的命令行結(jié)果:
nm -a ./build/applications/main.o.nostatic | grep test_func
U test_func
nm -a ./build/applications/main.o.static | grep test_func
000002d8 t test_func
OK,從中已經(jīng)可以看到重要區(qū)別了:在不帶static的版本中,main.c里定義的testfunc函數(shù)被認(rèn)為是一個(gè)外部函數(shù)(標(biāo)識(shí)為U),而被static修飾的卻是本地實(shí)現(xiàn)函數(shù)(標(biāo)識(shí)為T(mén))。 而標(biāo)識(shí)為U的函數(shù)是需要外部去實(shí)現(xiàn)的,這也就解釋了為何nostatic的版本會(huì)報(bào)undefined reference to 'testfunc' 錯(cuò)誤,因?yàn)閴焊蜎](méi)有外部的誰(shuí)去實(shí)現(xiàn)這個(gè)函數(shù)。
5.4 終極實(shí)驗(yàn)
5.4.1 補(bǔ)充測(cè)試代碼
為了驗(yàn)證好這幾個(gè)關(guān)鍵字的區(qū)別,以及為何加了inline還不內(nèi)聯(lián),如何才能真正的內(nèi)聯(lián),我補(bǔ)充了一下測(cè)試代碼:
#include
#if 0
/* only inline function : link error ! */
inline void test_func(int a, int b)
{
printf("%d, %d\n", a, b);
}
#endif
/* normal function: OK */
void test_func1(int a, int b)
{
printf("%d, %d\n", a, b);
}
/* static function: OK */
static void test_func2(int a, int b)
{
printf("%d, %d\n", a, b);
}
/* static inline function: OK, but no real inline */
static inline void test_func3(int a, int b)
{
printf("%d, %d\n", a, b);
}
/* always_inline is very important*/
#define FORCE_FUNCTION __attribute__((always_inline))
/* static inline function: OK, it real inline. */
FORCE_FUNCTION static inline void test_func4(int a, int b)
{
printf("%d, %d\n", a, b);
}
int main(int argc, const char *argv[])
{
printf("Hello world !\n");
/* call these functions with the same input praram */
//test_func(1, 2);
test_func1(1, 2); // normal
test_func2(1, 2); // static
test_func3(1, 2); // static inline (real inline ?)
test_func4(1, 2); // static inline (real inline ?)
return 0;
}
5.4.2 編譯驗(yàn)證
執(zhí)行編譯
gcc main.c -save-temps=obj -Wall -o test_static -Wl,-Map=test_static.map
成功編譯,運(yùn)行也完全沒(méi)有問(wèn)題。
./test_static
Hello world !
1, 2
1, 2
1, 2
1, 2
5.4.3 進(jìn)階分析
通過(guò)上面的章節(jié),我們可以知道,我們應(yīng)該重點(diǎn)分析.s文件和.o文件,因?yàn)?o文件不可讀,我們用nm-a
查看下:
nm -a test_static.o | grep test_func
0000000000000000 T test_func1
000000000000002e t test_func2
000000000000005c t test_func3
結(jié)果發(fā)現(xiàn)test_func4不在里面了,看樣子是被真正inline了? 我們打開(kāi).s文件確認(rèn)下:
.file "main.c"
.text
.section .rodata
.LC0:
.string "%d, %d\n"
.text
.globl test_func1
.type test_func1, @function
test_func1:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size test_func1, .-test_func1
.type test_func2, @function
test_func2:
.LFB1:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size test_func2, .-test_func2
.type test_func3, @function
test_func3:
.LFB2:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2:
.size test_func3, .-test_func3
.section .rodata
.LC1:
.string "Hello world !"
.text
.globl main
.type main, @function
main:
.LFB4:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
leaq .LC1(%rip), %rdi
call puts@PLT
movl $2, %esi
movl $1, %edi
call test_func1
movl $2, %esi
movl $1, %edi
call test_func2
movl $2, %esi
movl $1, %edi
call test_func3
movl $1, -8(%rbp)
movl $2, -4(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
nop
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE4:
.size main, .-main
.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
從中,我們可以看到testfunc1與testfunc2的區(qū)別是testfunc1是GLOBAL的,而testfunc2是LOCAL的;而testfunc2與testfunc3卻是完全一模一樣;也就是說(shuō)testfunc3使用static inline壓根就沒(méi)有被內(nèi)聯(lián)。 我們?cè)僬艺襱estfunc4,發(fā)現(xiàn)已經(jīng)找不到了,到底是不是內(nèi)聯(lián)了?我們?cè)倏纯磎ain函數(shù)里面調(diào)用的部分:
main:
.LFB4:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
leaq .LC1(%rip), %rdi
call puts@PLT
movl $2, %esi
movl $1, %edi
call test_func1 //調(diào)用test_func1函數(shù)
movl $2, %esi
movl $1, %edi
call test_func2 //調(diào)用test_func2函數(shù)
movl $2, %esi
movl $1, %edi
call test_func3 //調(diào)用test_func3函數(shù)
movl $1, -8(%rbp)
movl $2, -4(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
nop
movl $0, %eax
leave //“調(diào)用”test_func4函數(shù),使用了內(nèi)聯(lián),直接拷貝了代碼,并不是真的函數(shù)調(diào)用。
.cfi_def_cfa 7, 8
嘩,果然,這才是真正的內(nèi)聯(lián)啊,我們終于揭開(kāi)了這個(gè)神秘的面紗。
5.4 實(shí)踐經(jīng)驗(yàn)總結(jié)
- inline有利有弊,切記使用的時(shí)候,最好讓它跟static一起使用,否則可能導(dǎo)致的問(wèn)題超出你的想象。
- 加了inline,不是你想內(nèi)聯(lián),編譯器就一定會(huì)幫你內(nèi)聯(lián)的,還得看代碼的實(shí)現(xiàn)。
- 如果要強(qiáng)制內(nèi)聯(lián),還得加參數(shù)修飾,每個(gè)C編譯器的方法還不一樣,比如gcc的是使用_attribute((alwaysinline))修飾定義的函數(shù)即可。
6 更多分享
本項(xiàng)目的所有測(cè)試代碼和編譯腳本,均可以在我的github倉(cāng)庫(kù)01workstation中找到,歡迎指正問(wèn)題。
歡迎關(guān)注我的github倉(cāng)庫(kù)01workstation,日常分享一些開(kāi)發(fā)筆記和項(xiàng)目實(shí)戰(zhàn),歡迎指正問(wèn)題。
同時(shí)也非常歡迎關(guān)注我的CSDN主頁(yè)和專(zhuān)欄:
【CSDN主頁(yè):架構(gòu)師李肯】
【RT-Thread主頁(yè):架構(gòu)師李肯】
【C/C++語(yǔ)言編程專(zhuān)欄】
【GCC專(zhuān)欄】
【信息安全專(zhuān)欄】
【RT-Thread開(kāi)發(fā)筆記】
有問(wèn)題的話,可以跟我討論,知無(wú)不答,謝謝大家。
審核編輯:湯梓紅
-
GCC
+關(guān)注
關(guān)注
0文章
105瀏覽量
24822 -
static
+關(guān)注
關(guān)注
0文章
33瀏覽量
10356 -
inline
+關(guān)注
關(guān)注
0文章
4瀏覽量
1628
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論