c/c++编译器配置(交叉编译重要参数)、交叉编译动态库与as配置、mk初步

gcc/g++/clang,相当于javac:

了解c/c++编译器的基本使用,能够在后续移植第三方框架进行交叉编译时,清楚的了解应该传递什么参数。

clang:

clang 是一个C、C++、Object-C的轻量级编译器。基于LLVM (LLVM是以C++编写而成的构架编译器的框架系统,可以说是一个用于开发编译器相关的库)

gcc:

GNU C编译器。原本只能处理C语言,很快扩展,变得可处理C++。(GNU计划,又称革奴计划。目标是创建一套完全自由的操作系统)

g++:

GNU c++编译器

gcc、g++、clang都是编译器。

  • gcc和g++都能够编译c/c++,但是编译时候行为不同。
    这块需要特别的注意,并非gcc是为c而生,而g++是为c++而生的。

  • clang也是一个编译器,对比gcc,它具有编译速度更快、编译产出更小等优点,但是某些软件在使用clang编译时候因为源码中内容的问题会出现错误。

  • clang++与clang就相当于gcc与g++。

对于gcc与g++:

  1. 后缀为.c的源文件,gcc把它当作是C程序,而g++当作是C++程序;后缀为.cpp的,两者都会认为是c++程序

  2. g++会自动链接c++标准库stl,gcc不会

  3. gcc不会定义__cplusplus宏,而g++会

编译器过程:

一个C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)、和连接(linking)才能变成可执行文件。

  • 预处理
    gcc -E main.c  -o main.i 

    -E的作用是让gcc在预处理结束后停止编译。

    ​预处理阶段主要处理include和define等。它把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替。

  • 编译阶段

    gcc -S main.i -o main.s

      -S的作用是编译后结束,编译生成了汇编文件。

    ​ 在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。

  • 汇编阶段

    gcc -c main.s -o main.o

    ​汇编阶段把 .s文件翻译成二进制机器指令文件.o,这个阶段接收.c, .i, .s的文件都没有问题。

  • 连接阶段

    gcc -o main main.s

    ​链接阶段,链接的是函数库。在main.c中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明。系统把这些函数实现都被做到名为libc.so的动态库。
    函数库一般分为静态库和动态库两种:

    • 静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。Linux中后缀名为”.a”。
    • 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库。Linux中后缀名为”.so”,如前面所述的libc.so就是动态库。gcc在编译时默认使用动态库。

    静态库节省时间:不需要再进行动态链接,需要调用的代码直接就在代码内部。

    动态库节省空间:如果一个动态库被两个程序调用,那么这个动态库只需要在内存中。
    关于这两者的区别咱们用一个具体的例子来形象的说明一下:
    1、假如有一个静态库a.a,它里面包含一个test函数,然后有个源文件source.c,它里面有个test1函数,而这个源文件需要链接a.a这个静态库,当编译完成之后source.a里面就拥有了test+test1两个函数了,也就是编译期就将所有的符号加入到.so库中。
    2、假如有一个动态库a.so,它里面包含一个test函数,然后有个源文件source.c,它里面有个test1函数,而这个源文件需要链接a.o这个动态库,当编译完成之后source.a里面就只有一个test函数,在运行时会动态的加载a.so。

    注:Java中在不经过封装的情况下只能直接使用动态库,也就是说:

了解了上面的一大堆理论之后,下面来动手做实验,先新建一个main.c文件:

然后用gcc命令来将其编译运行一下:

那如果将main这个可执行文件放到Android手机上,能否也能正常执行输出呢?咱们来将它放到手机的sdcard上【注意:该手机需要的文件的执行权限才行,否则会报无权限,反正我用的带root的模拟器执行都不行,最后找了部彻底root的华为机子来做的实验】,如下:

这是为啥呢?其实是Android手机的CPU不同,所以CPU的指令集也不同,要在mac上编译出来的可执行文件能在Android上也能运行这里就需要涉及到交叉编译相关的东东了,其实在NDK中提供有交叉编译工具,先进咱们的SDK来瞅一下:

下面咱们手动尝试通过NDK的交互编译工具来尝试一下,前提当然得下载Android NDK才行,具体直接上官网下就成了,我的电脑已经下载好了,所以下面关心的就是如何来进行交叉编译啦,首先当然得用上图中的NDK提供的gcc啦,为了方便使用咱们先将这个gcc的文件路径配置成临时的环境变量,省得每次编译时需要写一大堆的路径,如下:

好,咱们来用它来对main.c进行编译一下:

原因是由于此时需要用NDK提供的头文件来进行链接编译了, 那如何指定头文件的查找路径为NDK提供的头文件呢,有如下方式可以指定,下面先来了解一下【非常重要,是编译三方库非常重要的知识点】

  • --sysroot=XX
    使用XX作为这一次编译的头文件与库文件的查找目录,查找XX下面的 usr/include、usr/lib目录。
  • -isysroot XX
    头文件查找目录,覆盖--sysroot ,查找 XX/usr/include。什么意思,比如说"gcc --sysroot=目录1 main.c",如果main.c中依赖于头文件和库文件,则会到目录1中的user/include和user/lib目录去查找,而如果"gcc --sysroot=目录1 -isysroot 目录2 main.c"意味着会查找头文件会到目录2中查找而非--sysroot所指定的目录1下的/usr/include了,当然查找库文件还是在目录1下的user/lib目录去查找。
  • -isystem XX
    指定头文件查找路径(直接查找根目录)。比如"gcc --sysroot=目录1 -isysroot 目录2 -isystem 目录3  -isystem 目录4 main.c"意味着头文件查找除了会到目录2下的/usr/include,还会到isystem指定的目录3和目录4下进行查找,注意:这个isystem指定的目录就是头文件查找的全路径,而非像isysroot所指定的目录还需要定位到/usr/include目录。
  • -IXX
    头文件查找目录。

其查找头文件的优先级为:
-I -> -isystem -> sysroot

比如说:“gcc --sysroot=目录1 -isysroot 目录2 -isystem 目录3  -isystem 目录4 -I目录5 main.c”,其头文件首先会去目录5找,如果没找到则会到目录3和4找,如果还没找到则会到目录2找。

  • -LXX
    指定库文件查找目录。
  • -lxx.so
    指定需要链接的库名。

我们之前在写JNI程序时用到了Android的日志,如下:

其这个头文件中的具体实现库其实就是在NDK中的这个目录里面,如下:

其实在Android Sutdio创建支付C++工程时其实默认就将这个头文件库的查找在CMakeLists.txt已经进行声明了,如下:

如果用参数的形式来指定库查找目录其实就可以这样写:“gcc -L/Users/xiongwei/android-sdks/Android/sdk/ndk-bundle/platforms/android-21/arch-arm/usr/lib -llog”, 当然还可以用--sysroot来指令库文件的查找路径,只是路径指定需要在/usr/lib之前就成,如“gcc --sysroot/Users/xiongwei/android-sdks/Android/sdk/ndk-bundle/platforms/android-21/arch-arm/ -llog”。

明白了上面的参数之后,下面咱们采用交叉编译的方式来对我们的main.c进行重新编译,由于NDK的路径比较深,所以还是采用临时环境变量的方式来弄,现在就是要指定头文件的查找目录,所以头文件的位置涉及到两处:

所以先把这个路径定义上:

基本上有这个路径就可以了,但是在NDK16或NDK17还有如下头文件查找路径:

所以可以用-isystem参数来指定,如下:

另外还有一个子目录需要指定:

当然还是可以用-isystem参数来指定,如下:

那咱们再来加上这个CFLAGS参数编译一下:

呃,貌似配错了,具体是因为写得有问题,如下:

所以修改一下:

此时就正常编译了,咱们此时用file命令来查看一下该生成的可执行程序的文件信息,可以有个新发现:

正好就是Android能运行的指令集,所以下面咱们再将这个main导到手机上,然后这次来运行看能否见证奇迹:

然后进入到adb shell中进行执行,如下:

pie 位置无关的可执行程序:

但是有可能在windows中既始用交叉编译在手机上执行时也会报错,例如:

此就就需要加一个-pie参数了,如下:

所以手动交叉编译成Android手机能正常执行的统一加"-pie"参数就成了。

费了这么多功夫就为了能编一个能在Android手机上执行的可执行文件有啥作用么?作用其实是非常之大的哈,在之后的学习中会有体现滴,明白了这个手动编译的原理,对于将来任务三方库要编译成能在Android运行库,明白了以上内容就能让自己变得得心应手,练内功滴!!

交叉编译动态库与as配置:

在上面咱们编译出来的并非是一个动态库而只是一个可执行文件,要想要在Android工程中来使用JNI就必须将期编译成.so的动态库,因为:

所以咱们接下来利用交叉编译工具来编成动态库,然后在Android Studio中进行使用,下面先来编写一个新的.c源文件:

此时需要将它编译成动态库,就需要加一个“-fPIC -shared”参数,具体用法如下:

所以咱们来使用一下,注意还是使用NDK提供的交叉编译的GCC命令哈:

注意一下so的名称是以libXXX.so为规则,因为我们要在Android工程中来使用它,所以将它拷到咱们的Android工具当中,如下:

然后还需要建一个cpu架构相关的文件夹,类似于我们在平常引入三方.so时一样,比如:

所以校仿:

那接下来咱们就是要在程序中来调用这个动态库中的test()方法,由于没有提供这个动态库相关的头文件,所以可以使用如下关键字:

代码已经写好了,不过要正式运行之前还得进行CMakeLists.txt一系列的配置才行,下面一步步来进行配置,首先我们的so的目录在编译时是需要依赖于它但是目前还没有指令编译时查找库的路径,根据之前介绍的交叉编译的参数可以使用如下:

那如何在CMakeLists.txt中来设置呢,涉及到一些规则,记住就成了,如下:

其中还有一个小技巧,就是对于CPU架构文件夹的指定可以使用动态的方式而不用手动写死,不是不同的CPU架构的.so都是不一样的嘛:

涉及到需要修改的地方在它:

可以改用如下这种动态的方式:

这样当我们要编译其它的CPU架构时就可以动态的替换,这里先还原写死的方式,待后面需要的时候再用这个动态的方式,接着来则需要指定要链接的.so库,配置如下:

其中也就类似于写了如下参数:

好了,接着还需要去build.gradle中进行NDK的配置,首先指定我们要编译的CPU架构,目前只支持"armeabi-v7a",所以配置如下:

然后编译运行一下:

这是为啥?其实有一个非常小的细节没有注意造成:

再次编译:

所以更改一下:

再次编译,发现编译终于木有问题了,接下来咱们在MainActivity中来调用一下jni:

然后再次编译运行:

正常输出啦,但是有可能在其它手机上会输出如下异常:

此时就需要在调用之前先将咱们的libTest.so动态库给加载进来,如下:

至于为啥要再加load一次我们的生成的Test动态库,其实还是跟动静态库有关,如果是引用的静态库的话就不会有这个问题,这个在之后再做实验来说明这个问题。下面还是回到gradle对ndk配置相关的东东,在上面做实现的工程是因为建项目时就已经勾选了支付NDK的环境,如下:

那如果对于一个没有勾选这个支持的Android工程我们怎么来加入对NDK的支持呢?下面建一个全新的不支持NDK的Android工程:

然后接下来就是来在build.gradle中来进行配置将该工程变为支持NDK的,如下:

如果不确认写得对不对,可以点击看一下能否链接到源码,如果能链接到源码那写得肯定是对滴,如下:

 然后继续:

这种语法其实看着不是很符合java的语法,其实也可以用另外一种面向对象的方式来配置,具体如下:

但是貌似在我的Android Studio中这种语法不支持,所以咱们还是以上面标红的方式来配置,基本默认新建带NDK的工程就是使用的这种方式,接下来来配置要编译的CPU架构:

然后在外层还有一个externalNativeBuild配置,这次我们可以改用面向对象的方式,如下:

那这两个NDK相关的配置有啥区别呢?其实是有区别的:

由于咱们想通过mk的方式来进行编译,所以可以在外层这样写:

 

所以我们可以在src下新建一个Android.mk文件,如下:

Android.mk

微小 GNU makefile 片段。

将源文件分组为模块。 模块是静态库、共享库或独立可执行文件。 可在每个 Android.mk 文件中定义一个或多个模块,也可在多个模块中使用同一个源文件。 

 

关于Android.mk的编译脚本的编写先往后放,这里关于ndk配置还差一个东东,如下:

其实是这样的:

好,as中关于ndk的配置相关的基本已经配好了,接下来就是新建一个c/c++的源文件咱们来尝试着编译一下:

那这个源代码该要如何进行编译呢?这里就需要用到了我们建的Android.mk这个编译脚本文件啦,首先来熟悉一下大概的语法:

变量和宏

定义自己的任意变量。在定义变量时请注意,NDK 构建系统会预留以下变量名称:

  •  LOCAL_ 开头的名称,例如 LOCAL_MODULE
  •  PRIVATE_NDK_ 或 APP 开头的名称。构建系统在内部使用这些变量。
  • 小写名称,例如 my-dir。构建系统也是在内部使用这些变量。

如果为了方便而需要在 Android.mk 文件中定义自己的变量,建议在名称前附加 MY_

常用内置变量

变量名含义示例
BUILD_STATIC_LIBRARY 构建静态库的Makefile脚本 include $(BUILD_STATIC_LIBRARY)
PREBUILT_SHARED_LIBRARY 预编译共享库的Makeifle脚本 include $(PREBUILT_SHARED_LIBRARY)
PREBUILT_STATIC_LIBRARY 预编译静态库的Makeifle脚本 include $(PREBUILT_STATIC_LIBRARY)
TARGET_PLATFORM Android API 级别号 TARGET_PLATFORM := android-22
TARGET_ARCH CUP架构 arm arm64 x86 x86_64
TARGET_ARCH_ABI CPU架构 armeabi armeabi-v7a arm64-v8a

模块描述变量

变量名描述
LOCAL_MODULE_FILENAME 覆盖构建系统默认用于其生成的文件的名称 LOCAL_MODULE := foo LOCAL_MODULE_FILENAME := libnewfoo
LOCAL_CPP_FEATURES 特定 C++ 功能 支持异常:LOCAL_CPP_FEATURES := exceptions
LOCAL_C_INCLUDES 头文件目录查找路径 LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_CFLAGS 构建 C  C++ 的编译参数  
LOCAL_CPPFLAGS c++  
LOCAL_STATIC_LIBRARIES 当前模块依赖的静态库模块列表  
LOCAL_SHARED_LIBRARIES    
LOCAL_WHOLE_STATIC_LIBRARIES --whole-archive 将未使用的函数符号也加入编译进入这个模块
LOCAL_LDLIBS 依赖 系统库 LOCAL_LDLIBS := -lz

导出给引入模块的模块使用:

LOCAL_EXPORT_CFLAGS

LOCAL_EXPORT_CPPFLAGS

LOCAL_EXPORT_C_INCLUDES

LOCAL_EXPORT_LDLIBS

上面大致了解之后接下来需要在Android.mk中加入编译规则,具体如下:

所以依照上面的规则将其编写到Android.mk中:

咱们可以将其路径打印看一下:

不过在编译前还得先把mk整个配置给填充完,所以:

然后咱们来编译一下:

然后我们看一下编译出来的动态库:

这是因为我们在NDK这块只配了它:

那如果我们增加一个"x86"呢?

然后咱们再看一下编出来的APK中包含的动态库的类型:

以上就是通过手动的方式来给咱们的一个普通Android工程增加Ndk的支持,下面来继续解读一下咱们在.mk中编写的脚本的含义:

假如要有多源文件则以空格分开,如果想换行的话可以以“\”,比如:

好了,这次学习的东东说实话有些杂,但是这些知识点是非常非常之重要的基础,只有把基础打牢了才能在未来的NDK学习之路走得更加的远,坚持!!!

posted on 2018-11-12 13:55  cexo  阅读(6366)  评论(0编辑  收藏  举报

导航