Makefile相关

Makefile相关

编译过程

编译过程分为四大过程:

  1. 预处理:完成宏替换,文件引入,除去空行、注释等,为下一步编译做准备。使用,命令gcc -E test.c -o test.i-E指gcc在预处理完成后停止后序的操作,-o指定输出的文件。
  2. 编译:将预处理后的代码编译成汇编代码,在这个阶段中,首先要检查代码的规范性、是否有语法错误等,检查无误后把代码翻译成汇编语言;编译程序执行的时候,会先分析语法,词法语义生成中间代码,最后对代码优化。大多数编译程序会直接产生可执行的机器码,有些是先产生汇编语言一级符号代码文件,在调用汇编程序加工处理产生机器可执行的目标文件。使用命令gcc -S test.i -o test.s -S表示gcc在编译后停止后面的操作
  3. 汇编:把编译阶段生成的.s文件转成二进制目标代码也就是机器码(01序列)。使用命令gcc -c test.s -o test.o -c表示gcc在汇编处理后停止后面的链接操作
  4. 连接:将多个目标文件以及所需的库文件连接生成可执行目标文件的过程。使用命令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 world

1
2
[root@cdh-master test]# ./test
hello world

上面的步骤是分开来做的,我们可以通过gcc -o test test.c命令直接生成一个test可执行文件

静态库和动态库

静态库:

  • 静态库就是一些目标文件(一般以.o结尾)的集合,静态库一般以.a结尾,只用于生成可执行文件阶段。
  • 在链接步骤中,连接器将从库文件中取得所需代码,复制到可执行文件中。这种库就是静态库。
  • 特点是可执行文件中包含了库代码的一份完整的拷贝,在编译过程中被载入程序中。
  • 缺点就是多次使用就会有多分冗余的拷贝,并且对程序的更新、部署、和发布带来麻烦,比如静态库有更新,那么所有使用它的程序都需要重新编译发布。

动态库:

  • 在链接阶段没有被复制到程序中,而是在程序运行时,由系统动态加载到内存中供程序调用。
  • 系统只需载入一次动态库,不同程序就可以得到内存总相同动态库的副本,因此节省了很多内存

例子:编译静态库,先定义3个源文件too.h,tool.c,main.c用来求一个数组中的最大值

1
2
[root@cdh-master test]# ls
main.c tool.c tool.h

tool.h

1
2
3
#pragma once

int find_max(int arr[],int n);

tool,c

1
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.c

1
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

动态库和静态库的区别:

  1. 静态库在程序编译时链接到代码中,程序运行的时候不在需要静态库,因此体积比较大,每次编译都需要从新载入静态代码,内存开销大。
  2. 动态库在程序编译期间不会链接到目标代码中,而是在程序运行时才被载入,因此体积比较小,不过运行的时候需要指定动态库的路径。系统每次只需载入一次动态库,不同程序可以得到内存中相同的动态库的副本,内存开销比较小。

Makefile

为什么要写Makefile文件?当项目非常庞大时,让构建过程,自动化,简单

  • Makefile 定义了一系列的规则来指定哪些文件需要优先编译,哪些文件需要重新编译,以及如何进行链接操作。
  • Makefile就是“自动化编译”告诉make命令如何编译和链接。

Makefile包含以下五个部分:

  • 显示规则:如何生成一个或多个目标文件
  • 隐晦规则:自动推导功能
  • 变量定义:一般是字符串
  • 文件指示:1 一个Makefile中引用另一个Makefile 2 根据某些情况执行有效部分 3 定义多行
  • 注释:使用#

规则:

  • target:目标文件可以是Object File 也可以是执行文件,还可以是标签
  • prerequistites:依赖文件,即要成成的那个target所需要的文件或者其他target
  • command:make需要执行的命令

Makefile是如何工作的:

默认情况下,输入make命令后会执行下面步骤:

  1. make会在当前目录下寻找名字叫Makefile或者makefile的文件
  2. 如果找到了,它会找文件中第一个目标文件(target),并把这个target作为最终目标文件。比如前面例子中的main文件
  3. 如果找不到main文件或者main多以来的.o文件的修改时间比main文件要新,那么它会执行后面定义的命令来生成main文件
  4. 如果main所依赖的.o文件也存在,那么make会在当前文件夹中找目标为.o的文件的依赖,若找到则根据规则生成.o文件
  5. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 不带参数
define FUNC
$(info echo "hello")
endef

#调用
$(call FUNC)
------------
输出:hello

#带参数
define FUNC1
$(info echo $(1)$(2))
endef
#调用
$(call FUNC1,hello,world)
----------
输出:hello world

使用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
9
OBJECT=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
2
3
4
5
6
7
8
9
10
SOURCES=$(wildcard *.c)
OBJECT=$(patsubst %.c,%.o,$(SOURCES))
main:$(OBJECT)
gcc $^ -o $@
%.o:%.c
gcc -c $^ -o $@
.PHONY:clean
clean:
rm -f *.o
rm -f main

使用SOURCES关键字定义资源。wildcard关键字是找出该目录下所有的.c源文件。patsubst关键字是把所有的.c替换成.o。执行之后效果跟前面一样。

Makefile中使用函数:

无参函数

1
2
3
4
5
define func
$(info "hello world")
endef
//调用
$(call func)

使用define 定义一个名叫func的函数,使用$(call func)调用。保存之后,回到文件夹中执行make命令,会看到输出hello world。

有参函数

1
2
3
4
5
define func1
$(info $(1) $(2))
endef

$(call func1,hello,world)

使用define 定义一个名叫func1的函数, $(1) $(2)代表要输入的参数,最后通过call调用函数,保存回到文件夹中执行make命令,会看到输出hello world。

make的工作流程

GNU的make工作步骤如下:

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

Android.mak

Android.mk是一个向Android NDK构建系统描述NDK项目的GNU makefile 片段。主要用来编译生成以下几种:

  1. APK程序:一般的Android应用程序,系统级别的直接push
  2. Java库:JAVA类库,编译打包生成JAR文件
  3. C/C++应用程序:可执行的C/C++应用程序
  4. C/C++静态库:编译生成C/C++静态库,并打包成.a文件。
  5. 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
13
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)
OCAL_MODULE :=module2
LOCAL_SRC_FILES :=module2.c
include $(BUILD_SHARED_LIBRARY)

编译静态库

Android应用程序不能直接使用静态库,不过可以使用静态库来编译动态库。比如在把第三方代码添加到原生项目中时,可以不用直接把第三方源码放入原生项目中,而是将第三方源码编译成静态库,然后并入共享库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LOCAL_PATH := $(call my-dir)

#第三方AVI库
include $(CLEAR_VARS)
OCAL_MODULE :=avilib
LOCAL_SRC_FILES :=abilib.c platfrom_posix.c
include $(BUILD_SHARED_LIBRARY)

#原生模块
include $(CLEAR_VARS)
LOCAL_MODULE :=module
LOCAL_SRC_FILES :=module.c
#将静态模块添加到LOCAL_STATIC_LIBRARIES变量
LOCAL_STATIC_LIBRARIES :avilib
include $(BUILD_SHARED_LIBRARY)

使用共享库共享通用模块

静态库可以保证源代码模块化,但是当静态库与共享库相连时,它就变成了共享库的一部分。

在多个共享库与静态库相连接时,需要将通用模块的多个副本与不同的共享库重复相连,这样就会增大APP的大小。这时候可以将通过用模块作为共享库,不过这样必须是一个NDK项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LOCAL_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项目之间共享模块

  1. 首先先将avilib源码移动到NDK项目以外的位置,比如C:\android\shared-modules\transcode\avilib
  2. 作为共享模块,avilib需要有自己的Android.mk文件
  3. 以transcode/avilib为参数调用函数宏import-module添加到NDK项目的Android.mk文档末尾。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#avilib模块自己的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE :=avilib
LOCAL_SRC_FILES :=abilib.c platfrom_posix.c
include $(BUILD_SHARED_LIBRARY)

----------------------------
#使用共享模块的NDK项目1的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
OCAL_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)
OCAL_MODULE :=module2
LOCAL_SRC_FILES :=module2.c
LOCAL_SHARED_LIBRARIES :avilib
include $(BUILD_SHARED_LIBRARY)
$(call import-module,transcode/avilib)

使用预编译库

如果想在不发布代码的情况下将模块发布给他人或者想使用共享模块的预编译版本来急速编译过程

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
2
3
4
5
6
7
#独立可执行模块的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
OCAL_MODULE :=module
LOCAL_SRC_FILES :=module.c
LOCAL_SHARED_LIBRARIES :avilib
include $(BUILD_EXECUTABLE)

注意:

  • 如果我们本地库libhello-jni.so依赖于libTest.so(可以使用NDK下的ndk-depends查看so的依赖关系)
  • 在Android6.0版本之前,需要在加载本地库钱先加载被依赖的so库
  • 在Android6.0版本之后,不能再使用预编译的动态库(静态库没问题)
1
2
3
4
5
#Android 6.0版本之前
System.loadLibrary("Test");
Systen.loadLibrary("hello-jni");
#Android 6.0版本之后
System.loadLibrary("hello-jni")

Andorid.mk的简单使用

使用AndroidStudio新建一个空项目。

在MainActivity中定义native方法nativeTest

1
public static native int nativeTest();

在main文件夹的同级新建一个ndkbuild目录,里面创建两个文件”hello-jni.c”和”Andorid.mk”

1
2
3
4
5
6
7
#include <jni.h>
int test(){
return 123456;
}
jint Java_com_chs_ndktest_MainActivity_nativeTest(){
return test();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#定义模块当前的路径(必须定义在文件开头,只需要定义一次)
LOCAL_PATH := $(call my-dir)

#Makefile中可以引入其他的Makefile文件
#编译模块时,清空当前环境变量(LOCAL_PATH除外)
include $(CLEAR_VARS)

#当前模块名(这里会生成libhello-jni)
LOCAL_MODULE :=hello-jni

#编译所需要的源文件 多个文件以空格隔开
LOCAL_SRC_FILES :=hello-jni.c

#表示当前模块将要被编译成一个共享库
include $(BUILD_SHARED_LIBRARY)

修改gradle文件,配置支持的cup和Android.mk的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
defaultConfig {
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还要了解一下。

# c/c++

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×