05 May 2021
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»]
}
这里有两种定义方式
- 内置的plugin,比如我们熟知的
java
- 外部依赖的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
,resolutionStrategy
和repositories
三个子块, 分别用于:
- 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只服务于当前这个工程,并不需要发布到仓库中,知识后我们就可以受用buildSrc
。
buildSrc
是一个特殊的模块名,是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上,欢迎下载测试
04 Jan 2021
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目录里执行构建,并执行构建结果
- 有的文档里可能会用其他的函数引入动态库。我们这里使用的库是3.x引入的官方推荐的方式
- 添加头文件查找目录可以用其他方式代替:
在minus目录下的CMakeLists.txt里增加头文件的定义
# 这里INTERFACE的意思是当前这个动态库不需要使用到,但是使用这个动态库的需要
target_include_directories(minus
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}
)
然后根目录的CMakeLists.txt里的target_include_directories
声明的${CMAKE_CURRENT_SOURCE_DIR}/src/minus
就可以不要了。
添加预编译动态库
下一步我们将在外部先编译一个外部库,然后引入这个外部库。假设我们已经编译好这个外部库,并将头文件和动态库文件放到根目录下的libs
和include
├── 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
11 Dec 2020
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
07 Dec 2020
发展历程
- so本地加密,导入内存解密,壳加载器跑完不再做其他事情
- 程序正常运行时,壳可以重新接管控制权
- vmp保护(第4代加壳)
思路
-
- 破坏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)
-
- 代码混淆
- llvm源码级混淆(Clang+LLVM): Clang作为LLVM 的一个编译器前端,对源程序进行词法分析和语义分析,形成AST(抽象语法树) ,最后用LLVM作为后端代码的生成器
- 花指令:在C语言中,内嵌arm汇编的方式,可加入arm花指令,迷惑IDA
-
- so vmp保护:写一个ART虚拟执行so中被保护的代码
性能问题
- 破坏ELF结构基本不会影响性能
- 加解密会影响性能
- 代码混淆对性能影响比较大
- so vmp分为有源和无源,会影响性能。
结论
可以先解决静态分析问题,动态加解密先从静态加载搞起
- 为什么说SO加固+无源码VMP是最佳的Android手游安全保护方案?
- Android SO壳的发展历程
07 Dec 2020
目前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 功能)。
这个要重点做说明
三个对象
- 主体
- 目标
访问对象,一般是文件,包括普通文件,设备文件,网络设备文件
- 规则
可以看到Android给所有应用分配一个一个u
的用户名, 这里角色也不是很重要,重点关注域。
域的访问规则由策略文件提供
Trusty 可信执行环境 (TEE)
Trusty 是一种安全的操作系统 (OS),可为 Android 提供可信执行环境 (TEE)。Trusty 操作系统与 Android 操作系统在同一处理器上运行,但 Trusty 通过硬件和软件与系统的其余组件隔离开来。