吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 18949|回复: 44
收起左侧

[Android 原创] 新手的JNI学习总结

  [复制链接]
Enigma_G 发表于 2017-4-26 20:08
0x00 闲言碎语


一直以来在吾爱学到了挺多东西,还从没发过贴,所以自己写了一篇新手学习的总结和大家一起分享,只是一些很简单的东西,希望各位大佬师傅们不要笑话了,有错误也请指出

0x01 一个NDK demo

新建项目工程

   
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package myapplication.mask.com.myapplication;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
 
 
 
public class MainActivity extends AppCompatActivity {
 
static{
    System.loadLibrary("demo");//载入类库
}
 
public native static String FromNative();//声明Native方法
 
 
 
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    Toast.makeText(this,FromNative(),Toast.LENGTH_SHORT).show();
}
}


然后Make Project

下面切换到`app/src/main/java`目录下,执行下面的命令生成一个JNI头文件


   
[Java] 纯文本查看 复制代码
1
javah myapplication.mask.com.myapplication.MainActivity


1.png


我们来看看这个头文件都有些啥

   
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
   #include <jni.h> //需要导入jni.h头文件
   /* Header for class myapplication_mask_com_myapplication_MainActivity */
    
   #ifndef _Included_myapplication_mask_com_myapplication_MainActivity
   #define _Included_myapplication_mask_com_myapplication_MainActivity
   #ifdef __cplusplus
   extern "C" {
   #endif
   /*
    * Class: myapplication_mask_com_myapplication_MainActivity /*类名*/
    * Method:FromNative   /*方法名*/
    * Signature: ()Ljava/lang/String; /*签名信息(是不是很像smali代码?!!反正我第一反应是这个*/
    */
   JNIEXPORT jstring JNICALL Java_myapplication_mask_com_myapplication_MainActivity_FromNative
     (JNIEnv *, jclass);//这个Native方法的声明
    
   #ifdef __cplusplus
   }
   #endif
   #endif



这里有个概念那就是我怎么知道这个声明的方法就是我要调用的呢?


这个是JNI规定的写法,为了方便,编译时会自动在头文件生成,包含的内容一看便知,将包名中的"
."换成了"_"然后+“_”+类名

   
[Java] 纯文本查看 复制代码
1
myapplication_mask_com_myapplication_MainActivity



为了方便,我把这个头文件重命名为demo.h,然后新建一个JNI目录,将demo.h移动到JNI下

3.png

然后开始编译,
将项目的build.gradle改成如下:

   
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apply plugin: 'com.android.model.application' //此处加了model
   model {  //对应加上model
   android {
       compileSdkVersion 25
       buildToolsVersion "24.0.1"
       defaultConfig {
           applicationId "myapplication.mask.com.myapplication"
           minSdkVersion.apiLevel 19 //此处修改了
           targetSdkVersion.apiLevel 25//此处修改了
           versionCode 1
           versionName "1.0"
           testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
       }
       buildTypes {
           release {
               minifyEnabled false
               proguardFiles.add(file("proguard-rules.pro"))//此处修改了
           }
       }
       ndk {
           moduleName "demo" //库名
           stl "stlport_static"//以静态链接方式使用的stlport版本的STL
           ldLibs.addAll(["log","z","android"])
           abiFilters.addAll(['armeabi','armeabi-v7a']) //abi平台
       }
       //sourceSets { main { jni.srcDirs = ['src/main/jni', 'src/main/jni/'] } }
   }
   }
    
   dependencies {
   compile fileTree(dir: 'libs', include: ['*.jar'])
   androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
       exclude group: 'com.android.support', module: 'support-annotations'
   })
   compile 'com.android.support:appcompat-v7:25.1.0'
   testCompile 'junit:junit:4.12'
   }



再来修改工程下的build.gradle:

  
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Top-level build file where you can add configuration options common to all sub-projects/modules.
   
  buildscript {
  repositories {
      jcenter()
  }
  dependencies {
      classpath 'com.android.tools.build:gradle-experimental:0.8.3' //此处需要修改gradle的版本号
 
      // NOTE: Do not place your application dependencies here; they belong
      // in the individual module build.gradle files
  }
  }
   
  allprojects {
  repositories {
      jcenter()
  }
  }
   
  task clean(type: Delete) {
  delete rootProject.buildDir
  }

   

其他编译方式:

[http://www.tuicool.com/articles/3mu22ie](http://www.tuicool.com/articles/3mu22ie)

[http://www.jianshu.com/p/9d001d9 ... utm_medium=referral](http://www.jianshu.com/p/9d001d9 ... utm_medium=referral)


----------

然后新建一个demo.cpp文件

   
[C++] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
//
   // Created by mask on 2017/4/25.
   //
   #include <iostream>
   #include "demo.h"
   #include <jni.h>
    
   using namespace std;
    
   /*头文件中的信息,可以直接复制过来
    * Class: myapplication_mask_com_myapplication_MainActivity
    * Method:FromNative
    * Signature: ()Ljava/lang/String;
    */
   JNIEXPORT jstring JNICALL Java_myapplication_mask_com_myapplication_MainActivity_FromNative
     (JNIEnv *env, jclass clazz)
     {
   return env->NewStringUTF("Hello from JNI!");
     }



然后运行:

2.png


----------

如果用c写的话,demo.c
   
  
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
//
  // Created by mask on 2017/4/25.
  //
 
  #include "demo.h"
  #include <jni.h>
   
 
   
  /*
   * Class: myapplication_mask_com_myapplication_MainActivity
   * Method:FromNative
   * Signature: ()Ljava/lang/String;
   */
  JNIEXPORT jstring JNICALL Java_myapplication_mask_com_myapplication_MainActivity_FromNative
    (JNIEnv *env, jclass clazz)
    {
  return (*env)->NewStringUTF(env,"Hello from JNI!");
    }


可以发现这(*env)其实是个二级指针,而c++则是使用了一级指针,至于分别指向了哪,不急,下面开始切入正题

----------

0x02 反汇编
--------

那么可以把AS关了,把刚刚那么应用解压一下,你会发现有一个lib目录,这就是一般情况下库文件存放的地方

4.png

IDA打开那个so文件(我一般喜欢打开armeabi下的)

这个就是我们编写的Native函数,跟进去




5.png


   
[Asm] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
; =============== S U B R O U T I N E =======================================
   .text:00000D78
   .text:00000D78 ; Attributes: bp-based frame fpd=8
   .text:00000D78
   .text:00000D78 ; _DWORD __fastcall _JNIEnv::NewStringUTF(_JNIEnv *__hidden this, const char *)
   .text:00000D78 WEAK _ZN7_JNIEnv12NewStringUTFEPKc
   .text:00000D78 _ZN7_JNIEnv12NewStringUTFEPKc  ; CODE XREF: Java_myapplication_mask_com_myapplication_MainActivity_FromNative+14p
   .text:00000D78
   .text:00000D78 var_8   = -8
   .text:00000D78 var_4   = -4
   .text:00000D78
   .text:00000D78 PUSH{R7,LR}
   .text:00000D7A SUB SP, SP, #8
   .text:00000D7C ADD R7, SP, #0
   .text:00000D7E STR R0, [R7,#8+var_4]
   .text:00000D80 STR R1, [R7,#8+var_8]
   .text:00000D82 LDR R3, [R7,#8+var_4]
   .text:00000D84 LDR R2, [R3]
   .text:00000D86 MOVSR3, #0x29C
   .text:00000D8A LDR R3, [R2,R3]
   .text:00000D8C LDR R1, [R7,#8+var_4]
   .text:00000D8E LDR R2, [R7,#8+var_8]
   .text:00000D90 MOVSR0, R1
   .text:00000D92 MOVSR1, R2
   .text:00000D94 BLX R3
   .text:00000D96 MOVSR3, R0
   .text:00000D98 NOP
   .text:00000D9A MOVSR0, R3
   .text:00000D9C MOV SP, R7
   .text:00000D9E ADD SP, SP, #8
   .text:00000DA0 POP {R7,PC}
   .text:00000DA0 ; End of function _JNIEnv::NewStringUTF(char const*)


一句一句分析:

导入函数

   
[Asm] 纯文本查看 复制代码
1
2
EXPORT Java_myapplication_mask_com_myapplication_MainActivity_FromNative
   .text:00000DA4 Java_myapplication_mask_com_myapplication_MainActivity_FromNative


将R7和LR寄存器的值压入栈(什么?啥是寄存器?啥是内存栈?呐,这个都不知道的话,下面你忽略吧0.0),LR寄存器是用来保存下一条指令的

   
[Asm] 纯文本查看 复制代码
1
PUSH{R7,LR}



SP = SP -8

SP寄存器是用来保存栈顶元素的,而内存栈是自下到上,降序的,此处SP减去8,就是为了开辟出一段栈空间

   
[Asm] 纯文本查看 复制代码
1
SUB SP, SP, #8


R7 = SP + 0

此处相当于R7 = SP,将SP的值传给R7,为什么这样做呢?这其实是一种保护机制,因为SP是时刻指向栈顶的,可以看到,下面的一些操作都是以SP为基地址进行的,那么我们这里用R7来替代SP,,将SP的值保存在R7中,我们后边调用函数出栈时,在将R7的值还给SP,这样可以保证栈平衡

   
[Asm] 纯文本查看 复制代码
1
ADD R7, SP, #0


将R0寄存器的值赋给(R7+8-4)地址处,这是一个写操作

   
[Asm] 纯文本查看 复制代码
1
STR R0, [R7,#8+var_4]


将R1寄存器的值赋给(R7+8-8)地址处,这是一个写操作

   
[Asm] 纯文本查看 复制代码
1
STR R1, [R7,#8+var_8]



那么RO,R1寄存器的值是什么呢?在ARM中,前4个参数是用R0-R3来保存的(如果多余4个参数,剩下的则是用栈来操作)

那么JNI函数的第一个参数是一个JNIEnv 类型的指针`*env`,第二个参数是一个jclass类型的参数clazz或者是一个jobject类型的object,呐,这个函数其实只有一个参数,那就是JNIEnv类型的指针`*env`,为啥没有第二个,因为压根就啥也没操作,这里就只是return了一个字符串而已。。。


将第一个参数保存到R3寄存器

   
[Asm] 纯文本查看 复制代码
1
LDR R3, [R7,#8+var_4]


`aHelloFromJni - 0xDB6`的值赋给R2,这里有个小tip,你可以鼠标右键点击那个“=”,他会自动帮你转会为最终地址

   
[Asm] 纯文本查看 复制代码
1
LDR R2, =(aHelloFromJni - 0xDB6)


6.png


R2 = R2 + PC

PC寄存器为当前指令,但是这里需要注意,之前我也搞错过,ARM在执行命令时,其实是分三步走的

1.取址

2.编译

3.执行

那么当我执行第一条命令时,第二条指令在编译,第三条指令在取址,过程大概如下:

     取址------------------编译-----------------------执行
                             取址-----------------------编译-----------------执行
                                                          取址-----------------编译-----------------执行
那么就好理解了,此时PC的值0x00000DB6

那么R2此时的值为(aHelloFromJni - 0xDB6+0xDB6)处的值,那么就是`Hello from JNI!`后面的注释也有给出

  
[Asm] 纯文本查看 复制代码
1
ADD R2, PC



将R3的值赋给R0,并且会影响标志位

   
[Asm] 纯文本查看 复制代码
1
MOVS R0, R3


将R2的值赋给R1,并且会影响标志位

   
[Asm] 纯文本查看 复制代码
1
MOVS R1, R2


上面两步其实是为子函数做准备,将`*env``Hello from JNI!`进行传参,和之前说的一样,是用R0,R1保存前两个参数





BL进行函数跳转,根据注释可以知道,调用的是_`JNIEnv::NewStringUTF(char const*)`函数,有兴趣可以跟过去分析,过程都差不多

   
[Asm] 纯文本查看 复制代码
1
BL  _ZN7_JNIEnv12NewStringUTFEPKc ; _JNIEnv::NewStringUTF(char const*)


NOP为空操作,就是啥也不干

   
[Asm] 纯文本查看 复制代码
1
MOVS R3, R0

   
   
[Asm] 纯文本查看 复制代码
1
2
3
NOP
     
    MOVS R0, R3



这里将R7的值赋给SP,和我们之前说的一样

   
[Asm] 纯文本查看 复制代码
1
MOV SP, R7

这里SP = SP+8,回收调之前开辟的空间

   
[Asm] 纯文本查看 复制代码
1
ADD SP, SP, #8


出栈,此时LR的值保存到PC中,说明结束上述操作,开始下一条指令

   
[Asm] 纯文本查看 复制代码
1
POP {R7,PC}


函数结束

  
[Asm] 纯文本查看 复制代码
1
; End of function Java_myapplication_mask_com_myapplication_MainActivity_FromNative



----------


汇编分析完了,我们再来看看伪代码,可以更好的理解刚刚的操作,F5大法好哇

7.png

哗擦,居然识别出来了。。。。。。。还想演示一遍修复参数呢!!!!

待我翻下刚刚C版本写的那个

8.png

呐,稳,没识别出来23333

----------


开始修复

之前说过,JNI函数的第一个参数一般默认是JNIEnv的结构体指针,那么这里我们要让IDA能够识别JNI结构体,我们需要导入一个JNI.h头文件(我的IDA好像自带),这个文件是啥,待会再说,这个IDA.h你可以去网上下载,其实NDK就自带这个

9.png

导入该文件,成功会有提示,提示啥我忘了,反正会告诉你导入成功了

然后

10.png

修复参数类型,输入JNIEnv*

11.png

我这里函数都给我识别出来了。。。,其实应该还有一步,如果你的函数没有正确识别的话,右键你的函数名,会显示Force call type,点击这个就能正确识别了



----------
这里还有几个疑问没解决

1.JNIEnv* env这个指针到底指向哪?

2.JNI.h是干啥的?

3.第二个参数类型何时是jclass何时是jobject



0x03 JNI.h
----------


先来看看这个JNI.h是啥

12.png

这里其实是一些数据类型转换,比如JAVA中的

   
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
#ifdef HAVE_INTTYPES_H
   # include <inttypes.h>  /* C99 */
   typedef uint8_t jboolean;   /* unsigned 8 bits */
   typedef int8_t  jbyte;  /* signed 8 bits */
   typedef uint16_tjchar;  /* unsigned 16 bits */
   typedef int16_t jshort; /* signed 16 bits */
   typedef int32_t jint;   /* signed 32 bits */
   typedef int64_t jlong;  /* signed 64 bits */
   typedef float   jfloat; /* 32-bit IEEE 754 */
   typedef double  jdouble;/* 64-bit IEEE 754 */
   #else
   typedef unsigned char   jboolean;   /* unsigned 8 bits */
   typedef signed char jbyte;  /* signed 8 bits */
   typedef unsigned short  jchar;  /* unsigned 16 bits */
   typedef short   jshort; /* signed 16 bits */
   typedef int jint;   /* signed 32 bits */
   typedef long long   jlong;  /* signed 64 bits */
   typedef float   jfloat; /* 32-bit IEEE 754 */
   typedef double  jdouble;/* 64-bit IEEE 754 */




这里是C和C++中的一些差别


   
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#ifdef __cplusplus
    /*
     * Reference types, in C++
     */
    class _jobject {};
    class _jclass : public _jobject {};
    class _jstring : public _jobject {};
    class _jarray : public _jobject {};
    class _jobjectArray : public _jarray {};
    class _jbooleanArray : public _jarray {};
    class _jbyteArray : public _jarray {};
    class _jcharArray : public _jarray {};
    class _jshortArray : public _jarray {};
    class _jintArray : public _jarray {};
    class _jlongArray : public _jarray {};
    class _jfloatArray : public _jarray {};
    class _jdoubleArray : public _jarray {};
    class _jthrowable : public _jobject {};
     
    typedef _jobject*   jobject;
    typedef _jclass*jclass;
    typedef _jstring*   jstring;
    typedef _jarray*jarray;
    typedef _jobjectArray*  jobjectArray;
    typedef _jbooleanArray* jbooleanArray;
    typedef _jbyteArray*jbyteArray;
    typedef _jcharArray*jcharArray;
    typedef _jshortArray*   jshortArray;
    typedef _jintArray* jintArray;
    typedef _jlongArray*jlongArray;
    typedef _jfloatArray*   jfloatArray;
    typedef _jdoubleArray*  jdoubleArray;
    typedef _jthrowable*jthrowable;
    typedef _jobject*   jweak;
     
     
    #else /* not __cplusplus */
     
    /*
     * Reference types, in C.
     */
    typedef void*   jobject;
    typedef jobject jclass;
    typedef jobject jstring;
    typedef jobject jarray;
    typedef jarray  jobjectArray;
    typedef jarray  jbooleanArray;
    typedef jarray  jbyteArray;
    typedef jarray  jcharArray;
    typedef jarray  jshortArray;
    typedef jarray  jintArray;
    typedef jarray  jlongArray;
    typedef jarray  jfloatArray;
    typedef jarray  jdoubleArray;
    typedef jobject jthrowable;
    typedef jobject jweak;



简单看看就好,下面来看看关键

13.png


这个就是*env所指向的结构体了,下面是它所包含的一系列函数,我们来找找之前的`NewStringUTF`函数

14.png

呐,参数和我们刚刚在汇编分析时描述的一样。那么刚刚的第一个第二个问题算是解决了。但是细心的朋友肯定注意到了一个细节

这个和JNIEnv在一块的另一个结构体是他娘的啥?凭这打破砂锅问到底的精神,我说

15.png

这个JavaVM其实是虚拟机在JNI层的一个代表,一个进程中只有一个JavaVM,他是进程级的,那么相对的,JINEnv其实是线程级的。那么他们必然是有关系的,有啥关系?你等着

看到这个JavaVm结构体了么?这里有个函数

16.png

通过调用这个函数我们就可以获得这个线程的JNIEnv结构体,干嘛要获得?等你要的时候你就晓得了
   
   
[C] 纯文本查看 复制代码
1
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)


那么再来看看第三个问题,什么时候是jclass,什么时候是object?

这个其实很好理解,在Java层,如果你声明的是一个static函数,那么他就是jclass,如果不是static函数,那就是jobject,很好理解的


0x04 函数注册
----

在JNI注册函数有两种注册方法,一种是静态注册,就是刚才演示的那种,在Java层声明,在JNI定义,下面来讲第二种,动态注册

将动态注册前,先看看Java层是怎样找到JNI层中对应的函数的,之前说过JNI库中默认使用`Java_myapplication_mask_com_myapplication_MainActivity_FromNative`这种格式,那么可以理解为JNI会为库和java层建立某种联系,注册函数其实就是建立这种联系,然后进行查找,作用类似指针,其实你就可以理解为指针,那么JNI中是如何建立这种关系的呢?

JNI.h中有这样一个结构体

17.png

   
[C] 纯文本查看 复制代码
1
2
3
4
5
typedef struct {
   const char* name; //函数名
   const char* signature;//签名信息
   void*   fnPtr;//这个就是函数对应的指针和上面说的吻合,果然是个指针
   } JNINativeMethod;

   


呐,找到这个关系了,怎么去注册呢?这就要分析源码了,源码位置在`\frameworks\base\core\jni\AndroidRunTime.cpp`,感兴趣可以看看

18.png

[C] 纯文本查看 复制代码
1
2
3
4
5
6
7
8
/*
  * Register native methods using JNI.
  */
 /*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
 const char* className, const JNINativeMethod* gMethods, int numMethods)
 {
 return jniRegisterNativeMethods(env, className, gMethods, numMethods);
 }



可以看到这里回调了一个函数,行,我说

诺,在这呢\dalvik\libnativehelper\JNIHelp.c

19.png

   
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static jclass findClass(C_JNIEnv* env, const char* className) {
   JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
   return (*env)->FindClass(e, className);
   }
    
   extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
   const JNINativeMethod* gMethods, int numMethods)
   {
   JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
    
   LOGV("Registering %s natives", className);
    
   scoped_local_ref<jclass> c(env, findClass(env, className));
   if (c.get() == NULL) {
   LOGE("Native registration unable to find class '%s', aborting", className);
   abort();
   }
    
   if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
   LOGE("RegisterNatives failed for '%s', aborting", className);
   abort();
   }
    
   return 0;
   }



绕了半天。。。又回到JNI去了23333

那么我们动态注册的时候,只需要完成下面操作即可

  
[C] 纯文本查看 复制代码
1
2
jclass clazz = (*env) -> FindClass(env,className);
  (*env) ->RegisterNatives(env,clazz,gmethods,numMethods);


那么注册在哪操作呢???呐,需要在一个JNI_Onload()函数中,这个函数的第一个参数就是JavaVM* vm
(动态注册必须实现这个函数,静态注册则不需要,但是这个函数通常可以用来初始化一些操作)

0x05 结语
----

其实东西不多,只是自己学习过程中的一点总结而以,文中所有的源码文件我已上传到了我的github,https://github.com/oMasko/Mask_Blog/tree/master/content/JNI_Notes
(本来想打包附件上传,但是好像太大了)
-------

免费评分

参与人数 14吾爱币 +12 热心值 +14 收起 理由
ABCD + 1 + 1 干货~绝对干货~
梅川酷酷 + 1 + 1 已答复!
科技成就生活 + 1 + 1 用心讨论,共获提升!
情殇邪魔剑、 + 1 谢谢@Thanks!
ligang024 + 1 + 1 谢谢@Thanks!
52lxw + 1 + 1 热心回复!
XhyEax + 1 + 1 果断收藏
superlukia + 1 + 1 谢谢@Thanks!
yuluo5566 + 1 + 1 我很赞同!
1254981099 + 1 + 1 用心讨论,共获提升!
wahejiahua + 1 + 1 楼主的知识还是很全面的,这样的整体分析很利于进步
likang + 1 + 1 从正向到逆向就服你!
litianping + 1 谢谢楼主分析,现在Android ndk的开发一般都用CMake模式了。
xiaozhou2017 + 1 + 1 热心回复!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

胖子哦 发表于 2017-4-26 20:57
路过看看
zhanghong 发表于 2017-4-26 21:02
我叫ADORE 发表于 2017-4-26 23:22
看看        。。。。。。。。。。。。。。。。。。。。。。。。。等等大四 大大大大大
likang 发表于 2017-4-27 14:58
只懂正向的我居然看懂了
 楼主| Enigma_G 发表于 2017-4-27 15:12
likang 发表于 2017-4-27 14:58
只懂正向的我居然看懂了

嘻嘻,谢谢
Charlie_M 发表于 2017-4-27 16:50
谢谢楼主支招 现在有福拉 呵呵
ee22ee 发表于 2017-4-27 21:24
学习学习,感谢分享
269960696 发表于 2017-4-28 09:27
谢谢分享,太实用了
yuluo5566 发表于 2017-4-28 17:11
非常感谢楼主的详细分析,学习了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-4-6 12:04

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表