Makefile相关
编译过程
编译过程分为四大过程:
- 预处理:完成宏替换,文件引入,除去空行、注释等,为下一步编译做准备。使用,命令
gcc -E test.c -o test.i
-E指gcc在预处理完成后停止后序的操作,-o指定输出的文件。 - 编译:将预处理后的代码编译成汇编代码,在这个阶段中,首先要检查代码的规范性、是否有语法错误等,检查无误后把代码翻译成汇编语言;编译程序执行的时候,会先分析语法,词法语义生成中间代码,最后对代码优化。大多数编译程序会直接产生可执行的机器码,有些是先产生汇编语言一级符号代码文件,在调用汇编程序加工处理产生机器可执行的目标文件。使用命令
gcc -S test.i -o test.s
-S表示gcc在编译后停止后面的操作 - 汇编:把编译阶段生成的.s文件转成二进制目标代码也就是机器码(01序列)。使用命令
gcc -c test.s -o test.o
-c表示gcc在汇编处理后停止后面的链接操作 - 连接:将多个目标文件以及所需的库文件连接生成可执行目标文件的过程。使用命令
gcc text.o -o test
最后生成可执行文件test
最后执行./test即可执行该文件
例子:
现在linux中的一个文件夹下面有一个test.c的源文件,里面就一个输出hello world的方法1
2
3
4
5
6
7
8
9[root@cdh-master test]# ls
test.c
---------------------------
[root@cdh-master test]# cat test.c
#include <stdio.h>
int main(){
printf("hello world");
return 0;
}
第一步:执行预处理使用命令gcc -E test.c -o test.i
会输出一个test.i文件1
2
3[root@cdh-master test]# gcc -E test.c -o test.i
[root@cdh-master test]# ls
test.c test.i
第二步:执行编译命令gcc -S test.i -o test.s
会输出一个test.s文件1
2
3[root@cdh-master test]# gcc -S test.i -o test.s
[root@cdh-master test]# ls
test.c test.i test.s
第三步:执行汇编命令gcc -c test.s -o test.o
会输出一个test.o文件。1
2
3[root@cdh-master test]# gcc -c test.s -o test.o
[root@cdh-master test]# ls
test.c test.i test.o test.s
第四步:执行链接命令gcc test.o -o test
生成一个名为test的可执行文件。1
2
3[root@cdh-master test]# gcc test.o -o test
[root@cdh-master test]# ls
test test.c test.i test.o test.s
最后通过./test
执行文件,输出hello world1
2[root@cdh-master test]# ./test
hello world
上面的步骤是分开来做的,我们可以通过gcc -o test test.c
命令直接生成一个test可执行文件
静态库和动态库
静态库:
- 静态库就是一些目标文件(一般以.o结尾)的集合,静态库一般以.a结尾,只用于生成可执行文件阶段。
- 在链接步骤中,连接器将从库文件中取得所需代码,复制到可执行文件中。这种库就是静态库。
- 特点是可执行文件中包含了库代码的一份完整的拷贝,在编译过程中被载入程序中。
- 缺点就是多次使用就会有多分冗余的拷贝,并且对程序的更新、部署、和发布带来麻烦,比如静态库有更新,那么所有使用它的程序都需要重新编译发布。
动态库:
- 在链接阶段没有被复制到程序中,而是在程序运行时,由系统动态加载到内存中供程序调用。
- 系统只需载入一次动态库,不同程序就可以得到内存总相同动态库的副本,因此节省了很多内存
例子:编译静态库,先定义3个源文件too.h,tool.c,main.c用来求一个数组中的最大值
1 | [root@cdh-master test]# ls |
tool.h1
2
3#pragma once
int find_max(int arr[],int n);
tool,c1
2
3
4
5
6
7
8
9
10
11
12#include "tool.h"
int find_max(int arr[], int n) {
int max = arr[0];
int i = 0;
for (; i < n; i++) {
if (arr[i]>max) {
max = arr[i];
}
}
return max;
}
main.c1
2
3
4
5
6
7
8
9#include <stdio.h>
#include "tool.h"
int main() {
int arr[] = {1,3,6,8,5,7,9};
int max = find_max(arr,7);
printf("max=%d\n",max);
return 0;
}
开始编译静态库:
第一步:使用gcc -c tool.c
可以生成一个tool.o文件1
2[root@cdh-master test]# ls
main.c tool.c tool.h tool.o
第二步:使用命令ar rcs libtool.a tool.o
生成静态库文件tool.a文件1
2
3[root@cdh-master test]# ar rcs libtool.a tool.o
[root@cdh-master test]# ls
libtool.a main.c tool.c tool.h tool.o
第三步:编译可执行文件,并连接静态库,使用命令gcc -o main main.c -L. -ltool
-l执行要链接的库,-L表示去目标目录寻找文件,这里使用.
表示当前文件夹1
2
3[root@cdh-master test]# gcc -o main main.c -L. -ltool
[root@cdh-master test]# ls
libtool.a main main.c tool.c tool.h tool.o
最后执行main文件./main
1
2[root@cdh-master test]# ./main
max=9
开始编译动态库:
第一步跟生成.o文件,跟前面一样
第一步:使用gcc -c tool.c
可以生成一个tool.o文件(目标文件)1
2[root@cdh-master test]# ls
main.c tool.c tool.h tool.o
第二步:使用命令gcc -shared -fPIC -o libtool.so tool.o
生成libtool.so文件1
2
3[root@cdh-master test]# gcc -shared -fPIC -o libtool.so tool.o
[root@cdh-master test]# ls
libtool.a libtool.so main main.c tool.c tool.h tool.o
第三步:编译可执行文件并连接动态库,跟前面的链接静态库一样gcc -o main main.c -L. -ltool
,如果当前目录有同名的静态库和动态库比如现在libtool.a libtool.so
虚拟机会先加载动态库,最后生成main可执行文件
第四步:./main
执行文件1
2[root@cdh-master test]# ./main
./main: error while loading shared libraries: libtool.so: cannot open shared object file: No such file or directory
报错,找不到共享的libraries,可以通过ldd命令查看main文件依赖了那些库1
2
3
4
5[root@cdh-master test]# ldd main
linux-vdso.so.1 => (0x00007fff24348000)
libtool.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f6ec319f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6ec3569000)
可以看到libtool.so => not found
找不到,因为它会默认到系统默认路径下寻找,这里没有配置环境变量,所以找不到,我们需要给他执行加载目录。这里使用.
指定当前目录LD_LIBRARY_PATH=. ./main
1
2[root@cdh-master test]# LD_LIBRARY_PATH=. ./main
max=9
动态库和静态库的区别:
- 静态库在程序编译时链接到代码中,程序运行的时候不在需要静态库,因此体积比较大,每次编译都需要从新载入静态代码,内存开销大。
- 动态库在程序编译期间不会链接到目标代码中,而是在程序运行时才被载入,因此体积比较小,不过运行的时候需要指定动态库的路径。系统每次只需载入一次动态库,不同程序可以得到内存中相同的动态库的副本,内存开销比较小。
Makefile
为什么要写Makefile文件?当项目非常庞大时,让构建过程,自动化,简单
- Makefile 定义了一系列的规则来指定哪些文件需要优先编译,哪些文件需要重新编译,以及如何进行链接操作。
- Makefile就是“自动化编译”告诉make命令如何编译和链接。
Makefile包含以下五个部分:
- 显示规则:如何生成一个或多个目标文件
- 隐晦规则:自动推导功能
- 变量定义:一般是字符串
- 文件指示:1 一个Makefile中引用另一个Makefile 2 根据某些情况执行有效部分 3 定义多行
- 注释:使用#
规则:
- target:目标文件可以是Object File 也可以是执行文件,还可以是标签
- prerequistites:依赖文件,即要成成的那个target所需要的文件或者其他target
- command:make需要执行的命令
Makefile是如何工作的:
默认情况下,输入make命令后会执行下面步骤:
- make会在当前目录下寻找名字叫Makefile或者makefile的文件
- 如果找到了,它会找文件中第一个目标文件(target),并把这个target作为最终目标文件。比如前面例子中的main文件
- 如果找不到main文件或者main多以来的.o文件的修改时间比main文件要新,那么它会执行后面定义的命令来生成main文件
- 如果main所依赖的.o文件也存在,那么make会在当前文件夹中找目标为.o的文件的依赖,若找到则根据规则生成.o文件
- make再用.o文件声明make的终极任务,也就是执行文件main
环境变量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++语言编译器的编译选项 | 无 |
Makefile中的自动变量
自动变量 | 描述 |
---|---|
$* | 目标文件的名称,不包含扩展名 |
$@ | 目标文件的名称,包含扩展名 |
$+ | 所有的依赖文件,以空格隔开,可能含有重复的文件 |
$^ | 所有的依赖文件,以空格隔开,不重复 |
$< | 依赖项中第一个依赖文件的名称 |
$? | 依赖项中所有比目标文件新的依赖文件 |
Makefile中的函数
1 | # 不带参数 |
使用Makefile的例子:
还是使用前面的几个源文件,tool.h,tool.c,main.c
。文件少的时候我们可以一个命令一个命令的去敲去编译,文件多的时候,就会非常麻烦,所以把命令都在在Makefile文件中,使用的时候直接make就完事。
在上面三个文件所在目录执行vim Makefile
命令创建一个Makefile文件并编辑1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#main是最终目标,第一行是最终目标
#:后的文件都是它的依赖
#下一行用TAB键开头 写上需要生成目标文件需要执行的命令
main:main.o tool.o
gcc main.o tool.o -o main
#main.o是二级目标
#main.c是main.o的依赖
#gcc -c main.c是需要执行的命令
main.o:main.c
gcc -c main.c
tool.o:tool.c
gcc -c tool.c
#PHONY关键字代表为目标
.PHONY:clean
#clean清除所有的.o结尾的文件和执行文件
clean:
rm -f *.o
rm -f main
编辑完成之后,执行make命令,就会直接生成main.o,tool.o,main这些文件。
第一行是最终目标,和它所需要的依赖,如果它的依赖也需要别的依赖来生成,就从下面按照相同的格式在写一行,以此类推。
.PHONY关键字代表为目标。比如上面例子中标记clean,如果我们源文件的目录中有个叫clean的文件,如果没有用.PHONY标记,就会去编译clean文件。这里用它标记了,那么clean就只能执行下面定义的删除方法了。
前面的Makefile文件已经可以简化我们的命令的了,但是如果文件多了还是比较麻烦,我们可以继续简化。1
2
3
4
5
6
7
8
9OBJECT=main.o tool.o
main:$(OBJECT)
gcc $^ -o $@
%.o:%.c
gcc -c $^ -o $@
.PHONY:clean
clean:
rm -f *.o
rm -f main
定义一个OBJECT变量来保存依赖文件
$^表示所有依赖,$@表示目标,%.o:%.c是使用通配符来表示所有的.o文件有所有的.c文件生成。执行make之后效果跟前面的一样。
我们发现虽然定义了一个OBJECT变量,后面需要依赖的文件还得手动写,如果文件很多还是很麻烦的,还可以继续优化
1 | SOURCES=$(wildcard *.c) |
使用SOURCES关键字定义资源。wildcard关键字是找出该目录下所有的.c源文件。patsubst关键字是把所有的.c替换成.o。执行之后效果跟前面一样。
Makefile中使用函数:
无参函数
1 | define func |
使用define 定义一个名叫func的函数,使用$(call func)
调用。保存之后,回到文件夹中执行make命令,会看到输出hello world。
有参函数1
2
3
4
5define func1
$(info $(1) $(2))
endef
$(call func1,hello,world)
使用define 定义一个名叫func1的函数, $(1) $(2)代表要输入的参数,最后通过call调用函数,保存回到文件夹中执行make命令,会看到输出hello world。
make的工作流程
GNU的make工作步骤如下:
- 读入所有的Makefile
- 读入被include的其他的Makefile
- 初始化文件中的变量
- 推导隐晦规则,并法分析所有的规则
- 为所有的目标文件创建依赖关系链
- 根据依赖关系,决定哪些目标要重新生成
- 执行生成命令
Android.mak
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文件样例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19#定义模块当前的路径(必须定义在文件开头,只需要定义一次)
LOCAL_PATH := $(call my-dir)
#Makefile中可以引入其他的Makefile文件
#编译模块时,清空当前环境变量(LOCAL_PATH除外)
include $(CLEAR_VARS)
#当前模块名(这里会生成libhello-jni)
LOCAL_MODULE :=hello-jni
#编译所需要的源文件 多个文件以空格隔开
LOCAL_SRC_FILES :=hello-jni.c
#需要的头文件
LOCAL_C_INCLUDES
#编译需要的动态库
LOCAL_SHARED_LIBRARIES
#表示当前模块将要被编译成一个共享库
include $(BUILD_SHARED_LIBRARY)
一个Android.mk可能编译产生多个共享库模块,比如下面代码会产生libmodeule1.so和libmodule2.so两个动态库1
2
3
4
5
6
7
8
9
10
11
12
13LOCAL_PATH := $(call my-dir)
#模块1
include $(CLEAR_VARS)
LOCAL_MODULE :=module1
LOCAL_SRC_FILES :=module1.c
include $(BUILD_SHARED_LIBRARY)
#模块2
include $(CLEAR_VARS)
OCAL_MODULE :=module2
LOCAL_SRC_FILES :=module2.c
include $(BUILD_SHARED_LIBRARY)
编译静态库
Android应用程序不能直接使用静态库,不过可以使用静态库来编译动态库。比如在把第三方代码添加到原生项目中时,可以不用直接把第三方源码放入原生项目中,而是将第三方源码编译成静态库,然后并入共享库。
1 | LOCAL_PATH := $(call my-dir) |
使用共享库共享通用模块
静态库可以保证源代码模块化,但是当静态库与共享库相连时,它就变成了共享库的一部分。
在多个共享库与静态库相连接时,需要将通用模块的多个副本与不同的共享库重复相连,这样就会增大APP的大小。这时候可以将通过用模块作为共享库,不过这样必须是一个NDK项目1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21LOCAL_PATH := $(call my-dir)
#第三方AVI库
include $(CLEAR_VARS)
LOCAL_MODULE :=avilib
LOCAL_SRC_FILES :=abilib.c platfrom_posix.c
include $(BUILD_SHARED_LIBRARY)
#原生模块1
include $(CLEAR_VARS)
OCAL_MODULE :=module1
LOCAL_SRC_FILES :=module1.c
LOCAL_SHARED_LIBRARIES :avilib
include $(BUILD_SHARED_LIBRARY)
#原生模块2
include $(CLEAR_VARS)
OCAL_MODULE :=module2
LOCAL_SRC_FILES :=module2.c
LOCAL_SHARED_LIBRARIES :avilib
include $(BUILD_SHARED_LIBRARY)
多个NDK项目之间共享模块
- 首先先将avilib源码移动到NDK项目以外的位置,比如C:\android\shared-modules\transcode\avilib
- 作为共享模块,avilib需要有自己的Android.mk文件
- 以transcode/avilib为参数调用函数宏import-module添加到NDK项目的Android.mk文档末尾。
1 | #avilib模块自己的Android.mk文件 |
使用预编译库
如果想在不发布代码的情况下将模块发布给他人或者想使用共享模块的预编译版本来急速编译过程1
2
3
4
5
6
7#预编译共享模块的Android.mk文件
LOCAL_PATH := $(call my-dir)
#第三方预编译库
include $(CLEAR_VARS)
LOCAL_MODULE :=avilib
LOCAL_SRC_FILES :=libavilib.so
include $(BUILD_SHARED_LIBRARY)
编译独立的可执行文件
为了方便测试和进行快速开发,可以编译成可执行文件。不用打包成APK就可以复制到Andorid设备上执行
1 | #独立可执行模块的Android.mk文件 |
注意:
- 如果我们本地库libhello-jni.so依赖于libTest.so(可以使用NDK下的ndk-depends查看so的依赖关系)
- 在Android6.0版本之前,需要在加载本地库钱先加载被依赖的so库
- 在Android6.0版本之后,不能再使用预编译的动态库(静态库没问题)
1 | #Android 6.0版本之前 |
Andorid.mk的简单使用
使用AndroidStudio新建一个空项目。
在MainActivity中定义native方法nativeTest
1 | public static native int nativeTest(); |
在main文件夹的同级新建一个ndkbuild目录,里面创建两个文件”hello-jni.c”和”Andorid.mk”
1 | #include <jni.h> |
1 | #定义模块当前的路径(必须定义在文件开头,只需要定义一次) |
修改gradle文件,配置支持的cup和Android.mk的路径1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19defaultConfig {
applicationId "com.chs.ndktest"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild{
ndkBuild{
//例子使用模拟器测试,所以编译x86的,现在主流手机都是arm-v7a的
abiFilters "x86"
}
}
}
externalNativeBuild{
ndkBuild{
path "src/main/ndkbuild/Android.mk"
}
}
这时候build工程,查看生成的apk文件,在lib目录下就可以看到一个hello-jni.so的文件
最后在MainActivity中加载hello-jni.so动态库,并调用方法显示在TextView上1
2
3
4
5
6
7
8
9
10{
System.loadLibrary("hello-jni");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.tv_test);
textView.setText("hello"+nativeTest());
}
运行在模拟器上就会看到TextView上会显示hello12346,成功。
如今AndroidStudio对Makefile的支持基本上不支持了。现在都用CMake语法,不过HIA有一些老项目会用到,所以Makefile还要了解一下。