编译流程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");
复制代码