1 問題來源
今天偶然留意到RT-Thread論壇的一個問題帖子,它的題目是RTT-VSCODE插件編譯RTT工程與RTT Studio結果不符,這種編譯問題是我最喜歡深扒的,于是我點進去看了看。
得知,它的核心問題就是有一個類似這樣定義的函數(為了簡要說明問題,我精簡了代碼):
/* 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;
}
然后,問題就是 同一套工程代碼在RT-Thread Studio上能夠編譯通過,但在VSCODE上卻產生錯誤,這個錯誤居然是undefined reference to ‘test_func’。
2 問題分析
看到undefined reference to ‘testfunc’這個錯誤,熟悉C代碼編譯流程的都知道,這是一個典型的鏈接錯誤,也就是說錯誤發在鏈接階段,鏈接錯誤的原因是找不到testfunc函數的實現體。
相信你一定也有許多問號??????
test_func不是定義在main.c里面嗎?????
不就在main函數的上面嗎??????
怎么可能會發生鏈接錯誤呢??????
我們平時寫函數不就是這樣寫的嗎??????
難道這個inline作妖??????
3 知識點分析
3.1 inline關鍵字是干嘛的?
準確來說,它這個inline是一個C++關鍵字,在函數聲明或定義中,函數返回類型前加上關鍵字inline,即可以把函數指定為內聯函數。但是由于市面上的大部分C編譯器都可以兼容部分C++的關鍵字和語法,所以我們也經常見到inline出現在C代碼中。
3.2 inline與宏定義有什么區別?
- 宏定義發生在預編譯處理階段,它僅僅是做字符串的替換,沒有任何的語法規則檢查,比如類型不匹配,宏展開后的各種語法問題,的確讓人比較頭疼;
- inline函數則是發生在編譯階段,有完整的語法檢查,在Debug版本中也可以跟普通函數一樣,正常打斷點進行調試;
- 由于處理的階段不一樣,這就導致如果宏函數展開后仍然是一個函數調用的話,它是具有調用函數的開銷,包括函數進棧出棧等等;而inline函數卻僅僅是函數代碼的拷貝替換,并不會發生函數調用的開銷,在這一點上inline具有很高的執行效率。
3.3 inline函數與普通函數有什么區別?
正如上面提及的,普通函數的調用在匯編上有標準的 push 壓實參指令,然后 call 指令調用函數,給函數開辟棧幀,函數運行完成,有函數退出棧幀的過程;而 inline 內聯函數是在編譯階段,在函數的調用點將函數的代碼展開,省略了函數棧幀開辟回退的調用開銷,效率高。
3.4 static函數與普通函數有什么區別?
兩者唯一的區別在于可見范圍不一樣:
- 不被static關鍵字修飾的函數,它在整個工程范圍內,全局都可以調用,即其屬性是global的;只要函數參與了編譯,且最后鏈接的時候把函數的.o文件鏈接進去了,是不會報undefined reference to ‘xxx’的;
- 被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里面添加下面的測試代碼:
/* 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;
}
特此說明下,我使用的交叉編譯鏈是:gcc-arm-none-eabi-5_4-2016q3/bin/arm-none-eabi-gcc
然后使用scons編譯,果然編譯成功了,運行rtthread.elf,功能一切正常。
而當我去掉static的時候,期望中的鏈接錯誤果然出現了。
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.
為了做進一步驗證,我在rtconfig.py里面的CFLAGS加了一個編譯選項:-save-temps=obj;這個選項的作用就是在編譯的過程中,把中間過程文件也同步輸出,這里的中間文件有以下幾個:
xxx.o 文件:這是最終對應單個C文件生成的二進制目標文件,這個文件是最終參與鏈接成可執行文件的。
xxx.s 文件:這是由預編譯處理后的xxx.i文件編譯得到的匯編文件,里面描述的是匯編指令;
xxx.i 文件:這是預編譯處理之后的文件,比如想宏定義被展開之后是怎么樣的,就可以看這個文件;
關于使用GCC編譯C程序的完整過程這個話題,我已經整理出來了,分享分享給大家,畢竟這個知識點,對于解決編譯問題可是幫助非常大的。
5.2 實踐結果分析
為了做對比,我把整個編譯執行了兩次,一次是加上static的,一次是不加static的;
5.2.1 .i文件對比
對比結果如下,使用的是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)
結果我們發現如我們期望一樣,nostatic的僅比static的少了一個static修飾符,其他都是一樣的。
5.2.2 .s文件對比
.s文件使用文本對比工具,發現加了static的.s文件,里面有test_func的匯編實現代碼,而不加的這個函數直接就被優化掉了,壓根就找不到它的實現。
5.2.3 .o文件對比
由于.o文件已經不是可讀的文本文件了,我們只能通過一些命令行工具來查看,這里推薦linux命令行下的nm工具,具體用途和方法可以使用man nm
查看下。這里直接給出對比的命令行結果:
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,從中已經可以看到重要區別了:在不帶static的版本中,main.c里定義的testfunc函數被認為是一個外部函數(標識為U),而被static修飾的卻是本地實現函數(標識為T)。 而標識為U的函數是需要外部去實現的,這也就解釋了為何nostatic的版本會報undefined reference to 'testfunc' 錯誤,因為壓根就沒有外部的誰去實現這個函數。
5.4 終極實驗
5.4.1 補充測試代碼
為了驗證好這幾個關鍵字的區別,以及為何加了inline還不內聯,如何才能真正的內聯,我補充了一下測試代碼:
#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 編譯驗證
執行編譯
gcc main.c -save-temps=obj -Wall -o test_static -Wl,-Map=test_static.map
成功編譯,運行也完全沒有問題。
./test_static
Hello world !
1, 2
1, 2
1, 2
1, 2
5.4.3 進階分析
通過上面的章節,我們可以知道,我們應該重點分析.s文件和.o文件,因為.o文件不可讀,我們用nm-a
查看下:
nm -a test_static.o | grep test_func
0000000000000000 T test_func1
000000000000002e t test_func2
000000000000005c t test_func3
結果發現test_func4不在里面了,看樣子是被真正inline了? 我們打開.s文件確認下:
.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的區別是testfunc1是GLOBAL的,而testfunc2是LOCAL的;而testfunc2與testfunc3卻是完全一模一樣;也就是說testfunc3使用static inline壓根就沒有被內聯。 我們再找找testfunc4,發現已經找不到了,到底是不是內聯了?我們再看看main函數里面調用的部分:
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 //調用test_func1函數
movl $2, %esi
movl $1, %edi
call test_func2 //調用test_func2函數
movl $2, %esi
movl $1, %edi
call test_func3 //調用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 //“調用”test_func4函數,使用了內聯,直接拷貝了代碼,并不是真的函數調用。
.cfi_def_cfa 7, 8
嘩,果然,這才是真正的內聯啊,我們終于揭開了這個神秘的面紗。
5.4 實踐經驗總結
- inline有利有弊,切記使用的時候,最好讓它跟static一起使用,否則可能導致的問題超出你的想象。
- 加了inline,不是你想內聯,編譯器就一定會幫你內聯的,還得看代碼的實現。
- 如果要強制內聯,還得加參數修飾,每個C編譯器的方法還不一樣,比如gcc的是使用_attribute((alwaysinline))修飾定義的函數即可。
6 更多分享
本項目的所有測試代碼和編譯腳本,均可以在我的github倉庫01workstation中找到,歡迎指正問題。
歡迎關注我的github倉庫01workstation,日常分享一些開發筆記和項目實戰,歡迎指正問題。
同時也非常歡迎關注我的CSDN主頁和專欄:
【CSDN主頁:架構師李肯】
【RT-Thread主頁:架構師李肯】
【C/C++語言編程專欄】
【GCC專欄】
【信息安全專欄】
【RT-Thread開發筆記】
有問題的話,可以跟我討論,知無不答,謝謝大家。
審核編輯:湯梓紅
-
GCC
+關注
關注
0文章
107瀏覽量
24857 -
static
+關注
關注
0文章
33瀏覽量
10383 -
inline
+關注
關注
0文章
4瀏覽量
1642
發布評論請先 登錄
相關推薦
評論