NDK
NDK
在Android开发中,NDK是一组用于开发C和C++代码的工具集合。它允许开发者使用C/C++编写底层代码,并与Java代码相结合。
NDK的文件配置
如需为您的应用编译和调试原生代码,您需要以下组件:
- Android 原生开发套件 (NDK):这是一套可让您在 Android 应用中使用 C 和 C++ 代码的工具。
- CMake:这是一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果您只计划使用 ndk-build,则不需要此组件。
- LLDB:Android Studio 用于调试原生代码的调试程序。默认情况下,安装 Android Studio 时会随同安装 LLDB。
JNI(Java Native Interface)
jni用于java代码和其他语言所写的代码进行交互
JNI中的NDK文件和正常的java文件的区别在于
- 在MainActivity的位置会声明external fun stringFromJNI(): String这个函数是cpp文件的索引
- 同时在 build.gradle.kts 中会多出externalNativeBuild来对于生成的NDK文件的arm构架,x86/64构架的描述
- 同时多出了cpp文件以外还要CMakeLists的对于cmake的版本信息描述
多线程创建
#include <jni.h>
#include <string>
#include <android/log.h>
#include <pthread.h>
#define TAG "Tag_data"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,"tag",__VA_ARGS__);
void myThread() {
LOGD("myThread_data");
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_chen_anative_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
pthread_t pthread1;
pthread_create(&pthread1, nullptr, reinterpret_cast<void *(*)(void *)>(myThread), nullptr);//线程创建
pthread_join(pthread1, nullptr);//等待线程执行完成
LOGD("pthread_create Success");
pthread_exit(nullptr);//线程退出
return env->NewStringUTF(hello.c_str());
}
JNI_OnLoad
这个函数是在加载so文件加载过程中自动会去加载的一个函数,加载的会比MainActivity早,我们可以去构造这个函数,去指定需要执行代码
JNIEXPORT jint JNI_OnLoad(JavaVM *vm ,void *reserved)
{
JNIEnv *env = nullptr;
if(vm->GetEnv((void **)&env,JNI_VERSION_1_6) != JNI_OK)
{
LOGD("GETevn failed");
return -1;
}
LOGD("this is JNI_OnLOAD");
return JNI_VERSION_1_6;
}
//这个函数的返回值是JNI的版本,可以通过比对获取Env的版本查看是否符合JNI的版本
JavaVM的结构:
上面的vm类下的GetEnv函数去获取了jni中用于交互的函数,并且比较了对于的jni的版本号,我们来看看JavaVM结构体的结构体形式
c++版:
struct _JavaVM {
const struct JNIInvokeInterface* functions;
#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
c版:
struct JNIInvokeInterface {
void* reserved0;
void* reserved1;
void* reserved2;
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
通过以上我们其实可以看到的是c++版的参数少于c版的,其实可以看出来是因为c++一开始去创建了c版的JNIInvokeInterface函数的对象,然后通过指针去找到对应的函数的
SO中函数注册的过程
so中函数是如何进行注册使用的?其实是通过的JNI_OnLoad方法中进行函数注册加载的
extern "C"
JNIEXPORT jstring JNICALL
Java_com_chen_javaandso_MainActivity_stringFromJNI1(JNIEnv *env, jobject thiz,jint a ,jstring b) {
// TODO: implement stringFromJNI1()
return env->NewStringUTF(" ");
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm ,void *reserved)
{
JNIEnv *env = nullptr;
if(vm->GetEnv((void **)&env,JNI_VERSION_1_6) != JNI_OK)
{
LOGD("GETevn failed");
return -1;
}
// typedef struct {
// const char* name;
// const char* signature;
// void* fnPtr;
// }
jclass Mainactivity = env->FindClass("com/chen/javaandso/MainActivity");
JNINativeMethod method[]=
{
{"stringFromJNI1","(ILjava/lang/String;Ljava/lang/String",
(void*)Java_com_chen_javaandso_MainActivity_stringFromJNI}
};
env->RegisterNatives(Mainactivity,method,(int)sizeof(method)/sizeof(JNINativeInterface));
return JNI_VERSION_1_6;
}
这里我们可以看到的是,先获取了jclass类的函数对象,通过JNINativeMethod的method数组进行的函数注册
// typedef struct {
// const char* name;
// const char* signature;
// void* fnPtr;
// }
这里注册的是name——>函数名 signature——>对应参数以及返回值类型 fnPtr——>对应的函数指针
多个cpp文件编译成一个so文件
创建了多个cpp文件之后,假如要编译成一个so文件,我们知道编译使用NDK链接器到所有的cpp文件为一个so需要的是CMake,所以首先是CMakeList的文件包含cpp文件
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp
main.cpp
)
以及在使用过程的函数声明
void test();
extern "C" JNIEXPORT jstring JNICALL
Java_com_chen_javaandso_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */){
std::string hello = "Hello from C++";
test();
return env->NewStringUTF(hello.c_str());
}
编译多个so文件
编译多个so文件需要多个CMakeList的配置文件去配置不同的so文件
add_library(
chen_chen_chen1//so名
SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp//对应so所使用的cpp文件
main.cpp
)
add_library(
chen_chen_chen2
SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
demo.cpp
)
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(
chen_chen_chen1//链接所对应的so名
# List libraries link to the target library
android//以下就是对应的需要进行链接的库android和log库
log)
target_link_libraries(
chen_chen_chen2
# List libraries link to the target library
android
log)
SO直接的相互调用
so之间的相互调用利用的是在按照包的路径下进行的so的访问,而进行的相互调用
首先是获取包的路径以下是Kotlin语法(java语言包装下的语言)
fun getPath(cxt: Context): String? {//这里的String? 是返回String或者null数据
val pm = cxt.packageManager//获取包管理器
val pkgList = pm.getInstalledPackages(0)//得到已经下载好的包名
if (pkgList == null || pkgList.size == 0) return null
for (pi in pkgList) {//增强for循环遍历包名
if (pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/")//查看对应的包名中的app是否有"/data/app/"字符串
&& pi.packageName.startsWith("com.chen.javaandso")//包名是否包含"com.chen.javaandso"
) {
//Log.e("xiaojianbang", pi.applicationInfo.nativeLibraryDir);
return pi.applicationInfo.nativeLibraryDir//是就直接返回path了
}
}
return null
}
Mainactivity函数
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
Log.d("Tag", "getPath:"+getPath(getApplicationContext()));//调用getApplicationContext()来获取需要的Context类
val path: String? = getPath(getApplicationContext()+"/libchen_chen_chen2.so");//获取path
val stringVar: String = path as String;//转字符串,假如是null就会抛出异常
stringVar+="/libchen_chen_chen2.so"
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
Log.d("tag","oncreate");
// Example of a call to a native method
binding.sampleText.text = stringFromJNI(stringVar);
}
private external fun stringFromJNI(string: String): String
然后直接交给cpp里面去实现
extern "C" JNIEXPORT jstring JNICALL
Java_com_chen_javaandso_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */,
jstring path) {
std::string hello = "Hello from C++";
const char* cpath = env->GetStringUTFChars(path, nullptr);//这里是java转c++的转char函数
if (cpath == nullptr) {
// 处理 JNIEnv::GetStringUTFChars 返回 nullptr 的情况
return nullptr;
}
void* soinfo = dlopen(cpath, RTLD_NOW);//这里去获取对应路径下的so文件的句柄
if (soinfo == nullptr) {
// 处理 dlopen 失败的情况
__android_log_print(ANDROID_LOG_ERROR, "JNI", "Failed to load library: %s", dlerror());
env->ReleaseStringUTFChars(path, cpath);
return nullptr;
}
//由于我们是通过的对应路径去查找的so文件的函数名,所以这里创建了一个函数指针去得到对应的函数句柄,然后直接调用
void (*def)(char*) = reinterpret_cast<void (*)(char*)>(dlsym(soinfo, "_Z7seconedv"));//直接去利用句柄去实现查找对应的函数,这里的函数名由于没有加上extern "C",所以会在执行过程中改变
if (def == nullptr) {
// 处理 dlsym 找不到符号的情况
__android_log_print(ANDROID_LOG_ERROR, "JNI", "Failed to find symbol: %s", dlerror());
dlclose(soinfo);
env->ReleaseStringUTFChars(path, cpath);
return nullptr;
}
def(nullptr);
dlclose(soinfo); // 关闭动态库句柄
env->ReleaseStringUTFChars(path, cpath); // 释放 JNI 字符串
return env->NewStringUTF(hello.c_str());
}
这里是可以成功找到对应的so以及so下对应的函数的
假如我们想要直接进行so之间的交互也可以通过在创建so的时候,用链接器将两个so提前链接起来
这样通过声明就可以之间进行不同so的函数之间的使用
add_library(
firstso
SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
senconedso
native-lib.cpp
main.cpp
)
add_library(
senconedso
SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
demo.cpp
)
通过JNI去创建Java对象
我们现在使用JNI大多数是为了将c++和java代码进行交互,所以这里其实是用c++的语法来实现创建java的对象,这里的方式和反射的意思差不多,通过找到对应的类和方法,去调用对应的构造函数,实现对象的创建
使用NewObject创建(直接调用构造函数)
jclass claszz = env->FindClass("com/chen/c/NDKDemo");
jmethodID methodID = env->GetMethodID(claszz,"<init>","()V");
jobject jobject1 = env->NewObject(claszz,methodID);
LOGD("Object:%p",jobject1);
使用AllocObject创建(通过先开辟空间再调用构造函数)
jclass clazz1 = env->FindClass("com/chen/c/NDKDemo");
jmethodID methodID2 = env->GetMethodID(clazz1, "<init>", "(Ljava/lang/String;I)V");
jobject ReflectDemoObj2 = env->AllocObject(clazz1);
jstring jstr = env->NewStringUTF("from jni str");
env->CallNonvirtualVoidMethod(ReflectDemoObj2, clazz1, methodID2, jstr, 100);
通过JNI去访问java类的属性
获取静态字段的属性
//获取静态字段
jfieldID static_FieldID =
env->GetStaticFieldID(claszz,"privateStaticStringField","Ljava/lang/String;");
jstring static_string = static_cast<jstring>(env->GetStaticObjectField(claszz, static_FieldID));
const char* String1 = env->GetStringUTFChars(static_string, nullptr);
LOGD("privateStaticString: %s", String1);
获取对象字段
jfieldID fieldID1 = env->GetFieldID(claszz,"publicStringField","Ljava/lang/String;");
jstring object_string = static_cast<jstring>(env->GetObjectField(jobject1, fieldID1));
const char* object_field = env->GetStringUTFChars(object_string, nullptr);
LOGD("publicStringField: %s", object_string);
env->ReleaseStringUTFChars(object_string, object_field);
修改字段
jfieldID static_FieldID =
env->GetStaticFieldID(claszz,"privateStaticStringField","Ljava/lang/String;");
env->SetStaticObjectField(claszz,static_FieldID,env->NewStringUTF("xiaojianbang"));
jstring static_string = static_cast<jstring>(env->GetStaticObjectField(claszz, static_FieldID));
const char* String1 = env->GetStringUTFChars(static_string, nullptr);
LOGD("privateStaticString: %s", String1);
对于字符数组的修改和打印
//获取数组字段 ID
jfieldID bytearray = env->GetFieldID(claszz,"byteArray","[B");
jbyteArray jbyteArray1 = static_cast<jbyteArray>(env->GetObjectField(jobject1, bytearray));
int _byteArrayLength = env->GetArrayLength(jbyteArray1);
// private byte[] byteArray = new byte[]{1,2,3,4,5,6,7,8,9,10};
// 获取数组字段 ID
// 修改数组字段
char javaByte[10];
for(int i = 0; i < 10; i++){
javaByte[i] = static_cast<char>(100 - i);
}//创建数组
const jbyte *java_array = reinterpret_cast<const jbyte *>(javaByte);//转为jbyte类型,用于之后进行SetByteArrayRegion
env->SetByteArrayRegion(jbyteArray1, 0, _byteArrayLength, java_array);
// 获取数组字段
jbyteArray byteArray = static_cast<jbyteArray>(env->GetObjectField(jobject1, bytearray));
_byteArrayLength = env->GetArrayLength(byteArray);
char* str = reinterpret_cast<char *>(env->GetByteArrayElements(byteArray, nullptr));
for(int i = 0; i< _byteArrayLength; i++){
LOGD("str[%d]=%d", i, str[i]);
}
// env->ReleaseByteArrayElements(jbyteArray, reinterpret_cast<jbyte *>(cbyteArray), 0);
通过JNI去访问Java方法
调用静态函数
jclass ReflectDemoClazz = env->FindClass("com/xiaojianbang/ndk/NDKDemo");
jmethodID publicStaticFuncID =
env->GetStaticMethodID(ReflectDemoClazz, "publicStaticFunc", "()V");
env->CallStaticVoidMethod(ReflectDemoClazz, publicStaticFuncID);
调用对象函数
这里注意一下,由于我们是去调用的是对象函数,也就是说我们的调用的是对象的方法,那么method对应的就不再是jclass了而应该是对应初始化了对象之后的对象了
jmethodID privateFuncID =
env->GetMethodID(claszz, "privateFunc", "(Ljava/lang/String;I)Ljava/lang/String;");
//"privateFunc"
jmethodID ReflectDemoInit =
env->GetMethodID(claszz, "<init>", "(Ljava/lang/String;)V");//先初始化对象
//"<init>"
jstring str1 = env->NewStringUTF("this is from NDK");
jobject ReflectDemoObj = env->NewObject(claszz, ReflectDemoInit, str1);//这里就是使用的method就是初始了对象的method
jstring str2 = env->NewStringUTF("this is from JNI");
jstring retval =
static_cast<jstring>(env->CallObjectMethod(ReflectDemoObj, privateFuncID, str2, 1000));
allObjectMethodA 的使用 :传联合体args
jvalue args[2];
string str2 = env->NewStringUTF("this is from JNI");
args[0].l = str2;
args[1].i = 1000;
jstring retval =
static_cast<jstring>(env->CallObjectMethodA(ReflectDemoObj, privateFuncID, args));
const char* cpp_retval = env->GetStringUTFChars(retval, nullptr);
LOGD("cpp_retval: %s", cpp_retval);
env->ReleaseStringUTFChars(retval, cpp_retval);
参数是数组,返回值是数组的函数的方法调用
private static int[] privateStaticFunc(String[] str){
StringBuilder retval = new StringBuilder();
for(String i : str) {
retval.append(i);
}
Log.d("xiaojianbang", "this is privateStaticFunc: " + retval.toString());
return new int[]{0,1,2,3,4,5,6,7,8,9};
}
这里首先要创建对应的需要的String str作为参数,传入的jobjectArray需要用env->SetObjectArrayElement来设置
jclass claszz = env->FindClass("com/chen/c/NDKDemo");
jmethodID methodID = env->GetMethodID(claszz,"<init>","()V");
jmethodID privateStaticFunc = env->GetStaticMethodID(claszz,"privateStaticFunc","([Ljava/lang/String;)[I");
//下面参数设定
jclass StringClazz = env->FindClass("java/lang/String");
jobjectArray StringArr = env->NewObjectArray(3, StringClazz, nullptr);
for(int i = 0; i < 3; i++){
jstring str3 = env->NewStringUTF("NDK");
env->SetObjectArrayElement(StringArr, i, str3);
}
//传参
jintArray int_array = static_cast<jintArray>(env->CallStaticObjectMethod(claszz,
privateStaticFunc,StringArr));
int *c_array = env->GetIntArrayElements(int_array, nullptr);
LOGD("cintArr[0]=%d", c_array[0]);
NDK中的内存管理(局部引用)
-
局部引用:大部分的jni函数调用之后的返回值都是局部引用,这种局部引用在整个函数体执行完成之后就会自动的释放到,即使是调用的全局变量,在局部引用赋值之后也会被释放掉,不能由局部引用去传递。同时,局部引用的数目也是有限的,需要去使用env——>DeleteLocalRef来删除局部引用
-
同时可以使用env——>PushLocalFrame(num)和env——>PopLocalFrame(nullptr)来去自动删除局部引用
-
同时假如我们想要去实现全局引用就要使用env->NewGlobalRef
jclass claszz = env->FindClass("com/chen/javaandso/NDKDemo"); NDKClass = static_cast<jclass>(env->NewGlobalRef(claszz));
子线程中获取java类
- 直接Java的系统类就可以直接去使用Java的包名进行直接引用
void myThread() {
JNIEnv * env = nullptr;
jclass MainactivityClaszz = env->FindClass("java/lang/String");
LOGD("MainactivityClaszz:%p",&MainactivityClaszz);
}
- 通过在构造全局变量,在JNI_Onload里面去进行Findclass之后直接调用全局变量,直接去
JNIEXPORT jint JNI_OnLoad(JavaVM *vm,void *reserved)
{
globalVM =vm;
JNIEnv *env = nullptr;
vm->GetEnv((void **)&env,JNI_VERSION_1_6);
jclass mainativities_temp = env->FindClass("com/chen/anative/MainActivity");
mainativities_temp= static_cast<jclass>(env->NewGlobalRef(mainativities_temp));
}
- 由于我们在子线程中的classloader会变换,会导致使用不同的classloader分析出的同一个函数,结果却是不一样的,所有先要进行classloader的获取,在JNI_Onload中先获取全局变量的getclassLoader,获取loader
JNIEXPORT jint JNI_OnLoad(JavaVM *vm,void *reserved)
{
globalVM =vm;
JNIEnv *env = nullptr;
vm->GetEnv((void **)&env,JNI_VERSION_1_6);
jclass MainActivity = env->FindClass("com/chen/anative/MainActivity");
jclass javaClass = env->FindClass("java/lang/Class");
jmethodID methodId_getClassLoader = env->GetMethodID(javaClass,"getClassLoader", "()Ljava/lang/ClassLoader;");
jobject tempclass = env->CallObjectMethod(MainActivity,methodId_getClassLoader);
get_classloader = env->NewGlobalRef(tempclass);
}
同时在子线程中利用loadclass加载classloader,通过调用来获取jclass的主线程类
void myThread() {
JNIEnv * env = nullptr;
jclass class_classloader = env->FindClass("java/lang/ClassLoader");
jmethodID methodid_loadclass = env->GetMethodID(class_classloader,"loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
jclass Mainactivity = static_cast<jclass>(env->CallObjectMethod(get_classloader,
methodid_loadclass,
env->NewStringUTF(
"com.chen.anative.MainActivity")));
if(Mainactivity == nullptr)
{
LOGD("NULLPTR");
pthread_exit(0);
}
jmethodID JNI_methodID = env->GetMethodID(Mainactivity,"stringFromJNI","()Ljava/lang/String");
LOGD("myThread env->GetMethodID:%p",JNI_methodID);
}
实现子线程获取classloader来调用获取主线程类
init和init_array
init和init_array都是程序中比JNI_Onload还要加载的更早的函数,这里可能会存放so层的代码解码以及去混淆的程序,从而在程序执行到主函数时可以直接获取解密之后的代码。
init初始化:
extern "C" void _init()
{
LOGD("extern \"C\" void _init()");
}
init_array初始化:
__attribute__ ((constructor(180), visibility("hidden"))) void initArrayTest1(){
LOGD("initArrayTest1");
}
__attribute__ ((constructor(150))) void initArrayTest2(){
LOGD("initArrayTest2");
}
__attribute__ ((constructor(101))) void initArrayTest3(){
LOGD("initArrayTest3");
}
这里的(constructor(150))是构造函数,且最后的数字越小,执行时刻越早,但是前一百的数字大多数是由系统调用的,所有一般我们以100往后的数字开始使用, visibility("hidden")这里参数表示可见度为隐藏
onCreateNative化
我们知道要是我们的代码直接在java层就直接能够进行反编译读取那么是不安全的,所有我们要做的就是native化,也就是将关键代码存到我们的so层里面去,尽可能的保护起来
这里我们先演示一下native:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
结果:
extern "C" JNIEXPORT void JNICALL
Java_com_chen_anative_MainActivity_onCreate(
JNIEnv * env,
jobject MainActivity thiz,
jobject Bundle saved_instance_state)
{
// super.onCreate(savedInstanceState)
jclass AppCompatActivityclazz = env->FindClass("androidx/appcompat/app/AppCompatActivity");
//找到super对于的类androidx/appcompat/app/AppCompatActivity
jmethodID Oncreate_method = env->GetMethodID(AppCompatActivityclazz,"onCreate", "(Landroid/os/Bundle;Landroid/os/PersistableBundle;)V");
//找到了类之后就要去找方法(因为oncreate是super类的方法所有先找类再找方法)
env->CallNonvirtualVoidMethod(thiz,AppCompatActivityclazz,Oncreate_method,saved_instance_state);
//这里是子类调用父类,所以使用的是Nonvirtual,第一个参数就是父类
}
这里最终实现的过程就是 super.onCreate(savedInstanceState),只能转为了native上,进行了保护
其实这样的转换是很简单的,和frida的使用有点像
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Example of a call to a native method
binding.sampleText.text = stringFromJNI()
所以其实全部都能进行native了
jclass Activity = env->FindClass("android/app/Activity");
jmethodID LayoutInflater = env->GetMethodID(Activity,"getLayoutInflater", "()Landroid/view/LayoutInflater;");
jclass ActivityMainBinding = env->FindClass("com/chen/anative/databinding/ActivityMainBinding");
jmethodID inflateID= env->GetStaticMethodID(ActivityMainBinding,"inflate","(Landroid/view/LayoutInflater;)Lcom/chen/anative/databinding/ActivityMainBinding;");
jobject inflate_result = env->CallStaticObjectMethod(ActivityMainBinding,inflateID);
这一大串就实现了 binding = ActivityMainBinding.inflate(layoutInflater) 并且将ActivityMainBinding类型的返回值赋值给了inflate_result。
标签:NDK,String,void,nullptr,开发,env,chen,JNI From: https://www.cnblogs.com/ovo-fisherman/p/18412571