在嵌入式中一般談到C Library大家都會想到glibc,glibc是GUN旗下的一個C標準庫。那麼libc又是什麼呢?對於這個名詞的定義有點歧義,有些人把所有的C標準庫都統稱為libc,而有部分人認為libc是最開始linux下的標準庫。
所以說C標準庫也是多種多樣的,不同平臺都有所不同,比如嵌入式中非常小型的uClibc等等。
大家如果有對C庫感興趣的可以去簡單看一些源碼,裡面的寶藏也是特別的豐富。下面作者提供glic和uClibc的網站,大家可以到網站下載對應的源碼進行研究。
glibc官方網站 : http://www.gnu.org/software/libc/
uClibc官方網站 : https://uclibc-ng.org/
由於uClibc相對glibc來說小得多,所以在嵌入式中也是經常使用到,作者也是特意下載了源碼並解壓出來了,證明訪問路徑是OK的:
並用sourceInsight打開找到了printf的具體實現:
由於glibc和uClibc中的printf實現相對嵌套比較多,不便於直接分析,後面作者會選擇相對比較有條理的printf函數實現進行分析講解,設計實現上都是大同小異,如果大家感興趣也可以下載源碼進行閱讀。
C語言函數參數一定會入棧嗎?入棧一定是從右向左嗎?
其實這是一個與平臺和處理器相關的問題,所以需要具體情況具體分析,首先大家要明確函數參數和定義的局部變量大部分都是存在堆棧中(不過也可以通過藉助寄存器來傳遞,比如說之前剖析register關鍵字文章中把局部變量放到寄存器中提高效率),使用完畢以後通過堆棧指針的移動進行自動的釋放。
對於X86-32bit平臺一般都是從右向左參數入棧,而對於X86-64bit為了提高程序運行效率,會把前面的部分參數通過對應的寄存器進行傳遞,如果有更多的參數就通過壓棧進行處理,所以需要根據具體的平臺和編譯器進行分析。那麼為什麼作者這裡首先提到入棧順序呢?因為printf需要實現可變參數,那麼肯定是需要有約定的傳參數的規則,該約定的規則就決定了函數內部如何獲得對應參數。
對於大部分小夥伴在平時的開發中基本上都是使用固定的參數類型,不過對於類似於printf這種用戶接口使用型函數,實現可變參數就顯得更加具有靈活性。學習過C++的小夥伴應該有種感覺,可變參數有點類似於函數重載,不過C的可變參數必須包含一個參數。下面作者簡單的實現一個可變參數函數使用demo供大家參考。
1#include<stdio.h>
2#include<stdarg.h>
3
4/***********************************
5 * Fuction: sCalSum
6 * Author :(公眾號:最後一個bug)
7 **********************************/
8int sCalSum(int Num,...)
9{
10 //定義獲取參數列表結構體
11 va_list ap;
12 int sum = 0;
13 int i = 0 ;
14 //定位起始變量
15 va_start(ap, Num);
16
17 for(i = 0 ;i < Num;i++)
18 {
19 //根據參數類型進行索引
20 sum+= va_arg(ap,int);
21 }
22 //結束變量獲取,並釋放資源
23 va_end(ap);
24 return sum;
25}
26
27
28int main(void)
29{
30 printf("%d + %d = %d\n",1,2,sCalSum(2,1,2));
31 printf("%d + %d + %d = %d\n",1,2,4,sCalSum(3,1,2,4));
32 printf("%d + %d + %d + %d = %d\n",1,2,4,8,sCalSum(4,1,2,4,8));
33 return 0;
34 }
前面的兩個知識點都是為下面printf源碼分析鋪路的,浮點在處理器中運算是比較耗時間的,同時佔用的資源也是非常多的,所以很多集成開發環境或者編譯連結工具都會為標準庫提供精簡版本供大家選擇。
特別是對於使用單片機的小夥伴調用庫相關的函數,如果精簡版本能夠滿足需求,就儘量使用精簡版本,如果覺得精簡版本還是太佔用資源,那就自己手動編寫修改吧,所以printf中的浮點處理成為了精簡的一部分,如果在使用過程中發現使用printf列印不了浮點可以查看一下是不是libc中不支持浮點列印等相關功能。(下圖是IAR編譯工具中的相關配置選項)
為了方便大家學習和理解,所以這裡並沒有選用非常複雜的函數實現,而是選用IAR中的精簡版printf跟大家講講思路:(下面的代碼截圖均來自IAR安裝目錄,IAR安裝目錄下還有很多其他寶藏,大家可以參考學習)
在調用printf函數都會使用到頭文件#include<stdio>,所以大家搜索該文件即可,然後順著包含關係可以找到其他函數設計實現,所以推薦大家使用SI編輯器閱碼。
下面作者截取了printf函數實現,大家仔細觀察會發現printf竟然還有返回值,估計80%的小夥伴都沒有使用過吧。
從printf函數形式來看來和我們前面實現的可變參數實現並沒有太大的區別,只是說第一個參數變成了指針,這個指針就是平時所指定的參數格式,如"ADCSample:%d",函數內部就是通過解析該字符串獲得後面傳入參數的具體類型等信息,從而進行相應的轉化處理。
vprintf函數會最終調用vfprintf, vfprintf調用vsprintf,如下圖所示
對於vfprintf函數中vsprintf僅僅只是通過ap參數和pFormat格式進行轉化為pstr,通過pstr把最終的輸出信息通過fputs進行輸出,所以你只需要改寫對應 fputs就可以把最終輸出到對應的終端上(比如串口,LCD屏幕等等),所以玩stm32使用重新位串口輸出也就是同樣的道理了。
下面我們來具體看看vsprintf裡面的實現思路,vsprintf會調用vsnprintf,同時通過宏定義限制了最終通過printf的長度。
printf函數的基本實現就跟大家講解到這裡,其實很多libc並沒有想像中那麼神秘,大家如果在平時使用libc的過程中發現了相關問題完全可以通過閱讀相關源碼進行分析處理,也可以對其源碼進行改寫來滿足自身需求,自所謂"見源碼如見真理"。
好了,這裡是公眾號:「最後一個bug」,一個為大家打造的技術知識提升基地。同時非常感謝各位小夥伴的支持,我們下期精彩見!
推薦好文 點擊藍色字體即可跳轉
☞【重磅】【完全解讀】RTOS中的任務是線程?進程?還是協程?
☞【漲知識】OS下的內存使用原來這麼複雜
☞【原理分析】來看看慣性輪自平衡自行車實現原理
☞【重磅】剖析MCU的IAP升級軟體設計(設計思路篇)
☞ 【典藏】別怪"浮點數"太坑(C語言版本)
☞GUI必備知識之「告別」亂碼(淺顯易懂)
☞【典藏】大佬們都在用的結構體進階小技巧
☞聽說因為代碼沒"對齊"程序就奔了?(深度剖析)
☞【典藏】自製小型GUI界面框架(設計思想篇)