編譯流程html
完成宏替換、文件引入,以及去除空行、註釋等,爲下一步的編譯作準備;也就是對各類預處理命令進行處理,包括頭文件的包含、宏定義的擴展、條件編譯的選擇等。java
// test.c文件內容
#include <stdio.h>
int main(){
printf("hello world!\n");
return 0;
}
複製代碼
對test.c文件進行預處理:android
$ gcc -E test.c -o test.i
複製代碼
此時,test.i 就是 test.c 預編譯後的產物,體積會增大,此時test.i仍是一個文本文件,能夠用文本編譯器打開查看。shell
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 868 "/usr/include/stdio.h" 3 4
# 2 "test.c" 2
# 3 "test.c"
int main(){
printf("hello world\n");
return 0;
}
複製代碼
上面是預處理後test.i文件的部份內容,下面對test.i文件進行編譯:編程
$ gcc -S test.i -o test.s
複製代碼
此時,test.s 就是 test.i 文件彙編後的產物,一樣也能夠用文本編譯器打開查看。ubuntu
彙編就是把編譯階段生成的".s"文件轉成二進制目標代碼,也就是機器代碼(01序列)。數組
.file "test.c"
.text
.section .rodata
.LC0:
.string "hello world"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rdi
call puts@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
.section .note.GNU-stack,"",@progbits
複製代碼
上面是編譯後生成的test.s文件裏的彙編代碼,下面對test.s文件進行彙編:bash
$ gcc -c test.s -o test.o
複製代碼
連接就是將多個目標文件以及所需的庫文件連接生成可執行目標文件的過程。ide
下面對test.o進行連接:模塊化
$ gcc test.o -o test
$ ./test
hello world!
複製代碼
通常狀況下,咱們會使用gcc命令,一步生成可執行文件,簡化編譯流程:
$ gcc -o test test.c
$ ./test
hello world!
複製代碼
# 生成目標文件
$ gcc -c test.c -o test.o
# 使用ar命令將目標文件打包成靜態庫
$ ar libtest.a test.o
ar: creating libtest.a
# 使用ar t libtest.a 查看靜態庫內容
$ar t libtest.a
test.o
複製代碼
選項rcs各自的含義:
# 首先生成目標文件
$ gcc -c test.c -o test.o
# 使用-fPIC和-shared生成動態庫
$ gcc -shared -fPIC -o libtest.so test.o
複製代碼
fPIC:全稱是 Position Independent Code, 用於生成位置無關代碼。
編寫一個工具方法(tool.h + tool.c文件),查找出數組的最大值:
// tool.h 文件
int find_max(int arr[], int n);
// tool.c 文件
#include "tool.h"
int find_max(int arr[], int n){
int max = arr[0];
int i;
for(i = 0; i < n; i++){
if(arr[i] > max){
max = arr[i];
}
}
return max;
}
複製代碼
在main.c文件中,調用tool.h的find_max函數:
// main.c 文件
#include <stdio.h>
#include "tool.h"
int main(){
int arr[] = {1,3,5,8,2};
int max = find_max(arr, 5);
printf("max = %d\n", max);
return 0;
}
複製代碼
編譯tool靜態庫:
# 編譯tool.c。能夠省略"-o tool.o",默認gcc會生成一個與tool.c同名的.o文件。
$ gcc -c tool.c
# 編譯生成libtool.a靜態庫
$ ar rcs libtool.a tool.o
# 編譯main可執行文件。
# -l用來指定要連接的庫,後面接庫的名字;-L表示編譯程序根據指定路徑尋找庫文件。
$ gcc -o main main.c -L. -ltool
$ ./main
max = 8
複製代碼
能夠用ldd命令查看main文件依賴了哪些庫:
$ ldd main
複製代碼
# 編譯tool.c,生成tool.o
$ gcc -c tool.c
# 編譯生成libtool.so動態庫
$ gcc -shared -fPIC -o libtool.so tool.o
# 編譯main可執行文件
$ gcc -o main main.c -L. -ltool
$ ./main
./main: error while loading shared libraries: libtool.so: cannot open shared object file: No such file or directory
複製代碼
注意,當靜態庫與動態庫同名時,gcc會優先加載動態庫。即,此時目錄下即有libtool.a,又有libtool.so,編譯main時指定了-ltool,gcc會連接libtool.so!
能夠用ldd命令查看main文件依賴了哪些庫:
$ ldd main
複製代碼
能夠看到,libtool.so找不到,這是由於在系統的默認動態連接庫路徑下沒有這個libtool.so文件,能夠在執行以前,給main設置環境變量解決:
# 將當前目錄設置到環境變量中
$ LD_LIBRARY_PATH=. ./main
max = 8
複製代碼
LD_LIBRARY_PATH
指定查找共享庫,即動態連接庫時,除默認路徑之外,其餘的路徑。
載入時刻不一樣:
靜態庫
:在程序編譯時會連接到目標代碼中,程序運行時再也不須要靜態庫,所以體積較大。並且每次編譯都須要載入靜態代碼,所以內存開銷大。動態庫
:在程序編譯時不會被連接到目標代碼中,而是在程序運行時才被載入,程序運行時須要動態庫存在,所以體積較小。並且系統只需載入一次動態庫,不一樣程序能夠獲得內存中相同的動態庫副本,所以內存開銷小。在一個工程中,源文件不少,按類型、功能、模塊分別被存放在若干個目錄中,須要按必定的順序、規則進行編譯,這時就須要使用到makefile。
makefile是make工具的配置腳本,默認狀況下,make命令會在當前目錄下去尋找該文件(按順序找尋文件名爲「GNUmakefile」
、「makefile」
、「Makefile」
的文件)。
在這三個文件名中,最好使用「Makefile」這個文件名,由於,這個文件名第一個字符爲大寫,這樣有一種顯目的感受。 最好不要用「GNUmakefile」,這個文件是GNU的make識別的。有另一些make只對全小寫的「makefile」文件名敏感。 可是基本上來講,大多數的make都支持「makefile」和「Makefile」這兩種默認文件名。
固然,配置文件的文件名也能夠不是makefile,好比:config.debug,這時須要經過 -f
或 --file
指定配置文件,即:
# 使用-f
$ make -f config.debug
# 使用--file
$ make --file config.debug
複製代碼
makefile包含如下五個:
顯示規則
:說明了如何生成一個或多個目標文件。隱晦規則
:make有自動推導功能,能夠用隱晦規則來簡寫makefile。變量定義
:在makefile中能夠變量一系列的變量,變量通常是字符串,相似c語言中的宏,當makefile被執行時,其中的變量都會被擴展相應的位置上。文件指示
:包括3個部分:①在makefile引用另外一個makefile,相似C語言中的include;②根據條件指定makefile中的有效部分,相似C語言中的預編譯#if同樣;③定義多行的命令。註釋
:只有行註釋,使用#字符表示。target ... : prerequisites ...
command
或者:
target ... : prerequisites ... ; command
複製代碼
若prerequisites與command在同一行,須要用
;
分隔。 若prerequisites與command不在同一行,則command前面須要用tab鍵開頭。 另外,若是命令太長,能夠用\
做爲換行符。
告訴make,文件的依賴關係,以及如何生成目標文件。prerequisites中,若是有一個及以上的文件比target要新的話,target就會被認爲是過期的,須要從新生成,command就會被執行,從而生成新的target。
# 當前目錄存在main.c、tool.c、tool.h三個文件
# 下面是makefile文件內容
main: main.o tool.o
gcc main.o tool.o -o main
.PHONY: clean
clean:
-rm main *.o
-----------------------------
// 執行 make 後輸出以下:
cc -c -o main.o main.c
cc -c -o tool.o tool.c
gcc main.o tool.o -o main
// 而且生成了一個可執行文件main
複製代碼
-o
:指定可執行文件的名稱。clean
:標籤,不會生成「clean」文件,這樣的target稱之爲「僞目標」,僞目標的名字不能和文件名重複。clean通常放在文件最後。.PHONY
:顯示地指明clean是一個「僞目標」。make會自動推導main.o、tool.o如何生成。 僞目標的名字不能和文件名重複,即當前目錄下,不能有clean文件。 能夠經過
make clean
執行刪除命令。
默認方式下,輸入make命令後:
objects = main.o tool.o
main: $(objects)
gcc $(objects) -o main
.PHONY: clean
clean:
-rm main $(objects)
-----------------------------
// 執行 make 後輸出以下:
cc -c -o main.o main.c
cc -c -o tool.o tool.c
gcc main.o tool.o -o main
複製代碼
objects
,因而,咱們就能夠很方便地在咱們的makefile中以「$(objects)
」的方式來使用這個變量了。# 語法格式
include <filename>
# 舉個例子,你有這樣幾個 Makefile:a.mk、b.mk、c.mk,還有一個文件叫 # foo.make,以及一個變量$(bar),其包含了 e.mk 和 f.mk
include foo.make *.mk $(bar)
# 等價於:
include foo.make a.mk b.mk c.mk e.mk f.mk
# 若是文件找不到,而你但願make時不理會那些沒法讀取的文件而繼續執行
# 能夠在include前加一個減號「-」,如:
-include <filename>
複製代碼
使用include關鍵字能夠把其它Makefile包含進來,include語法格式: include <filename>
MAKEFILES
若是當前環境中字義了環境變量 MAKEFILES,那麼,make會把這個變量中的值作一個相似於 include 的動做。這個變量中的值是其它的 Makefile,用空格分隔。只是,它和include不一樣的是,從這個環境中引入的Makefile的「目標」不會起做用,若是環境變量中定義的文件發現錯誤,make也會不理。可是建議不要使用這個環境變量,由於只要這個變量一被定義,那麼當你使用make時,全部的Makefile都會受到它的影響。 也許有時候Makefile出現了奇怪的事,那麼能夠查看當前環境中有沒有定義這個變量。
變量名 | 描述 | 默認值 |
---|---|---|
CC | C語言編譯器的名稱 | cc |
CPP | C語言預處理器的名稱 | $(CC) -E |
CXX | C++語言編譯器的名稱 | g++ |
RM | 刪除文件程序的名稱 | rm -f |
CFLAGS | C語言編譯器的編譯選項 | 無 |
CPPFLAGS | C語言預處理器的編譯選項 | 無 |
CXXFLAGS | C++語言編譯器的編譯選項 | 無 |
自動變量 | 描述 |
---|---|
$* | 目標文件的名稱,不包含擴展名 |
$@ | 目標文件的名稱,包含擴展名 |
$+ | 全部的依賴文件,以空格隔開,可能含有重複的文件 |
$^ | 全部的依賴文件,以空格隔開,不重複 |
$< | 依賴項中第一個依賴文件的名稱 |
$? | 依賴項中全部比目標文件新的依賴文件 |
define本質是定義一個多行的變量,沒辦法直接調用,但能夠在call的做用下,看成函數來使用。
define FUNC
$(info echo "hello")
endef
$(call FUNC)
--------------------
輸出:hello
複製代碼
define FUNC1
$(info echo $(1)$(2))
endef
$(call FUNC1,hello,world)
--------------------
輸出:hello world
複製代碼
GNU的make工做時的執行步驟以下:
1~5是第一階段,6~7爲第二階段。在第一階段中,若是定義的變量被使用了,那麼make會把變量展開在使用的位置,可是make並非徹底的立刻展開,若是變量出如今依賴關係的規則中,那麼只有當這條依賴被決定要使用的時候,變量纔會被展開。
Android.mk是一個向Android NDK構建系統描述NDK項目的GNU makefile片斷。主要用來編譯生成如下幾種:
這是一個簡單的Android.mk文件的內容:
# 定義模塊當前路徑(必須定義在文件開頭,只需定義一次)
LOCAL_PATH := $(call my-dir)
# 清空當前環境變量(LOCAL_PATH除外)
include $(CLEAR_VARS)
# 當前模塊名(這裏會生成libhello-jni.so)
LOCAL_MODULE := hello-jni
# 當前模塊包含的源代碼文件
LOCAL_SRC_FILES := hello-jni.c
# 表示當前模塊將被編譯成一個共享庫
include $(BUILD_SHARED_LIBRARY)
複製代碼
LOCAL_
開頭的變量,如:LOCAL_MODULE
、LOCAL_SRC_FILES
。這樣作是由於編譯系統在單次執行中,會解析多個構建文件和模塊定義,而以LOCAL_
開頭的變量是全局變量,因此描述每一個模塊以前,都會聲明CLEAR_VARS
變量,能夠避免衝突。一個Android.mk可能編譯產生多個共享庫模塊。
LOCAL_PATH := $(call my-dir)
# 模塊1
include $(CLEAR_VARS)
LOCAL_MODULE := module1
LOCAL_SRC_FILES := module1.c
include $(BUILD_SHARED_LIBRARY)
# 模塊2
include $(CLEAR_VARS)
LOCAL_MODULE := module2
LOCAL_SRC_FILES := module2.c
include $(BUILD_SHARED_LIBRARY)
複製代碼
這裏會產生libmodule1.so和libmodule2.so兩個動態庫。
雖然Android應用程序不能直接使用靜態庫,但靜態庫能夠用來編譯動態庫。好比在將第三方代碼添加到原生項目中時,能夠不用直接將第三方源碼包括在原生項目中,而是將第三方源碼編譯成靜態庫,而後併入共享庫。
LOCAL_PATH := $(call my-dir)
# 第三方AVI庫
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := avilib.c platform_posix.c
include $(BUILD_STATIC_LIBRARY)
# 原生模塊
include $(CLEAR_VARS)
LOCAL_MODULE := module
LOCAL_SRC_FILES := module.c
# 將靜態庫模塊名添加到LOCAL_STATIC_LIBRARIES變量
LOCAL_STATIC_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)
複製代碼
靜態庫能夠保證源代碼模塊化,可是當靜態庫與共享庫相連時,它就變成了共享庫的一部分。在多個共享庫的狀況下,多個共享庫與靜態庫鏈接時,須要將通用模塊的多個副本與不一樣的共享庫重複相連,這樣就增長了APP的大小。這種狀況,能夠將通用模塊做爲共享庫。
LOCAL_PATH := $(call my-dir)
# 第三方AVI庫
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := avilib.c platform_posix.c
include $(BUILD_SHARED_LIBRARY)
# 原生模塊1
include $(CLEAR_VARS)
LOCAL_MODULE := module1
LOCAL_SRC_FILES := module1.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)
# 原生模塊2
include $(CLEAR_VARS)
LOCAL_MODULE := module2
LOCAL_SRC_FILES := module2.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)
複製代碼
以上的作法必須基於同一個NDK項目。
C:\android\shared-modules\transcode\avilib
。transcode/avilib
爲參數調用函數宏import-module
添加到NDK項目的Android.mk文檔末尾。
import-module
函數宏在NDK版本r5之後纔有。
# avilib模塊本身的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := avilib.c platform_posix.c
include $(BUILD_SHARED_LIBRARY)
---------------------------------------------
# 使用共享模塊的NDK項目1的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := module1
LOCAL_SRC_FILES := module1.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)
$(call import-module,transcode/avilib)
---------------------------------------------
# 使用共享模塊的NDK項目2的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := module2
LOCAL_SRC_FILES := module2.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)
$(call import-module,transcode/avilib)
複製代碼
小心細的你在看到$(call import-module,transcode/avilib)
這句時,必定會問,爲何NDK會知道要去C:\android\shared-modules\
目錄下面找transcode/avilib
呢?是的,NDK並無這麼智能,默認狀況下,import-module
函數宏只會搜索AndroidNDK下面的sources目錄。
如個人NDK路徑是:
C:\Users\lqr\AppData\Local\Android\Sdk\ndk-bundle
,那麼import-module
函數宏默認的尋找目錄就是C:\Users\lqr\AppData\Local\Android\Sdk\ndk-bundle\sources
要正確使用import-module
,就須要對NDK_MODULE_PATH進行配置,把C:\android\shared-modules\
配置到環境變量中便可,當有多個共享庫目錄時,用;
隔開。
更多關於import-module的介紹,請翻到文末查看。
如今咱們手上有第三方預編譯好的庫libavilib.so,想集成到本身項目中使用,則須要在Android.mk中進行以下配置:
# 預編譯共享模塊的Android.mk文件
LOCAL_PATH := $(call my-dir)
# 第三方預編譯的庫
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := libavilib.so
include $(PREBUILT_SHARED_LIBRARY)
複製代碼
能夠看到,LOCAL_SRC_FILES
指向的再也不是源文件,而是預編譯好的libavilib.so,相對於LOCAL_PATH的位置。
爲了方便測試和進行快速開發,能夠編譯成可執行文件。不用打包成APK就能夠獲得到Android設備上直接執行。
# 獨立可執行模塊的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := module
LOCAL_SRC_FILES := module.c
LOCAL_STATIC_LIBRARIES := avilib
include $(BUILD_EXECUTABLE)
複製代碼
假如咱們本地庫libhello-jni.so依賴於libTest.so(可使用NDK下的ndk-depends查看so的依賴關係)。
// Android 6.0版本以前:
System.loadlibrary("Test");
System.loadlibrary("hello-jni");
// Android 6.0版本以後:
System.loadlibrary("hello-jni");
複製代碼