1 前言
月初的時候我整理發出的技術文章:《【gcc編譯優化系列】一文帶你了解C代碼到底是如何被編譯的》,已經收到了好幾個朋友的點贊了;同時,特別驚喜的是,還收到論壇元老級別的大佬 aozima推薦和打賞,真的倍感榮幸。
這也從側面證實了,我選擇的這個【C代碼編譯】話題的確是不少人遇到的困惑,而我寫的關于編譯問題的文章也正好體現它的價值,所以我也想著盡快把C代碼編譯的第二篇文章輸出來了,也真心希望這份實戰分析能幫助到更多人解決他們遇到的各式各樣的編譯問題。
事與愿違,最近事多,一拖稿就是3個星期,這不,直到現在(再過幾個小時就2022了)才發出來。
2 回顧
2.1 主要內容
上一篇文章我也交代過,第一篇文章側重于結合gcc編譯器把C代碼編譯的整個流程介紹清楚,第二章文章會側重于結合真實的代碼場景分析遇到什么樣的編譯問題應該怎么樣去解決。
一句話:上篇注重理論基礎,本篇注重實戰演練 !
2.2 知識點回顧
再次強調下第一篇的理論基礎:
C代碼編譯的步驟,需要經歷預編譯、編譯、匯編、鏈接等幾個關鍵步驟,最后才能生成二進制文件,而這個二進制文件就是能被CPU識別并正確執行指令的唯一憑證。
我重新補畫了一張圖,它基本覆蓋了一個C工程代碼在開發階段的生命周期,本次的實戰演練也是主要圍繞著這張圖展開。
值得注意的是,本文所提及的【編譯問題】主要在2/3/4/5/6這幾個環節。
3 實戰分析
下面我會就每個階段,結合案例指出需要注意的事項或者分析會遇到哪類問題,以及這類問題應該如何去解決。
注意:下文中截取的代碼案例均來自RTT技術論壇。
3.1 代碼編寫階段
在代碼編寫階段,其實也沒多少好說的,但還是有人會遇到編譯出錯的問題,為什么呢?
這里提幾點經驗之談:
- 安裝IDE軟件或開發工具,亦或建立workspace,盡量盡量盡量不要用中文,哪怕英文差一些,搞個拼音也好一些;
- 如果是在Windows下編寫、編譯代碼,強烈建議不要搞太深的目錄,你的workspace-path盡量路徑短一些,不能保證每個編譯器對這種長路勁目錄都支持得很好;
- 代碼注釋也盡量別中文,因中文編碼問題導致的代碼文件亂碼之后狼狽不堪的例子真的太多了;如果一定要用中文,大家一定要約定好編碼格式,GBK還是UTF-8,這里建議用UTF-8,關于它們的區別與聯系,推薦閱讀。
- 代碼編寫風格盡量遵循規范,你的開發團隊要求怎么樣就怎么樣,這種沒有優劣之分,有了規范就是需要遵守。
隨手抓幾個論壇中在代碼編寫階段遇到編譯問題的帖子看看:
Windows命令行限制導致的編譯問題:本質還是上面提及的路徑不用太長,目錄不要太深;在Windows下可以把命令行的內容先寫入一個文本文件中轉下,繞開這個問題,很多SDK的編譯流程在Windows下就是這么玩的,比較麻煩。
中文編碼問題:本質還是少寫中文注釋,哪怕蹩腳一點的英文,這不還有翻譯軟件嗎?確保語法沒問題就行了。
中文編碼相關的問題,還不少勒:
代碼編寫規范的問題:大家約定好了,就一起共同遵守吧!
3.2 預編譯階段
預編譯階段要做的事情,參考我的上一篇文章,這里重點介紹幾種非常容易在預編譯階段遇到的編譯問題。
為了更好地展示編譯錯誤,我在下面提示編譯錯誤的時候,都采用英文為主輔以中文的方式來描述。
3.2.1 No such file or directory (找不到某個文件或目錄)
這里舉幾個典型的例子:
例子1:問題的根源,頭文件未包含,可能還包含頭文件嵌套包含的問題,比如頭文件A包含了頭文件B,報的是沒找到頭文件B,那么include頭文件A就能解決;
例子2:問題還是出在include上面,這個頭文件也的確是存在的,但是編譯器報找不到這個頭文件,原因在于頭文件所在的目錄沒有在編譯器檢索頭文件的列表里面,這個檢索列表一般包括:系統級別的include目錄(linux下/usr/include、/usr/local/include這些),編譯器所在include目錄,最后就是用戶自定義目錄。指定這個目錄,有不同的方法,以gcc編譯為例,是需要在CFLAGS里面添加-Ixxx(I是大寫的,xxx可以是相對路徑也可以是絕對路徑),具體可以參考下這篇文章。另外提一點,如果你的工程中有同名的頭文件,一定要注意他們被搜索的順序,這是決定你的代碼究竟include哪個頭文件的唯一參考。
例子3:這個問題的本質還是頭文件查找的問題。
例子4:這個本質也是頭文件查找路勁的問題,樓主通過把頭文件拷貝到指定路勁解決了的問題,但比較好的解決方案,我認為是將原頭文件的路徑添加進頭文件檢索的路徑比較好。
總結一下:出現No such file or directory的時候重點排查幾個方面:
- 先確認提示的這個文件是否真的不存在?如果文件存在,往下排查;
- 如果文件是頭文件,那么首先應該考慮的是頭文件所在的目錄是否在編譯器頭文件檢索的列表里面?
- 如果文件是C文件,則需要排查下Makefile或CMake或sconc的配置對該C文件的添加情況。
3.2.2 宏定義的問題
在C代碼中,可以說宏定義真的是太常見了,可以說是無處不在。
可以毫不客氣地說,沒有哪一個號稱精通C語言的大神玩宏定義玩得不6的。
在宏定義中,經常遇到的編譯問題就是,宏定義是如何展開的?
宏定義展開之后,對上下文代碼編譯的影響是什么?
搞清楚這個,解決因宏定義引發的編譯問題就一點都不難了。
欲知,如何查看宏定義展開后的代碼原型,你可以嘗試跳到 4.2.2 章節提前了解。
另外,還有一種宏定義報錯比較常見的就是某個宏定義定義在a.h頭文件中,但是引用該頭文件的C文件沒有包括該頭文件,導致編譯報錯;這種錯誤,本質是一個3.2.1提到的頭文件包含問題。
下面就宏定義編譯報錯的問題,截取論壇中比較典型的錯誤:
例子1:錯誤的宏定義導致編譯失敗。
例子2:這個本質還是編譯器宏定義的問題,引發的頭文件包含一系列的問題。
例子3:頭文件中缺少GCC宏定義的判斷,導致有些頭文件或宏定義沒有定義,進而編譯報錯。
另外,在論壇的問題里面檢索宏定義的時候,發現了這么一個問題,留給大家討論討論。
3.2.3 條件編譯的問題
常用的條件編譯有:
- #if 一般后面接一個宏定義表達式,當表達式的值為非0時,#if后面的代碼參與編譯,否則就不參與;
- #else 與#if類似;
- #elif 這是條件編譯的嵌套寫法,支持多個條件編譯;
- #idef 一般后面接一定宏定義,當這個宏定義有被定義時(可能是頭文件中顯式地使用#define定義,也有可能是通過編譯選項傳遞進去,比如gcc就支持-Dxxx來定義宏定義),后面的代碼將會參與編譯,否則就不參與;
- #if defined(xxx) *這是#ifdef*的另一種寫法,本質是等價的;
- #endif 表示條件編譯代碼的結束。
這里有幾點補充一下:
- 一個宏定義xxx沒有被定義時,它的值默認是0,所以使用#if xxx的結果是false;
- #ifdef和#if defined(xxx)這種寫法只管宏定義,是定義了還是沒有定義,而不管宏定義的值是多少;如果既要關注有沒有定義,又要關注定義的值是多少,請使用#if;
- 使用#if時,后面的表達式是可以使用 &&、||等邏輯運算符的。
在這種條件編譯的預處理下,往往我們看到很多可裁剪、可移植的代碼都是使用這些手段來實現。
舉個rt-thread內核組件代碼的例子:
/* 摘自rt-thread/components/finsh/finsh.h片段 */
/*
* Copyright (c) 2006-2021, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2010-03-22 Bernard first version
*/
#ifndef __FINSH_H__
#define __FINSH_H__
#include
#if defined(_MSC_VER)
#pragma section("FSymTab$f",read)
#endif
typedef long (*syscall_func)(void);
#ifdef FINSH_USING_SYMTAB
#ifdef __TI_COMPILER_VERSION__
#define __TI_FINSH_EXPORT_FUNCTION(f) PRAGMA(DATA_SECTION(f,"FSymTab"))
#endif
#ifdef FINSH_USING_DESCRIPTION
#ifdef _MSC_VER
#define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
const char __fsym_##cmd##_name[] = #cmd; \
const char __fsym_##cmd##_desc[] = #desc; \
__declspec(allocate("FSymTab$f")) \
const struct finsh_syscall __fsym_##cmd = \
{ \
__fsym_##cmd##_name, \
__fsym_##cmd##_desc, \
(syscall_func)&name \
};
#pragma comment(linker, "/merge:FSymTab=mytext")
#elif defined(__TI_COMPILER_VERSION__)
#define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
__TI_FINSH_EXPORT_FUNCTION(__fsym_##cmd); \
const char __fsym_##cmd##_name[] = #cmd; \
const char __fsym_##cmd##_desc[] = #desc; \
const struct finsh_syscall __fsym_##cmd = \
{ \
__fsym_##cmd##_name, \
__fsym_##cmd##_desc, \
(syscall_func)&name \
};
#else
#define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
const char __fsym_##cmd##_name[] RT_SECTION(".rodata.name") = #cmd; \
const char __fsym_##cmd##_desc[] RT_SECTION(".rodata.name") = #desc; \
RT_USED const struct finsh_syscall __fsym_##cmd RT_SECTION("FSymTab")= \
{ \
__fsym_##cmd##_name, \
__fsym_##cmd##_desc, \
(syscall_func)&name \
};
#endif
#else
#ifdef _MSC_VER
#define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
const char __fsym_##cmd##_name[] = #cmd; \
__declspec(allocate("FSymTab$f")) \
const struct finsh_syscall __fsym_##cmd = \
{ \
__fsym_##cmd##_name, \
(syscall_func)&name \
};
#pragma comment(linker, "/merge:FSymTab=mytext")
#elif defined(__TI_COMPILER_VERSION__)
#define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
__TI_FINSH_EXPORT_FUNCTION(__fsym_##cmd); \
const char __fsym_##cmd##_name[] = #cmd; \
const struct finsh_syscall __fsym_##cmd = \
{ \
__fsym_##cmd##_name, \
(syscall_func)&name \
};
#else
#define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
const char __fsym_##cmd##_name[] = #cmd; \
RT_USED const struct finsh_syscall __fsym_##cmd RT_SECTION("FSymTab")= \
{ \
__fsym_##cmd##_name, \
(syscall_func)&name \
};
#endif
#endif /* end of FINSH_USING_DESCRIPTION */
#endif /* end of FINSH_USING_SYMTAB */
如果是第一次接觸這種條件編譯的代碼,或者即便是老手,第一次閱讀這種N層條件編譯嵌套的代碼時,分分鐘被勸退。
所以在這一小節講到的條件編譯問題,不單單是代碼編譯的時候會遇到,你首先閱讀代碼就會遇到。
如果不幸你的代碼顯示在被條件編譯括起來的地方編譯報錯了,那么你第一時間應該要搞清楚你的代碼究竟哪個代碼塊是應該被編譯的,因為很多時候往往是因為你的宏定義開關沒有正確配置,而導致把不該編譯的代碼編譯進去,自然就編譯報錯了。
所以面對這種條件編譯的代碼,第一時間就是弄明白,并確認被編譯器預處理展開后的代碼,是不是你要的代碼?
怎么確認呢?
你可以嘗試跳到 4.2.2 章節提前了解。
因篇幅問題,這里僅截取一個論壇中關于條件編譯的問題,以供大家參考。
3.3 編譯階段
編譯階段,對于新手而言,可能也是一個編譯出錯的重災區。
為什么呢?
因為這里就涉及到C語言的基礎語法了,對基礎語法的不夠了解和理解不夠細致,就可能會出一些編譯錯誤。
下面舉幾個比較典型的例子看看:
例子1:這個編譯報錯的原因是結構體原型升級了,名稱也改了,然后新的版本中某些成員變量沒有了,進而引發語法報錯。
例子2:這個問題的核心是沒有包含對應的頭文件沒導致結構體類型無法識別,進而報了語法錯誤。
例子3:這個問題本質是因宏開關不恰當導致結構體的成員變量被消失而引發的語法錯誤問題。
3.4 匯編階段
匯編階段,相對來說,編譯出錯的情況會少很多,為啥呢?
這是因為,我們大部分時候都是寫高級的C語言,很少部分是寫匯編代碼,如果由C代碼到匯編代碼這個階段出錯,很有可能就是傳遞給編譯的參數沒搞對。
很典型的就是ARM處理器下,它的ARM指令集和Thumb指令集是不一樣的;如果你寫的是C語言和匯編混合編程(很多底層代碼都有這種寫法需求),那么你一定要搞清楚,你寫的是ARM指令還是Thumb指令,對應傳遞給編譯器的編譯選項是不一樣的。
下面舉幾個比較典型的例子看看:
例子1:這個應該是匯編指令的問題,問題帖子中有給答復。
例子2:不同編譯器直接的編譯轉換問題,涉及到匯編指令,可以到評論席看看大家的答復。
例子3:具體編譯器對匯編代碼的支持問題,看評論席。
3.5 鏈接階段
鏈接階段是整個完成編譯的關鍵一步,你寫的所有C代碼,能不能完整串起來,變成一個可執行程序,就得看這一步,所以往往這一步也是編譯報錯的重災區。
這里羅列幾種常見的編譯鏈接錯誤:
3.5.1 undefined reference to ‘xxx’
這個錯誤真的真的太常見了,很多人一見這個就說是“編譯報錯”,其實準確的術語應該是“鏈接報錯”。
它的錯誤的核心就是:xxx函數(符號)在你的所有C代碼(已經被編譯成.o文件)和你加入參與鏈接的所有庫文件(包括靜態庫和動態庫,當然也包括標準C庫),都找不到xxx的實現。
明白了這一點之后,你就應該知道如何解決這種鏈接報錯的編譯問題了。
舉幾個論壇中的問題看看,真的就是重災區。
解決這類問題,我的思路一般就以下幾步:
- 確認這個xxx符號是函數還是宏定義,還是變量;
- 如果是宏定義,應該就是你的宏定義沒被包含進來,導致宏定義沒有被展開;
- 如果是變量或者函數,確認下這其中的代碼是否參與了編譯,具體可以看.i文件或者.o文件;
- 還有一種情況,如果是庫函數(標準庫或者第三方庫),確認下這個函數在哪個庫,這個庫文件是否在你的鏈接列表里面,比如gcc編譯就是使用-lxxx指定鏈接的庫。
3.5.2 cannot find -lxxx
這種就表示xxx是一個庫文件,在鏈接的時候找不到它,一般解決這類問題,可以從以下幾個方面考慮:
- 首先確認xxx庫文件有沒有搞錯?在gcc編譯下,一個libxxx.a的靜態庫或lixxxx.so的動態庫,這里的庫名稱是xxx而不是libxxx.a或libxxx.so,新手往往容易忽略;
- 確認下庫檢索的路勁對不對,比如一些第三方庫,在gcc下要使用-Lxxx把你庫的路徑傳進去才能找到你的庫;
- 檢查下有沒有庫函數遞歸依賴的問題,以及庫函數鏈接順序的問題,往往也容易出錯。
3.5.3 multiple definition of ’xxx‘
這個錯誤就是如其錯誤提示的那樣,重復定義了,排查問題的關鍵是找到那里重復定義了。
舉幾個典型例子看看:
例子1:函數重復定義錯誤,評論席有答案。
例子2:兩個.c文件都有clear函數,鏈接得時候都搞一起了,能不重復定義嗎?static函數了解下。
例子3:main函數重復定義,這個還是比較少見,見評論席。
3.6 轉換階段
這個轉換階段一般出問題的情況也很少,在使用GCC編譯的時候,這一步使用的objcopy命令,它的一般用法是:
生成bin文件:
objcopy -O binary xxx.elf xxx.bin
生存hex文件:
objcopy -O ihex xxx.elf xxx.hex
往往在實際工程項目中,還有添加-R選項:
objcopy -O binary -R .eh_frame -R .init -R .fini -R .comment xxx.elf xxx.bin
objcopy -O ihex -R .eh_frame -R .init -R .fini -R .comment xxx.elf xxx.hex
其中-R選項表示:去掉這些段。
$ objcopy -h
Usage: objcopy [option(s)] in-file [out-file]
Copies a binary file, possibly transforming it in the process
The options are:
-R --remove-section Remove section from the output
--remove-relocations Remove relocations from section
3.7 其他階段
下載運行階段 -> 功能測試階段 -> 解BUG階段 -> 版本發布階段
本文對這些階段不做過多闡述,畢竟每個芯片平臺有不同的下載方式及調試運行的方法,每個功能的測試方法也差異很大,每個人調試解決BUG的方法各有不同,各自為政,為我所用即可。
到了版本發布這一階段,基本就功能穩定了,且已達到規劃的功能需求,開始走軟件發布流程了,這也是每一個項目最期待能走到的階段。這里特別需要注意的是(血淚的教訓):軟件發布一定要有規范的流程,且發布的代碼一定要能被追蹤到指定的提交記錄,否則出了問題,你可能會欲哭無淚,還可能被各種DISS。
4 分享幾個經驗
4.1 分享幾個非常奇葩的編譯問題
4.1.1 宏定義的這種寫法
例子1:一個freeRTOS的宏定義的問題,之前有寫過一篇文章(【freeRTOS開發筆記】記一次坑爹的freerTOS-v9.0.0升級到freeRTOS-v10.4.4),感興趣的可以一看。
4.1.2 static和inline搞什么
例子2:static與inline修飾函數定義的問題,可以參考這個(帖子](https://club.rt-thread.org/ask/question/431613.html),之前也針對這個問題寫過一篇文章(【gcc編譯優化系列】static與inline的區別與聯系),感興趣可以一看。
4.1.3 環境變量的鍋
例子3:環境變量引發的不可思議的問題?看看這個問題!環境變量的使用,請謹慎!
4.1.4 身邊的例子
例子4:最后報一個我實際工作中,同事遇到的朋友,當時排查了一會才發現端倪。
報的錯誤是:multiple definition of ’xxx‘!
我當時的排查思路也是按照上面羅列的幾點一個個排查,發現都不是那些。
奇怪呢!還能玩出花來?最后還是 4.2.2章節的方法,把鏈接的完整log輸出來一看,傻眼了!
居然有重復的.o文件添加到鏈接中,這不重復定義才怪呢!
下面簡單復盤下當時的場景,有則改之,無則加勉!
我們整個編譯構建是基于Makefile來的,整體劃分了N個模塊,每個子模塊有自己獨立的Makefile, 該模塊下所有需要參與編譯的代碼會在Makefile中列出來, 正常的情況下,類似這樣的寫法:
SRC_C-y := ./src/a.c
SRC_C-y += ./src/b.c
SRC_C-y += ./src/c.c
SRC_C-y += ./src/d.c
這樣最后參與鏈接的.o文件就是:a.o b.o c.o d.o,編譯沒有問題; 后面這哥們不知道怎么手誤碰還是怎么著,把冒號給改成了加號:
SRC_C-y += ./src/a.c
SRC_C-y += ./src/b.c
SRC_C-y += ./src/c.c
SRC_C-y += ./src/d.c
結果最后參與鏈接的.o文件就是:a.o b.o c.o d.o ... a.o b.o c.o d.o ... 也就是這個模塊的.o文件都拷貝了一份,那當然會報重復定義啊!
后面進一步跟進,發現構建流程有點缺陷,每個子模塊的Makefile會被加載兩次, 這就導致了如果按第二種寫法,SRC_C-y就會變成:./src/a.c ./src/b.c ./src/c.c ./src/d.c ./src/a.c ./src/b.c ./src/c.c ./src/d.c 但是按第一種寫法就不會,這個就需要了解下Makefile中 := 和 += 的語法區別了,之前寫過一篇文章,感興趣的可以了解下。
總之,當時覺得這個問題真的有點狗血,大家引以為戒。
4.2 分享幾個常用于排查編譯問題的方法
4.2.1 打開編譯過程的完整log輸出
為什么要這么做?
因為這樣你能看到你的編譯構建環境傳遞給編譯器的具體細節,從而了解更多編譯器的行為。
4.2.1.1 Makefile構建環境下
一般做編譯構建的時候,為了保持編譯log輸出的整潔性,都會把編譯的完整輸出默認關閉,你能看到的就是類似這樣的:
CC components/mqtt/src/impl/MQTTConnectClient.o
CC components/mqtt/src/impl/MQTTDeserializePublish.o
CC components/mqtt/src/impl/MQTTPacket.o
CC components/mqtt/src/impl/MQTTSerializePublish.o
CC components/mqtt/src/impl/MQTTSubscribeClient.o
CC components/mqtt/src/impl/MQTTUnsubscribeClient.o
CC components/mqtt/src/impl/iotx_mqtt_client.o
CC components/mqtt/src/mqtt_api.o
甚至連CC是哪種編譯器你都看不到,可能是gcc,也可能是ARMCC,也可能是其他。
那么如何打開編譯log的完整輸出呢?你可以嘗試下如下命令,在輸入make的時候,添加一下控制變量:
make V=1
或
make VERBOSE=1
這樣,你看到的編譯輸出就是非常完整的,具體到你用了哪個編譯器,傳入了哪些參數,一看便知,只不過log是真的多而長,考驗你的對log信息的檢索能力的時候來了。
"arm-none-eabi-gcc" -c -MD -DMCU_FAMILY="mcu_xxx" -DSYSINFO_PRODUCT_MODEL="XXX_xxx" -DSYSINFO_DEVICE_NAME="xxx" -DuECC_PLATFORM=uECC_arch_other -mcpu=arm968e-s -march=armv5te -mthumb -mthumb-interwork -mlittle-endian -w -save-temps=obj -DCFG_OS_FREERTOS=1 -DWIFI_BLE_COEXIST -DBK_DEBUG_UART=BK_UART_1 -DBK_CLI_UART=BK_UART_1 -DBK_CLI_ENABLE=0 -DUSR_CLI_MAX_COMMANDS=96 -DBLE_5_0 -DEN_LONG_MTU -DEN_COMBO_NET -DEN_AUTH -ggdb -Os -Wall -Wfatal-errors -fsigned-char -ffunction-sections -fdata-sections -fno-common -std=gnu11 -DPLATFORM="xxx" -include /xxx/application/bbb/xxxos_config.h -Wall -Werror -Wno-unused-variable -Wno-unused-parameter -Wno-implicit-function-declaration -Wno-type-limits -Wno-sign-compare -Wno-pointer-sign -Wno-uninitialized -Wno-return-type -Wno-unused-function -Wno-unused-but-set-variable -Wno-unused-value -Wno-strict-aliasing -Wall -Werror -Wno-unused-variable -Wno-unused-parameter -Wno-implicit-function-declaration -Wno-type-limits -Wno-sign-compare -Wno-pointer-sign -Wno-uninitialized -Wno-return-type -Wno-unused-function -Wno-unused-but-set-variable -Wno-unused-value -Wno-strict-aliasing -Wall -Werror -Wno-unused-variable -Wno-unused-parameter -Wno-implicit-function-declaration -Wno-type-limits -Wno-sign-compare -Wno-pointer-sign -Wno-uninitialized -Wno-return-type -Wno-unused-function -Wno-unused-but-set-variable -Wno-unused-value -Wno-strict-aliasing -I/xxx/platform/mcu/xxx/bk_sdk/config -I/xxx/platform/mcu/xxx/bk_sdk/release -I/xxx/platform/mcu/xxx/bk_sdk/xxx/func/ble_wifi_exchange -I/xxx//components/wireless/bluetooth/ble/host/profile -I/xxx//include/wireless/bluetooth/blemesh -I/xxx//include/network/coap -I/xxx//include/network/hal -I/xxx//include/network/http -I/xxx//include/network/lwm2m -I/xxx//include/network/umesh -I/xxx//include/network/athost -I/xxx//include/network/sal -I/xxx//include/network/netmgr -I/xxx//include/network/rtp -I/xxx//include/utility/yloop -DBUILD_BIN -DCLI_CONFIG_SUPPORT_BOARD_CMD=1 -DCONFIG_xxxos_CLI_BOARD -DCONFIG_xxxos_UOTA_BREAKPOINT -DCONFIG_xxxos_CLI_STACK_SIZE=4096 -DDISABLE_SECURE_STORAGE=1 -DCFG_I2C1_ENABLE=1 -Dxxxos_LOOP -DINFRA_COMPAT -DINFRA_MD5 -DINFRA_NET -DINFRA_SHA256 -DINFRA_TIMER -DINFRA_STRING -Dxxxos_COMP_CLI -Dxxxos_COMP_KV -DMBEDTLS_CONFIG_FILE="mbedtls_config.h" -DCONFIG_HTTP_SECURE=1 -DCOAP_SERV_MULTITHREAD -Dxxxos_COMP_VFS -D__FILENAME__='"mem.c"' -o /xxx/out/bbb/bbb@xxx/modules/platform/mcu/xxx/bk_sdk/xxx/func/lwip_intf/lwip-2.0.2/src/core/mem.o /xxx/platform/mcu/xxx/bk_sdk/xxx/func/lwip_intf/lwip-2.0.2/src/core/mem.c
這個輸出log是gcc的,所以得對gcc的編譯參數有所了解才行,比如-Dxxx表示宏定義,-Ixxx表示頭文件搜索目錄等等。
注意:make加V=1或VERBOSE=1,是一般寫得比較好的Makefile都會這么做,但不代表每個寫Makefile的人都會這么做。
4.2.1.2 CMake構建環境下
方法一:
與Makefile類似,CMake也有完整輸出的開關,在輸入make之后,增加一個控制變量:
make VERBOSE=1
不加VERBOSE=1的效果是:
[ 16%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_import.c.o
[ 16%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init.c.o
[ 17%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_copy.c.o
[ 17%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_multi.c.o
[ 17%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_set.c.o
[ 17%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_set_int.c.o
[ 18%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_size.c.o
[ 18%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_invmod.c.o
[ 18%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_invmod_slow.c.o
[ 18%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_is_square.c.o
[ 19%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_jacobi.c.o
[ 19%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_karatsuba_mul.c.o
[ 19%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_karatsuba_sqr.c.o
[ 20%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_kronecker.c.o
[ 20%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_lcm.c.o
[ 20%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_lshd.c.o
[ 20%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_mod.c.o
[ 21%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_mod_2d.c.o
[ 21%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_mod_d.c.o
加上VERBOSE=1的效果:
[ 3%] Building C object src/CMakeFiles/yyy.dir/yyy/bncore.c.o
cd /yyy/build/x86_release/src && /usr/bin/gcc -DENCRYPTO_MODE=AES -DHAL_SHIELD=0 -I/sysroot/usr/include -I/yyy/inc -I/yyy/inc/hal -I/yyy/src/hal/9x07/linux -I/yyy/inc/hal/log -I/yyy/inc/hal/crypto -I/yyy/inc/hal/crypto/tommath -I/yyy/inc/hal/crypto/skb -I/yyy/inc/hal/asn1 -I/yyy/inc/uicc_framework -I/yyy/inc/uicc_framework/apdu -I/yyy/inc/uicc_framework/channel -I/yyy/inc/uicc_framework/comm -I/yyy/src/uicc_framework/comm -I/yyy/inc/uicc_framework/dispatcher -I/yyy/inc/uicc_framework/profile -I/yyy/inc/uicc_framework/filesystem -I/yyy/inc/uicc_framework/nvm -I/yyy/inc/uicc_framework/utils -I/yyy/inc/uicc_framework/ppi -I/yyy/inc/uicc_framework/tf -I/yyy/inc/applet -I/yyy/inc/applet/common -I/yyy/inc/applet/common/util -I/yyy/inc/applet/common/uicc -I/yyy/inc/applet/ecasd -I/yyy/inc/applet/isdp -I/yyy/inc/applet/usim -I/yyy/inc/applet/isdr -I/yyy/inc/applet/nusim -Wall -pthread -fPIC -Wno-missing-braces -s -m32 -Werror -Wno-unused-function -Wno-unused-variable -Wno-unused-value -O2 -Os -Wmissing-prototypes -Wstrict-prototypes -DDEBUG=0 -I/yyy/build/x86_release -o CMakeFiles/yyy.dir/yyy/bncore.c.o -c /yyy/src/yyy/bncore.c
其實CMake最后的本質就是Makefile。
方法二:
在CMakeLists.txt配置文件的相應位置中,新增以下設置項,也可以達到上面的效果。
-
set ( CMAKE_VERBOSE_MAKEFILE on )
注意:有了這個配置之后呢,就只需要輸入make了,但是它的不好之處就是,你不想完整輸出的時候,你還得去改CMakeLists.txt配置文件,使用上沒有那么方便。所以我個人推薦使用方法一。
4.2.1.3 scons構建環境下
這個構建方式也是RTT支持的,我們可以看下它的編譯完整輸出是怎么樣的:
scons --verbose
不加控制項,輸出如下:
$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
scons: building associated VariantDir targets: build
CC build/kernel/components/dfs/src/dfs.o
CC build/kernel/components/dfs/src/dfs_file.o
CC build/kernel/components/dfs/src/dfs_fs.o
CC build/kernel/components/dfs/src/dfs_posix.o
CC build/kernel/components/drivers/i2c/i2c-bit-ops.o
CC build/kernel/components/drivers/i2c/i2c_core.o
CC build/kernel/components/drivers/i2c/i2c_dev.o
CC build/kernel/components/drivers/misc/pin.o
CC build/kernel/components/drivers/mtd/mtd_nand.o
CC build/kernel/components/drivers/mtd/mtd_nor.o
CC build/kernel/components/drivers/rtc/rtc.o
CC build/kernel/components/drivers/rtc/soft_rtc.o
CC build/kernel/components/drivers/sdio/block_dev.o
CC build/kernel/components/drivers/sdio/mmc.o
CC build/kernel/components/drivers/sdio/mmcsd_core.o
CC build/kernel/components/drivers/sdio/sd.o
CC build/kernel/components/drivers/sdio/sdio.o
CC build/kernel/components/drivers/serial/serial.o
CC build/kernel/components/drivers/spi/sfud/src/sfud.o
CC build/kernel/components/drivers/spi/sfud/src/sfud_sfdp.o
CC build/kernel/components/drivers/spi/spi_core.o
添加verbose控制項之后的輸出:
arm-none-eabi-gcc -o build/kernel/components/finsh/msh_file.o -c -march=armv7-a -marm -msoft-float -Wall -g -gdwarf-2 -O0 -DHAVE_CCONFIG_H -D__RTTHREAD__ -DRT_USING_NEWLIB -I. -Idrivers -Iapplications -I/yyyrt-thread/include -I/yyyrt-thread/libcpu/arm/common -I/yyyrt-thread/libcpu/arm/cortex-a -I/yyyrt-thread/components/cplusplus -I/yyyrt-thread/components/drivers/include -I/yyyrt-thread/components/drivers/spi -I/yyyrt-thread/components/drivers/spi/sfud/inc -I/yyyrt-thread/components/net/sal_socket/include -I/yyyrt-thread/components/net/sal_socket/include/socket -I/yyyrt-thread/components/net/sal_socket/impl -I/yyyrt-thread/components/net/sal_socket/include/dfs_net -I/yyyrt-thread/components/net/sal_socket/include/socket/sys_socket -I/yyyrt-thread/components/net/netdev/include -I/yyyrt-thread/components/net/lwip-2.1.2/src -I/yyyrt-thread/components/net/lwip-2.1.2/src/include -I/yyyrt-thread/components/net/lwip-2.1.2/src/arch/include -I/yyyrt-thread/components/net/lwip-2.1.2/src/include/netif -I/yyyrt-thread/components/libc/compilers/common -I/yyyrt-thread/components/libc/compilers/gcc/newlib -I/yyyrt-thread/components/libc/posix/src -I/yyyrt-thread/components/libc/posix/pthreads -I/yyyrt-thread/components/libc/posix/signal -I/yyyrt-thread/components/libc/posix/termios -I/yyyrt-thread/components/libc/posix/aio -I/yyyrt-thread/components/libc/posix/getline -I/yyyrt-thread/components/lwp -I/yyyrt-thread/components/dfs/include -I/yyyrt-thread/components/dfs/filesystems/devfs -I/yyyrt-thread/components/dfs/filesystems/elmfat -I/yyyrt-thread/components/dfs/filesystems/ramfs -I/yyyrt-thread/components/dfs/filesystems/romfs -I/yyyrt-thread/components/finsh -I/yyyrt-thread/examples/utest/testcases/kernel /yyyrt-thread/components/finsh/msh_file.c
至于具體的編譯輸出的log啥含義,還得看具體的編譯器。gcc是我們常用的,這個還是要熟悉。
4.2.2 打開編譯過程的中間文件的輸出
4.2.2.1 gcc編譯環境下
這個選項我在上一篇文章也提到過,這里用一個小結再簡單介紹下,對于排查編譯問題以及排查匯編代碼級的性能問題,用過都說好。
這個參數就是-save-temps=obj
,我們來實踐下:
gcc/gcc_helloworld$ ./build.sh clean
Clean build done !
gcc/gcc_helloworld$
gcc/gcc_helloworld$ ls
build.sh main.c README.md sub.c sub.h
gcc/gcc_helloworld$
gcc/gcc_helloworld$ ./build.sh allinone
gcc -c main.c -o main.o -save-temps=obj
gcc -c sub.c -o sub.o -save-temps=obj
gcc main.o sub.o -o test
gcc/gcc_helloworld$
gcc/gcc_helloworld$ ls
build.sh main.c main.i main.o main.s README.md sub.c sub.h sub.i sub.o sub.s test
就這樣,.i文件、.s文件、以及.o文件都同時輸出來了。
如果工程中,只有一個main.c的源文件的話,還可以這樣就一步搞定。
gcc main.c -o test -save-temps=obj
這些.i文件、.s文件、以及.o文件,我們稱之為中間臨時文件。
總結:
- 查看預處理之后的代碼文件,請看.i文件 (再復雜的條件編譯你也不怕,在這個文件里面,全部暴露原型)
- 查看C代碼生成的對應匯編代碼,請看.s文件
- 查看C代碼對應的符號表信息,請看.o文件
4.2.2.2 KEIL構建環境下
其實KEIL里面也有對應的設置項,我手上沒有現成的IDE環境,我網上找了一篇文章,介紹得還不錯,大家可以參考下。
4.3 友情提醒
生命有限,有效編碼。
請尊重你自己寫的每一行代碼。
請保證你的代碼編譯永遠都是:0 warning 0 error 0 bug 。
5 新年祝福
愿大家新的一年,如虎添翼,展翅高飛,2022,逐夢起航!
6 更多分享
歡迎關注我的github倉庫01workstation,日常分享一些開發筆記和項目實戰,歡迎指正問題。
同時也非常歡迎關注我的專欄,有問題的話,可以跟我討論,知無不答,謝謝大家。
審核編輯 黃昊宇
-
C語言
+關注
關注
180文章
7608瀏覽量
137134 -
GCC
+關注
關注
0文章
107瀏覽量
24856 -
編譯
+關注
關注
0文章
659瀏覽量
32911
發布評論請先 登錄
相關推薦
評論