交叉編譯入門

思想覺悟發表於2020-02-17

通過這篇文章瞭解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;
}
複製程式碼
  1. 預處理

​ gcc -E main.c -o main.i

-E的作用是讓gcc在預處理結束後停止編譯。

​ 預處理階段主要處理include和define等。它把#include包含進來的.h 檔案插入到#include所在的位置,把源程式中使用到的用#define定義的巨集用實際的字串代替。

  1. 編譯階段

​ gcc -S main.i -o main.s

-S的作用是編譯後結束,編譯生成了彙編檔案。

​ 在這個階段中,gcc首先要檢查程式碼的規範性、是否有語法錯誤等,以確定程式碼的實際要做的工作,在檢查無誤後,gcc把程式碼翻譯成組合語言。

  1. 彙編階段

​ gcc -c main.s -o main.o

彙編階段把 .s檔案翻譯成二進位制機器指令檔案.o,這個階段接收.c, .i, .s的檔案都沒有問題。

  1. 連結階段

​ 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來進行交叉編譯。

  1. 首先找到clang工具鏈
NDK路徑/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi19-clang
複製程式碼
  1. 執行命令
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
複製程式碼

我們發現報錯了

找不到stdio.h標頭檔案
這種錯誤是說在我們編的時候編譯器找不到我們引入的 stdio.h 標頭檔案,那怎麼告訴編譯器 stdio.h 標頭檔案在哪裡呢? 下面知識點說明怎麼指定這些報錯的標頭檔案

我們通過引數告訴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
複製程式碼

還是報錯

找不到types.h標頭檔案
因為找不到<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呼叫測試即可。

最後如果你對音視訊開發感興趣可掃碼關注,後續我們共同探討,共同進步。

交叉編譯入門

相關文章