日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【嵌入式】C语言高级编程-内建函数(11)

發(fā)布時間:2024/4/24 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【嵌入式】C语言高级编程-内建函数(11) 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

00. 目錄

文章目錄

    • 00. 目錄
    • 01. 內(nèi)建函數(shù)概述
    • 02. 常用內(nèi)建函數(shù)
    • 03. C 標(biāo)準(zhǔn)庫的內(nèi)建函數(shù)
    • 04. 內(nèi)核中的 likely 和 unlikely
    • 05. 附錄

01. 內(nèi)建函數(shù)概述

內(nèi)建函數(shù),顧名思義,就是編譯器內(nèi)部實現(xiàn)的函數(shù)。這些函數(shù)跟關(guān)鍵字一樣,可以直接使用,無須像標(biāo)準(zhǔn)庫函數(shù)那樣,要 #include 對應(yīng)的頭文件才能使用。

內(nèi)建函數(shù)的函數(shù)命名,通常以 __builtin 開頭。這些函數(shù)主要在編譯器內(nèi)部使用,主要是為編譯器服務(wù)的。內(nèi)建函數(shù)的主要用途如下。

  • 用來處理變長參數(shù)列表;
  • 用來處理程序運行異常;
  • 程序的編譯優(yōu)化、性能優(yōu)化;
  • 查看函數(shù)運行中的底層信息、堆棧信息等;
  • C 標(biāo)準(zhǔn)庫函數(shù)的內(nèi)建版本。

因為內(nèi)建函數(shù)是編譯器內(nèi)部定義,主要由編譯器相關(guān)的工具和程序調(diào)用,所以這些函數(shù)并沒有文檔說明,而且變動而頻繁。對于程序開發(fā)者來說,不建議使用這些函數(shù)。

但有些函數(shù),對于我們了解程序運行的底層信息、編譯優(yōu)化很有幫助,而且在 Linux 內(nèi)核中也經(jīng)常使用這些函數(shù),所以還是很有必要去了解 Linux 內(nèi)核中常用的一些內(nèi)建函數(shù)。

02. 常用內(nèi)建函數(shù)

**__builtin_return_address(LEVEL)

這個函數(shù)用來返回當(dāng)前函數(shù)或調(diào)用者的返回地址。函數(shù)的參數(shù) LEVEl 表示函數(shù)調(diào)用鏈中的不同層次的函數(shù),各個值代表的意義如下。

  • 0:返回當(dāng)前函數(shù)的返回地址;
  • 1:返回當(dāng)前函數(shù)調(diào)用者的返回地址;
  • 2:返回當(dāng)前函數(shù)調(diào)用者的調(diào)用者的返回地址;
  • ……

程序示例

void f(void) {int *p;p = __builtin_return_address(0);printf("f return address: %p\n",p);p = __builtin_return_address(1);;printf("func return address: %p\n",p);p = __builtin_return_address(2);;printf("main return address: %p\n",p);printf("\n"); } void func(void) {int *p;p = __builtin_return_address(0);printf("func return address: %p\n",p);p = __builtin_return_address(1);;printf("main return address: %p\n",p);printf("\n");f(); }int main(void) {int *p;p = __builtin_return_address(0);printf("main return address: %p\n",p);printf("\n"); func();printf("goodbye!\n");return 0; }

C 語言函數(shù)在調(diào)用過程中,會將當(dāng)前函數(shù)的返回地址、寄存器等現(xiàn)場信息保存在堆棧中,然后才會跳到被調(diào)用函數(shù)中去執(zhí)行。當(dāng)被調(diào)用函數(shù)執(zhí)行結(jié)束后,根據(jù)保存在堆棧中的返回地址,就可以直接返回到原來的函數(shù)中繼續(xù)執(zhí)行。

在這個程序中,main() 函數(shù)調(diào)用 func() 函數(shù),在 main() 函數(shù)跳轉(zhuǎn)到 func() 函數(shù)執(zhí)行之前,會將程序正在運行的當(dāng)前語句的下一條語句(如下代碼所示)的地址保存到堆棧中,然后才去執(zhí)行 func(); 這條語句,跳到 func() 函數(shù)去執(zhí)行。func() 執(zhí)行完畢后,如何返回到 main() 函數(shù)呢?很簡單,將保存到堆棧中的返回地址賦值給 PC 指針,就可以直接返回到 main() 函數(shù),繼續(xù)往下執(zhí)行了。

每一層函數(shù)調(diào)用,都會將當(dāng)前函數(shù)的下一條指令地址,即返回地址壓入堆棧保存。各層函數(shù)調(diào)用就構(gòu)成 了一個函數(shù)調(diào)用鏈。在各層函數(shù)內(nèi)部,我們使用內(nèi)建函數(shù)就可以打印這個調(diào)用鏈上各個函數(shù)的返回地址。程序的運行結(jié)果如下。

main return address:0040124Bfunc return address:004013C3 main return address:0040124Bf return address:00401385 func return address:004013C3 main return address:0040124B

**__builtin_frame_address(LEVEL)

在函數(shù)調(diào)用過程中,還有一個“棧幀”的概念。函數(shù)每調(diào)用一次,都會將當(dāng)前函數(shù)的現(xiàn)場(返回地址、寄存器等)保存在棧中,每一層函數(shù)調(diào)用都會將各自的現(xiàn)場信息都保存在各自的棧中。這個棧也就是當(dāng)前函數(shù)的棧幀,每一個棧幀有起始地址和結(jié)束地址,表示當(dāng)前函數(shù)的堆棧信息。多層函數(shù)調(diào)用就會有多個棧幀,每個棧幀里會保存上一層棧幀的起始地址,這樣各個棧幀就形成了一個調(diào)用鏈。很多調(diào)試器、GDB、包括我們的這個內(nèi)建函數(shù),其實都是通過回溯函數(shù)棧幀調(diào)用鏈來獲取函數(shù)底層的各種信息的。比如,返回地址 i、調(diào)用關(guān)系等。在 ARM 系統(tǒng)中,使用 FP 和 SP 這兩個寄存器,分別指向當(dāng)前函數(shù)棧幀的起始地址和結(jié)束地址。當(dāng)函數(shù)繼續(xù)調(diào)用或者返回,這兩個寄存器的值也會發(fā)生變化,總是指向當(dāng)前函數(shù)棧幀的起始地址和結(jié)束地址。

我們可以通過內(nèi)建函數(shù) __builtinframeaddress(LEVEL),查看函數(shù)的棧幀地址。

  • 0:查看當(dāng)前函數(shù)的棧幀地址
  • 1:查看當(dāng)前函數(shù)調(diào)用者的棧幀地址
  • ……
void func(void) {int *p;p = __builtin_frame_address(0);printf("func frame:%p\n",p);p = __builtin_frame_address(1);printf("main frame:%p\n",p); }int main(void) {int *p;p = __builtin_frame_address(0);printf("main frame:%p\n",p);printf("\n");func();return 0; }

執(zhí)行結(jié)果

main frame:0028FF48func frame:0028FF28 main frame:0028FF48

___builtin_constant_p(n)

編譯器內(nèi)部還有一些內(nèi)建函數(shù),主要用來編譯優(yōu)化、性能優(yōu)化,如 __builtinconstantp(n) 函數(shù)。該函數(shù)主要用來判斷參數(shù) n 在編譯時是否為常量,是常量的話,函數(shù)返回1;否則函數(shù)返回0。該函數(shù)常用于宏定義中,用于編譯優(yōu)化。一個宏定義,根據(jù)宏的參數(shù)是常量還是變量,可能實現(xiàn)的方法不一樣。在內(nèi)核中經(jīng)常看到這樣的宏。

#define _dma_cache_sync(addr, sz, dir) \ do { \if (__builtin_constant_p(dir)) \__inline_dma_cache_sync(addr, sz, dir); \else \__arc_dma_cache_sync(addr, sz, dir); \ } \ while (0);

很多計算或者操作在參數(shù)為常數(shù)時可能有更優(yōu)化的實現(xiàn),在這個宏定義中,我們實現(xiàn)了兩個版本。根據(jù)參數(shù)是否為常數(shù),我們可以靈活選用不同的版本。

__builtin_expect

內(nèi)建函數(shù) __builtin_expect 也常常用來編譯優(yōu)化。這個函數(shù)有兩個參數(shù),返回值就是其中一個參數(shù),仍是 exp。這個函數(shù)的意義主要就是告訴編譯器:參數(shù) exp 的值為 c 的可能性很大。然后編譯器可能就會根據(jù)這個提示信息,做一些分支預(yù)測上的代碼優(yōu)化。

參數(shù) c 跟這個函數(shù)的返回值無關(guān),無論 c 為何值,函數(shù)的返回值都是 exp。

int main(void) { int a;a = __builtin_expect(3,1);printf("a = %d\n",a);a = __builtin_expect(3,10);printf("a = %d\n",a);a = __builtin_expect(3,100);printf("a = %d\n",a);return 0; }

這個函數(shù)的主要用途就是編譯器的分支預(yù)測優(yōu)化?,F(xiàn)代 CPU 內(nèi)部,都有 cache 這個緩存器件。CPU 的運行速度很高,而外部 RAM 的速度相對來說就低了不少,所以當(dāng) CPU 從內(nèi)存 RAM 讀寫數(shù)據(jù)時就會有一定的性能瓶頸。為了提高程序執(zhí)行效率,CPU 都會通過 cache 這個 CPU 內(nèi)部緩沖區(qū)來緩存一定的指令或數(shù)據(jù)。CPU 讀寫內(nèi)存 RAM 中的數(shù)據(jù)時,會先到 cache 里面去看看能不能找到。找到的話就直接進行讀寫;找不到的話,cache 會重新緩存一部分內(nèi)存數(shù)據(jù)進來。CPU 讀寫 cache 的速度遠遠大于內(nèi)存 RAM,所以通過這種方式,可以提高系統(tǒng)的性能。

那 cache 如何緩存內(nèi)存數(shù)據(jù)呢?簡單來說,就是依據(jù)空間相近原則。比如 CPU 正在執(zhí)行一條指令,那么下一個指令周期,CPU 就會大概率執(zhí)行當(dāng)前指令的下一條指令。如果此時 cache 將下面幾條指令都緩存到 cache 里面,下一個指令周期 CPU 就可以直接到 cache 里取指、翻譯、執(zhí)行,從而使運算效率大大提高。

但有時候也會出現(xiàn)意外。比如程序在執(zhí)行過程中遇到函數(shù)調(diào)用、if 分支、goto 跳轉(zhuǎn)等程序結(jié)構(gòu),會跳到其它地址執(zhí)行,那么緩存到 cache 中的指令就不是 CPU 要獲取的指令。此時,我們就說 cache 沒有命中,cache 會重新緩存正確的指令代碼給 CPU 讀取,這就是 cache 工作的基本流程。

有了這個理論基礎(chǔ),我們在編寫程序時,遇到 if/switch 這種選擇分支的程序結(jié)構(gòu),可以將大概率發(fā)生的分支寫在前面,這樣程序運行時,因為大概率發(fā)生,所以大部分時間就不需要跳轉(zhuǎn),程序就相當(dāng)于一個順序結(jié)構(gòu),從而提高 cache 的命中率。內(nèi)核中已經(jīng)實現(xiàn)一些相關(guān)的宏,如 likely 和 unlikely,用來提醒程序員優(yōu)化程序。

03. C 標(biāo)準(zhǔn)庫的內(nèi)建函數(shù)

在 GNU C 編譯器內(nèi)部,實現(xiàn)了一些和 C 標(biāo)準(zhǔn)庫函數(shù)類似的內(nèi)建函數(shù)。這些函數(shù)跟 C 標(biāo)準(zhǔn)庫函數(shù)功能相似,函數(shù)名也相同,只是在前面加了一個前綴 __builtin。如果你不想使用 C 庫函數(shù),也可以加個前綴,直接使用對應(yīng)的內(nèi)建函數(shù)。

常見的標(biāo)準(zhǔn)庫函數(shù)如下:

  • 內(nèi)存相關(guān)的函數(shù):memcpy 、memset、memcmp
  • 數(shù)學(xué)函數(shù):log、cos、abs、exp
  • 字符串處理函數(shù):strcat、strcmp、strcpy、strlen
  • 打印函數(shù):printf、scanf、putchar、puts

使用與 C 標(biāo)準(zhǔn)庫對應(yīng)的內(nèi)建函數(shù)

#include <stdio.h> #include <string.h>int main(void) {char str[100];__builtin_memcpy(str, "hello shenzhen", strlen("hello shenzhen"));__builtin_puts(str);return 0; }

執(zhí)行結(jié)果

deng@itcast:~/tmp$ ./a.out hello shenzhen

通過運行結(jié)果我們看到,使用與 C 標(biāo)準(zhǔn)庫對應(yīng)的內(nèi)建函數(shù),同樣也能實現(xiàn)字符串的復(fù)制和打印,實現(xiàn) C 標(biāo)準(zhǔn)庫函數(shù)的功能。

04. 內(nèi)核中的 likely 和 unlikely

Linux 內(nèi)核中,使用 __builtin_expect 內(nèi)建函數(shù),定義了兩個宏。

#define likely(x) __builtin_expect(!!(x),1) #define unlikely(x) __builtin_expect(!!(x),0)

這兩個宏的主要作用,就是告訴編譯器:某一個分支發(fā)生的概率很高,或者說很低,基本不可能發(fā)生。編譯器就根據(jù)這個提示信息,就會去做一些分值預(yù)測的編譯優(yōu)化。在這兩個宏定義有一個細節(jié),就是對宏的參數(shù) x 做兩次取非操作,這是為了將參數(shù) x 轉(zhuǎn)換為布爾類型,然后與 1 和 0 作比較,告訴編譯器 x 為真或為假的可能性很高。

我們接下來舉個例子,讓大家感受下,使用這兩個宏后,編譯器在分支預(yù)測上的一些編譯變化。

#include <stdio.h>int main(void) {int a = 0;scanf("%d", &a);if (0 == a){printf("%d", 1);printf("%d", 2);printf("\n");}else{printf("%d", 5);printf("%d", 6);printf("\n");}return 0; }

在這個程序中,根據(jù)我們輸入變量 a 的值,程序會執(zhí)行不同的分支代碼。我們接著對這個程序反匯編,生成對應(yīng)的匯編代碼。

deng@itcast:~/tmp$ arm-linux-gcc test.c deng@itcast:~/tmp$ arm-linux-objdump -D a.out 00008438 <main>:8438: e92d4800 push {fp, lr}843c: e28db004 add fp, sp, #48440: e24dd008 sub sp, sp, #88444: e3a03000 mov r3, #08448: e50b3008 str r3, [fp, #-8]844c: e59f207c ldr r2, [pc, #124] ; 84d0 <main+0x98>8450: e24b3008 sub r3, fp, #88454: e1a00002 mov r0, r28458: e1a01003 mov r1, r3845c: ebffffc7 bl 8380 <_init+0x5c>8460: e51b3008 ldr r3, [fp, #-8]8464: e3530000 cmp r3, #08468: 1a00000a bne 8498 <main+0x60>846c: e59f305c ldr r3, [pc, #92] ; 84d0 <main+0x98>8470: e1a00003 mov r0, r38474: e3a01001 mov r1, #18478: ebffffbd bl 8374 <_init+0x50>847c: e59f304c ldr r3, [pc, #76] ; 84d0 <main+0x98>8480: e1a00003 mov r0, r38484: e3a01002 mov r1, #28488: ebffffb9 bl 8374 <_init+0x50>848c: e3a0000a mov r0, #108490: ebffffb4 bl 8368 <_init+0x44>8494: ea000009 b 84c0 <main+0x88>8498: e59f3030 ldr r3, [pc, #48] ; 84d0 <main+0x98>849c: e1a00003 mov r0, r384a0: e3a01005 mov r1, #584a4: ebffffb2 bl 8374 <_init+0x50>84a8: e59f3020 ldr r3, [pc, #32] ; 84d0 <main+0x98>84ac: e1a00003 mov r0, r384b0: e3a01006 mov r1, #684b4: ebffffae bl 8374 <_init+0x50>84b8: e3a0000a mov r0, #1084bc: ebffffa9 bl 8368 <_init+0x44>84c0: e3a03000 mov r3, #084c4: e1a00003 mov r0, r384c8: e24bd004 sub sp, fp, #484cc: e8bd8800 pop {fp, pc}84d0: 0000854c andeq r8, r0, ip, asr #10

觀察 main 函數(shù)的反匯編代碼,我們看到:匯編代碼的結(jié)構(gòu)就是基于我們的 if/else 分支先后順序,依次生成對應(yīng)的匯編代碼(看 8468:bne 8498跳轉(zhuǎn))。我們接著改一下代碼,使用 unlikely 修飾 if 分支,告訴編譯器,這個 if 分支小概率發(fā)生,或者說不可能發(fā)生。

#include <stdio.h>int main(void) {int a = 0;scanf("%d", &a);if (unlikely(0 == a)){printf("%d", 1);printf("%d", 2);printf("\n");}else{printf("%d", 5);printf("%d", 6);printf("\n");}return 0; }

對這個程序添加 -O2 優(yōu)化參數(shù)編譯,并對生成的可執(zhí)行文件 a.out 反匯編。

$ arm-linux-gnueabi-gcc -O2 expect.c$ arm-linux-gnueabi-objdump -D a.out 00010438 <main>:10438: e92d4010 push {r4, lr}1043c: e59f4080 ldr r4, [pc, #128] 10440: e24dd008 sub sp, sp, #810444: e5943000 ldr r3, [r4]10448: e1a0100d mov r1, sp1044c: e59f0074 ldr r0, [pc, #116]10450: e58d3004 str r3, [sp, #4]10454: ebfffff1 bl 10420 <__isoc99_scanf@plt>10458: e59d3000 ldr r3, [sp]1045c: e3530000 cmp r3, #010460: 0a000010 beq 104a8 <main+0x70>10464: e3a02005 mov r2, #510468: e59f105c ldr r1, [pc, #92]1046c: e3a00001 mov r0, #110470: ebffffe7 bl 10414 <__printf_chk@plt>10474: e3a02006 mov r2, #610478: e59f104c ldr r1, [pc, #76]1047c: e3a00001 mov r0, #110480: ebffffe3 bl 10414 <__printf_chk@plt>10484: e3a0000a mov r0, #1010488: ebffffde bl 10408 <putchar@plt>1048c: e59d2004 ldr r2, [sp, #4]10490: e5943000 ldr r3, [r4]10494: e3a00000 mov r0, #010498: e1520003 cmp r2, r31049c: 1a000007 bne 104c0 <main+0x88>104a0: e28dd008 add sp, sp, #8104a4: e8bd8010 pop {r4, pc}104a8: e3a02001 mov r2, #1104ac: e59f1018 ldr r1, [pc, #24]104b0: e1a00002 mov r0, r2104b4: ebffffd6 bl 10414 <__printf_chk@plt>104b8: e3a02002 mov r2, #2104bc: eaffffed b 10478 <main+0x40>

我們對 if 分支條件表達式使用 unlikely 修飾,告訴編譯器這個分支小概率發(fā)生。在編譯器開啟優(yōu)化編譯條件下,通過生成的反匯編代碼(10460:beq 104a8),我們可以看到,編譯器將小概率發(fā)生的 if 分支匯編代碼放在了后面,將 else 分支的匯編代碼放在了前面,這樣就確保了程序在執(zhí)行時,大部分時間都不需要跳轉(zhuǎn),直接按順序執(zhí)行下面大概率發(fā)生的分支代碼。

在 Linux 內(nèi)核中,你會發(fā)現(xiàn)很多地方使用 likely 和 unlikely 宏修飾,此時你應(yīng)該知道它們的用途了吧。

05. 附錄

參考: C語言嵌入式Linux高級編程

總結(jié)

以上是生活随笔為你收集整理的【嵌入式】C语言高级编程-内建函数(11)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。