戰(zhàn)指南:從編譯流程到工具鏈驗(yàn)證的完整路徑)
簡(jiǎn)介GCCGNU Compiler Collection是 C/C 開(kāi)發(fā)中最常用的開(kāi)源編譯器這份 zip 壓縮包為 Windows 環(huán)境下的 MinGW 版 GCC 工具鏈面向需要在 Linux/Windows 上編寫(xiě)、編譯和調(diào)試 C/C 程序的開(kāi)發(fā)者。包內(nèi)共 1508 個(gè)文件大小約 71.1MB核心包括 700 余個(gè)標(biāo)準(zhǔn)頭文件.h和 200 余個(gè) C 頭文件.hpp以及 190 個(gè)靜態(tài)庫(kù).a、20 個(gè)動(dòng)態(tài)庫(kù).dll和 78 個(gè)可執(zhí)行工具.exe同時(shí)附帶 man 幫助文檔、許可證文件、MinGW 環(huán)境配置腳本等基本覆蓋 GNU 工具鏈的完整組成部分。已有 497 人學(xué)習(xí)下載比較適合剛接觸命令行編譯、想擺脫 IDE 限制或需要跨平臺(tái)編譯環(huán)境的初學(xué)者參考。通過(guò)解壓并配置環(huán)境變量即可直接調(diào)用 gcc/g 完成預(yù)處理、編譯、匯編、鏈接全過(guò)程包內(nèi)頭文件可隨時(shí)查閱標(biāo)準(zhǔn)庫(kù)接口靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)則為程序鏈接和分發(fā)提供了便利整體上是一套開(kāi)箱即用的 C/C 編譯基礎(chǔ)資源。1. GCC編譯器資源落地為什么C/C開(kāi)發(fā)者的工具鏈核心繞不開(kāi)它不少做C/C開(kāi)發(fā)的工程師都有過(guò)類(lèi)似的場(chǎng)景代碼在IDE里點(diǎn)幾個(gè)按鈕就能跑可真到部署到服務(wù)器、嵌入式設(shè)備或者要跨平臺(tái)交叉編譯時(shí)圖形界面就幫不上忙了最后兜底的還是命令行下的gcc編譯器。這份資源沒(méi)有花哨的東西核心目標(biāo)是把C/C標(biāo)準(zhǔn)下最常用的一套gcc工具鏈、標(biāo)準(zhǔn)庫(kù)關(guān)聯(lián)和編譯參數(shù)整理成可以直接拿來(lái)用的起點(diǎn)。它解決的是實(shí)際問(wèn)題你拿到一份陌生工程怎么快速確認(rèn)編譯器、鏈接器、標(biāo)準(zhǔn)庫(kù)都正常怎么用最合適的參數(shù)把它編起來(lái)、跑起來(lái)。適合剛轉(zhuǎn)向命令行編譯的初學(xué)者也適合需要頻繁換環(huán)境的熟手拿來(lái)做基線檢查。2. 編譯鏈與標(biāo)準(zhǔn)支持從源碼到可執(zhí)行文件的四步拆解2.1 預(yù)處理、編譯、匯編、鏈接GCC四步流程細(xì)看很多人在命令行里只會(huì)用一條gcc hello.c -o hello不知道這條命令背后其實(shí)拆成了四件事預(yù)處理、編譯、匯編、鏈接。我一般在拿到陌生環(huán)境時(shí)不急著編整個(gè)項(xiàng)目而是先寫(xiě)一個(gè)最小的hello.c強(qiáng)制走一遍中間過(guò)程把每個(gè)階段的產(chǎn)物翻開(kāi)看一眼。mkdir -p /tmp/gcc_demo cd /tmp/gcc_demo cat hello.c EOF #include stdio.h #define GREETING Hello, GCC\n int main(void) { printf(GREETING); return 0; } EOF gcc -E hello.c -o hello.i # 預(yù)處理頭文件展開(kāi) 宏替換 wc -l hello.i # 展開(kāi)之后的行數(shù)會(huì)比源碼多很多 gcc -S hello.i -o hello.s # 編譯生成匯編代碼 head -20 hello.s # 看一眼匯編長(zhǎng)什么樣 gcc -c hello.s -o hello.o # 匯編生成目標(biāo)文件 file hello.o # 目標(biāo)文件是 ELF 的不可執(zhí)行類(lèi)型 gcc hello.o -o hello # 鏈接解析符號(hào)生成可執(zhí)行文件 ./hello邏輯上第一步-E做的是把stdio.h的內(nèi)容原樣展開(kāi)到.i文件里同時(shí)把GREETING替換成字符串字面量。第二步-S負(fù)責(zé)把 C 代碼翻譯成當(dāng)前平臺(tái)的匯編指令像printf的調(diào)用在 x86_64 平臺(tái)會(huì)體現(xiàn)為call指令。第三步-c把匯編變成機(jī)器碼此時(shí).o文件里還保留著printf的未定義引用。最后一步鏈接器負(fù)責(zé)把printf的地址從 C 標(biāo)準(zhǔn)庫(kù)通常就是 libc 里的實(shí)現(xiàn)解析進(jìn)來(lái)湊成一個(gè)能直接執(zhí)行的文件。這里有個(gè)對(duì)新手很有用的習(xí)慣如果某天你發(fā)現(xiàn)宏展開(kāi)結(jié)果和自己的預(yù)期不符或者某個(gè)#ifdef分支走錯(cuò)了就去把gcc -E的產(chǎn)物存下來(lái)用編輯器搜索關(guān)鍵宏。不要在源碼里猜.i文件就是最直接的證據(jù)。對(duì)熟手來(lái)說(shuō)-S看匯編也是排查性能問(wèn)題的手段之一比如循環(huán)體內(nèi)的函數(shù)調(diào)用有沒(méi)有被內(nèi)聯(lián)-O2下有沒(méi)有自動(dòng)向量化一眼就能看出來(lái)。2.2 標(biāo)準(zhǔn)怎么定-std參數(shù)直接決定哪些語(yǔ)法是合法的-std是讓很多初學(xué)者蒙圈的地方。C 語(yǔ)言有 C89/C99/C11/C17/C23C 有 C98/11/14/17/20/23GCC 如果不顯式指定通常默認(rèn)采用一個(gè)較新的標(biāo)準(zhǔn)但默認(rèn)值隨編譯器版本走。問(wèn)題在于不同發(fā)行版、不同工具鏈版本默認(rèn)標(biāo)準(zhǔn)不同同一段代碼在這個(gè)機(jī)器上能編換臺(tái)機(jī)器就報(bào)語(yǔ)法錯(cuò)誤。這就是為什么要顯式寫(xiě)-std。一個(gè)典型例子是 for 循環(huán)內(nèi)聲明變量。這段代碼在 C89 標(biāo)準(zhǔn)下直接不合法/* for_loop.c */ #include stdio.h int main(void) { for (int i 0; i 3; i) { printf(%d\n, i); } return 0; }gcc -stdc89 for_loop.c -o for_loop # 報(bào)錯(cuò)for loop initial declarations only allowed in C99 gcc -stdc99 for_loop.c -o for_loop # 通過(guò)這說(shuō)明-std不是可有可無(wú)的裝飾它會(huì)改變編譯器對(duì)語(yǔ)法的判定規(guī)則。默認(rèn)標(biāo)準(zhǔn)不指定時(shí)GCC 實(shí)際用的是帶 GNU 擴(kuò)展的標(biāo)準(zhǔn)比如gnu17它會(huì)額外允許typeof、語(yǔ)句表達(dá)式等非 ISO 語(yǔ)法。你代碼里一旦用了這些擴(kuò)展換到別的編譯器可能就編不過(guò)。對(duì)于跨平臺(tái)工程我一般會(huì)把-stdc11或者-stdc17固定寫(xiě)進(jìn)構(gòu)建腳本而不是依賴(lài)默認(rèn)值。C 那邊同理。C11 引入了nullptrC14 有了泛型 LambdaC17 支持結(jié)構(gòu)化綁定C20 加了概念和協(xié)程。寫(xiě)代碼時(shí)按哪個(gè)標(biāo)準(zhǔn)來(lái)寫(xiě)直接決定你能用什么特性。驗(yàn)證標(biāo)準(zhǔn)也很簡(jiǎn)單用宏看編譯器支持情況echo | gcc -dM -E - | grep __STDC_VERSION__ # 打印 C 標(biāo)準(zhǔn)版本宏 echo | g -dM -E -x c - | grep __cplusplus # 打印 C 標(biāo)準(zhǔn)版本宏輸出結(jié)果里__STDC_VERSION__如果是 201112L就代表 C11如果是 201710L就代表 C17。__cplusplus的值 201703L 對(duì)應(yīng) C17。這個(gè)命令是排查編譯環(huán)境最基礎(chǔ)的驗(yàn)證動(dòng)作格版本號(hào)也不用猜。2.3 頭文件搜索路徑與多文件工程-I、-L、-l 的配合工程一旦大起來(lái)源碼不會(huì)全堆在一個(gè)目錄。GCC 默認(rèn)只在系統(tǒng)目錄找頭文件比如/usr/include、/usr/local/include項(xiàng)目自己的include目錄必須用-I顯式加進(jìn)去。鏈接庫(kù)同理.a或.so文件所在目錄用-L指定具體的庫(kù)名用-l指定。gcc -I./include -L./lib -lmylib app.c -o app這里的規(guī)則是-I./include告訴編譯器頭文件在include目錄下-L./lib告訴鏈接器庫(kù)文件在lib目錄下-lmylib表示鏈接名為libmylib.a或libmylib.so的庫(kù)lib前綴和.a、.so后綴可以省略。還有個(gè)細(xì)節(jié)#include xxx.h和#include xxx.h的搜索順序不一樣前者優(yōu)先搜索-I路徑和系統(tǒng)路徑后者優(yōu)先搜索源碼文件所在目錄。頭文件同名沖突時(shí)這就是坑的源頭。多文件工程通常還要區(qū)分 C 和 C 兩種編譯器驅(qū)動(dòng)。.c文件用gcc編.cpp、.cc文件用g編兩者的差異在鏈接階段最明顯。C 程序哪怕只有一個(gè).cpp文件如果用gcc直接鏈接經(jīng)常會(huì)報(bào)找不到operator new這類(lèi) C 標(biāo)準(zhǔn)庫(kù)符號(hào)換成g就正常了。原因在于g鏈接時(shí)會(huì)自動(dòng)帶入 libstdc這不是玄學(xué)是編譯驅(qū)動(dòng)程序的默認(rèn)鏈接參數(shù)不同。3. 編譯選項(xiàng)實(shí)戰(zhàn)優(yōu)化級(jí)別、警告控制與調(diào)試信息的最佳配置3.1 優(yōu)化級(jí)別從 -O0 到 -O3、-Os、-Ofast 怎么選優(yōu)化選項(xiàng)對(duì)最終生成的二進(jìn)制影響巨大卻常被新人忽略。GCC 的優(yōu)化不是簡(jiǎn)單把代碼改快而是會(huì)在編譯期做指令重排、循環(huán)展開(kāi)、內(nèi)聯(lián)、常量折疊等一系列變換。這意味著優(yōu)化級(jí)別越高編譯時(shí)間越長(zhǎng)調(diào)試信息與源碼行的對(duì)應(yīng)關(guān)系越模糊。選項(xiàng)行為適用場(chǎng)景-O0不做優(yōu)化編譯最快調(diào)試體驗(yàn)最好日常 Debug、單步調(diào)試-O1基本優(yōu)化二進(jìn)制略小執(zhí)行略快輕度調(diào)試 性能兼顧-O2除空間換時(shí)間外的大多數(shù)優(yōu)化發(fā)布常用發(fā)布版通用選擇-O3在 -O2 基礎(chǔ)上加更激進(jìn)優(yōu)化可能增大體積計(jì)算密集、明確不需要調(diào)試-Os針對(duì)二進(jìn)制體積優(yōu)化嵌入式、存儲(chǔ)受限設(shè)備-Ofast-O3 基礎(chǔ)上忽略嚴(yán)格標(biāo)準(zhǔn)合規(guī)性追求極致性能慎用我自己常用的組合是調(diào)試用-O0 -g發(fā)布用-O2 -g出問(wèn)題時(shí)用-O0 -fsanitizeaddress復(fù)現(xiàn)。-Ofast我極少用因?yàn)樗鼤?huì)開(kāi)啟-ffast-math對(duì)浮點(diǎn)運(yùn)算結(jié)果會(huì)產(chǎn)生不符合 IEEE 754 標(biāo)準(zhǔn)的改變涉及跨平臺(tái)數(shù)據(jù)交換時(shí)容易翻車(chē)。驗(yàn)證優(yōu)化效果不需要跑到實(shí)際業(yè)務(wù)里一個(gè)簡(jiǎn)單函數(shù)就能看出差異int sum(int n) { int total 0; for (int i 0; i n; i) total i; return total; }gcc -O0 -c sum.c -o sum_O0.o gcc -O2 -c sum.c -o sum_O2.o objdump -d sum_O0.o | grep -A5 sum objdump -d sum_O2.o | grep -A5 sum用objdump對(duì)比兩個(gè)目標(biāo)文件的匯編你會(huì)發(fā)現(xiàn)-O2版本很可能直接計(jì)算出數(shù)學(xué)公式循環(huán)整個(gè)被優(yōu)化掉了??吹竭@種差異就不會(huì)再糾結(jié)優(yōu)化是不是只影響執(zhí)行速度這種層面了。3.2 警告控制-Wall、-Wextra、-Wpedantic 與 -Werror 的取舍GCC 的警告選項(xiàng)是分層的不是簡(jiǎn)單開(kāi)關(guān)。-Wall名字看著像“所有警告”實(shí)際只開(kāi)了一部分常見(jiàn)的比如未使用變量、隱式函數(shù)聲明。-Wextra補(bǔ)充了更多比如 signed/unsigned 比較、多余逗號(hào)。-Wpedantic強(qiáng)制警告那些不符合 ISO C/C 標(biāo)準(zhǔn)但 GCC 允許的寫(xiě)法。gcc -Wall -Wextra -Wpedantic app.c -o app實(shí)踐中我見(jiàn)到的工程大多會(huì)開(kāi)-Wall -Wextra但-Werror就要小心了。把警告變錯(cuò)誤的好處是 CI 不會(huì)放過(guò)任何一個(gè)潛在問(wèn)題代價(jià)是換編譯器版本時(shí)新版本新增的警告會(huì)讓整個(gè)項(xiàng)目一夜之間編不過(guò)。剛把項(xiàng)目從一套舊工具鏈遷到新版時(shí)最常見(jiàn)的翻車(chē)現(xiàn)場(chǎng)就是這里。我一般推薦的策略是開(kāi)發(fā)階段開(kāi)-Wall -Wextra但不開(kāi)-Werror讓警告作為提示發(fā)布構(gòu)建再加-Werror并且配合-Wno-unused-function這類(lèi)選項(xiàng)排除已知無(wú)害的警告范圍。這樣既能擋住真問(wèn)題又不會(huì)因?yàn)槟硞€(gè)無(wú)關(guān)函數(shù)沒(méi)用到就阻塞發(fā)布。3.3 調(diào)試信息-g、-g3、-ggdb3 的顆粒度差異調(diào)試信息不是只有開(kāi)和關(guān)兩檔。默認(rèn)的-g會(huì)生成行號(hào)、局部變量、函數(shù)簽名等信息這夠大部分場(chǎng)景用了。-g3在-g基礎(chǔ)上額外包含宏定義信息你用 gdb 調(diào)試時(shí)可以info macro GREETING查看宏展開(kāi)。-ggdb3則是專(zhuān)門(mén)為 gdb 優(yōu)化格式的精簡(jiǎn)檔位常用于配合 GDB 的宏調(diào)試擴(kuò)展。gcc -O0 -g3 app.c -o app_debug gdb ./app_debug (gdb) info macro GREETING一個(gè)常見(jiàn)誤解是-g會(huì)生成體積很大的二進(jìn)制所以發(fā)布版不加-g。實(shí)際上發(fā)布版保留-g并不影響運(yùn)行性能只是文件體積大一些。線上出崩潰時(shí)readelf 和 addr2line 都能基于調(diào)試信息把指令地址翻譯回源碼行。我發(fā)布版一直保留-g代價(jià)是多占磁盤(pán)收益是線上問(wèn)題排查從黑匣子變成可回溯。3.4 靜態(tài)庫(kù)與動(dòng)態(tài)庫(kù)生成ar、-fPIC、-shared 組合把通用代碼打成庫(kù)是工程化必須掌握的技能。靜態(tài)庫(kù)本質(zhì)是一堆.o文件的歸檔動(dòng)態(tài)庫(kù)則需要編譯成位置無(wú)關(guān)代碼。# 生成靜態(tài)庫(kù) gcc -c math_ops.c -o math_ops.o ar rcs libmath_ops.a math_ops.o gcc app.c -L. -lmath_ops -o app_static # 生成動(dòng)態(tài)庫(kù) gcc -fPIC -shared math_ops.c -o libmath_ops.so gcc app.c -L. -lmath_ops -o app_dynamic靜態(tài)庫(kù)生成時(shí)ar rcs中的r表示插入或替換成員c表示創(chuàng)建歸檔s表示寫(xiě)入索引。動(dòng)態(tài)庫(kù)的-fPIC必須加因?yàn)楣蚕韼?kù)需要支持被加載到進(jìn)程地址空間的任意位置如果編譯時(shí)用的是絕對(duì)地址動(dòng)態(tài)裝載器就沒(méi)法重定位。-shared表示生成.so而非可執(zhí)行文件。鏈接動(dòng)態(tài)庫(kù)后的可執(zhí)行文件運(yùn)行時(shí)會(huì)根據(jù)LD_LIBRARY_PATH和系統(tǒng)默認(rèn)路徑去查找.so這個(gè)查找順序是若干個(gè)坑的總源頭下一章展開(kāi)講。4. GCC常見(jiàn)問(wèn)題避坑鏈接失敗、動(dòng)態(tài)庫(kù)缺失與優(yōu)化級(jí)陷阱4.1 編譯通過(guò)卻鏈接失敗undefined reference現(xiàn)象源文件編譯生成.o一切正常最后鏈接時(shí)報(bào)undefined reference to xxx。新手最容易遇到因?yàn)閳?bào)錯(cuò)信息里的函數(shù)名和代碼里一樣但看不出問(wèn)題在哪。原因最常見(jiàn)的是函數(shù)有聲明沒(méi)定義比如頭文件里聲明了int calc(int);某個(gè).c文件里調(diào)用了它卻沒(méi)有源文件實(shí)現(xiàn)。還有一種情況是庫(kù)鏈接順序不對(duì)靜態(tài)庫(kù)的解析是從左到右的被依賴(lài)的庫(kù)必須放在依賴(lài)它的對(duì)象之后。解決先nm xxx.o | grep U 查看該目標(biāo)文件有那些未定義符號(hào)確認(rèn)函數(shù)是否真的沒(méi)實(shí)現(xiàn)。庫(kù)順序問(wèn)題用gcc main.o -lmylib -o app把被依賴(lài)庫(kù)放在后面。復(fù)雜項(xiàng)目如果庫(kù)互相依賴(lài)直接多個(gè)-l參數(shù)各寫(xiě)一遍。4.2 -O2 下程序輸出詭異未定義行為在作祟現(xiàn)象同樣的代碼-O0跑得好好的開(kāi)-O2之后就崩潰或者輸出錯(cuò)亂而且位置不固定。原因代碼里存在未定義行為典型包括有符號(hào)整數(shù)溢出、數(shù)組越界、懸空指針、同一表達(dá)式內(nèi)對(duì)同一變量讀寫(xiě)兩次。優(yōu)化器默認(rèn)認(rèn)為代碼沒(méi)有未定義行為所以它會(huì)基于這個(gè)假設(shè)做變換把潛在的 UB 放大成可見(jiàn)錯(cuò)誤。解決不要靠猜直接用 GCC 的消毒器復(fù)現(xiàn)。gcc -O0 -fsanitizeaddress,undefined app.c -o app_sanitize運(yùn)行時(shí)它會(huì)精確打印哪一行代碼觸發(fā)了問(wèn)題。從那以后遇到優(yōu)化級(jí)下的詭異問(wèn)題我第一反應(yīng)永遠(yuǎn)是開(kāi)檢測(cè)器而不是換優(yōu)化級(jí)別。4.3 動(dòng)態(tài)庫(kù)運(yùn)行時(shí)找不到cannot open shared object file現(xiàn)象鏈接時(shí)-lmath_ops沒(méi)問(wèn)題./app_dynamic運(yùn)行時(shí)報(bào)error while loading shared libraries: libmath_ops.so: cannot open shared object file。原因鏈接器找到.so只負(fù)責(zé)鏈接期解析運(yùn)行時(shí)動(dòng)態(tài)裝載器按RPATH、LD_LIBRARY_PATH、系統(tǒng)緩存路徑的順序查找。如果.so在程序目錄旁邊這些路徑里都沒(méi)有自然找不到。解決臨時(shí)用LD_LIBRARY_PATH. ./app_dynamic驗(yàn)證長(zhǎng)期用編譯期固定路徑gcc app.c -L. -lmath_ops -Wl,-rpath,$ORIGIN -o app_dynamic$ORIGIN是運(yùn)行時(shí)實(shí)際所在目錄把庫(kù)放在可執(zhí)行文件同目錄就不會(huì)丟。發(fā)行機(jī)器上也可以用ldconfig配置系統(tǒng)緩存但更新系統(tǒng)后容易被打回原形我一般優(yōu)先rpath。4.4 C和C混編鏈接符號(hào)被改名導(dǎo)致找不到C函數(shù)現(xiàn)象C 代碼調(diào)用一個(gè)用 C 寫(xiě)的庫(kù)函數(shù)報(bào)undefined reference to c_func明明鏈接參數(shù)、頭文件路徑都正確。原因C 支持函數(shù)重載所以編譯器在生成符號(hào)名時(shí)會(huì)加入?yún)?shù)類(lèi)型信息這個(gè)過(guò)程叫名字修飾。而 C 語(yǔ)言沒(méi)有重載函數(shù)符號(hào)名就是函數(shù)名本身。GCC 默認(rèn)按當(dāng)前源語(yǔ)言來(lái)決定是否修飾C 文件里聲明 C 函數(shù)時(shí)如果沒(méi)有特殊標(biāo)記鏈接器找的是修飾后的符號(hào)找不到原始的 C 符號(hào)。解決在 C 文件里用extern C包裹 C 函數(shù)聲明或者讓 C 庫(kù)頭文件自身兼容 C#ifdef __cplusplus extern C { #endif int c_func(int a); #ifdef __cplusplus } #endif__cplusplus宏在 C 編譯下才會(huì)定義C 編譯時(shí)此宏不存在這樣同一份頭文件對(duì)兩種語(yǔ)言都能正常使用。庫(kù)接口如果用了這個(gè)標(biāo)準(zhǔn)寫(xiě)法普通用戶只要正常#include就不會(huì)踩坑。4.5 -Werror 遇新編譯器崩盤(pán)新增警告一夜之間變錯(cuò)誤現(xiàn)象項(xiàng)目在一個(gè)環(huán)境編得很好CI 跑在另一個(gè)環(huán)境升級(jí)完編譯器或換發(fā)行版后原本只是警告的提示全變成錯(cuò)誤構(gòu)建直接失敗。原因新版本 GCC 會(huì)不斷增加新的警告類(lèi)別比如舊版沒(méi)啟用-Wstringop-overflow新版默認(rèn)開(kāi)啟。當(dāng)你開(kāi)-Werror時(shí)這些新警告就成了新錯(cuò)誤。同一份代碼沒(méi)有改動(dòng)純粹是編譯器變嚴(yán)了。解決不要關(guān)閉-Werror或一口氣全關(guān)按警告類(lèi)別精準(zhǔn)壓制。先用gcc -Wall -Wextra ... -o /dev/null 2 warnings.log把當(dāng)前所有警告輸出到文件識(shí)別新警告類(lèi)別再用-Wno-具體類(lèi)別排除。比如-Wno-unused-parameter只消除未使用參數(shù)的誤報(bào)警告其他警告仍然保持錯(cuò)誤級(jí)別。升級(jí)編譯器后先跑一次warnings.log對(duì)比比在 CI 上頻繁試錯(cuò)效率高得多。5. 驗(yàn)證GCC工具鏈的完整性一個(gè)腳本確認(rèn)編譯器、標(biāo)準(zhǔn)和鏈接都可用拿到一個(gè)新環(huán)境或者裝好一套 GCC 工具鏈后最好一次性確認(rèn)三件事編譯器能跑、標(biāo)準(zhǔn)版本符合預(yù)期、鏈接器能正常生成可執(zhí)行文件。下面是我一直用的自檢腳本直接存成一個(gè).sh文件執(zhí)行即可#!/bin/bash set -e echo C 編譯器與標(biāo)準(zhǔn) gcc --version | head -n 1 echo | gcc -dM -E - | grep __STDC_VERSION__ echo C 編譯器與標(biāo)準(zhǔn) g --version | head -n 1 echo | g -dM -E -x c - | grep __cplusplus echo 鏈接與運(yùn)行驗(yàn)證 cat /tmp/gcc_self_check.c EOF #include stdio.h int main(void) { printf(gcc toolchain ok\n); return 0; } EOF gcc /tmp/gcc_self_check.c -o /tmp/gcc_self_check /tmp/gcc_self_check rm -f /tmp/gcc_self_check.c /tmp/gcc_self_check腳本前半段用-dM -E -讓預(yù)處理器只輸出宏定義而不實(shí)際編譯grep過(guò)濾出標(biāo)準(zhǔn)版本宏。__STDC_VERSION__的值能直接告訴我 C 標(biāo)準(zhǔn)是 C11 還是 C17__cplusplus的值告訴我 C 標(biāo)準(zhǔn)是什么。后半段構(gòu)造一個(gè)真實(shí)可執(zhí)行文件驗(yàn)證編譯、鏈接到運(yùn)行的完整鏈路。如果__STDC_VERSION__的值比預(yù)期舊比如你寫(xiě)的是 C17 代碼但環(huán)境默認(rèn)只支持 C99那就說(shuō)明需要在構(gòu)建腳本里顯式加-stdc17。如果鏈接驗(yàn)證失敗說(shuō)明不只是 GCC 本體有問(wèn)題問(wèn)題可能出在缺少標(biāo)準(zhǔn)庫(kù)或者鏈接器配置錯(cuò)誤這時(shí)先查ld --version再查/usr/include、/usr/lib是否存在。環(huán)境自檢這種事熟練以后大概一分鐘就搞定。從那以后我拿到任何一臺(tái)新機(jī)器不管之前有沒(méi)有裝過(guò)開(kāi)發(fā)工具都會(huì)強(qiáng)制先跑一遍這個(gè)腳本再開(kāi)始寫(xiě)代碼省掉太多中途才發(fā)現(xiàn)工具鏈有問(wèn)題的尷尬。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取