gradle plugin入门

gradle的核心部分仅仅提供了基本的功能和通用的机制,比如task, plugin, project的概念实现,groovy/kotlin语言的实现接口。我们所熟知的功能其实大部分都是由plugin提供的。比如JavaCompile提供了java代码的构建,提供了java代码构建过程中的参数配置的代码块,源文件的目录结构的配置和默认约定等。具体的说,gradle plugin可以为我们提供以下的几个功能

  • 扩展了gradle的模型,增加新的DSL元素,让我们可以为插件动态的提供配置信息
  • 可以为工程提供能力,比如定义一个task提供打印依赖树的能力
  • 为其他插件的必填配置提供默认的值,比如在一个工程内的所有模块都用到的底层依赖提供统一的版本号

gradle plugin的类型

  • Script plugin
  • Binary plugin

Script plugin就是简单的一个gradle文件,可以是在本地,也可以是一个远程的url。Script plugin是自解析的;也就是说,它一经引用就会被解析执行。一般是一些陈述性质的脚本。比如在另外的gradle里定义的ext代码块

apply from '{replace with the uri of your script file}'

Binary plugin指的是实现了Plugin接口的类,这个类可以是在脚本里,也可以是打包好的jar文件,或者是在buildSrc模块里,或者是一个单独发布的Plugin。一般我们说的插件也就是这里的Binary plugin。这也是我们下面我们要介绍的Plugin类型

gradle plugin使用

一般我们是使用发布到仓库的的plugin。有两种方式引入

  • plugins 代码块里定义plugin(plugins id)
  • 在主代码里声明(apply plugin)

plugins代码块中定义plugin

plugins {
    id «plugin id»
    id «plugin id» version «plugin version» [apply «false»]
}

这里有两种定义方式

  1. 内置的plugin,比如我们熟知的java
  2. 外部依赖的plugin,需要声明版本

延迟apply

默认情况下在plugins代码块里声明的plugin都会被立即apply,也就是Plugin类里的apply函数是否会被立即执行。可以通过在当前plugin id 声明后面添加apply fasle来让plugin延迟apply。比如

// build.gradle
plugins {
    id 'com.example.hello' version '1.0.0' apply false
    id 'com.example.goodbye' version '1.0.0' apply false
}
// hello-a/build.gradle
plugins {
    id 'com.example.hello'
}
// hello-b/build.gradle
plugins {
    id 'com.example.hello'
}
// hello-c/build.gradle
plugins {
    id 'com.example.goodbye'
}

甚至于你可以通过再重新定义plugin的版本,在子模块里使用自己认为更好的plugin版本

plugin的仓库

一旦脚本开始执行,gradle会搜索声明的plugin有没有在核心plugins里(gradle自带的); 如果没有,就去中心仓库里找,默认是去官方的中心仓库,可以在plugins.gradle.org中搜索。

可以在settings.gradle中设置plugin的仓库地址

// settings.gradle
pluginManagement {
    repositories {
        maven {
            url './maven-repo'
        }
        gradlePluginPortal()
        ivy {
            url './ivy-repo'
        }
    }
}

如果使用nexus仓库,可以创建一个proxy类型的仓库,代理地址填写https://plugins.gradle.org/m2/

pluginManagement块可以声明在settings.gradle或者init.gradle里,有plugins,resolutionStrategyrepositories三个子块, 分别用于:

  • plugins: 声明plugin
  • resolutionStrategy: 允许自定义规则来修改plugin的版本,添加plugin等
  • repositories: 声明plugin的插件仓库地址

详情参考官方文档

gradle plugin的开发

首先在build.gradle中引入Plugin的开发依赖库

plugin {
   id 'java-gradle-plugin'
}

dependencies {
    // Use the latest Groovy version for building this library
    implementation  gradleApi()
}

然后编写代码,下面是一个用Java编写的简单插件类。当插件被加载的时候,apply函数会自动执行

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyFirstPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        System.out.println("Hello from the GreetingPlugin");
    }
}

可能有人会有疑问,如果我一个工程里包含多个Plugin如何识别的。 可以这样理解,这里依赖的是一个包含了plugin类的jar包(通过gradle的版本管理下载到本地缓存),然后调用apply plugin语句标明我们要使用哪个Plugin类。 我们也可以自己打包一个jar文件,然后依赖进来,然后再apply plugin jar中的类。(使用略) 至于使用id的时候,一个id只会对应一个plugin实现类,后面会具体讲到

有时候我们会在插件里添加多个不同的任务来完成不同的功能。这时候我们就可以自定义任务类

import org.gradle.api.DefaultTask;

public class HelloTask extends DefaultTask {

    public void run() {
        System.out.println("Hello from task " + getPath() + "!");
    }
}

然后将HelloTask这个任务添加到插件中

public void apply(Project project) {
    ...
    // 在插件的apply函数中添加
    project.getTasks().create("hello", HelloTask.class);
    ...
    // 或者也可以这样
    project.task('taskName') {
            doLast {
                println 'Hello from the GreetingPlugin'
            }
    ...
}
        

gradle plugin的发布

发布分为两种。

  • 第一种就跟普通的Java Library一样;但是这种发布的Plugin的使用只能使用apply plugin的方式使用。
  • 第二种会附带发布到gradle的plugin仓库(中心仓库)。

普通发布

gradle plugin也是一个jar包,我们可以类似Java Library或者类似Android Library的方式发布

plugins {
	id 'maven'
}


afterEvaluate { project ->
    uploadArchives {
        repositories {
            mavenDeployer {
                configurePOM(pom)

                repository(url: getReleaseRepositoryUrl()) {
                    authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
                }
                snapshotRepository(url: getSnapshotRepositoryUrl()) {
                    authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
                }
            }
        }
    }
}

发布之后,我们就能够用以apply plugin的方式使用了。

// build.gradle
buildscript {
	dependencies {
        classpath '{group-id}:{artification}:{version}'
    }
}
// module/build.gradle
apply plugin: '{plugin-class}'

plugin id发布

这种发布方式让gradle可以用plugins id的方式使用plugin

plugins {
    id 'java-gradle-plugin'
    id 'maven-publish' // 发布插件
}

// plugins id发布了两个部分,jar包和索引部分,这里声明jar包的包名和版本,这里的版本和plugins id的版本是一致的
group 'com.example.plugin.MyFirstPlugin'
version '1.0.0'

gradlePlugin {
    plugins { // 一个工程可以包含多个plugin,所以在这个代码块里可以声明多个plugin节点
        myPlugins { // 这里是一个自定义的节点,可以起自己喜欢的名字,只要不占用保留字
            id = 'com.example.plugin.MyFirstPlugin' // 这个就是我们的plugin id
            implementationClass = 'com.example.plugin.MyFirstPlugin' // plugin id的实现类
        }
    }
}

publishing {
    repositories {
        maven {
            url getRepositoryUrl()
            credentials {
                username getRepositoryUsername()
                password getRepositoryPassword()
            }
        }
    }
}

这里时候就可以使用gradle publish命令进行发布了

TIP: 构建的插件的时候使用的JDK版本需要和使用插件时候环境的JDK版本需要保持一致,否者会报错;Android Studio一般用的自带的JDK是1.8,而我们一般开发用的IDEA用的自定义版本是11(文章编写的时候),需要将IDEA的JDK版本降到1.8(建议),或者Android Studio升到11。

使用buildscript模块

有时候我们的plugin只服务于当前这个工程,并不需要发布到仓库中,知识后我们就可以受用buildSrcbuildSrc是一个特殊的模块名,是gradle保留的模块名,不能用于自定义模块,不需要在settings.gradle中添加到模块列表中(会报错)

buildSrc的模块创建不管是IDEA还是Android Studio都没有提供模板,我们就按照普通的Java模块创建。 删除掉不必要文件和声明。

├── build.gradle
└── src
    └── main
        └── java
            └── com
                └── example
                    └── plugin
                         └── MyFirstPlugin.java

我们这里选择用Java来开发gradle plugin,也可以选择groovy或者kotlin,代码都是类似的。只是需要在build.gradle里引入对应的语言依赖和相对应构建插件。

我们以刚才的MyFirstPlugin类举例。开发完buildSrc模块后就可以在其他模块的build.gradle中依赖具体的某个plugin实现类

// module/build.gradle

apply plugin: com.example.plugin.MyFirstPlugin

buildSrc有性能问题,有建议用composite builds代替;但是现在还是可以用的,也是官方建议的一种使用方式

写在最后

plugin的开发作为不管是Java开发也好,android开发也好,应该是一项需要掌握的技能。除了官网意外,google的android插件是一个很好的学习范例 本文提到的代码也发布到github上,欢迎下载测试

CMake入门

CMake是个一个开源的跨平台自动化建构系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录、多个应用程序与多个库。 它用配置文件控制建构过程(build process)的方式和Unix的make相似,只是CMake的配置文件取名为CMakeLists.txt。CMake并不直接建构出最终的软件,而是产生标准的建构档(如Unix的Makefile或Windows Visual C++的projects/workspaces),然后再依一般的建构方式使用。这使得熟悉某个集成开发环境(IDE)的开发者可以用标准的方式建构他的软件,这种可以使用各平台的原生建构系统的能力是CMake和SCons等其他类似系统的区别之处。 CMake配置文件(CMakeLists.txt)可设置源代码或目标程序库的路径、产生适配器(wrapper)、还可以用任意的顺序建构可执行文件。CMake支持in-place建构(二进档和源代码在同一个目录树中)和out-of-place建构(二进档在别的目录里),因此可以很容易从同一个源代码目录树中建构出多个二进档。CMake也支持静态与动态程序库的建构。

“CMake”这个名字是”Cross platform Make”的缩写。虽然名字中含有”make”,但是CMake和Unix上常见的“make”系统是分开的,而且更为高端。 它可与原生建置环境结合使用,例如:make、ninja、苹果的Xcode与微软的Visual Studio。

第一个工程

我们先简单的构建一个Hello World

/************main.c****************/
#include <stdio.h>
int main() {
  printf("hello world!\n");
  return 0;
}

写一个CMake

#CMakeLists.txt
# 定义最低版本
cmake_minimum_required(VERSION 3.4.1)

# 定义工程名
project(main)

# 定义目标名和源文件
add_executable(main main.c)

在构建的时候,我们习惯于将构建产物放到一个独立的目录中去,这个目录一般命名为build

$ mkdir build && cd build
$ cmake .. # 生成makefile
$ make     # 使用make命令生成目标文件, 也可以用cmake命令间接调用: cmake --build .

到这里我们完成了第一个工程的构建。 在这个构建过程,我们使用了三个内置的方法来定义我们的工程

  • cmake_minimum_required
  • project
  • add_executable

这些方法的定义都可以在cmake的官方文档中找到

多文件工程的代码组织

一般工程不会只有一个文件,对于多文件的工程,最简单的是在add_executable中添加多个文件

...
add_executable(main main.c add.c)

或者更一般的使用正则

...
add_executable(main *.c )

或者我们一般将源码放在src目录下,然后使用file命名组织源码

...
file(GLOB_RECURSE SRC_CORE src/*.c)
add_executable(main ${SRC_CORE})

添加头文件

..
target_include_directories(main PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
...

同时我们还可以根据不同的平台来过滤编译的代码

...
# 非32位系统剔除掉dalvik, 这是android构建中用到的
if(NOT ${CMAKE_ANDROID_ARCH_ABI} STREQUAL "armeabi-v7a")
    list(FILTER SRC_CORE EXCLUDE REGEX "${PROJECT_SOURCE_DIR}/src/dalvik/.*")
endif()

添加编译选项

我们在CMakeLists中添加对未使用变量的警告

set(CMAKE_C_FLAGS "-Wunused-variable)

set函数用于设置变量,可以是自定义的变量,也可以是环境变量。环境变量 我们用到的主要有CMAKE__FLAGS

  • CFLAGS
  • CXXFLAGS
  • CMAKE_CUDA_FLAGS
  • CMAKE_Fortran_FLAGS

用于设置安装目录的CMAKE_PREFIX_PATH

参考官方文档

构建library

构建library使用add_library命令。 我们在src目录下创建一个minus的目录,用于存放libmunus.so的代码的构建结果。同时创建以下三个文件

// src/minus/minus.h
#ifndef ADD_H
#define ADD_H
int minus(int a, int b);
#endif
// src/minus/minus.c
#include "minus.h"
int minus(int a, int b) {
  return a - b;
}

# src/minus/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(libminus)
add_library(minus SHARED minus.c)

接着按照习惯在minus目录下创建build目录进行构建

$ mkdir build && cd build
$ cmake .. # 生成makefile
$ make     # 使用make命令生成目标文件, 也可以用cmake命令间接调用: cmake --build .

我们可以在build目录里看到libminus.so

如果是macos应该是看到libminus.dylib

用源码的形式使用动态库

接下来我们要用子工程的方式引用上节的动态库。使用add_subdirectory来使用这个动态库

# src/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)

project(main)

file(GLOB_RECURSE SRC_CORE src/*.c)
# 我们需要过滤掉minus目录下的文件,那是libminus.so的源码文件
list(FILTER SRC_CORE EXCLUDE REGEX "${PROJECT_SOURCE_DIR}/src/minus/.*")

# 添加子依赖
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/minus)

add_executable(main ${SRC_CORE})

# 添加Library
target_link_libraries(main minus)

# 添加头文件查找目录
target_include_directories(main
	PUBLIC
	target_include_directories(main PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
	${CMAKE_CURRENT_SOURCE_DIR}/src/minus)

更改main.c的源码,测试动态库是否有用

#include <stdio.h>
#include "add.h"
#include "minus.h"
int main(){
  printf("1+2=%d\n", add(1, 2));
  printf("2-1=%d\n", minus(1, 2));
  return 0;
}

最后在根目录的build目录里执行构建,并执行构建结果

  1. 有的文档里可能会用其他的函数引入动态库。我们这里使用的库是3.x引入的官方推荐的方式
  1. 添加头文件查找目录可以用其他方式代替: 在minus目录下的CMakeLists.txt里增加头文件的定义
     # 这里INTERFACE的意思是当前这个动态库不需要使用到,但是使用这个动态库的需要
     target_include_directories(minus
           INTERFACE
           ${CMAKE_CURRENT_SOURCE_DIR}
    )
    

    然后根目录的CMakeLists.txt里的target_include_directories声明的${CMAKE_CURRENT_SOURCE_DIR}/src/minus就可以不要了。

添加预编译动态库

下一步我们将在外部先编译一个外部库,然后引入这个外部库。假设我们已经编译好这个外部库,并将头文件和动态库文件放到根目录下的libsinclude

├── include
│   └── multiple.h
├── libs
│   └── libmultiple.so

并在CMakeLists.txt中添加依赖


# 添加multiple动态库的引入
add_library( multiple
             SHARED
             IMPORTED
		)

# 声明动态库的位置和头文件的位置
set_target_properties( # Specifies the target library.
             multiple

             PROPERTIES

             IMPORTED_LOCATION # Provides the path to the library you want to import.

             ${CMAKE_SOURCE_DIR}/libs/libmultiple.so

             INTERFACE_INCLUDE_DIRECTORIES

             ${CMAKE_SOURCE_DIR}/include

             )

add_executable(main ${SRC_CORE})

# 添加依赖
target_link_libraries(main minus multiple)

写完构建脚本后,我们再在main.c中添加测试代码

#include <stdio.h>
#include "minus/minus.h"
#include "add.h"
#include "multiple.h"

int main(){
  printf("1+2=%d\n", add(1, 2));
  printf("2-1=%d\n", minus(2, 1));
  printf("2*2=%ld\n", multiple(2, 2));
  return 0;
}

在根目录中的build子目录中执行构建,并执行结果

➜  build git:(master) ./main
1+2=3
2-1=1
2*2=4

如果是其他系统,预构建的动态库需要做替换,我这边就不写多平台的实现了。留给你们做练习

以源码方式添加第三方动态库

一般源码方式的第三方的动态库都是以压缩包或者git仓库的方式存在的,使用方式大概有几种

  • 最简单的使用方式是将源码下载到当前目录下,并用add_subdirectory的方式进行依赖。
  • 如果是git仓库可以使用git submodule的方式将第三方仓库加入到源码管理中
  • 使用ExternalProject相关API进行管理
  • 使用FetchContent相关的API进行管理

第一种方式更新源码比较麻烦,无法进行快速切换; 第二种方式使用过程中感觉比较不好不好使用。多层级git仓库的缺点网上有相关的讨论,可以自行去查找 第三种方式在构建过程中采取下载,配置文件比较不好写 我们这里采用第四种方式

# FetchContent需要额外引入
include(FetchContent)
# 声明fetch对象的属性
FetchContent_Declare(
  division-lib
  GIT_REPOSITORY    git@git.sdp.nd:cmk/division-lib.git
  GIT_TAG           master
  SOURCE_DIR        "${CMAKE_CURRENT_BINARY_DIR}/division"
)

# available
FetchContent_MakeAvailable(division-lib)
...
add_executable(main ${SRC_CORE})
...
# 添加头文件的搜索路径,当然也可以像上面那样修改division库的CMakeList.txt,这里就无需添加了
target_include_directories(main PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/division/src)

添加测试代码

#include <stdio.h>
#include "minus/minus.h"
#include "add.h"
#include "multiple.h"
#include "division.h"

int main(){
  printf("1+2=%d\n", add(1, 2));
  printf("2-1=%d\n", minus(2, 1));
  printf("2*2=%ld\n", multiple(2, 2));
  printf("4/2=%f\n", divide(4, 2));
  return 0;
}

在根目录中的build子目录中执行构建,并执行结

➜  build git:(master) ✗ ./main
1+2=3
2-1=1
2*2=4
4/2=2.000000

使用CMake构建android动态连接口

CMake是个一个开源的跨平台自动化建构系统。CMake并不直接建构出最终的软件,而是产生标准的建构工程文件(如Unix的Makefile或Windows Visual C++的projects/workspaces),然后再依一般的建构方式使用。在这里,CMake生成的是Ninja的构建工程(Ninja是一个专注于速度的小型构建系统[1],由Evan Martin于2010年在Chrome团队工作时开发),然后再用Ninja来生成动态链接库

基于构建效率和代码结构考虑,我们后续的jni项目建议使用cmake进行c++项目的构建

一个简单的例子

CMake的配置文件取名为CMakeLists.txt。

########## CMakeLists.txt ##########

# 最低版本要求,版本要求大于等于3.4.1
cmake_minimum_required(VERSION 3.4.1)

# 定义动态链接库的名字、类型和源码
add_library( # Specifies the name of the library.
             native-lib
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             main.c)

# 定义需要链接的其他库
# Links your native library against one or more other native libraries.
target_link_libraries( # Specifies the target library.
                        native-lib # 这里声明目标库
                        android    # 随后目标库需要链接的库
                        log)       # 可以继续往下加,这里都是NDK里的

在当前目录里创建源码

// main.c

#include <android/log.h>
#include <jni.h>

#define TAG "MyApplication"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__)

#define JNI_API_DEF(f) Java_com_nd_app_factory_imapp0nd_MainActivity_##f

JNIEXPORT jint JNICALL
JNI_API_DEF(show)(JNIEnv *env, jobject thiz) {
  LOGW("hello world");
  return 5;
}

在这里需要的文件都生成好了,我们需要调用构建命令

${ANDROID_SDK_HOME}/cmake/3.6.3155560/bin/cmake \
-H. \
-B./arm64-v8a \
-G"Android Gradle - Ninja" \
-DANDROID_ABI=arm64-v8a \
-DANDROID_NDK=${ANDROID_SDK_HOME}/ndk/21.3.6528147 \
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=./release/obj/arm64-v8a \
-DCMAKE_MAKE_PROGRAM=${ANDROID_SDK_HOME}/cmake/3.6.3155560/bin/ninja \
-DCMAKE_TOOLCHAIN_FILE=${ANDROID_SDK_HOME}/ndk/21.3.6528147/build/cmake/android.toolchain.cmake \
-DANDROID_NATIVE_API_LEVEL=23 \
-DANDROID_TOOLCHAIN=clang
  • 第一个参数H表示CMakeLists.txt所在的路径。我们这里使用当前路径.
  • 第二个参数B表示生成的构建工程目录,如果不存在CMake会为我们创建.这里这里使用./arm64-v8a
  • 第三个参数G表示我们生成的构建工程是什么,默认应该是make。我们这里需要使用Ninja,所以使用Android Gradle - Ninja
  • 第四个参数DANDROID_ABI是我们要生成的so的ABI
  • 第五个参数DCMAKE_LIBRARY_OUTPUT_DIRECTORY是ndk路径
  • 第六个参数DCMAKE_LIBRARY_OUTPUT_DIRECTORY是最终so的输出路径,这里声明后,会最终输出给Ninja工程的配置文件.我们这里使用./release/obj/arm64-v8a
  • 第七个参数DCMAKE_MAKE_PROGRAM表示最终构建的构建工具路径,我们使用Ninja,所以我们这里使用Ninja的路径${ANDROID_SDK_HOME}/cmake/3.6.3155560/bin/ninja
  • 第八个参数DCMAKE_TOOLCHAIN_FILE表示使用的工具链的地址
  • 第九个参数DANDROID_NATIVE_API_LEVEL是android api
  • 第十个参数DANDROID_TOOLCHAIN表示android构建工具链

其中从第四个参数开始,都是自定义的,会写入到最终的构建系统的

运行完构建命令后,如果我们sdk和ndk的版本/路径都没有问题,会输出

 $ ${ANDROID_SDK_HOME}/cmake/3.6.3155560/bin/cmake \
-H. \
-B./arm64-v8a \
-G"Android Gradle - Ninja" \
-DANDROID_ABI=arm64-v8a \
-DANDROID_NDK=${ANDROID_SDK_HOME}/ndk/21.3.6528147 \
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=./release/obj/arm64-v8a \
-DCMAKE_MAKE_PROGRAM=${ANDROID_SDK_HOME}/cmake/3.6.3155560/bin/ninja \
-DCMAKE_TOOLCHAIN_FILE=${ANDROID_SDK_HOME}/ndk/21.3.6528147/build/cmake/android.toolchain.cmake \
-DANDROID_NATIVE_API_LEVEL=23 \
-DANDROID_TOOLCHAIN=clang

-- Configuring done
-- Generating done
-- Build files have been written to: /Users/mk/security/test/cmakeTest/arm64-v8a

然后我们再运行Ninja生成最终的结果

$ $ANDROID_HOME/cmake/3.6.3155560/bin/ninja -C ./arm64-v8a/
ninja: Entering directory `./arm64-v8a/'
[1/1] Linking C shared library release/obj/arm64-v8a/libnative-lib.so

最终我们就得到了libnative-lib.so

使用自定义高版本cmake

由于Android Sdk里自带的cmake版本太低,有些API或者子模块可能会不存在,所以,我们有时候会使用自定义的cmake版本。 测试发现新版本的cmake(3.19.2)也是支持${ANDROID_SDK_HOME}/cmake/3.10.2.4988404/bin/ninja(1.8)的,所以我们可以仅替换掉cmake的路径,以及-G参数(新版本cmake没有 Android Gradle - Ninja这个generator里,统一使用Ninja

修改后的命令行变成

${where-you-install-cmake}/cmake \
-H. \
-B./arm64-v8a \
-G"Ninja" \
-DANDROID_ABI=arm64-v8a \
-DANDROID_NDK=${ANDROID_SDK_HOME}/ndk/21.3.6528147 \
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=./release/obj/arm64-v8a \
-DCMAKE_MAKE_PROGRAM=${ANDROID_SDK_HOME}/cmake/3.10.2.4988404/bin/ninja \
-DCMAKE_TOOLCHAIN_FILE=${ANDROID_SDK_HOME}/ndk/21.3.6528147/build/cmake/android.toolchain.cmake \
-DANDROID_NATIVE_API_LEVEL=23 \
-DANDROID_TOOLCHAIN=clang

so加固技术分析

发展历程

  1. so本地加密,导入内存解密,壳加载器跑完不再做其他事情
  2. 程序正常运行时,壳可以重新接管控制权
  3. vmp保护(第4代加壳)

思路

    1. 破坏ELF(so)文件结构 —— 增加静态分析难度
      • 破坏ELF Helader: 对e_shoff, e_shnum, e_shstrndx, e_shentsize字段处理,变为无效值,导致IDA无法解析该SO文件
      • 删除Section Header: 在动态链接过程中, Section Header不会被用到,可以随意删除,导致IDA无法打开该文件
  • 2.动态加解密
    • 2.1. 有源码so可以加密Section或者函数 —— 解密过程放到源so或者java层
    • 2.2 无源码加密Section或者函数: 将解密函数放在另一个so中,只需保证解密函数在被加密函数执行前执行即可。执行时机的选择:(1)在linker执行.init_array时(2)在OnLoad函数中。注意:解密so一定要放在被解密so后加载,否则,搜索进程空间找不到被解密的so。解密后需要写文件,会被动态获取
      • 针对这种情况,考虑从内存中加载so(自定义linker)
    1. 代码混淆
      • llvm源码级混淆(Clang+LLVM): Clang作为LLVM 的一个编译器前端,对源程序进行词法分析和语义分析,形成AST(抽象语法树) ,最后用LLVM作为后端代码的生成器
      • 花指令:在C语言中,内嵌arm汇编的方式,可加入arm花指令,迷惑IDA
    1. so vmp保护:写一个ART虚拟执行so中被保护的代码

性能问题

  1. 破坏ELF结构基本不会影响性能
  2. 加解密会影响性能
  3. 代码混淆对性能影响比较大
  4. so vmp分为有源和无源,会影响性能。

结论

可以先解决静态分析问题,动态加解密先从静态加载搞起

  1. 为什么说SO加固+无源码VMP是最佳的Android手游安全保护方案?
  2. Android SO壳的发展历程

安卓系统安全特性

目前android系统提供了一套比较完成的安全体系,只要有以下几个特性组成

应用沙盒

Android 平台利用基于用户的 Linux 保护机制识别和隔离应用资源,为此,Android 会为每个 Android 应用分配一个独一无二的用户 ID (UID),并在自己的进程中运行。Android 会使用此 UID 设置一个内核级应用沙盒。

应用签名

通过应用签名,开发者可以标识应用创作者并更新其应用,而无需创建复杂的接口和权限。在 Android 平台上运行的每个应用都必须有开发者的签名。

身份验证

Android 采用通过用户身份验证把关的加密密钥机制,该机制需要加密密钥存储区以及服务提供商和用户身份验证程序。

在配有指纹传感器的设备上,用户可以注册一个或多个指纹,并使用这些指纹解锁设备以及执行其他任务。Gatekeeper 子系统会在可信执行环境 (TEE) 中执行设备解锁图案/密码身份验证。

Android 9 及更高版本包含 Android 受保护的确认功能,使用户能够正式确认关键交易(如付款)。

生物识别

Android 9 及更高版本包含一个 BiometricPrompt API,应用开发者可以使用该 API 采用与设备和模态无关的方式将生物识别身份验证集成到其应用中。只有极为安全的生物识别技术才能与 BiometricPrompt 集成。

加密

设备经过加密后,所有由用户创建的数据在存入磁盘之前都会自动加密,并且所有读取操作都会在将数据返回给调用进程之前自动解密数据。加密可确保未经授权方在尝试访问相应数据时无法读取数据。

密钥库

Android 提供了一个由硬件支持的密钥库,以提供生成密钥、导入和导出非对称密钥、导入原始对称密钥、使用适当的填充模式进行非对称加密和解密等功能。

SELinux

作为 Android 安全模型的一部分,Android 使用安全增强型 Linux (SELinux) 对所有进程强制执行强制访问控制 (MAC)(SELinux是MAC的一种实现),甚至包括以 Root/超级用户权限运行的进程(Linux 功能)。

这个要重点做说明

三个对象

  • 主体
    • 访问者,一般是进程
  • 目标 访问对象,一般是文件,包括普通文件,设备文件,网络设备文件
  • 规则
    • 确定访问者是否能够访问目标

      安全上下文

      SELinux分配一个三字符串上下文,包含了用户名、角色和域(或者类型)。主体和目标都会拥有一个安全上下文

      # 这是一个普通的android应用的安全上下文
      gtaxlltechn:/ # ps -Z 9644                                                                                                                       
      LABEL                          USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME                       
      u:r:untrusted_app:s0:c512,c768 u0_a349       9644  2520 2237912 207196 SyS_epoll+   e5b62ebc S com.nd.app.factory.egvod
      

      我们也可以看一下文件的安全上下文

      # 普通文件
      gtaxlltechn:/ # ls -Z /sdcard/alipay/                                                                                                            
      u:object_r:sdcardfs:s0 com.eg.android.AlipayGphone
      # 系统文件
      gtaxlltechn:/ # ls -Z /etc/hosts                                                                                                                 
      u:object_r:system_file:s0 /etc/hosts
      

可以看到Android给所有应用分配一个一个u的用户名, 这里角色也不是很重要,重点关注域。 域的访问规则由策略文件提供

Trusty 可信执行环境 (TEE)

Trusty 是一种安全的操作系统 (OS),可为 Android 提供可信执行环境 (TEE)。Trusty 操作系统与 Android 操作系统在同一处理器上运行,但 Trusty 通过硬件和软件与系统的其余组件隔离开来。