AndroidNDK——makefile语法详解

1、编译流程详解

编译流程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
复制代码
  • 选项-E:让gcc在预处理结束后中止编译,test.i文件为预处理后输出的文件。
  • 选项-o:指定输出文件。

此时,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
复制代码
  • 选项-S:让gcc在编译结束后中止编译过程,"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
复制代码
  • 选项-c:让gcc在汇编结束后中止编译过程,"test.o"文件为汇编后生成的机器码目标文件。

四、连接

连接就是将多个目标文件以及所需的库文件连接生成可执行目标文件的过程。ide

下面对test.o进行连接:模块化

$ gcc test.o -o test
$ ./test
hello world!
复制代码
  • 选项-o:本质上是一个重命名选项。不使用-o选项时,默认生成的是a.out文件。这里生成的是可执行文件test。
  • ./test执行后输出hello world!

五、简化

通常状况下,咱们会使用gcc命令,一步生成可执行文件,简化编译流程:

$ gcc -o test test.c
$ ./test
hello world!
复制代码

2、 静态库与动态库原理

一、 静态库

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:建立文档索引,在建立较大的库时,可以加快编译速度。

二、 动态库

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, 用于生成位置无关代码。

三、案例

编写一个工具方法(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 指定查找共享库,即动态连接库时,除默认路径之外,其余的路径。

四、区别总结

载入时刻不一样:

  • 静态库:在程序编译时会连接到目标代码中,程序运行时再也不须要静态库,所以体积较大。并且每次编译都须要载入静态代码,所以内存开销大。
  • 动态库:在程序编译时不会被连接到目标代码中,而是在程序运行时才被载入,程序运行时须要动态库存在,所以体积较小。并且系统只需载入一次动态库,不一样程序能够获得内存中相同的动态库副本,所以内存开销小。

3、makefile走读与语法基础

一、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
复制代码

二、makefile里有什么

makefile包含如下五个:

  • 显示规则:说明了如何生成一个或多个目标文件。
  • 隐晦规则:make有自动推导功能,能够用隐晦规则来简写makefile。
  • 变量定义:在makefile中能够变量一系列的变量,变量通常是字符串,相似c语言中的宏,当makefile被执行时,其中的变量都会被扩展相应的位置上。
  • 文件指示:包括3个部分:①在makefile引用另外一个makefile,相似C语言中的include;②根据条件指定makefile中的有效部分,相似C语言中的预编译#if同样;③定义多行的命令。
  • 注释:只有行注释,使用#字符表示。

三、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。

四、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:标签,不会生成“clean”文件,这样的target称之为“伪目标”,伪目标的名字不能和文件名重复。clean通常放在文件最后。
  • .PHONY:显示地指明clean是一个“伪目标”。

make会自动推导main.o、tool.o如何生成。 伪目标的名字不能和文件名重复,即当前目录下,不能有clean文件。 能够经过 make clean 执行删除命令。

五、makefile如何工做

默认方式下,输入make命令后:

  • make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
  • 若是找到,它会找文件中第一个目标文件(target),并把这个target做为最终的目标文件,如前面示例中的“main”。
  • 若是main文件不存在,或main所依赖的.o文件的修改时间要比main文件要新,那么它会执行后面所定义的命令来生成main文件。
  • 若是main所依赖的.o文件也存在,那么main会在当前文件中找目标为.o文件的依赖性,若找到则根据规则生成.o文件。
  • make再用.o文件声明make的终极任务,也就是执行文件“main”。

六、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)”的方式来使用这个变量了。

七、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>

八、环境变量MAKEFILES

MAKEFILES

若是当前环境中字义了环境变量 MAKEFILES,那么,make会把这个变量中的值作一个相似于 include 的动做。这个变量中的值是其它的 Makefile,用空格分隔。只是,它和include不一样的是,从这个环境中引入的Makefile的“目标”不会起做用,若是环境变量中定义的文件发现错误,make也会不理。可是建议不要使用这个环境变量,由于只要这个变量一被定义,那么当你使用make时,全部的Makefile都会受到它的影响。 也许有时候Makefile出现了奇怪的事,那么能够查看当前环境中有没有定义这个变量。

九、Makefile预约义变量

变量名 描述 默认值
CC C语言编译器的名称 cc
CPP C语言预处理器的名称 $(CC) -E
CXX C++语言编译器的名称 g++
RM 删除文件程序的名称 rm -f
CFLAGS C语言编译器的编译选项
CPPFLAGS C语言预处理器的编译选项
CXXFLAGS C++语言编译器的编译选项

十、Makefile自动变量

自动变量 描述
$* 目标文件的名称,不包含扩展名
$@ 目标文件的名称,包含扩展名
$+ 全部的依赖文件,以空格隔开,可能含有重复的文件
$^ 全部的依赖文件,以空格隔开,不重复
$< 依赖项中第一个依赖文件的名称
$? 依赖项中全部比目标文件新的依赖文件

十一、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
复制代码

十二、make的工做流程

GNU的make工做时的执行步骤以下:

  1. 读入全部的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析全部规则。
  5. 为全部的目标文件建立依赖关系链。
  6. 根据依赖关系,决定哪些目标要从新生成。
  7. 执行生成命令。

1~5是第一阶段,6~7为第二阶段。在第一阶段中,若是定义的变量被使用了,那么make会把变量展开在使用的位置,可是make并非彻底的立刻展开,若是变量出如今依赖关系的规则中,那么只有当这条依赖被决定要使用的时候,变量才会被展开。

3、Android.mk基础

一、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文件。

二、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_MODULELOCAL_SRC_FILES。这样作是由于编译系统在单次执行中,会解析多个构建文件和模块定义,而以LOCAL_开头的变量是全局变量,因此描述每一个模块以前,都会声明CLEAR_VARS变量,能够避免冲突。
  • LOCAL_MODULE:定义当前模块名,模块名必须惟一,并且不能包含空格。模块名为"hello-jni"时,会生成libhello-jni.so,若是模块名为"libhello-jni"时,则生成的仍是libhello-jni.so!
  • LOCAL_SRC_FILES:当前模块包含的源文件,当源文件有多个时,用空格隔开。

三、编译多个共享库

一个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项目。

六、在多个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的介绍,请翻到文末查看。

七、使用预编译库

  • 想在不发布源代码的状况下将模块发布给他人。
  • 想使用共享库模块的预编译版来加速编译过程。

如今咱们手上有第三方预编译好的库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的依赖关系)。

  • 在Android6.0版本以前,须要在加载本地库前先加载被依赖的so。
  • 在Android6.0版本以后,不能再使用预编译的动态库(静态库没问题)。
// Android 6.0版本以前:
System.loadlibrary("Test");
System.loadlibrary("hello-jni");

// Android 6.0版本以后: 
System.loadlibrary("hello-jni");
复制代码

4、附加

一、import_module 详解

如下内容引用自 《import-module的注意事项与NDK_MODULE_PATH的配置》

相关文章
相关标签/搜索