通過這篇文章瞭解c/c++編譯器的基本使用,能夠在後續移植第三方框架進行交叉編譯時(編譯android可用的庫),清楚的瞭解應該傳遞什麼引數,怎麼傳遞引數給編譯器,各個引數的意義是什麼,從而為後面音視訊的深入學習編譯ffmpeg做好準備工作。
交叉編譯
交叉編譯就是程式的編譯環境和實際執行環境不一致,即在一個平臺上生成另一個平臺上的可執行程式碼。
比如NDK,你在Mac、Win或者Linux上生成的C/C++的程式碼要在Android平臺上執行,就需要使用到交叉編譯了。
通俗點說就是你的電腦和手機使用的CPU不同,所以CPU的指令集就不同,比如arm的指令集在X86上就不能執行。
常用的編譯工具鏈
gcc GNU C編譯器。原本只能處理C語言,很快擴充套件,變得可處理C++。(GNU計劃,又稱革奴計劃。目標是建立一套完全自由的作業系統)
Android在
NDK r18
之後徹底移除了gcc,預設使用clang編譯,所以使用不同版本的ndk對ffmpeg進行交叉編譯時會出現同樣的指令碼在舊版的ndk能編譯通過,但是舊版的就不編譯不通過的問題。
筆者會在後面的學習過程中使用最新的ndk對最新版的ffmpeg進行交叉編譯,並且會通過文章記錄學習過程,感興趣的同學可以持續關注。
g++ GNU c++編譯器
gcc和g++都能夠編譯c/c++,但是編譯時候行為不同。
對於gcc與g++會有以下區別:
-
字尾為.c的原始檔,gcc把它當作是C程式,而g++當作是C++程式;字尾為.cpp的,兩者都會認為是c++程式
-
g++會自動連結c++標準庫stl,gcc不會
-
gcc不會定義__cplusplus巨集,而g++會
clang clang 是一個C、C++、Object-C的輕量級編譯器。基於LLVM (LLVM是以C++編寫而成的構架編譯器的框架系統,可以說是一個用於開發編譯器相關的庫)
對比gcc,clang具有編譯速度更快、編譯產出更小等優點,但是某些軟體在使用clang編譯時候因為原始碼中內容的問題會出現錯誤。
另外clang++也是一個編譯器,clang++與clang就相當於gcc與g++的區別。
靜態庫和動態庫
-
靜態庫是指編譯連結時,把庫檔案的程式碼全部加入到可執行檔案中,因此生成的檔案比較大,但在執行時也就不再需要庫檔案了。Linux中字尾名為”.a”。
-
動態庫則與靜態庫相反,在編譯連結時並沒有把庫檔案的程式碼加入到可執行檔案中,而是在程式執行時由執行時連結檔案載入庫。Linux中字尾名為”.so”。gcc在編譯時預設使用動態庫。
總結起來就是靜態庫節省執行時間,動態庫節省執行空間,典型的時間換空間,在開發過程中可根據情況自行選擇。
Java中在不經過封裝的情況下只能直接使用動態庫。
編譯器過程
一個C/C++檔案要經過預處理(preprocessing)、編譯(compilation)、彙編(assembly)、和連線(linking)才能變成可執行檔案。
我們以最簡單的一個c語言程式來做一個例子:
#include <stdio.h>
int main(){
printf("hello c world\r\n");
return 0;
}
複製程式碼
- 預處理
gcc -E main.c -o main.i
-E的作用是讓gcc在預處理結束後停止編譯。
預處理階段主要處理include和define等。它把#include包含進來的.h 檔案插入到#include所在的位置,把源程式中使用到的用#define定義的巨集用實際的字串代替。
- 編譯階段
gcc -S main.i -o main.s
-S的作用是編譯後結束,編譯生成了彙編檔案。
在這個階段中,gcc首先要檢查程式碼的規範性、是否有語法錯誤等,以確定程式碼的實際要做的工作,在檢查無誤後,gcc把程式碼翻譯成組合語言。
- 彙編階段
gcc -c main.s -o main.o
彙編階段把 .s檔案翻譯成二進位制機器指令檔案.o,這個階段接收.c, .i, .s的檔案都沒有問題。
- 連結階段
gcc -o main main.s
連結階段,連結的是函式庫。在main.c中並沒有定義”printf”的函式實現,且在預編譯中包含進的”stdio.h”中也只有該函式的宣告。系統把這些函式實現都被做到名為libc.so的動態庫。
一步到位:
gcc main.c -o main
到這裡我們成功的在 mac 平臺生成了可執行檔案,執行./main
即可看到輸出。試想一下我們可以將這個可執行檔案拷貝到安卓手機上執行嗎?
肯定是不行的,主要原因是兩個平臺的 CPU 指令集不一樣,根本就無法識別指令,這時候交叉編譯就排上用場了。
如果你不信可以把 main 可執行檔案 push 到手機 /data/local/tmp 裡面執行驗證一下能否正確輸出。
也不一定必須要是/data/local/tmp這個路徑,push到任意一個有可讀可寫可執行的許可權的目錄下測試均可。
交叉編譯實驗
下面我們使用ndk來對main.c進行交叉編譯,看看編譯後的可執行檔案是不是真的能在Android上執行。
筆者這裡以armeabi為例,在mac平臺上進行交叉編譯。
既然是gcc被移除了,那我們使用clang來進行交叉編譯。
- 首先找到clang工具鏈
NDK路徑/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi19-clang
複製程式碼
- 執行命令
NDK路徑/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi19-clang -o main main.c
複製程式碼
在mac平臺能能正常生成可執行檔案main
,我們將可執行檔案用push到/data/local/tmp
這個目錄下,然後使用adb執行./main
即可看到輸出hello c world
。說明我們的交叉編譯成功了。
如果不使用clang,如何gcc進行交叉編譯呢?
首先也是先找到gcc的工具鏈
NDK路徑/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc
複製程式碼
然後執行gcc編譯命令
NDK路徑/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc -o main main.c
複製程式碼
我們發現報錯了
我們通過引數告訴gcc工具鏈到那個目錄下去尋找標頭檔案,傳遞引數進去再次試一下
NDK路徑/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc --sysroot=NDK路徑/platforms/android-21/arch-arm -isystem NDK路徑/sysroot/usr/include -pie -o main main.c
複製程式碼
還是報錯
因為找不到<asm/types.h>標頭檔案,我們進去-isystem配置的標頭檔案查詢目錄中發現aarch64-linux-android和arm-linux-androideabi都存在asm的子目錄,所以編譯器就不知道用那個了,我們再指定一下即可。NDK路徑/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc --sysroot=NDK路徑/platforms/android-21/arch-arm -isystem NDK路徑/sysroot/usr/include -isystem NDK路徑/sysroot/usr/include/arm-linux-androideabi -pie -o main main.c
複製程式碼
終於成功了,我們將可執行檔案push到手機的/data/local/tmp
這個目錄下,然後使用adb執行./main
即可看到輸出hello c world
。
在這裡我們使用了clang和gcc進行了交叉編譯發現clang更加的簡單,直接找到工具鏈的路徑即可進行編譯了,但是gcc就比較複雜了,需要指定多個引數。
這裡需要需要我們明白每個引數的意思是什麼:
--sysroot=XX
使用xx作為這一次編譯的標頭檔案與庫檔案的查詢目錄,查詢下面的 usr/include usr/lib目錄
-isysroot XX
標頭檔案查詢目錄,覆蓋--sysroot ,查詢 XX/usr/include
-isystem XX
指定標頭檔案查詢路徑(直接查詢根目錄)
-IXX
標頭檔案查詢目錄
優先順序:
-I -> -isystem -> sysroot (前面的優先順序更高)
複製程式碼
例如 gcc --sysroot=目錄1 -isysroot 目錄2 -isystem 目錄3 -I目錄4 main.c
的意思就是 查詢 目錄1/usr/lib 的庫檔案、 查詢目錄2 /usr/include 的標頭檔案、 查詢 目錄3 下的標頭檔案、 查詢 目錄4 下的標頭檔案。
-L:XX
指定庫檔案查詢目錄
-lxx.so
指定需要連結的庫名
複製程式碼
例如:
gcc -L目錄1 -l庫名
連結ndk的日誌庫:
gcc -LC:NDK路徑\platforms\android-21\arch-arm\usr\lib
-llog -lGLESv2
或者是 gcc --sysroot=NDK路徑\platforms\android-21\arch-arm -llog -lGLESv2
生成動態庫
gcc -fPIC -shared main.c -o libTest.so
或者
clang -fPIC -shared main.c -o libTest.so
即使不加-fPIC也可以生成.so檔案,但是對於原始檔有要求,
因為不加fPIC編譯的so必須要在載入到使用者程式的地址空間時重定向所有目標地址,所以在它裡面不能引用其它地方的程式碼。
要想驗證編譯出來的so動態庫能不能正常使用,通過JNI呼叫測試即可。
最後如果你對音視訊開發感興趣可掃碼關注,後續我們共同探討,共同進步。