0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內(nèi)不再提示

【gcc編譯優(yōu)化系列】static與inline的區(qū)別與聯(lián)系

嵌入式物聯(lián)網(wǎng)開發(fā) ? 來源:嵌入式物聯(lián)網(wǎng)開發(fā) ? 作者:嵌入式物聯(lián)網(wǎng)開發(fā) ? 2022-07-11 09:08 ? 次閱讀

1 問題來源

今天偶然留意到RT-Thread論壇的一個問題帖子,它的題目是RTT-VSCODE插件編譯RTT工程與RTT Studio結果不符,這種編譯問題是我最喜歡深扒的,于是我點進去看了看。

得知,它的核心問題就是有一個類似這樣定義的函數(shù)(為了簡要說明問題,我精簡了代碼):

/* 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上卻產(chǎn)生錯誤,這個錯誤居然是undefined reference to ‘test_func’。

2 問題分析

看到undefined reference to ‘testfunc’這個錯誤,熟悉C代碼編譯流程的都知道,這是一個典型的鏈接錯誤,也就是說錯誤發(fā)在鏈接階段,鏈接錯誤的原因是找不到testfunc函數(shù)的實現(xiàn)體。

相信你一定也有許多問號??????

test_func不是定義在main.c里面嗎?????

不就在main函數(shù)的上面嗎??????

怎么可能會發(fā)生鏈接錯誤呢??????

我們平時寫函數(shù)不就是這樣寫的嗎??????

難道這個inline作妖??????

3 知識點分析

3.1 inline關鍵字是干嘛的?

準確來說,它這個inline是一個C++關鍵字,在函數(shù)聲明或定義中,函數(shù)返回類型前加上關鍵字inline,即可以把函數(shù)指定為內(nèi)聯(lián)函數(shù)。但是由于市面上的大部分C編譯器都可以兼容部分C++的關鍵字和語法,所以我們也經(jīng)常見到inline出現(xiàn)在C代碼中。

3.2 inline與宏定義有什么區(qū)別?

  1. 宏定義發(fā)生在預編譯處理階段,它僅僅是做字符串的替換,沒有任何的語法規(guī)則檢查,比如類型不匹配,宏展開后的各種語法問題,的確讓人比較頭疼;
  2. inline函數(shù)則是發(fā)生在編譯階段,有完整的語法檢查,在Debug版本中也可以跟普通函數(shù)一樣,正常打斷點進行調(diào)試;
  3. 由于處理的階段不一樣,這就導致如果宏函數(shù)展開后仍然是一個函數(shù)調(diào)用的話,它是具有調(diào)用函數(shù)的開銷,包括函數(shù)進棧出棧等等;而inline函數(shù)卻僅僅是函數(shù)代碼的拷貝替換,并不會發(fā)生函數(shù)調(diào)用的開銷,在這一點上inline具有很高的執(zhí)行效率。

3.3 inline函數(shù)與普通函數(shù)有什么區(qū)別?

正如上面提及的,普通函數(shù)的調(diào)用在匯編上有標準的 push 壓實參指令,然后 call 指令調(diào)用函數(shù),給函數(shù)開辟棧幀,函數(shù)運行完成,有函數(shù)退出棧幀的過程;而 inline 內(nèi)聯(lián)函數(shù)是在編譯階段,在函數(shù)的調(diào)用點將函數(shù)的代碼展開,省略了函數(shù)棧幀開辟回退的調(diào)用開銷,效率高。

3.4 static函數(shù)與普通函數(shù)有什么區(qū)別?

兩者唯一的區(qū)別在于可見范圍不一樣:

  1. 不被static關鍵字修飾的函數(shù),它在整個工程范圍內(nèi),全局都可以調(diào)用,即其屬性是global的;只要函數(shù)參與了編譯,且最后鏈接的時候把函數(shù)的.o文件鏈接進去了,是不會報undefined reference to ‘xxx’的;
  2. 被static關鍵字修飾的函數(shù),只能在其定義的C文件內(nèi)可見,即其屬性由global變成了local,這個時候如果有另一個C文件的函數(shù)想調(diào)用這個static的函數(shù),那么對不起,最終鏈接階段會報undefined reference to ‘xxx’錯誤的。

4 解決方案

回到前文的問題,該如何解決這個問題呢?我的想法,有兩種解決思路:

4.1 放棄inline函數(shù)的優(yōu)勢,將inline函數(shù)修改為普通函數(shù)

這個方法很簡單,無非就是去掉inline,做個降維處理,把inline函數(shù)變成普通函數(shù),自然編譯鏈接就不會報錯。但我想,既然寫代碼的原作者加了inline,肯定是希望用上inline的高效率的特性,所以去掉inline顯然不是一個明智的選擇。

4.2 對inline函數(shù)加上static修飾

這一個做法,就可以很聰明地把它的問題給解決了。一個函數(shù)被static和inline修飾,證明這個函數(shù)是一個靜態(tài)的內(nèi)聯(lián)函數(shù),它的可見范圍依然是當前C文件,且同時具備inline函數(shù)的特性。

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的時候,期望中的鏈接錯誤果然出現(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.

為了做進一步驗證,我在rtconfig.py里面的CFLAGS加了一個編譯選項:-save-temps=obj;這個選項的作用就是在編譯的過程中,把中間過程文件也同步輸出,這里的中間文件有以下幾個:

xxx.o 文件:這是最終對應單個C文件生成的二進制目標文件,這個文件是最終參與鏈接成可執(zhí)行文件的。

xxx.s 文件:這是由預編譯處理后的xxx.i文件編譯得到的匯編文件,里面描述的是匯編指令;

xxx.i 文件:這是預編譯處理之后的文件,比如想宏定義被展開之后是怎么樣的,就可以看這個文件;

關于使用GCC編譯C程序的完整過程這個話題,我已經(jīng)整理出來了,分享分享給大家,畢竟這個知識點,對于解決編譯問題可是幫助非常大的。

5.2 實踐結果分析

為了做對比,我把整個編譯執(zhí)行了兩次,一次是加上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) 

結果我們發(fā)現(xiàn)如我們期望一樣,nostatic的僅比static的少了一個static修飾符,其他都是一樣的。

5.2.2 .s文件對比

.s文件使用文本對比工具,發(fā)現(xiàn)加了static的.s文件,里面有test_func的匯編實現(xiàn)代碼,而不加的這個函數(shù)直接就被優(yōu)化掉了,壓根就找不到它的實現(xiàn)。

5.2.3 .o文件對比

由于.o文件已經(jīng)不是可讀的文本文件了,我們只能通過一些命令行工具來查看,這里推薦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,從中已經(jīng)可以看到重要區(qū)別了:在不帶static的版本中,main.c里定義的testfunc函數(shù)被認為是一個外部函數(shù)(標識為U),而被static修飾的卻是本地實現(xiàn)函數(shù)(標識為T)。 而標識為U的函數(shù)是需要外部去實現(xiàn)的,這也就解釋了為何nostatic的版本會報undefined reference to 'testfunc' 錯誤,因為壓根就沒有外部的誰去實現(xiàn)這個函數(shù)。

5.4 終極實驗

5.4.1 補充測試代碼

為了驗證好這幾個關鍵字的區(qū)別,以及為何加了inline還不內(nèi)聯(lián),如何才能真正的內(nèi)聯(lián),我補充了一下測試代碼:

#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 編譯驗證

執(zhí)行編譯

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 進階分析

通過上面的章節(jié),我們可以知道,我們應該重點分析.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

結果發(fā)現(xiàn)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的區(qū)別是testfunc1是GLOBAL的,而testfunc2是LOCAL的;而testfunc2與testfunc3卻是完全一模一樣;也就是說testfunc3使用static inline壓根就沒有被內(nèi)聯(lián)。 我們再找找testfunc4,發(fā)現(xiàn)已經(jīng)找不到了,到底是不是內(nèi)聯(lián)了?我們再看看main函數(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)啊,我們終于揭開了這個神秘的面紗。

5.4 實踐經(jīng)驗總結

  • inline有利有弊,切記使用的時候,最好讓它跟static一起使用,否則可能導致的問題超出你的想象。
  • 加了inline,不是你想內(nèi)聯(lián),編譯器就一定會幫你內(nèi)聯(lián)的,還得看代碼的實現(xiàn)。
  • 如果要強制內(nèi)聯(lián),還得加參數(shù)修飾,每個C編譯器的方法還不一樣,比如gcc的是使用_attribute((alwaysinline))修飾定義的函數(shù)即可。

6 更多分享

本項目的所有測試代碼和編譯腳本,均可以在我的github倉庫01workstation中找到,歡迎指正問題。

歡迎關注我的github倉庫01workstation,日常分享一些開發(fā)筆記和項目實戰(zhàn),歡迎指正問題。

同時也非常歡迎關注我的CSDN主頁和專欄:

【CSDN主頁:架構師李肯】

【RT-Thread主頁:架構師李肯】

【C/C++語言編程專欄】

【GCC專欄】

信息安全專欄】

【RT-Thread開發(fā)筆記】

有問題的話,可以跟我討論,知無不答,謝謝大家。

審核編輯:湯梓紅

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • GCC
    GCC
    +關注

    關注

    0

    文章

    105

    瀏覽量

    24798
  • static
    +關注

    關注

    0

    文章

    33

    瀏覽量

    10341
  • inline
    +關注

    關注

    0

    文章

    4

    瀏覽量

    1624
收藏 人收藏

    評論

    相關推薦

    GCC編譯優(yōu)化系列】前后編譯的兩版本固件bin大小不一樣?

    GCC編譯優(yōu)化系列】前后編譯的兩個版本固件bin大小不一樣,怎么辦?
    的頭像 發(fā)表于 09-09 09:01 ?4386次閱讀
    【<b class='flag-5'>GCC</b><b class='flag-5'>編譯</b><b class='flag-5'>優(yōu)化</b><b class='flag-5'>系列</b>】前后<b class='flag-5'>編譯</b>的兩版本固件bin大小不一樣?

    Linux 下GCC編譯

    一、Linux 下多文件編譯 在上一篇 Linux 下的 C 編程我們知道了 Linux 下的編譯器為 GCC ,以及如何使用 GCC 進行編譯
    的頭像 發(fā)表于 09-11 15:18 ?2299次閱讀
    Linux 下<b class='flag-5'>GCC</b>的<b class='flag-5'>編譯</b>

    使用gcc編譯優(yōu)化與不優(yōu)化問題

    同樣的程序,使用gcc編譯優(yōu)化與不優(yōu)化的結果不一代碼如下:1. #include 2.3. int main()4. {5.int i = 1;6.7.i
    發(fā)表于 09-27 10:33

    請問static inline有什么作用?

    其實統(tǒng)一接口,大家都需要實現(xiàn)這個接口,如果不用static,那就很有可能重名。編譯就會出錯了。這里的接口 指什么呢?? 可以截圖看么??static inline 是內(nèi)聯(lián)的:小函數(shù),而
    發(fā)表于 04-28 06:56

    編譯cmsis_gcc.h文件時有上百個警告信息是怎么回事?

    編譯時 cmsis_gcc.h 這個文件有上百個警告信息,提示的警告信息都一樣,不知道是怎么回事,請問這個警告信息是為什么?能不能修改一下給消除掉#define __STATIC_FORCEINLINE __attribute
    發(fā)表于 05-09 09:54

    【原創(chuàng)精選】RT-Thread征文精選技術文章合集

    默認的鏈接腳本【GCC編譯優(yōu)化系列staticinline
    發(fā)表于 07-26 14:56

    AVR系列單片機GCC免費編譯工具

    AVR系列單片機GCC免費編譯工具
    發(fā)表于 04-13 15:23 ?54次下載

    淺談gcc編譯

    3.3 gcc編譯器 GNU CC(簡稱為gcc)是GNU項目中符合ANSI C標準的編譯系統(tǒng),能夠編譯用C、C++和Object C等語言
    發(fā)表于 10-18 13:48 ?0次下載

    常見gcc編譯警告整理以及解決方法

     GCC有很多的編譯選項,警告選項;指定頭文件、庫路徑;優(yōu)化選項。本文針整理一下GCC的警告選項以及gcc
    發(fā)表于 11-14 11:19 ?2.1w次閱讀

    GCC編譯優(yōu)化指南

    (cpp) → 編譯(gcc或g++) → 匯編(as) → 連接(ld) ;括號中表示每個階段所使用的程序,它們分別屬于 GCC 和 Binutils 軟件包。顯然的,優(yōu)化應當從
    發(fā)表于 04-02 14:36 ?486次閱讀

    gcc編譯優(yōu)化系列】如何獲取gcc默認的鏈接腳本

    我們都知道在一般的嵌入式開發(fā)中,使用gcc編譯固件的一般流程是,先把所有的.c文件和.s文件編譯成.o文件,然后把所有的.o文件鏈接成一個elf文件,最后由elf文件導出bin文件。 那么在鏈接成
    的頭像 發(fā)表于 07-11 09:15 ?3205次閱讀

    GCC編譯優(yōu)化系列】實戰(zhàn)分析C代碼遇到的編譯問題及解決思路

    GCC編譯優(yōu)化系列】實戰(zhàn)分析C工程代碼可能遇到的編譯問題及其解決思路
    的頭像 發(fā)表于 07-10 23:15 ?1323次閱讀
    【<b class='flag-5'>GCC</b><b class='flag-5'>編譯</b><b class='flag-5'>優(yōu)化</b><b class='flag-5'>系列</b>】實戰(zhàn)分析C代碼遇到的<b class='flag-5'>編譯</b>問題及解決思路

    GCC編譯優(yōu)化系列】multiple-definition

    GCC編譯優(yōu)化系列】這種讓人看不懂的multiple-definition真的有點讓人頭疼
    的頭像 發(fā)表于 07-11 09:26 ?6784次閱讀
    【<b class='flag-5'>GCC</b><b class='flag-5'>編譯</b><b class='flag-5'>優(yōu)化</b><b class='flag-5'>系列</b>】multiple-definition

    GCC編譯優(yōu)化系列】-specs=kernel.specs

    GCC編譯優(yōu)化系列GCC編譯鏈接時候--specs=kernel.specs鏈接屬性究竟是個
    的頭像 發(fā)表于 07-11 09:25 ?3219次閱讀
    【<b class='flag-5'>GCC</b><b class='flag-5'>編譯</b><b class='flag-5'>優(yōu)化</b><b class='flag-5'>系列</b>】-specs=kernel.specs

    LL庫中常見關鍵字__STATIC_INLINE

    LL庫中常見關鍵字__STATIC_INLINE,其定義見cmsis_gcc.h
    的頭像 發(fā)表于 07-24 11:30 ?1310次閱讀
    LL庫中常見關鍵字__<b class='flag-5'>STATIC_INLINE</b>