1 前言
月初的時(shí)候我整理發(fā)出的技術(shù)文章:《【gcc編譯優(yōu)化系列】一文帶你了解C代碼到底是如何被編譯的》,已經(jīng)收到了好幾個(gè)朋友的點(diǎn)贊了;同時(shí),特別驚喜的是,還收到論壇元老級(jí)別的大佬 aozima推薦和打賞,真的倍感榮幸。
這也從側(cè)面證實(shí)了,我選擇的這個(gè)【C代碼編譯】話題的確是不少人遇到的困惑,而我寫(xiě)的關(guān)于編譯問(wèn)題的文章也正好體現(xiàn)它的價(jià)值,所以我也想著盡快把C代碼編譯的第二篇文章輸出來(lái)了,也真心希望這份實(shí)戰(zhàn)分析能幫助到更多人解決他們遇到的各式各樣的編譯問(wèn)題。
事與愿違,最近事多,一拖稿就是3個(gè)星期,這不,直到現(xiàn)在(再過(guò)幾個(gè)小時(shí)就2022了)才發(fā)出來(lái)。
2 回顧
2.1 主要內(nèi)容
上一篇文章我也交代過(guò),第一篇文章側(cè)重于結(jié)合gcc編譯器把C代碼編譯的整個(gè)流程介紹清楚,第二章文章會(huì)側(cè)重于結(jié)合真實(shí)的代碼場(chǎng)景分析遇到什么樣的編譯問(wèn)題應(yīng)該怎么樣去解決。
一句話:上篇注重理論基礎(chǔ),本篇注重實(shí)戰(zhàn)演練 !
2.2 知識(shí)點(diǎn)回顧
再次強(qiáng)調(diào)下第一篇的理論基礎(chǔ):
C代碼編譯的步驟,需要經(jīng)歷預(yù)編譯、編譯、匯編、鏈接等幾個(gè)關(guān)鍵步驟,最后才能生成二進(jìn)制文件,而這個(gè)二進(jìn)制文件就是能被CPU識(shí)別并正確執(zhí)行指令的唯一憑證。
我重新補(bǔ)畫(huà)了一張圖,它基本覆蓋了一個(gè)C工程代碼在開(kāi)發(fā)階段的生命周期,本次的實(shí)戰(zhàn)演練也是主要圍繞著這張圖展開(kāi)。
值得注意的是,本文所提及的【編譯問(wèn)題】主要在2/3/4/5/6這幾個(gè)環(huán)節(jié)。
3 實(shí)戰(zhàn)分析
下面我會(huì)就每個(gè)階段,結(jié)合案例指出需要注意的事項(xiàng)或者分析會(huì)遇到哪類問(wèn)題,以及這類問(wèn)題應(yīng)該如何去解決。
注意:下文中截取的代碼案例均來(lái)自RTT技術(shù)論壇。
3.1 代碼編寫(xiě)階段
在代碼編寫(xiě)階段,其實(shí)也沒(méi)多少好說(shuō)的,但還是有人會(huì)遇到編譯出錯(cuò)的問(wèn)題,為什么呢?
這里提幾點(diǎn)經(jīng)驗(yàn)之談:
- 安裝IDE軟件或開(kāi)發(fā)工具,亦或建立workspace,盡量盡量盡量不要用中文,哪怕英文差一些,搞個(gè)拼音也好一些;
- 如果是在Windows下編寫(xiě)、編譯代碼,強(qiáng)烈建議不要搞太深的目錄,你的workspace-path盡量路徑短一些,不能保證每個(gè)編譯器對(duì)這種長(zhǎng)路勁目錄都支持得很好;
- 代碼注釋也盡量別中文,因中文編碼問(wèn)題導(dǎo)致的代碼文件亂碼之后狼狽不堪的例子真的太多了;如果一定要用中文,大家一定要約定好編碼格式,GBK還是UTF-8,這里建議用UTF-8,關(guān)于它們的區(qū)別與聯(lián)系,推薦閱讀。
- 代碼編寫(xiě)風(fēng)格盡量遵循規(guī)范,你的開(kāi)發(fā)團(tuán)隊(duì)要求怎么樣就怎么樣,這種沒(méi)有優(yōu)劣之分,有了規(guī)范就是需要遵守。
隨手抓幾個(gè)論壇中在代碼編寫(xiě)階段遇到編譯問(wèn)題的帖子看看:
Windows命令行限制導(dǎo)致的編譯問(wèn)題:本質(zhì)還是上面提及的路徑不用太長(zhǎng),目錄不要太深;在Windows下可以把命令行的內(nèi)容先寫(xiě)入一個(gè)文本文件中轉(zhuǎn)下,繞開(kāi)這個(gè)問(wèn)題,很多SDK的編譯流程在Windows下就是這么玩的,比較麻煩。
中文編碼問(wèn)題:本質(zhì)還是少寫(xiě)中文注釋,哪怕蹩腳一點(diǎn)的英文,這不還有翻譯軟件嗎?確保語(yǔ)法沒(méi)問(wèn)題就行了。
中文編碼相關(guān)的問(wèn)題,還不少勒:
代碼編寫(xiě)規(guī)范的問(wèn)題:大家約定好了,就一起共同遵守吧!
3.2 預(yù)編譯階段
預(yù)編譯階段要做的事情,參考我的上一篇文章,這里重點(diǎn)介紹幾種非常容易在預(yù)編譯階段遇到的編譯問(wèn)題。
為了更好地展示編譯錯(cuò)誤,我在下面提示編譯錯(cuò)誤的時(shí)候,都采用英文為主輔以中文的方式來(lái)描述。
3.2.1 No such file or directory (找不到某個(gè)文件或目錄)
這里舉幾個(gè)典型的例子:
例子1:?jiǎn)栴}的根源,頭文件未包含,可能還包含頭文件嵌套包含的問(wèn)題,比如頭文件A包含了頭文件B,報(bào)的是沒(méi)找到頭文件B,那么include頭文件A就能解決;
例子2:?jiǎn)栴}還是出在include上面,這個(gè)頭文件也的確是存在的,但是編譯器報(bào)找不到這個(gè)頭文件,原因在于頭文件所在的目錄沒(méi)有在編譯器檢索頭文件的列表里面,這個(gè)檢索列表一般包括:系統(tǒng)級(jí)別的include目錄(linux下/usr/include、/usr/local/include這些),編譯器所在include目錄,最后就是用戶自定義目錄。指定這個(gè)目錄,有不同的方法,以gcc編譯為例,是需要在CFLAGS里面添加-Ixxx(I是大寫(xiě)的,xxx可以是相對(duì)路徑也可以是絕對(duì)路徑),具體可以參考下這篇文章。另外提一點(diǎn),如果你的工程中有同名的頭文件,一定要注意他們被搜索的順序,這是決定你的代碼究竟include哪個(gè)頭文件的唯一參考。
例子3:這個(gè)問(wèn)題的本質(zhì)還是頭文件查找的問(wèn)題。
例子4:這個(gè)本質(zhì)也是頭文件查找路勁的問(wèn)題,樓主通過(guò)把頭文件拷貝到指定路勁解決了的問(wèn)題,但比較好的解決方案,我認(rèn)為是將原頭文件的路徑添加進(jìn)頭文件檢索的路徑比較好。
總結(jié)一下:出現(xiàn)No such file or directory的時(shí)候重點(diǎn)排查幾個(gè)方面:
- 先確認(rèn)提示的這個(gè)文件是否真的不存在?如果文件存在,往下排查;
- 如果文件是頭文件,那么首先應(yīng)該考慮的是頭文件所在的目錄是否在編譯器頭文件檢索的列表里面?
- 如果文件是C文件,則需要排查下Makefile或CMake或sconc的配置對(duì)該C文件的添加情況。
3.2.2 宏定義的問(wèn)題
在C代碼中,可以說(shuō)宏定義真的是太常見(jiàn)了,可以說(shuō)是無(wú)處不在。
可以毫不客氣地說(shuō),沒(méi)有哪一個(gè)號(hào)稱精通C語(yǔ)言的大神玩宏定義玩得不6的。
在宏定義中,經(jīng)常遇到的編譯問(wèn)題就是,宏定義是如何展開(kāi)的?
宏定義展開(kāi)之后,對(duì)上下文代碼編譯的影響是什么?
搞清楚這個(gè),解決因宏定義引發(fā)的編譯問(wèn)題就一點(diǎn)都不難了。
欲知,如何查看宏定義展開(kāi)后的代碼原型,你可以嘗試跳到 4.2.2 章節(jié)提前了解。
另外,還有一種宏定義報(bào)錯(cuò)比較常見(jiàn)的就是某個(gè)宏定義定義在a.h頭文件中,但是引用該頭文件的C文件沒(méi)有包括該頭文件,導(dǎo)致編譯報(bào)錯(cuò);這種錯(cuò)誤,本質(zhì)是一個(gè)3.2.1提到的頭文件包含問(wèn)題。
下面就宏定義編譯報(bào)錯(cuò)的問(wèn)題,截取論壇中比較典型的錯(cuò)誤:
例子1:錯(cuò)誤的宏定義導(dǎo)致編譯失敗。
例子2:這個(gè)本質(zhì)還是編譯器宏定義的問(wèn)題,引發(fā)的頭文件包含一系列的問(wèn)題。
例子3:頭文件中缺少GCC宏定義的判斷,導(dǎo)致有些頭文件或宏定義沒(méi)有定義,進(jìn)而編譯報(bào)錯(cuò)。
另外,在論壇的問(wèn)題里面檢索宏定義的時(shí)候,發(fā)現(xiàn)了這么一個(gè)問(wèn)題,留給大家討論討論。
3.2.3 條件編譯的問(wèn)題
常用的條件編譯有:
- #if 一般后面接一個(gè)宏定義表達(dá)式,當(dāng)表達(dá)式的值為非0時(shí),#if后面的代碼參與編譯,否則就不參與;
- #else 與#if類似;
- #elif 這是條件編譯的嵌套寫(xiě)法,支持多個(gè)條件編譯;
- #idef 一般后面接一定宏定義,當(dāng)這個(gè)宏定義有被定義時(shí)(可能是頭文件中顯式地使用#define定義,也有可能是通過(guò)編譯選項(xiàng)傳遞進(jìn)去,比如gcc就支持-Dxxx來(lái)定義宏定義),后面的代碼將會(huì)參與編譯,否則就不參與;
- #if defined(xxx) *這是#ifdef*的另一種寫(xiě)法,本質(zhì)是等價(jià)的;
- #endif 表示條件編譯代碼的結(jié)束。
這里有幾點(diǎn)補(bǔ)充一下:
- 一個(gè)宏定義xxx沒(méi)有被定義時(shí),它的值默認(rèn)是0,所以使用#if xxx的結(jié)果是false;
- #ifdef和#if defined(xxx)這種寫(xiě)法只管宏定義,是定義了還是沒(méi)有定義,而不管宏定義的值是多少;如果既要關(guān)注有沒(méi)有定義,又要關(guān)注定義的值是多少,請(qǐng)使用#if;
- 使用#if時(shí),后面的表達(dá)式是可以使用 &&、||等邏輯運(yùn)算符的。
在這種條件編譯的預(yù)處理下,往往我們看到很多可裁剪、可移植的代碼都是使用這些手段來(lái)實(shí)現(xiàn)。
舉個(gè)rt-thread內(nèi)核組件代碼的例子:
/* 摘自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層條件編譯嵌套的代碼時(shí),分分鐘被勸退。
所以在這一小節(jié)講到的條件編譯問(wèn)題,不單單是代碼編譯的時(shí)候會(huì)遇到,你首先閱讀代碼就會(huì)遇到。
如果不幸你的代碼顯示在被條件編譯括起來(lái)的地方編譯報(bào)錯(cuò)了,那么你第一時(shí)間應(yīng)該要搞清楚你的代碼究竟哪個(gè)代碼塊是應(yīng)該被編譯的,因?yàn)楹芏鄷r(shí)候往往是因?yàn)槟愕暮甓x開(kāi)關(guān)沒(méi)有正確配置,而導(dǎo)致把不該編譯的代碼編譯進(jìn)去,自然就編譯報(bào)錯(cuò)了。
所以面對(duì)這種條件編譯的代碼,第一時(shí)間就是弄明白,并確認(rèn)被編譯器預(yù)處理展開(kāi)后的代碼,是不是你要的代碼?
怎么確認(rèn)呢?
你可以嘗試跳到 4.2.2 章節(jié)提前了解。
因篇幅問(wèn)題,這里僅截取一個(gè)論壇中關(guān)于條件編譯的問(wèn)題,以供大家參考。
3.3 編譯階段
編譯階段,對(duì)于新手而言,可能也是一個(gè)編譯出錯(cuò)的重災(zāi)區(qū)。
為什么呢?
因?yàn)檫@里就涉及到C語(yǔ)言的基礎(chǔ)語(yǔ)法了,對(duì)基礎(chǔ)語(yǔ)法的不夠了解和理解不夠細(xì)致,就可能會(huì)出一些編譯錯(cuò)誤。
下面舉幾個(gè)比較典型的例子看看:
例子1:這個(gè)編譯報(bào)錯(cuò)的原因是結(jié)構(gòu)體原型升級(jí)了,名稱也改了,然后新的版本中某些成員變量沒(méi)有了,進(jìn)而引發(fā)語(yǔ)法報(bào)錯(cuò)。
例子2:這個(gè)問(wèn)題的核心是沒(méi)有包含對(duì)應(yīng)的頭文件沒(méi)導(dǎo)致結(jié)構(gòu)體類型無(wú)法識(shí)別,進(jìn)而報(bào)了語(yǔ)法錯(cuò)誤。
例子3:這個(gè)問(wèn)題本質(zhì)是因宏開(kāi)關(guān)不恰當(dāng)導(dǎo)致結(jié)構(gòu)體的成員變量被消失而引發(fā)的語(yǔ)法錯(cuò)誤問(wèn)題。
3.4 匯編階段
匯編階段,相對(duì)來(lái)說(shuō),編譯出錯(cuò)的情況會(huì)少很多,為啥呢?
這是因?yàn)?,我們大部分時(shí)候都是寫(xiě)高級(jí)的C語(yǔ)言,很少部分是寫(xiě)匯編代碼,如果由C代碼到匯編代碼這個(gè)階段出錯(cuò),很有可能就是傳遞給編譯的參數(shù)沒(méi)搞對(duì)。
很典型的就是ARM處理器下,它的ARM指令集和Thumb指令集是不一樣的;如果你寫(xiě)的是C語(yǔ)言和匯編混合編程(很多底層代碼都有這種寫(xiě)法需求),那么你一定要搞清楚,你寫(xiě)的是ARM指令還是Thumb指令,對(duì)應(yīng)傳遞給編譯器的編譯選項(xiàng)是不一樣的。
下面舉幾個(gè)比較典型的例子看看:
例子1:這個(gè)應(yīng)該是匯編指令的問(wèn)題,問(wèn)題帖子中有給答復(fù)。
例子2:不同編譯器直接的編譯轉(zhuǎn)換問(wèn)題,涉及到匯編指令,可以到評(píng)論席看看大家的答復(fù)。
例子3:具體編譯器對(duì)匯編代碼的支持問(wèn)題,看評(píng)論席。
3.5 鏈接階段
鏈接階段是整個(gè)完成編譯的關(guān)鍵一步,你寫(xiě)的所有C代碼,能不能完整串起來(lái),變成一個(gè)可執(zhí)行程序,就得看這一步,所以往往這一步也是編譯報(bào)錯(cuò)的重災(zāi)區(qū)。
這里羅列幾種常見(jiàn)的編譯鏈接錯(cuò)誤:
3.5.1 undefined reference to ‘xxx’
這個(gè)錯(cuò)誤真的真的太常見(jiàn)了,很多人一見(jiàn)這個(gè)就說(shuō)是“編譯報(bào)錯(cuò)”,其實(shí)準(zhǔn)確的術(shù)語(yǔ)應(yīng)該是“鏈接報(bào)錯(cuò)”。
它的錯(cuò)誤的核心就是:xxx函數(shù)(符號(hào))在你的所有C代碼(已經(jīng)被編譯成.o文件)和你加入?yún)⑴c鏈接的所有庫(kù)文件(包括靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù),當(dāng)然也包括標(biāo)準(zhǔn)C庫(kù)),都找不到xxx的實(shí)現(xiàn)。
明白了這一點(diǎn)之后,你就應(yīng)該知道如何解決這種鏈接報(bào)錯(cuò)的編譯問(wèn)題了。
舉幾個(gè)論壇中的問(wèn)題看看,真的就是重災(zāi)區(qū)。
解決這類問(wèn)題,我的思路一般就以下幾步:
- 確認(rèn)這個(gè)xxx符號(hào)是函數(shù)還是宏定義,還是變量;
- 如果是宏定義,應(yīng)該就是你的宏定義沒(méi)被包含進(jìn)來(lái),導(dǎo)致宏定義沒(méi)有被展開(kāi);
- 如果是變量或者函數(shù),確認(rèn)下這其中的代碼是否參與了編譯,具體可以看.i文件或者.o文件;
- 還有一種情況,如果是庫(kù)函數(shù)(標(biāo)準(zhǔn)庫(kù)或者第三方庫(kù)),確認(rèn)下這個(gè)函數(shù)在哪個(gè)庫(kù),這個(gè)庫(kù)文件是否在你的鏈接列表里面,比如gcc編譯就是使用-lxxx指定鏈接的庫(kù)。
3.5.2 cannot find -lxxx
這種就表示xxx是一個(gè)庫(kù)文件,在鏈接的時(shí)候找不到它,一般解決這類問(wèn)題,可以從以下幾個(gè)方面考慮:
- 首先確認(rèn)xxx庫(kù)文件有沒(méi)有搞錯(cuò)?在gcc編譯下,一個(gè)libxxx.a的靜態(tài)庫(kù)或lixxxx.so的動(dòng)態(tài)庫(kù),這里的庫(kù)名稱是xxx而不是libxxx.a或libxxx.so,新手往往容易忽略;
- 確認(rèn)下庫(kù)檢索的路勁對(duì)不對(duì),比如一些第三方庫(kù),在gcc下要使用-Lxxx把你庫(kù)的路徑傳進(jìn)去才能找到你的庫(kù);
- 檢查下有沒(méi)有庫(kù)函數(shù)遞歸依賴的問(wèn)題,以及庫(kù)函數(shù)鏈接順序的問(wèn)題,往往也容易出錯(cuò)。
3.5.3 multiple definition of ’xxx‘
這個(gè)錯(cuò)誤就是如其錯(cuò)誤提示的那樣,重復(fù)定義了,排查問(wèn)題的關(guān)鍵是找到那里重復(fù)定義了。
舉幾個(gè)典型例子看看:
例子1:函數(shù)重復(fù)定義錯(cuò)誤,評(píng)論席有答案。
例子2:兩個(gè).c文件都有clear函數(shù),鏈接得時(shí)候都搞一起了,能不重復(fù)定義嗎?static函數(shù)了解下。
例子3:main函數(shù)重復(fù)定義,這個(gè)還是比較少見(jiàn),見(jiàn)評(píng)論席。
3.6 轉(zhuǎn)換階段
這個(gè)轉(zhuǎn)換階段一般出問(wèn)題的情況也很少,在使用GCC編譯的時(shí)候,這一步使用的objcopy命令,它的一般用法是:
生成bin文件:
objcopy -O binary xxx.elf xxx.bin
生存hex文件:
objcopy -O ihex xxx.elf xxx.hex
往往在實(shí)際工程項(xiàng)目中,還有添加-R選項(xiàng):
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選項(xiàng)表示:去掉這些段。
$ 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 其他階段
下載運(yùn)行階段 -> 功能測(cè)試階段 -> 解BUG階段 -> 版本發(fā)布階段
本文對(duì)這些階段不做過(guò)多闡述,畢竟每個(gè)芯片平臺(tái)有不同的下載方式及調(diào)試運(yùn)行的方法,每個(gè)功能的測(cè)試方法也差異很大,每個(gè)人調(diào)試解決BUG的方法各有不同,各自為政,為我所用即可。
到了版本發(fā)布這一階段,基本就功能穩(wěn)定了,且已達(dá)到規(guī)劃的功能需求,開(kāi)始走軟件發(fā)布流程了,這也是每一個(gè)項(xiàng)目最期待能走到的階段。這里特別需要注意的是(血淚的教訓(xùn)):軟件發(fā)布一定要有規(guī)范的流程,且發(fā)布的代碼一定要能被追蹤到指定的提交記錄,否則出了問(wèn)題,你可能會(huì)欲哭無(wú)淚,還可能被各種DISS。
4 分享幾個(gè)經(jīng)驗(yàn)
4.1 分享幾個(gè)非常奇葩的編譯問(wèn)題
4.1.1 宏定義的這種寫(xiě)法
例子1:一個(gè)freeRTOS的宏定義的問(wèn)題,之前有寫(xiě)過(guò)一篇文章(【freeRTOS開(kāi)發(fā)筆記】記一次坑爹的freerTOS-v9.0.0升級(jí)到freeRTOS-v10.4.4),感興趣的可以一看。
4.1.2 static和inline搞什么
例子2:static與inline修飾函數(shù)定義的問(wèn)題,可以參考這個(gè)(帖子](https://club.rt-thread.org/ask/question/431613.html),之前也針對(duì)這個(gè)問(wèn)題寫(xiě)過(guò)一篇文章(【gcc編譯優(yōu)化系列】static與inline的區(qū)別與聯(lián)系),感興趣可以一看。
4.1.3 環(huán)境變量的鍋
例子3:環(huán)境變量引發(fā)的不可思議的問(wèn)題?看看這個(gè)問(wèn)題!環(huán)境變量的使用,請(qǐng)謹(jǐn)慎!
4.1.4 身邊的例子
例子4:最后報(bào)一個(gè)我實(shí)際工作中,同事遇到的朋友,當(dāng)時(shí)排查了一會(huì)才發(fā)現(xiàn)端倪。
報(bào)的錯(cuò)誤是:multiple definition of ’xxx‘!
我當(dāng)時(shí)的排查思路也是按照上面羅列的幾點(diǎn)一個(gè)個(gè)排查,發(fā)現(xiàn)都不是那些。
奇怪呢!還能玩出花來(lái)?最后還是 4.2.2章節(jié)的方法,把鏈接的完整log輸出來(lái)一看,傻眼了!
居然有重復(fù)的.o文件添加到鏈接中,這不重復(fù)定義才怪呢!
下面簡(jiǎn)單復(fù)盤(pán)下當(dāng)時(shí)的場(chǎng)景,有則改之,無(wú)則加勉!
我們整個(gè)編譯構(gòu)建是基于Makefile來(lái)的,整體劃分了N個(gè)模塊,每個(gè)子模塊有自己獨(dú)立的Makefile, 該模塊下所有需要參與編譯的代碼會(huì)在Makefile中列出來(lái), 正常的情況下,類似這樣的寫(xiě)法:
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,編譯沒(méi)有問(wèn)題; 后面這哥們不知道怎么手誤碰還是怎么著,把冒號(hào)給改成了加號(hà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
結(jié)果最后參與鏈接的.o文件就是:a.o b.o c.o d.o ... a.o b.o c.o d.o ... 也就是這個(gè)模塊的.o文件都拷貝了一份,那當(dāng)然會(huì)報(bào)重復(fù)定義?。?/p>
后面進(jìn)一步跟進(jìn),發(fā)現(xiàn)構(gòu)建流程有點(diǎn)缺陷,每個(gè)子模塊的Makefile會(huì)被加載兩次, 這就導(dǎo)致了如果按第二種寫(xiě)法,SRC_C-y就會(huì)變成:./src/a.c ./src/b.c ./src/c.c ./src/d.c ./src/a.c ./src/b.c ./src/c.c ./src/d.c 但是按第一種寫(xiě)法就不會(huì),這個(gè)就需要了解下Makefile中 := 和 += 的語(yǔ)法區(qū)別了,之前寫(xiě)過(guò)一篇文章,感興趣的可以了解下。
總之,當(dāng)時(shí)覺(jué)得這個(gè)問(wèn)題真的有點(diǎn)狗血,大家引以為戒。
4.2 分享幾個(gè)常用于排查編譯問(wèn)題的方法
4.2.1 打開(kāi)編譯過(guò)程的完整log輸出
為什么要這么做?
因?yàn)檫@樣你能看到你的編譯構(gòu)建環(huán)境傳遞給編譯器的具體細(xì)節(jié),從而了解更多編譯器的行為。
4.2.1.1 Makefile構(gòu)建環(huán)境下
一般做編譯構(gòu)建的時(shí)候,為了保持編譯log輸出的整潔性,都會(huì)把編譯的完整輸出默認(rèn)關(guān)閉,你能看到的就是類似這樣的:
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,也可能是其他。
那么如何打開(kāi)編譯log的完整輸出呢?你可以嘗試下如下命令,在輸入make的時(shí)候,添加一下控制變量:
make V=1
或
make VERBOSE=1
這樣,你看到的編譯輸出就是非常完整的,具體到你用了哪個(gè)編譯器,傳入了哪些參數(shù),一看便知,只不過(guò)log是真的多而長(zhǎng),考驗(yàn)?zāi)愕膶?duì)log信息的檢索能力的時(shí)候來(lái)了。
"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
這個(gè)輸出log是gcc的,所以得對(duì)gcc的編譯參數(shù)有所了解才行,比如-Dxxx表示宏定義,-Ixxx表示頭文件搜索目錄等等。
注意:make加V=1或VERBOSE=1,是一般寫(xiě)得比較好的Makefile都會(huì)這么做,但不代表每個(gè)寫(xiě)Makefile的人都會(huì)這么做。
4.2.1.2 CMake構(gòu)建環(huán)境下
方法一:
與Makefile類似,CMake也有完整輸出的開(kāi)關(guān),在輸入make之后,增加一個(gè)控制變量:
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
其實(shí)CMake最后的本質(zhì)就是Makefile。
方法二:
在CMakeLists.txt配置文件的相應(yīng)位置中,新增以下設(shè)置項(xiàng),也可以達(dá)到上面的效果。
-
set ( CMAKE_VERBOSE_MAKEFILE on )
注意:有了這個(gè)配置之后呢,就只需要輸入make了,但是它的不好之處就是,你不想完整輸出的時(shí)候,你還得去改CMakeLists.txt配置文件,使用上沒(méi)有那么方便。所以我個(gè)人推薦使用方法一。
4.2.1.3 scons構(gòu)建環(huán)境下
這個(gè)構(gòu)建方式也是RTT支持的,我們可以看下它的編譯完整輸出是怎么樣的:
scons --verbose
不加控制項(xiàng),輸出如下:
$ 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控制項(xiàng)之后的輸出:
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是我們常用的,這個(gè)還是要熟悉。
4.2.2 打開(kāi)編譯過(guò)程的中間文件的輸出
4.2.2.1 gcc編譯環(huán)境下
這個(gè)選項(xiàng)我在上一篇文章也提到過(guò),這里用一個(gè)小結(jié)再簡(jiǎn)單介紹下,對(duì)于排查編譯問(wèn)題以及排查匯編代碼級(jí)的性能問(wèn)題,用過(guò)都說(shuō)好。
這個(gè)參數(shù)就是-save-temps=obj
,我們來(lái)實(shí)踐下:
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文件都同時(shí)輸出來(lái)了。
如果工程中,只有一個(gè)main.c的源文件的話,還可以這樣就一步搞定。
gcc main.c -o test -save-temps=obj
這些.i文件、.s文件、以及.o文件,我們稱之為中間臨時(shí)文件。
總結(jié):
- 查看預(yù)處理之后的代碼文件,請(qǐng)看.i文件 (再?gòu)?fù)雜的條件編譯你也不怕,在這個(gè)文件里面,全部暴露原型)
- 查看C代碼生成的對(duì)應(yīng)匯編代碼,請(qǐng)看.s文件
- 查看C代碼對(duì)應(yīng)的符號(hào)表信息,請(qǐng)看.o文件
4.2.2.2 KEIL構(gòu)建環(huán)境下
其實(shí)KEIL里面也有對(duì)應(yīng)的設(shè)置項(xiàng),我手上沒(méi)有現(xiàn)成的IDE環(huán)境,我網(wǎng)上找了一篇文章,介紹得還不錯(cuò),大家可以參考下。
4.3 友情提醒
生命有限,有效編碼。
請(qǐng)尊重你自己寫(xiě)的每一行代碼。
請(qǐng)保證你的代碼編譯永遠(yuǎn)都是:0 warning 0 error 0 bug 。
5 新年祝福
愿大家新的一年,如虎添翼,展翅高飛,2022,逐夢(mèng)起航!
6 更多分享
歡迎關(guān)注我的github倉(cāng)庫(kù)01workstation,日常分享一些開(kāi)發(fā)筆記和項(xiàng)目實(shí)戰(zhàn),歡迎指正問(wèn)題。
同時(shí)也非常歡迎關(guān)注我的專欄,有問(wèn)題的話,可以跟我討論,知無(wú)不答,謝謝大家。
審核編輯 黃昊宇
-
C語(yǔ)言
+關(guān)注
關(guān)注
180文章
7604瀏覽量
136686 -
GCC
+關(guān)注
關(guān)注
0文章
107瀏覽量
24835 -
編譯
+關(guān)注
關(guān)注
0文章
657瀏覽量
32852
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論