c/c++編譯器配置(交叉編譯重要參數)、交叉編譯動態庫與as配置、mk初步

gcc/g++/clang,至關於javac:java

瞭解c/c++編譯器的基本使用,可以在後續移植第三方框架進行交叉編譯時,清楚的瞭解應該傳遞什麼參數。android

clang:c++

clang 是一個C、C++、Object-C的輕量級編譯器。基於LLVM (LLVM是以C++編寫而成的構架編譯器的框架系統,能夠說是一個用於開發編譯器相關的庫)shell

gcc:windows

GNU C編譯器。本來只能處理C語言,很快擴展,變得可處理C++。(GNU計劃,又稱革奴計劃。目標是建立一套徹底自由的操做系統)架構

g++:框架

GNU c++編譯器函數

gcc、g++、clang都是編譯器。工具

  • gcc和g++都可以編譯c/c++,可是編譯時候行爲不一樣。
    這塊須要特別的注意,並不是gcc是爲c而生,而g++是爲c++而生的。學習

  • clang也是一個編譯器,對比gcc,它具備編譯速度更快、編譯產出更小等優勢,可是某些軟件在使用clang編譯時候由於源碼中內容的問題會出現錯誤。

  • clang++與clang就至關於gcc與g++。

對於gcc與g++:

  1. 後綴爲.c的源文件,gcc把它看成是C程序,而g++看成是C++程序;後綴爲.cpp的,二者都會認爲是c++程序

  2. g++會自動連接c++標準庫stl,gcc不會

  3. gcc不會定義__cplusplus宏,而g++會

編譯器過程:

一個C/C++文件要通過預處理(preprocessing)、編譯(compilation)、彙編(assembly)、和鏈接(linking)才能變成可執行文件。

  • 預處理
    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的動態庫。
    函數庫通常分爲靜態庫和動態庫兩種:

    • 靜態庫是指編譯連接時,把庫文件的代碼所有加入到可執行文件中,所以生成的文件比較大,但在運行時也就再也不須要庫文件了。Linux中後綴名爲」.a」。
    • 動態庫與之相反,在編譯連接時並無把庫文件的代碼加入到可執行文件中,而是在程序執行時由運行時連接文件加載庫。Linux中後綴名爲」.so」,如前面所述的libc.so就是動態庫。gcc在編譯時默認使用動態庫。

    靜態庫節省時間:不須要再進行動態連接,須要調用的代碼直接就在代碼內部。

    動態庫節省空間:若是一個動態庫被兩個程序調用,那麼這個動態庫只須要在內存中。
    關於這二者的區別我們用一個具體的例子來形象的說明一下:
    一、假若有一個靜態庫a.a,它裏面包含一個test函數,而後有個源文件source.c,它裏面有個test1函數,而這個源文件須要連接a.a這個靜態庫,當編譯完成以後source.a裏面就擁有了test+test1兩個函數了,也就是編譯期就將全部的符號加入到.so庫中。
    二、假若有一個動態庫a.so,它裏面包含一個test函數,而後有個源文件source.c,它裏面有個test1函數,而這個源文件須要連接a.o這個動態庫,當編譯完成以後source.a裏面就只有一個test函數,在運行時會動態的加載a.so。

    注:Java中在不通過封裝的狀況下只能直接使用動態庫,也就是說:

瞭解了上面的一大堆理論以後,下面來動手作實驗,先新建一個main.c文件:

而後用gcc命令來將其編譯運行一下:

那若是將main這個可執行文件放到Android手機上,可否也能正常執行輸出呢?我們來將它放到手機的sdcard上【注意:該手機須要的文件的執行權限才行,不然會報無權限,反正我用的帶root的模擬器執行都不行,最後找了部完全root的華爲機子來作的實驗】,以下:

這是爲啥呢?實際上是Android手機的CPU不一樣,因此CPU的指令集也不一樣,要在mac上編譯出來的可執行文件能在Android上也能運行這裏就須要涉及到交叉編譯相關的東東了,其實在NDK中提供有交叉編譯工具,先進我們的SDK來瞅一下:

下面我們手動嘗試經過NDK的交互編譯工具來嘗試一下,前提固然得下載Android NDK才行,具體直接上官網下就成了,個人電腦已經下載好了,因此下面關心的就是如何來進行交叉編譯啦,首先固然得用上圖中的NDK提供的gcc啦,爲了方便使用我們先將這個gcc的文件路徑配置成臨時的環境變量,免得每次編譯時須要寫一大堆的路徑,以下:

好,我們來用它來對main.c進行編譯一下:

緣由是因爲此時須要用NDK提供的頭文件來進行連接編譯了, 那如何指定頭文件的查找路徑爲NDK提供的頭文件呢,有以下方式能夠指定,下面先來了解一下【很是重要,是編譯三方庫很是重要的知識點】

  • --sysroot=XX
    使用XX做爲這一次編譯的頭文件與庫文件的查找目錄,查找XX下面的 usr/include、usr/lib目錄。
  • -isysroot XX
    頭文件查找目錄,覆蓋--sysroot ,查找 XX/usr/include。什麼意思,好比說"gcc --sysroot=目錄1 main.c",若是main.c中依賴於頭文件和庫文件,則會到目錄1中的user/include和user/lib目錄去查找,而若是"gcc --sysroot=目錄1 -isysroot 目錄2 main.c"意味着會查找頭文件會到目錄2中查找而非--sysroot所指定的目錄1下的/usr/include了,固然查找庫文件仍是在目錄1下的user/lib目錄去查找。
  • -isystem XX
    指定頭文件查找路徑(直接查找根目錄)。好比"gcc --sysroot=目錄1 -isysroot 目錄2 -isystem 目錄3  -isystem 目錄4 main.c"意味着頭文件查找除了會到目錄2下的/usr/include,還會到isystem指定的目錄3和目錄4下進行查找,注意:這個isystem指定的目錄就是頭文件查找的全路徑,而非像isysroot所指定的目錄還須要定位到/usr/include目錄。
  • -IXX
    頭文件查找目錄。

其查找頭文件的優先級爲:
-I -> -isystem -> sysroot

好比說:「gcc --sysroot=目錄1 -isysroot 目錄2 -isystem 目錄3  -isystem 目錄4 -I目錄5 main.c」,其頭文件首先會去目錄5找,若是沒找到則會到目錄3和4找,若是還沒找到則會到目錄2找。

  • -LXX
    指定庫文件查找目錄。
  • -lxx.so
    指定須要連接的庫名。

咱們以前在寫JNI程序時用到了Android的日誌,以下:

其這個頭文件中的具體實現庫其實就是在NDK中的這個目錄裏面,以下:

其實在Android Sutdio建立支付C++工程時其實默認就將這個頭文件庫的查找在CMakeLists.txt已經進行聲明瞭,以下:

若是用參數的形式來指定庫查找目錄其實就能夠這樣寫:「gcc -L/Users/xiongwei/android-sdks/Android/sdk/ndk-bundle/platforms/android-21/arch-arm/usr/lib -llog」, 固然還能夠用--sysroot來指令庫文件的查找路徑,只是路徑指定須要在/usr/lib以前就成,如「gcc --sysroot/Users/xiongwei/android-sdks/Android/sdk/ndk-bundle/platforms/android-21/arch-arm/ -llog」。

明白了上面的參數以後,下面我們採用交叉編譯的方式來對咱們的main.c進行從新編譯,因爲NDK的路徑比較深,因此仍是採用臨時環境變量的方式來弄,如今就是要指定頭文件的查找目錄,因此頭文件的位置涉及到兩處:

因此先把這個路徑定義上:

基本上有這個路徑就能夠了,可是在NDK16或NDK17還有以下頭文件查找路徑:

因此能夠用-isystem參數來指定,以下:

另外還有一個子目錄須要指定:

固然仍是能夠用-isystem參數來指定,以下:

那我們再來加上這個CFLAGS參數編譯一下:

呃,貌似配錯了,具體是由於寫得有問題,以下:

因此修改一下:

此時就正常編譯了,我們此時用file命令來查看一下該生成的可執行程序的文件信息,能夠有個新發現:

正好就是Android能運行的指令集,因此下面我們再將這個main導到手機上,而後此次來運行看可否見證奇蹟:

而後進入到adb shell中進行執行,以下:

pie 位置無關的可執行程序:

可是有可能在windows中既始用交叉編譯在手機上執行時也會報錯,例如:

此就就須要加一個-pie參數了,以下:

因此手動交叉編譯成Android手機能正常執行的統一加"-pie"參數就成了。

費了這麼多功夫就爲了能編一個能在Android手機上執行的可執行文件有啥做用麼?做用實際上是很是之大的哈,在以後的學習中會有體現滴,明白了這個手動編譯的原理,對於未來任務三方庫要編譯成能在Android運行庫,明白了以上內容就能讓本身變得駕輕就熟,練內功滴!!

交叉編譯動態庫與as配置:

在上面我們編譯出來的並不是是一個動態庫而只是一個可執行文件,要想要在Android工程中來使用JNI就必須將期編譯成.so的動態庫,由於:

因此我們接下來利用交叉編譯工具來編成動態庫,而後在Android Studio中進行使用,下面先來編寫一個新的.c源文件:

此時須要將它編譯成動態庫,就須要加一個「-fPIC -shared」參數,具體用法以下:

因此我們來使用一下,注意仍是使用NDK提供的交叉編譯的GCC命令哈:

注意一下so的名稱是以libXXX.so爲規則,由於咱們要在Android工程中來使用它,因此將它拷到我們的Android工具當中,以下:

而後還須要建一個cpu架構相關的文件夾,相似於咱們在日常引入三方.so時同樣,好比:

因此校仿:

那接下來我們就是要在程序中來調用這個動態庫中的test()方法,因爲沒有提供這個動態庫相關的頭文件,因此可使用以下關鍵字:

代碼已經寫好了,不過要正式運行以前還得進行CMakeLists.txt一系列的配置才行,下面一步步來進行配置,首先咱們的so的目錄在編譯時是須要依賴於它可是目前尚未指令編譯時查找庫的路徑,根據以前介紹的交叉編譯的參數可使用以下:

那如何在CMakeLists.txt中來設置呢,涉及到一些規則,記住就成了,以下:

其中還有一個小技巧,就是對於CPU架構文件夾的指定可使用動態的方式而不用手動寫死,不是不一樣的CPU架構的.so都是不同的嘛:

涉及到須要修改的地方在它:

能夠改用以下這種動態的方式:

這樣當咱們要編譯其它的CPU架構時就能夠動態的替換,這裏先還原寫死的方式,待後面須要的時候再用這個動態的方式,接着來則須要指定要連接的.so庫,配置以下:

其中也就相似於寫了以下參數:

好了,接着還須要去build.gradle中進行NDK的配置,首先指定咱們要編譯的CPU架構,目前只支持"armeabi-v7a",因此配置以下:

而後編譯運行一下:

這是爲啥?其實有一個很是小的細節沒有注意形成:

再次編譯:

因此更改一下:

再次編譯,發現編譯終於木有問題了,接下來我們在MainActivity中來調用一下jni:

而後再次編譯運行:

正常輸出啦,可是有可能在其它手機上會輸出以下異常:

此時就須要在調用以前先將我們的libTest.so動態庫給加載進來,以下:

至於爲啥要再加load一次咱們的生成的Test動態庫,其實仍是跟動靜態庫有關,若是是引用的靜態庫的話就不會有這個問題,這個在以後再作實驗來講明這個問題。下面仍是回到gradle對ndk配置相關的東東,在上面作實現的工程是由於建項目時就已經勾選了支付NDK的環境,以下:

那若是對於一個沒有勾選這個支持的Android工程咱們怎麼來加入對NDK的支持呢?下面建一個全新的不支持NDK的Android工程:

而後接下來就是來在build.gradle中來進行配置將該工程變爲支持NDK的,以下:

若是不確認寫得對不對,能夠點擊看一下可否連接到源碼,若是能連接到源碼那寫得確定是對滴,以下:

 而後繼續:

這種語法其實看着不是很符合java的語法,其實也能夠用另一種面向對象的方式來配置,具體以下:

可是貌似在個人Android Studio中這種語法不支持,因此我們仍是以上面標紅的方式來配置,基本默認新建帶NDK的工程就是使用的這種方式,接下來來配置要編譯的CPU架構:

而後在外層還有一個externalNativeBuild配置,此次咱們能夠改用面向對象的方式,以下:

那這兩個NDK相關的配置有啥區別呢?實際上是有區別的:

因爲我們想經過mk的方式來進行編譯,因此能夠在外層這樣寫:

 

因此咱們能夠在src下新建一個Android.mk文件,以下:

Android.mk

微小 GNU makefile 片斷。

將源文件分組爲模塊。 模塊是靜態庫、共享庫或獨立可執行文件。 可在每一個 Android.mk 文件中定義一個或多個模塊,也可在多個模塊中使用同一個源文件。 

 

關於Android.mk的編譯腳本的編寫先日後放,這裏關於ndk配置還差一個東東,以下:

實際上是這樣的:

好,as中關於ndk的配置相關的基本已經配好了,接下來就是新建一個c/c++的源文件我們來嘗試着編譯一下:

那這個源代碼該要如何進行編譯呢?這裏就須要用到了咱們建的Android.mk這個編譯腳本文件啦,首先來熟悉一下大概的語法:

變量和宏

定義本身的任意變量。在定義變量時請注意,NDK 構建系統會預留如下變量名稱:

  •  LOCAL_ 開頭的名稱,例如 LOCAL_MODULE
  •  PRIVATE_NDK_ 或 APP 開頭的名稱。構建系統在內部使用這些變量。
  • 小寫名稱,例如 my-dir。構建系統也是在內部使用這些變量。

若是爲了方便而須要在 Android.mk 文件中定義本身的變量,建議在名稱前附加 MY_

經常使用內置變量

變量名 含義 示例
BUILD_STATIC_LIBRARY 構建靜態庫的Makefile腳本 include $(BUILD_STATIC_LIBRARY)
PREBUILT_SHARED_LIBRARY 預編譯共享庫的Makeifle腳本 include $(PREBUILT_SHARED_LIBRARY)
PREBUILT_STATIC_LIBRARY 預編譯靜態庫的Makeifle腳本 include $(PREBUILT_STATIC_LIBRARY)
TARGET_PLATFORM Android API 級別號 TARGET_PLATFORM := android-22
TARGET_ARCH CUP架構 arm arm64 x86 x86_64
TARGET_ARCH_ABI CPU架構 armeabi armeabi-v7a arm64-v8a

模塊描述變量

變量名 描述
LOCAL_MODULE_FILENAME 覆蓋構建系統默認用於其生成的文件的名稱 LOCAL_MODULE := foo LOCAL_MODULE_FILENAME := libnewfoo
LOCAL_CPP_FEATURES 特定 C++ 功能 支持異常:LOCAL_CPP_FEATURES := exceptions
LOCAL_C_INCLUDES 頭文件目錄查找路徑 LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_CFLAGS 構建 C  C++ 的編譯參數  
LOCAL_CPPFLAGS c++  
LOCAL_STATIC_LIBRARIES 當前模塊依賴的靜態庫模塊列表  
LOCAL_SHARED_LIBRARIES    
LOCAL_WHOLE_STATIC_LIBRARIES --whole-archive 將未使用的函數符號也加入編譯進入這個模塊
LOCAL_LDLIBS 依賴 系統庫 LOCAL_LDLIBS := -lz

導出給引入模塊的模塊使用:

LOCAL_EXPORT_CFLAGS

LOCAL_EXPORT_CPPFLAGS

LOCAL_EXPORT_C_INCLUDES

LOCAL_EXPORT_LDLIBS

上面大體瞭解以後接下來須要在Android.mk中加入編譯規則,具體以下:

因此依照上面的規則將其編寫到Android.mk中:

我們能夠將其路徑打印看一下:

不過在編譯前還得先把mk整個配置給填充完,因此:

而後我們來編譯一下:

而後咱們看一下編譯出來的動態庫:

這是由於咱們在NDK這塊只配了它:

那若是咱們增長一個"x86"呢?

而後我們再看一下編出來的APK中包含的動態庫的類型:

以上就是經過手動的方式來給我們的一個普通Android工程增長Ndk的支持,下面來繼續解讀一下我們在.mk中編寫的腳本的含義:

假如要有多源文件則以空格分開,若是想換行的話能夠以「\」,好比:

好了,此次學習的東東說實話有些雜,可是這些知識點是很是很是之重要的基礎,只有把基礎打牢了才能在將來的NDK學習之路走得更加的遠,堅持!!!

相關文章
相關標籤/搜索