AndroidNDK——makefile語法詳解
一、編譯流程詳解
編譯流程
- 編譯:將高級語言編寫的程序轉換為二進制代碼可執行性目標程序的過程
- 四大過程:預處理、編譯、匯編、鏈接
1、預處理
完成宏替換、文件引入,以及去除空行、注釋等,為下一步的編譯做准備;也就是對各種預處理命令進行處理,包括頭文件的包含、宏定義的擴展、條件編譯的選擇等。
// test.c文件內容
#include <stdio.h> int main(){ printf("hello world!\n"); return 0; }
對test.c文件進行預處理:
$ gcc -E test.c -o test.i
- 選項-E:讓gcc在預處理結束后停止編譯,test.i文件為預處理后輸出的文件。
- 選項-o:指定輸出文件。
此時,test.i 就是 test.c 預編譯后的產物,體積會增大,此時test.i還是一個文本文件,可以用文本編譯器打開查看。
2、編譯
- 將預處理后的代碼編譯成匯編代碼。在這個階段中,首先要檢查代碼的規范性、是否有語法錯誤等,以確定代碼實際要做的工作,在檢查無誤后,再把代碼翻譯成匯編語言。
- 編譯程序執行時,先分析,后綜合。分析,就是指詞法分析、語法分析、語義分析和中間代碼生成。綜合,就是指代碼優化和代碼生成。
- 大多數的編譯程序直接產生機器語言的目標代碼,形成可執行的目標文件,也有的是先產生匯編語言一級的符號代碼文件,再調用匯編程序進行翻譯和加工處理,最后產生可執行的機器語言目標文件。
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
- 選項-S:讓gcc在編譯結束后停止編譯過程,"test.s"文件為編譯后生成的匯編代碼。
此時,test.s 就是 test.i 文件匯編后的產物,同樣也可以用文本編譯器打開查看。
3、匯編
匯編就是把編譯階段生成的".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文件進行匯編:
$ gcc -c test.s -o test.o
- 選項-c:讓gcc在匯編結束后停止編譯過程,"test.o"文件為匯編后生成的機器碼目標文件。
4、鏈接
鏈接就是將多個目標文件以及所需的庫文件鏈接生成可執行目標文件的過程。
下面對test.o進行鏈接:
$ gcc test.o -o test
$ ./test hello world!
- 選項-o:本質上是一個重命名選項。不使用-o選項時,默認生成的是a.out文件。這里生成的是可執行文件test。
- ./test執行后輸出hello world!
5、簡化
一般情況下,我們會使用gcc命令,一步生成可執行文件,簡化編譯流程:
$ gcc -o test test.c
$ ./test hello world!
二、 靜態庫與動態庫原理
1、 靜態庫
1) 什么是靜態庫
- 靜態庫實際就是一些目標文件(一般以.o結尾)的集合,靜態庫一般以.a結尾,只用於生成可執行文件階段。
- 在鏈接步驟中,鏈接器將從庫文件取得所需代碼,復制到生成的可執行文件中。這種庫稱為靜態庫。其特點是可執行文件中包含了庫代碼的一份完整拷貝,在編譯過程中被載入程序中。缺點就是多次使用就會有多份冗余拷貝,並且對程序的更新、部署和發布會帶來麻煩,如果靜態庫有更新,那么所有使用它的程序都需要重新編譯、發布。
2) 生成靜態庫
- 首先生成test.o目標文件。
- 使用ar命令將test.o打包成libtest.a靜態庫。
# 生成目標文件
$ 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各自的含義:
- 選項r:更新或增加新文件到靜態庫中。
- 選項c:創建一個庫,不管存在與否,都創建。
- 選項s:創建文檔索引,在創建較大的庫時,能夠加快編譯速度。
2、 動態庫
1)什么是動態庫
- 動態庫在鏈接階段沒有被復制到程序中,而是在程序運行時由系統動態加載到內存中供程序調用。
- 系統只需載入一次動態庫,不同的程序可以得到內存中相同動態庫的副本,因此節省了很多內存。
2)生成動態庫
- 首先生成test.o目標文件。
- 使用-shared和-fPIC參數生成動態庫。
# 首先生成目標文件
$ gcc -c test.c -o test.o
# 使用-fPIC和-shared生成動態庫 $ gcc -shared -fPIC -o libtest.so test.o
fPIC:全稱是 Position Independent Code, 用於生成位置無關代碼。
3、案例
編寫一個工具方法(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; }
1)編譯&使用靜態庫
編譯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
2)編譯&使用動態庫
# 編譯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 指定查找共享庫,即動態鏈接庫時,除默認路徑以外,其他的路徑。
4、區別總結
載入時刻不同:
靜態庫 動態庫
三、makefile走讀與語法基礎
1、makefile是什么
在一個工程中,源文件很多,按類型、功能、模塊分別被存放在若干個目錄中,需要按一定的順序、規則進行編譯,這時就需要使用到makefile。
- makefile定義了一系列的規則來指定,哪些文件需要先編譯,哪些文件需要重新編譯,如何進行鏈接等操作。
- makefile就是“自動化編譯”,告訴make命令如何編譯和鏈接。
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
2、makefile里有什么
makefile包含以下五個:
顯示規則 隱晦規則 變量定義 文件指示 注釋
3、makefile的規則
target ... : prerequisites ...
command
或者:
target ... : prerequisites ... ; command
若prerequisites與command在同一行,需要用 ; 分隔。
若prerequisites與command不在同一行,則command前面需要用tab鍵開頭。
另外,如果命令太長,可以用 \ 作為換行符。
- target:目標文件。可以是ObjectFile,也可以是執行文件,還可以是標簽(Label);如果有多個文件,可以用空格隔開;可以使用通配符。
- prerequisites:依賴文件,既要生成那個target所需要的文件或其他target。
- command:make需要執行的命令。
makefile的作用:
告訴make,文件的依賴關系,以及如何生成目標文件。prerequisites中,如果有一個及以上的文件比target要新的話,target就會被認為是過時的,需要重新生成,command就會被執行,從而生成新的target。
4、makefile示例
# 當前目錄存在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 .PHONY
make會自動推導main.o、tool.o如何生成。
偽目標的名字不能和文件名重復,即當前目錄下,不能有clean文件。
可以通過 make clean 執行刪除命令。
5、makefile如何工作
默認方式下,輸入make命令后:
- make會在當前目錄下找名字叫“Makefile”或“makefile”的文件。
- 如果找到,它會找文件中第一個目標文件(target),並把這個target作為最終的目標文件,如前面示例中的“main”。
- 如果main文件不存在,或main所依賴的.o文件的修改時間要比main文件要新,那么它會執行后面所定義的命令來生成main文件。
- 如果main所依賴的.o文件也存在,那么main會在當前文件中找目標為.o文件的依賴性,若找到則根據規則生成.o文件。
- make再用.o文件聲明make的終極任務,也就是執行文件“main”。
6、makefile中使用變量
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
- 為了makefile的易維護,在makefile中我們可以使用變量。makefile的變量也就是一個字符串,理解成C語言中的宏可能會更好。
- 比如:我們聲明一個變量,叫 objects ,於是,我們就可以很方便地在我們的makefile中以“ $(objects) ”的方式來使用這個變量了。
7、makefile中引用其他的makefile
# 語法格式
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>
8、環境變量MAKEFILES
MAKEFILES
如果當前環境中字義了環境變量 MAKEFILES,那么,make會把這個變量中的值做一個類似於 include 的動作。這個變量中的值是其它的 Makefile,用空格分隔。只是,它和include不同的是,從這個環境中引入的Makefile的“目標”不會起作用,如果環境變量中定義的文件發現錯誤,make也會不理。但是建議不要使用這個環境變量,因為只要這個變量一被定義,那么當你使用make時,所有的Makefile都會受到它的影響。
也許有時候Makefile出現了奇怪的事,那么可以查看當前環境中有沒有定義這個變量。
9、Makefile預定義變量
變量名 | 描述 | 默認值 |
---|---|---|
CC | C語言編譯器的名稱 | cc |
CPP | C語言預處理器的名稱 | $(CC) -E |
CXX | C++語言編譯器的名稱 | g++ |
RM | 刪除文件程序的名稱 | rm -f |
CFLAGS | C語言編譯器的編譯選項 | 無 |
CPPFLAGS | C語言預處理器的編譯選項 | 無 |
CXXFLAGS | C++語言編譯器的編譯選項 | 無 |
10、Makefile自動變量
自動變量 | 描述 |
---|---|
$* | 目標文件的名稱,不包含擴展名 |
$@ | 目標文件的名稱,包含擴展名 |
$+ | 所有的依賴文件,以空格隔開,可能含有重復的文件 |
$^ | 所有的依賴文件,以空格隔開,不重復 |
$< | 依賴項中第一個依賴文件的名稱 |
$? | 依賴項中所有比目標文件新的依賴文件 |
11、Makefile函數
define本質是定義一個多行的變量,沒辦法直接調用,但可以在call的作用下,當作函數來使用。
不帶參數
define FUNC
$(info echo "hello")
endef
$(call FUNC)
--------------------
輸出:hello
帶參數
define FUNC1
$(info echo $(1)$(2)) endef $(call FUNC1,hello,world) -------------------- 輸出:hello world
12、make的工作流程
GNU的make工作時的執行步驟如下:
- 讀入所有的Makefile。
- 讀入被include的其它Makefile。
- 初始化文件中的變量。
- 推導隱晦規則,並分析所有規則。
- 為所有的目標文件創建依賴關系鏈。
- 根據依賴關系,決定哪些目標要重新生成。
- 執行生成命令。
1 5是第一階段,6 7為第二階段。在第一階段中,如果定義的變量被使用了,那么make會把變量展開在使用的位置,但是make並不是完全的馬上展開,如果變量出現在依賴關系的規則中,那么只有當這條依賴被決定要使用的時候,變量才會被展開。
三、Android.mk基礎
1、Android.mk簡介
Android.mk是一個向Android NDK構建系統描述NDK項目的GNU makefile片段。主要用來編譯生成以下幾種:
- APK程序:一般的Android應用程序,系統級別的直接push即可。
- JAVA庫:Java類庫,編譯打包生成JAR文件。
- C\C++應用程序:可執行的C\C++應用程序。
- C\C++靜態庫:編譯生成C\C++靜態庫,並打包成.a文件。
- C\C++共享庫:編譯生成共享庫,並打包成.so文件。
2、Android.mk基本格式
這是一個簡單的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)
- my-dir:是由編譯系統提供的宏函數,返回當前.mk文件的路徑。
- CLEAR_VARS:是由編譯系統提供的變量,指向一個特定的GNU makefile片段,可以清除除了LOCAL_PATH以外的以 LOCAL_ 開頭的變量,如: LOCAL_MODULE 、 LOCAL_SRC_FILES 。這樣做是因為編譯系統在單次執行中,會解析多個構建文件和模塊定義,而以 LOCAL_ 開頭的變量是全局變量,所以描述每個模塊之前,都會聲明 CLEAR_VARS 變量,可以避免沖突。
- LOCAL_MODULE:定義當前模塊名,模塊名必須唯一,而且不能包含空格。模塊名為"hello-jni"時,會生成libhello-jni.so,如果模塊名為"libhello-jni"時,則生成的還是libhello-jni.so!
- LOCAL_SRC_FILES:當前模塊包含的源文件,當源文件有多個時,用空格隔開。
3、編譯多個共享庫
一個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兩個動態庫。
4、編譯靜態庫
雖然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)
5、使用共享庫共享通用模塊
靜態庫可以保證源代碼模塊化,但是當靜態庫與共享庫相連時,它就變成了共享庫的一部分。在多個共享庫的情況下,多個共享庫與靜態庫連接時,需要將通用模塊的多個副本與不同的共享庫重復相連,這樣就增加了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項目。
6、在多個NDK項目間共享模塊
- 首先將avilib源代碼移動到NDK項目以外的位置,比如: C:\android\shared-modules\transcode\avilib 。
- 作為共享庫模塊,avilib需要有自己的Android.mk文件。
- 以 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的介紹,請翻到文末查看。
7、使用預編譯庫
- 想在不發布源代碼的情況下將模塊發布給他人。
- 想使用共享庫模塊的預編譯版來加速編譯過程。
現在我們手上有第三方預編譯好的庫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的位置。
8、編譯獨立的可執行文件
為了方便測試和進行快速開發,可以編譯成可執行文件。不用打包成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)
9、注意事項
假如我們本地庫libhello-jni.so依賴於libTest.so(可以使用NDK下的ndk-depends查看so的依賴關系)。
- 在Android6.0版本之前,需要在加載本地庫前先加載被依賴的so。
- 在Android6.0版本之后,不能再使用預編譯的動態庫(靜態庫沒問題)。
// Android 6.0版本之前:
System.loadlibrary("Test"); System.loadlibrary("hello-jni"); // Android 6.0版本之后: System.loadlibrary("hello-jni");
四、附加
1、import_module 詳解
以下內容引用自 《import-module的注意事項與NDK_MODULE_PATH的配置》
查看原文: AndroidNDK——makefile語法詳解