精华内容
下载资源
问答
  • 2.dex2jar:该工具作用是将classes.dex文件,反编译出源码(如果apk未加固),反编译出文件,使用jd-gui工具进行查看;3.Auto-Sign:自动签名工具,将重新打包的apk进行签名,如果不签名,无法安装使用。工具下载地址

    一、工具介绍:

    1.apktool:aapt.exe,apktool.bat,apktool.jar;三个在同一目录结合使用,用来反编译apk,apk重新打包;

    2.dex2jar:该工具作用是将classes.dex文件,反编译出源码(如果apk未加固),反编译出文件,使用jd-gui工具进行查看;

    3.Auto-Sign:自动签名工具,将重新打包的apk进行签名,如果不签名,无法安装使用。

    工具下载地址:http://download.csdn.net/detail/wxk105/9782180

    使用场景:项目源码丢失,只有线上apk,并且没有加固,要求修改apk接口地址,并且重新打包,再发布。

    二、工具使用:

    解压后工具包如图:

    这里写图片描述

    1.思路:使用工具dex2jar反编译,并用jd-gui工具进行查看项目结构,查看源码,并且找到接口地址类,修改接口地址;

    步骤一:下载好工具,将需要反编译的APK后缀名改为.rar或则 .zip,并解压,如图:

    这里写图片描述

    得到其中的classes.dex文件(它就是java文件编译再通过dx工具打包而成的),将获取到的

    classes.dex复制到解压出来的工具dex2jar-0.0.9.15 文件夹内

    在命令行下,进入到dex2jar.bat所在目录,输入命令:

    dex2jar.bat   classes.dex

    效果如下:

    这里写图片描述

    步骤二:运行结束后,在该目录下会生成一个classes_dex2jar.jar的文件,如图:

    这里写图片描述

    然后打开工具jd-gui文件夹里的jd-gui.exe,用该工具打开生成的classes_dex2jar.jar文件,便可以看到源码

    了,效果如下:

    这里写图片描述

    2.思路:apktool,反编译修改smali文件,进行重新打包,通过用jd-gui工具找到接口地址类后,与相应

    的smali文件进行对比,修改接口地址;

    下载上述工具中的apktool,解压得到3个文件:aapt.exe,apktool.bat,apktool.jar ,将需要反编译的

    APK文件放到该目录下,如图:

    这里写图片描述

    打开命令行界面(运行-CMD) ,定位到apktool文件夹,输入以下命令:

    apktool.bat d -f test.apk -o test    
    apktool -f [待反编译的apk] -o [反编译之后存放文件夹] 

    如图:

    这里写图片描述

    反编译之后会得到test 文件夹,打开test文件夹,里边就是反编译出来的各种资源文件

    这里写图片描述

    使用jd-gui查看源码找到地址类,然后在smali文件里找到地址的smali文件,更改smali文件内接口地址。
    如图:

    这里写图片描述

    smali文件内找到与之对应的地址smali文件

    这里写图片描述

    这里写图片描述

    修改接口smali文件里的地址将Ip更换成域名如图:

    这里写图片描述

    修改后,保存。

    3.重新打包。 执行打包命令

    apktool.bat b test

    在test文件内会多出两个文件如下图所示:

    这里写图片描述

    dist文件内就是我们需要的apk。

    4.签名apk,重新发布。

    接下来就要用到签名工具了,如果不经过签名是不能正确运行的。工具: auto-sign.zip

    将打包好的test.apk 拷贝到解压好的auto-sign文件夹下,并且重命名为 test.zip ,执行命令:

    java -jar signapk.jar testkey.x509.pem testkey.pk8 test.zip test_signed.zip 

    接下来输入 将test_signed.zip,更改为后缀名apk的文件,就可以了 :

    这里写图片描述

    5.运行之后,发现在5.0上运行会直接崩溃,android studio捕捉到错误所在:

    这里写图片描述

    这里是在jd-gui上查看到了源码位置

    错误原因:

     Caused by: java.lang.IllegalArgumentException: Service Intent must be explicit: Intent { act=com.sun3d.culturejingan.communication.link }

    如错误提示所示,在android 5.0版本以后,service intent必须为显式指出。

    那么就需要修改smali文件相关类,这里就用到将java2smali文件

    6.java2smali插件,地址:https://plugins.jetbrains.com/plugin/7385-java2smali

    插件安装,选择本地安装,不懂怎么安装插件,请百度下。

    找到相应问题的smali文件代码定位:

    这里写图片描述

    由于不太懂smali语法的书写,所以我们直接将写好java代码用插件转换为smali语法,拷贝进来,替换原有smali代码
    这是java代码
    这里写图片描述

    转换成smali代码

    这里写图片描述

    相应代码会直接展示,找到相应转换结果:

    这里写图片描述

    替换反编译出的相应smali文件里的相应代码,然后执行重新打包命令,签名命令,执行第3步。

    整个过程并不复杂,只要有耐心就可以更改成功。

    展开全文
  • 1. 阅读 Dalvik 字节码(通过 baksmali 反编译 dex 文件生成 samli 文件) 2. 阅读 java 代码(通过 dex2jar 生成 jar 文件,在 jd-gui 阅读 jar 文件) 常用步骤: 1. 反编译 apk 2. 通过 AndroidManifest.xml 查找主 ...

    静态分析:
    不运行代码的情况下,阅读反汇编代码掌握程序功能

    两种方法:
    1. 阅读 Dalvik 字节码(通过 baksmali 反编译 dex 文件生成 samli 文件)
    2. 阅读 java 代码(通过 dex2jar 生成 jar 文件,在 jd-gui 阅读 jar 文件)

    常用步骤:
    1. 反编译 apk
    2. 通过 AndroidManifest.xml 查找主 Activity

    3. 查看程序的入口函数:主 Activity 的 onCreate()
    4. 查看 Application 类(全局类,早于其他类启动)的 onCreate()函数,该函数通常用作授权检测

    定位关键代码常用方法:

    1. 信息反馈发:运行时信息
    2. 特征函数法:运行时行为
    3. 顺序查看法:执行流程
    4. 代码注入法:添加 Log

    Smali  代码格式:

    内部类的表示:
    MainActivity$1.smali:匿名内部类,多用于程序中的响应
    MainActivity$SNChecker.smali:成员内部类
    MainActivity.smali:外部类
    this$0 是内部类自动保留的一个指向所在外部类的引用。this 表示父类的引用,右边的 0 便是引用的层数,例如:ThirdInner 是 this

    this$X 型字段都被指定了 synthetic(合成的、编译器生成的)属性,表明他们是被编译器合成的、虚构的、非 java 代码指定的字段。
    构造函数执行步骤:
    1. 保存外部类的引用到本类的一个 synthetic 字段中
    2. 调用内部类的父类构造函数
    3. 内部类初始化

    展开全文
  • 想要修改必须要修改apktool 反编译出来的smali文件。这其中就会遇到问题,就是如何写smali??来完成功能。首先我们需要懂smali的语法 去goole 官方就可以看到相应文档进行学习。另外简单半简单的办法就是自己可以写...
    展开全文
  • 学习之前,先教没有反编译基础的同学学习如何反编译处samli文件。 Android Studio中,可以在 setting->plugins 中,安装 Java2Samli插件,重启IDE后,选择对应的类,点击 Build->compile to smali,该目录下就...

    学习之前,先教没有反编译基础的同学学习如何反编译处samli文件。
    Android Studio中,可以在 setting->plugins 中,安装 Java2Samli插件,重启IDE后,选择对应的类,点击 Build->compile to smali,该目录下就会自动生成smali文件了。

    本节列举了几个在项目实战中遇到的一些挑战,这些都是Java语言在编译实现上的一些特点,虽然这些特点与热修复没有直接关系,但深入研究它们对Android以及Java余元的理解都颇有收益。

    1 内部类编译

    有时候我们会发现,在修改外部类某个方法逻辑为访问内部类的某个方法时,最后打出来的补丁包竟然提示新增了一个方法,这真的很匪夷所思,所以有必要了解内部类在编译期间是怎么工作的,首先我们要知道内部类在编译期会被编译为跟外部类一样的顶级类。

    1.1 静态内部类/非静态内部类的区别

    这一点大家都很熟悉,非静态内部类持有外部类的引用,静态内部类不持有外部类的引用。
    所以在Android性能优化中建议Handle的实现尽量使用静态内部类,防止外部Activity类不能被回收导致可能的OOM。

    我们反编译为smali比较两者的不同点:

    //静态内部类
    # direct methods
    .method constructor <init>()V
       return-void
    .end method
    
    //非静态内部类,编译器会自动合成this$0域表示的就是外部类的引用
    .field final synthetic this$0:Lcom/rikkatheworld/demo/DexFixDemo;
    #direct methods
    .method constructor <int>(Lcom/rikkatheworld/demo/DexFixDemo;)Via
       .locals 1
       .param p1,"this$0"  #Lcom/rikkatheworld/demo/DexFixDemo;
    
       iput-object p1, p0, Lcom/rikkatheworld/demo/DexFixDemo$A;->this$0:Lcom/rikkatheworld/demo/DexFixDemo;
       return-void;
    .end   
    

    1.2 内部类和外部类互相访问

    既然内部类实际上跟外部类一样都是顶级类,既然都是顶级类,那是不是意味着对方私有的 method/field是无法被访问到的,事实上外部类为了访问内部类私有的域/方法,编译期间自动会为内部类生成 access$数字编号相关方法

    public class BaseBug {
        public void test(Context context) {
            InnerClass innerClass = new InnerClass("old apk");
            Toast.makeText(context.getApplicationContext(), innerClass.s, Toast.LENGTH_SHORT).show();
        }
    
        class InnerClass {
            private String s;
    
            private InnerClass(String s) {
                this.s = s;
            }
        }
    }
    

    此时外部类BaseBug为了能够访问内部类InnerClass的私有域s,编译器会自动为 InnerClass这个内部类合成 access$100方法,这个方法的实现简单返回私有域s的值。同样的如果此时匿名内部类需要访问外部类的私有属性/方法,那么外部类也会自动生成access$**相关方法提供给内部类使用。

    1.3 热部署解决方案

    上面说的东西对热修复来说,就产生了一种场景:
    打补丁前的test方法没访问inner.s,打补丁之后的 test方法访问了inner.s,那么补丁工具最后检测到了新增的 access$100方法。
    那么我们只要防止生成 access$**相关方法,就能走热部署方案,也就是底层替换方式热修复。所以只要满足一下条件,就能避免编译器自动生成 access$**的相关方法:

    • 一个外部类如果有内部类,把所有 methos/field的私有访问权限改成 protectedpublic或者默认访问权限
    • 同时把内部类的所有 method/field 的私有访问权限改成protected或者public或者默认访问权限

    2. 匿名内部类编译

    匿名内部类其实也是内部类,所以自然也有 1.1节所说明情况的影响,但是发现在新增一个匿名类,同时规避了1.1节的情况,但是最后仍然提示了method的新增,所以接下来了解匿名内部类跟非匿名内部类的区别,并且有怎么样的特殊性。

    2.1 匿名内部类编译命名规则

    匿名内部类顾名思义是没有名字的。匿名内部类的名称格式一般是 外部类$数字编号,后面是的数字编号,是编译器根据该匿名内部类在把外部类中出现的先后关系,依次累加命名的:

    public class DexFixDemo {
        public static void test(Context context) {
            /* new DialogInterface.OnClickListener(){
    
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    Log.d("Rikka","OnClickListener");
                }
            }; */
    
            new Thread("thread-1") {
                @Override
                public void run() {
                    Log.d("Rikka", "thread-1 thread");
                }
            }.start();
        }
    }
    

    修复后的APK新增 DialogInterface.OnClickListener这么一个内部类,但是最后补丁工具发现新增了 onClick方法,因为打补丁前只有一个 Thread匿名内部类,此时该类的名称是 DexFixDemo$1,然后打补丁后再test方法中新增了 DialogInterface.OnClickListener的匿名内部类。此时 DialogInterface.OnClickListener匿名内部类的名称是 DexFixDemo$1,Thread匿名内部类名称是 DexFixDemo$2,所以前后DexFixDemo$1类进行对比差异,这个时候已经完全乱套了。

    同样的道理,减少一个匿名内部类也存在相同的情况。

    2.2 热部署解决方案

    新增或减少匿名内部类,实际上对于热部署来说都是无解的,因为补丁工具拿到的是已经编译后的 .class文件,所以根本没法去区分是DexFixDemo$1或者是 DexFixDemo$2类。所以在这种情况下,如果有补丁热部署的需求,应该极力避免插入一个新的匿名内部类。当然如果 匿名内部类是插入到外部类的末尾,那么是允许的。

    3 有趣的域编译

    3.1 静态field,非静态field编译

    实际上在热部署中除了不支持 method/field的新增,同时也不支持 <clinit>的修复,这个方法会在 DVM中类加载的时候进行类初始化时调用。
    在Java源码中本身并没有 clinit这个方法,这个方法是Android编译器自动合成的。通过测试发现,静态field的初始化和静态代码块实际上就会被编译器编译在<clinit>这个方法中,所以我们有必要去了解一下 field/代码块 是到底时怎么编译的。

    来看个简单的实例。

    public class DexFixDemo {
        {
            i = 2;
        }
    
        private int i = 1;
    
        private static int j = 1;
    
        static {
            j = 2;
        }
    }
    

    反编译为smali看下:

    .method static constructor <clinit>()V //类初始化方法
       const/4 v0, 0x1
       sput v0, Lcom/rikkatheworld/hotfix;->j:I  //也就是j=1
       const/4 v0, 0x2
       sput v0, Lcom/rikkatheworld/hotfix;->j:I  //也就是j=2
       return-void
    .end method
    
    .method public constructor <init>()V //构造方法
       invoke-direct {p0}, Ljava/lang/Object;-><init>()V  //首先调用父类的默认构造函数
       const/4 v0, 0x2
       iput v0, p0, Locom/rikkatheworld/hotfix;->i:I  //就是i=2
       const/4 v0, 0x1
       iput v0, p0, Lcom/rikkatheworld/hotfix;->i:I   //就是i=1
       return-void
    .end method
    

    3.2 静态field初始化,静态代码块

    上面的示例中,能够很明显的看到静态field初始化和静态代码块被编译器翻译在了 <clinit>中。
    静态代码块和静态域初始化在clinit()中的先后关系就是两者出现在源码中的先后关系。

    所以上述例子中,最后的j的值为2。前面说过,类加载进行类初始化的时候,会去调用 clinit(),一个类仅加载一次。以下三种情况都会尝试去加载一个类:

    • 创建一个类的对象(new-instance指令)
    • 调用类的静态方法(invoke-static指令)
    • 获取类的静态域的值(sget指令)

    首先判断这个类有没有被加载过,如果没有被加载过,执行 dvmResolveClass->dvmLinkClass->dvmInitClass的流程,类的初始化时在dvmInitClass中。dvmInitClass这个函数首先会尝试会对父类进行初始化,然后调用本类的 clinit方法,所以此时 静态field得到初始化并且静态代码块得到执行。

    3.3 非静态field初始化,非静态代码块

    上面的示例中,能够明显的看到非静态 field初始化和非静态代码块被编译翻译在 <init>默认无参构造函数中。非静态field和非静态代码块在init方法中的先后顺序也跟两者在源码中出现的顺序一致,所以上述示例中最后 i == 1。

    实际上如果存在有参构造函数,那么每个有参构造函数都会执行一个非静态域的初始化和非静态代码块。

    构造函数都会被Android编译器自动翻译成<init>方法

    前面介绍过 clinit方法在类加载初始化的时候被调用,那么<init>构造函数方法肯定是对类对象进行初始化时候被调用的,简单来说创建一个对象就会对这个对象进行初始化,并调用这个对象相应的构造函数,看下这行代码 String s = new String("test")编译之后的样子。

    new-instance v0, Ljava/lang/String;
    invoke-direct {v0}, Ljava/lang/String;-><init>()V
    

    首先执行 new-instance指令,主要为对象分配堆内存空间,同时如果类之前没有被加载过,尝试加载类。然后执行 invoke-direct指令调用类的 init构造函数方法执行对象的初始化。

    3.4 热部署解决方案

    由于不支持 <clinit>方法的热部署,所以任何静态field初始化和静态代码块的变更都会被编译到clinit方法中,导致最后热部署失败,只能冷启动生效。如上所见,非静态field和非静态代码块的变更被编译到 <init>构造函数中,热部署模式下只是视为一个普通方法的变更,此时对热部署是没有影响的。

    4 final static域编译

    final static域首先是一个静态域,所以我们自然会认为其会编译到 clinit方法中,所以在自然热部署下也是不能变更,但是测试发现,final static修饰的基本类型或者 String常量类型,匪夷所思的竟然没有被编译到 clinit方法中去,见以下分析。

    4.1 final static域编译规则

    final static 即 静态常量域,看下 final static域被编译后的样子:

    public class DexFixDemo {
        static Temp t1 = new Temp();
        final static Temp t2 = new Temp();
    
        final static String s1 = new String("heihei");
        final static String s2 = "haha";
    
        static int i1 = 1;
        final static int i2 = 2;
    }
    

    看下反编译得到的smali文件:

    # static fields
    .field static i1:I = 0x0
    .field static final i2:I = 0x2
    .field static final s1:Ljava/lang/String;
    .field static final s2:Ljava/lang/String; = "haha"
    .field static t1:Lcom/rikkatheworld/hotfix/Temp;
    .field static final t2:Lcom/rikkatheworld/hotfix/Temp;
    
    # direct methods
    .method static constructor <clinit>()V
        .registers 2
        .prologue
        .line 8
        new-instance v0, Lcom/rikkatheworld/hotfix/Temp;
        invoke-direct {v0}, Lcom/rikkatheworld/hotfix/Temp;-><init>()V     //调用t1的构造方法
        sput-object v0, Lcom/rikkatheworld/hotfix/DexFixDemo;->t1:Lcom/rikkatheworld/hotfix/Temp;
        .line 9
        new-instance v0, Lcom/rikkatheworld/hotfix/Temp;
        invoke-direct {v0}, Lcom/rikkatheworld/hotfix/Temp;-><init>()V     //调用t2的构造方法
        sput-object v0, Lcom/rikkatheworld/hotfix/DexFixDemo;->t2:Lcom/rikkatheworld/hotfix/Temp;
        .line 11
        new-instance v0, Ljava/lang/String;
        const-string v1, "heihei"                              
        invoke-direct {v0, v1}, Ljava/lang/String;-><init>(Ljava/lang/String;)V  //调用s1构造 “heihei”
        sput-object v0, Lcom/rikkatheworld/hotfix/DexFixDemo;->s1:Ljava/lang/String;
        .line 14
        const/4 v0, 0x1
        sput v0, Lcom/rikkatheworld/hotfix/DexFixDemo;->i1:I             //初始化 i1 = 1
        return-void
    .end method
    ...
    

    我们发现 在 clinit中final static int i2 = 2final static String s2 = "haha"这两个静态域竟然没有被初始化,而其他的非 final静态域均在clinit函数中得到初始化。

    这里注意下 “haha”new String("heihei")的区别,前者是字符串常量,后者是引用类型。那这两个final static域(i2和s2)究竟在何处会初始化?
    事实上,类加载初始化 dvmInitClass在执行clinit方法之前,首先会执行 <initSFields>,这个方法的作用主要就是给 static域赋予默认值。

    如果是引用类型,那么默认初始值为NULL。上述代码示例中,那块区域有4个默认初始值,分别是 t1==NULL,t2==NULL,s1==NULL,s2=="haha",i1==0,i2==2,即这里:
    在这里插入图片描述
    t1、t2、s2、i1均在 这里完成初始化,然后在 clinit中赋值。而i2、s2在 initSFields得到默认值就是程序中设置的值了。

    现在我们知道了 static和 final static修饰field的区别了,简单来说:

    • final static修饰的原始类型和String类型域(非引用类型),并不会编译在 clinit方法中,而是在类初始化执行 initSFiedls()方法时得到了初始化赋值
    • final static修饰的引用类型,初始化仍然在clinit方法中。

    4.2 final static域优化原理

    另外一方面,我们经常会看到Android性能优化相关文档中介绍过,如果一个 field是常亮,那么推荐尽量使用 static final作为修饰符。很明显这句话不太对,得到优化的仅仅是final static原始类型和 String类型域(非引用类型),如果是引用类型,实际上不会得到任何优化的

    还是接着上面的示例,Temp直接引用 DexFixDemo的static变量:

    class Temp {
        public static void test(){
            int i1 = DexFixDemo.i1;
            int i2 = DexFixDemo.i2;
            
            Temp t1 = DexFixDemo.t1;
            Temp t2 = DexFixDemo.t2;
            
            String s1 = DexFixDemo.s1;
            String s2 = DexFixDemo.s2;
        }
    }
    

    看下反编译后的smali文件:

    .method public static test()V
        ...
        sget v0, Lcom/rikkatheworld/hotfix/DexFixDemo;->i1:I  // 通过sget获取到DexFixDemo中的i1并赋值给 v0
        .local v0, "i1":I    //将v0赋值给 i1
        
        const/4 v1, 0x2     //使用 const/4指令,将 0x2赋值给v1
        .local v1, "i2":I     //将v1 赋值给 i2
        
        sget-object v4, Lcom/rikkatheworld/hotfix/DexFixDemo;->t1:Lcom/rikkatheworld/hotfix/Temp;
        .local v4, "t1":Lcom/rikkatheworld/hotfix/Temp;
        
        sget-object v5, Lcom/rikkatheworld/hotfix/DexFixDemo;->t2:Lcom/rikkatheworld/hotfix/Temp;
        .local v5, "t2":Lcom/rikkatheworld/hotfix/Temp;
        
        sget-object v2, Lcom/rikkatheworld/hotfix/DexFixDemo;->s1:Ljava/lang/String;
        .local v2, "s1":Ljava/lang/String;
        
        const-string v3, "haha"   //使用 const-string指令获取 final static String类型,速度要比sget好一些
        .local v3, "s2":Ljava/lang/String;
        return-void
    .end method
    

    首先看下 Temp怎么获取 DexFixDemo.i2(final static域),直接通过 const/4 指令:

    const/4 vA, #+b   //前一个字节是opcode,后一个字节前4位是寄存器v1,后4位就是立即数的值 "0x02"
    
    HANDLE_OPCODE(OP_CONST_4 /*vA, #+B*/) {
        s4 tmpl;
        vdst = INST_A(inst);
        tmp = (s4) (INST_B(inst) << 28) >>28;
        SET_REGISTER(vdst, tmp);
    }
    FINISH(1);
    OP_END;
    

    const/4 指令的执行过程很简单,操作数在 dex文件中的位置就是在 opcode后一个字节。

    怎么获取 DexFixDemo.i1(非final域),就是通过sget指令。

    sget vAA, field@BBBB /* 前一个字节是opcode,后一个字节是寄存器v0,后两个字节是DexFixDemo.i1 这个field在dex文件结构中field在dex文件结构中 field区的索引值 */
    
    HANDLE_OPCODE(OP_CONST_4 /*vAA, #field@BBBB*/) {
        StaticField* sfield;
        vdst = INST_AA(inst);
        ref = FETCH(1);
        sfield = (StaticField*)dvmDexGetResolvedField(methodClassDex, ref);  // 1
        if(sfield == NULL) {  // 2
            EXPORT_PC(); // 3
            sfield = dvmResolveStaticFeild(curMethod->clazz, ref);  // 4
            if(sfield == NULL)
                GOTO_exceptionThrown();
            if(dvmDexGetResolvedField(methodClassDex, ref) == NULL) {
                JIT_STUB_HACK(dvmJitEndTraceSelect(self, pc));
            }
        }
        SET_REGISTER##_regisze(vdst, dvmGetStaticField##_ftype(sfield));  // 5
    }
    FINISH(2);
    

    注释1: 调用 dvmDexGetResolvedField()方法得到指定的区域,在上述例子中,这个区域Lcom/rikkatheworld/hotfix/DexFixDemo;->i1:I

    注释2:判断注释1中的区域有没有被解析过
    注释3:如果没有被解析过,则调用EXPORT_PC,它会调用 dvmResolveClass()解析类

    注释4:通过 dvmResolveStaticFeild()拿到静态域。
    注释5:返回静态域。

    可见此时 sget指令比 const/4指令的解析过程要复杂,所以final static基本类型可以得到优化。

    final static String类型引用 const-string指令的解析执行速度要比sget快一些。
    final static String类型的变量,在编译期间引用会被优化成 const-string指令,因为 const/4 获取的值是 立即数,但是 const-string指令获取的只是 字符串常量在dex文件结构中字符串常量区的索引ID,所以需要额外的一次字符串查找。
    dex文件中有一块区域存储这程序所有的字符串常量,最终这块区域会被虚拟机完整加载到内存中,这块区域也就是通常说的 “字符串常量区”内存。

    final static引用类型没有得到优化,是因为不管是不是final,最后都是通过 sget-object指令去获取该值,所以此时实际上从虚拟机运行性能方面来说得不到任何优化,此时final的作用,仅仅是让编译器能在编译期间检测到该final域有没有被修改。final域修改过在编译期就会直接报错。

    所以这里引出一个冷知识
    final字段只在编译期间起到作用----它可以在编译期阻止任何 final类型的修改,但是到了运行期,final就冇用了,这就说明,运行时使用反射是可以更改 final字段的…(网上一搜,果然有人试验过:利用反射修改final数据域
    在这里插入图片描述

    4.3 热部署解决方案

    • 修改final static基本类型或者String类型域(非引用类型域),由于在编译期间引用到基本类型的地方被立即数替换,引用到String类型的类型 的地方被常量池索引ID替换,所以在热部署模式下,最终所有引用到该 final static域的方法都会被替换。实际上此时仍然可以执行热部署方案
    • 修改final static引用类型域,是不允许的,因为这个field的初始化会被编译到clinit方法中,所以此时没法走热部署。

    5 有趣的方法编译

    5.1 应用混淆方法编译

    除了以上内部类和匿名内部类可能会造成method新增之后,我们发现项目如果应用了混淆方法编译,可能导致方法的内联和裁剪,那么最后也可能导致 method的新增或减少,以下介绍在哪些场景中会造成方法的内联或裁剪。

    5.2 方法内联

    实际上有好几种情况可能导致方法被内联掉。

    • 方法没有被其他任何地方引用,毫无疑问,该方法会被内联掉。
    • 方法足够简单,比如一个方法的实现就只有一行代码,该方法会被内联掉,那么任何调用该方法的地方都会被该方法的实现替换掉。
    • 方法只被一个地方引用,这个地方会被方法的实现替换掉。

    5.3 方法裁剪

    public class BaseBug {
        public static void test(Context context) {
            Log.d("BaseBug", "test");
        }
    }
    

    查看下生成的 mApping.txt文件:

    com.rikkatheworld.hotfix.BaseBug -> com.rikkatheworld.hotfix.a:
        void test$faab20d() -> a  //在括号中没有context参数
    

    此时test方法context参数没有被使用,所以test方法的context参数被裁剪。
    混淆任务首先生成 test$faab20d()裁剪过后的无参方法,然后再混淆。

    所以如果我们想要fix test方法时,里面用到context的参数,那么test方法的context参数不会被裁剪,补丁工具检测到新增了(test(context))方法。那么补丁只能走冷启动方案。

    怎么让该参数不被裁剪呢?我们只要让编译器在优化的时候认为引用了一个无用的参数就好了,可以采取的方法很多,这里介绍一种最有用的方法:

    public static void test(Context context) {
       if(Boolean.FALSE.booleanValue()) {
           context.getApplicationContext();
       }
       Log.d("BaseBug", "test");
    }
    

    注意,这里不能使用基本类型false,必须使用包装类Boolean,因为如过使用基本类型if语句很可能会被优化掉的。

    5.4 热部署解决方案

    实际上只要混淆配置文件加上 -dontoptimize选项就不会去做方法的裁减和内联。
    在一般情况下,项目的混淆配置都会使用到 Android SDK默认的混淆配置文件 proguard-android-optimize.txt或者 proguard-android.txt,两者的区别就是后者应用了 -dontoptimize这一项配置而前者没有用。

    preverification step :针对 .class文件的预校验,在 .class文件中加上 StackMa/StackMapTable信息,这样 Hotspot VM在类加载时执行类校验阶段会省去一些步骤,因此类加载会更快。

    我们知道Android虚拟机执行的是 dex文件格式,编译期间dx工具会把所有的 .class文件优化成 .dex文件,所以混淆库的域编译在Android中是没有任何意义的,反而会降低打包速度,Android虚拟机中有自己的一套代码校验逻辑(dvmVerifyClass)。所以Android中混淆配置一般都需要加上 -dontpreverify这一项。

    6 switch case语句编译

    6.1 关于switch case语句的编译差异

    由于在实现资源修复方案热部署的过程中(后面章节会讲到),要做新旧资源的ID替换,我们竟然发现存在switch case语句中的ID不会被替换掉的情况,所以有必要来探索下 switch case语句编译的特殊性。

    public void testContinue() {
            int temp = 2;
            int result = 0;
            switch (temp) {
                case 1:
                    result = 1;
                    break;
                case 3:
                    result = 3;
                    break;
                case 5:
                    result = 5;
                    break;
            }
        }
    
        public void testNotContinue() {
            int temp = 2;
            int result = 0;
            switch (temp) {
                case 1:
                    result = 1;
                    break;
                case 3:
                    result = 3;
                    break;
                case 10:
                    result = 10;
            }
        }
    

    看看上面两个方法编译出来有什么不同:

    .method public testContinue()V
        ...
        const/4 v1, 0x2
        .local v1, "temp":I
        const/4 v0, 0x0
        .local v0, "result":I
        packed-switch v1, :pswitch_data_c
        
        :pswitch_5
        return-void
        :pswitch_6
        const/4 v0, 0x1
        :pswitch_8
        const/4 v0, 0x3
        :pswitch_a
        const/4 v0, 0x5
    
        :pswitch_data_c
        .packed-switch 0x1
            :pswitch_6
            :pswitch_5
            :pswitch_8
            :pswitch_5
            :pswitch_a
        .end packed-switch
    .end method
    
    .method public testNotContinue()V
        ...
        const/4 v1, 0x2
        .local v1, "temp":I
        const/4 v0, 0x0
        .local v0, "result":I
        sparse-switch v1, :sswitch_data_e
    
        :sswitch_6
        const/4 v0, 0x1
        :sswitch_8
        const/4 v0, 0x3
        :sswitch_a
        const/16 v0, 0xa
    
        :sswitch_data_e
        .sparse-switch
            0x1 -> :sswitch_6
            0x3 -> :sswitch_8
            0xa -> :sswitch_a
        .end sparse-switch
    .end method
    

    testContinue() 的switch case语句被编译成 packed-switch指令
    testNotContinue() 的switch case语句被编译成 sparse-switch指令。
    比较两者的差异:
    ①testContinue的switch语句的case项是连续的几个比较相近的值1、3、5,。所以被编译为 packed-switch指令,可以看到几个连续的数中间的差值用: pswitch_5 补齐, :pswitch_5标签处直接return-void、
    ②testNotContinue的switch语句的case分别是1、3、10,很明显不够连续,所以被编译为 sparse-switch指令。编译器会决定怎样的值才算是连续的case。

    6.2 热部署解决方案

    一个资源ID肯定是 const final static变量,此时恰好switch case语句被编译 packed-switch指令,所以这个时候如果不做任何处理就会存在资源ID替换不完全的情况。
    解决这种情况方案其实很简单,修改smali反编译流程,碰到 packed-switch指令强制转为 sparse-switch指令, :pswitch_N等相关标签指令也需要强转为 :sswitch_N指令。然后做资源ID暴力替换,然后再回编译 smali为dex。再做类方法变更的检测,所以就需要经过反编译->资源ID替换->回编译,这也会使打补丁变得稍慢一些。

    7 泛型编译

    泛型是从Java5才引入的,我们发现泛型的使用,也可能导致method的新增,所以是时候了解一下泛型的编译过程了。

    7.1 为什么需要泛型

    • Java语言中泛型基本上完全在编译器中实现,由编译器执行类型检查和类型推断,然后生成普通的非泛型字节码,就是虚拟机完全无感知泛型的存在。这种技术称为泛型擦除。编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除。
    • Java5才引入泛型,所以扩展虚拟机指令集来支持泛型被认为是无法接受的,因为这会为Java厂商升级其JVM造成难以逾越的障碍。因此采用了可以完全在编译器中实现的擦除方法。

    我们知道了以上的两点,其中最重要的是类型擦除的理解,先来通过一个例子说明Java为什么需要泛型。Java5之前,要实现类似“泛型”的功能,由于Java类都是以Object为最上层的父类别,所以可以用Object来实现似“泛型”的功能。

    public class ObjectFoo {
        private Object foo;
    
        public Object getFoo() {
            return foo;
        }
    
        public void setFoo(Object foo) {
            this.foo = foo;
        }
    }
    
    // 代码调用示例
            ObjectFoo  foo = new ObjectFoo();
            foo.setFoo(new Boolean(true));
            Boolean b = (Boolean) foo.getFoo();  //正确
            String s = (String) foo.getFoo();   //运行时,类型转换失败, ClassCastException异常
    

    由于语法上并没有错误,所以编译器检查不出上面程序有误,真正的错误要在执行器才会发生。

    所以可以看到使用Obejct来实现“泛型”存在一些问题,因为必须要强制类型转换,但很多人可能忘记强制类型转换,或者是强转用错类型,然而由于语法上可以的,所以编译器检查不出错误。Java5之后,提出了针对泛型设计的解决方案。该方案在编译器进行类型安全监测,允许程序员在编译器就能监测到非法的类型,泛型解决方案如下:

        public class GenericFoo<T> {
            private T foo;
    
            public T getFoo() {
                return foo;
            }
    
            public void setFoo(T foo) {
                this.foo = foo;
            }
        }
    
    // 代码调用示例
            GenericFoo<Boolean>  foo = new GenericFoo();
            foo.setFoo(new Boolean(true));
            Boolean b = foo.getFoo();  //正确
            String s = (String) foo.getFoo();   //编译不通过
    

    很明显此时使用泛型的优势就体现出来了,可以在编译期就检查到了可能的异常。

    7.2泛型类型擦除

    我们来反编译一下上述 GenericFoo<T>的字节码:

    .method public getFoo()Ljava/lang/Object;
    .method public setFoo(Ljava/lang/Object;)V
    

    可以看到它是被编译成了Object,如果此时再定义一个 setFoo<Object foo>方法是行不通的,编译期会报重复方法定义。
    如果这样的 <T extends Numble>,那么是这样子的:

    .method public setFoo(Ljava/lang/Number;)V
    .method public getFoo()Ljava/lang/Number;
    

    所以我们知道 new T()这样使用泛型是编译不过的,因为类型擦除会导致实际上是 new Object(),所以是错误的

    7.3 类型擦除与多态的冲突和解决

        class A<T> {
            private T t;
    
            public T get() {
                return t;
            }
    
            public void set(T t) {
                this.t = t;
            }
        }
        
        class B extends A<Number> {
            private Number n;
    
            @Override  //跟父类返回值不一样,为什么重写父类get方法
            public Number get() {
                return n;
            }
    
            @Override   //跟父类方法参数类型不一样,为什么重写父类set方法
            public void set(Number number) {
                this.n = number;
            }
        }
        
        class C extends A {
            private Number n;
    
            @Override //跟父类返回值不一样,为什么重写父类get方法
            public Number get() {
                return n;
            }
    
            //@Override  重写父类get方法,因为方法参数类型不一样,这里没问题
            public void set(Number o) {
                this.n = o;
            }
        }
    

    按照前面类型擦除的分析,为什么类B的 set和get方法可以用 @Override而不报错。@Override表明这个方法是重写,我们知道重写的意思是子类的方法与父类中的某一方法具有相同的方法名,返回类型和参数表。
    但是根据前面的分析,基类A由于类型擦除的影响,set(T t)在字节码中实际上是 set(Object t),那么类B的方法 set(Number n)方法参数不一样,此时类B的set方法理论上来说应该重载而不是重写基类的set方法。但是我们的本意是重写,实现多态,可是类型擦除后,只能变为重载,这样,类型擦除就和多态有了冲突。

    实际上JVM采用了一个特殊的方法,来完成这项重写功能,那就是桥接。看下类B的字节码表示:

    .method public get()Ljava/lang/Number;
    .method public bridge synthetic get()Ljava/lang/Object;
        invoke-virtual {p0}, Lcom/rikkatheworld/hotfix/Main$B;->get()Ljava/lang/Number;
        move-result-object v0
        return-object v0
    .end method
    
    .method public set(Ljava/lang/Number;)V
    .method public bridge synthetic set(Ljava/lang/Object;)V
        check-cast p1, Ljava/lang/Number;
        invoke-virtual {p0, p1}, Lcom/rikkatheworld/hotfix/Main$B;->set(Ljava/lang/Number;)V
        return-void
    .end method
    

    我们可以发现编译器自动合成 set(Ljava/lang/Object)get()Ljava/lang/Object这两个桥接方法来重写父类,同时这两个桥接方法上实际上调用 B.print(Ljava/lang/Number)B.get()Ljava/lang/Number这两个重载方法。那么可以得到最终的结论:

    • 子类中真正重写基类方法的是编译器自动合成的桥接方法。而类B定义的get和set方法上面的@Override只不过是假象,桥接方法的内部实现是去调用自己重写的print方法而已。所以,虚拟机巧妙使用了桥接方法,来解决了类型擦除和多态的冲突。

    1.4 热部署解决方案

    前面类型擦除中说过,如果 B extends A 变成了 B extend A<Number>,那么就可能会新增对应的桥接方法。此时新增了方法,只能走冷部署。

    这这种情况下,如果要走热部署,应该避免类似上面那种修复。

    另一方面,实际上泛型方法内部会生成一个 dalvik/annotation/Signature这个系统注解:

        class A {
            public <A extends Number> void getA(A t) {
                System.out.println("t:" + t);
            }
        }
        
    //getA方法内注解
        .annotation system Ldalvik/annotation/Signature;
            value = {
                "<A:",
                "Ljava/lang/Number;",
                ">(TA;)V"
            }
        .end annotation
    

    如果现在把方法的签名换成 <B extends Number> void getA(B t),前面说过,泛型类型擦除方法的逻辑实际上没有发生任何变化,但是这个方法的注解却发生了变化:

        .annotation system Ldalvik/annotation/Signature;
            value = {
                "<B:",
                "Ljava/lang/Number;",
                ">(TB;)V"
            }
        .end annotation
    

    补丁工具发现这个方法发生了变化(不会排除注解的变化),然后对这个方法进行打包,此时打包时多余且没有任何意义的,所以热部署下,需要避免这种会导致浪费性能的修复。

    8 Lambda表达式编译

    Lambda表达式是Java7才引入的一种表达式,类似于匿名内部类,实际上又和匿名内部类有很大的差别,我们发现Lambda表达式的使用也可能导致方法的新增或减少,导致最后走不了热部署模式。

    8.1 Lambda表达式编译规则

    首先简单介绍下Lambda表达式,Lambda为Java添加了缺失的函数式编程特点,Java现在提供的最接近闭包的概念便是Lambda表达式。
    gradle就是基于groovy存在的大量闭包。

    函数式接口具有两个主要特征:它是一个接口,这个接口具有唯一的抽象方法;我们将同时满足这两个特性的接口称为函数式接口。

    比如Java标准库中的 java.lang.Runnablejava.util.Comparator都是典型的函数式接口。函数式接口跟匿名内部类的区别如下:

    • 关键字this,匿名类的this关键字指向匿名类,而Lambda表达式的this关键字指向包围Lambda表达式的类
    • 编译方式,Java编译器将Lambda表达式编译成类的私有方法,使用Java7的invokedynamic字节码指令来动态绑定这个方法。Java编译器将匿名内部类编译成 外部类$数字编号的新类。

    通过一个代码示例来讲解 Lambda表达式,匿名内部类前面已经详细介绍过。

    public class Main {
        private static int temp = 2;
    
        public static void main(String[] args) {
            new Thread(() -> System.out.println("java 8 lambda...")).start();
    
            test(s -> temp + 1);
        }
    
        public static void test(TInterface tInterface) {
            System.out.println("java8 TInterface lambda..." + tInterface.test("1"));
        }
    
        public interface TInterface {  //自定义函数式接口
            int test(String s);
        }
    }
    

    先通过javac转换成.class字节码文件,再用 javap -p -v Main.class反编译 .class文件,截取关键内容

    public class com.rikkatheworld.hotfix.Main
    Constant pool:
       ...
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=1, args_size=1
             0: new           #2                  // class java/lang/Thread
             3: dup
             4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
             9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
            12: invokevirtual #5                  // Method java/lang/Thread.start:()V
            15: invokedynamic #6,  0              // InvokeDynamic #1:test:()Lcom/rikkatheworld/hotfix/Main$TInterface;
            20: invokestatic  #7                  // Method test:(Lcom/rikkatheworld/hotfix/Main$TInterface;)V
            23: return-
    
      private static int lambda$main$1(java.lang.String);
      private static void lambda$main$0();
    }
    

    这里我们可以发现几点:

    • 编译期间自动生成私有静态的 lambda$main$**(*)方法,这个方法的实现就是lambda表达式里面的逻辑
    • invokedynamic指令执行Lambda表达式
    • 比较匿名内部类的区别,发现并没有在磁盘上生成 外部类$数字编号的新类。

    那么我们应该思考的是 invokedynamic指令的执行,最后为什么就调用到了 Main的私有静态 lambda$main$**(*)方法。我们首先看下JVM中关于 invokedynamic指令的介绍:在Java7 JVM中增加了一个新的指令 invokedynamic,用于支持动态语言,即允许方法调用可以在运行时指定类和方法,不必再编译的时候确定。
    字节码中每条 invokedynamic指令出现的位置称为一个动态调用点, invokedynamic指令后面跟一个指向常量池的调用点限定符(#3, #0),这个限定符会被解析成一个动态调用点。

    从上面的反编译后的内容中可以发现, invokedynamic指令执行时实际上会去调用 java/lang/invoke/LambdaMetafactorymatafactory静态方法。**这个静态方法实际上会在运行时生成一个实现函数式接口的具体类。**然后具体类会调用 Main的私有静态 lambda$main$**(*)方法。
    这就是 Sun/Oracle Hotspot VM对Lambda表达式的解释。

    那Android虚拟机是如何解释Lambda表达式的呢?
    我们知道Android虚拟机首先通过javac把源代码编译成.class,再通过 dx工具优化成 dex字节码文件。
    但是Android中如果要使用新的Java8语言特性,还需要使用新的 Jack工具链来替换掉旧的工具链来编译。
    新的Android工具链将Java源语言直接编译成Android可以读取的 Dalvik可执行文件字节码,且有自己的 .jack库格式。
    它试图取代 javac/dx/proguard/jarjar/multidex库等工具。以下是构建Android Dalvik可执行文件可用的两种工具键的对比:

    • 旧版javac工具键
      javac(.java -> .class) -> dx(.class -> .dex)
    • 新版 Jack工具链
      Jack(.java -> .java -> .dex)

    这里省略使用 Jack工具链编译代码的示例。
    .dex字节码和.class字节码文件对Lambda表达式处理的异同点:

    • 共同点:编译期间都会为外部类合成一个 static辅助方法,该方法内部逻辑实现Lambda表达式
    • 不同点:
      (1).class字节码中通过 inoke-dynamic指令执行Lambda表达式,而.dex字节码中执行Lambda表达式跟普通方法调用没有任何区别
      (2).class字节码运行时生成新类, .dex字节码中编译期间生成新类。

    8.2 热部署解决方案

    有了以上知识点做基础,同时知道打补丁是通过反编译为smali然后新APK跟基线APK做差异对比,得到最后的补丁包。

    新增一个Lambda表达式,会导致外部类新增一个辅助方法,所以此时不支持走热部署方案。

    那么如果只修改Lambda表达式内部的逻辑,此时看起来仅仅相当于修改了一个方法,所以此时看起来是允许走热部署的。事实上并非如此。我们忽略了一种情况,Lambda表达式访问外部非静态 field/method的场景。

    前面我么你知道 .dex字节码中 Lambda表达式在编译期间会自动生成新的辅助类。注意该辅助类是非静态的,所以该辅助类如果为了访问“外部类”的非静态 field/method就必须持有“外部类”的引用。如果该辅助类没有访问“外部类”的field/method,那么就不会持有“外部类”的引用。这里注意这个辅助类和内部类的区别。我们前面说过如果是非静态内部类的话一定会持有外部类的引用的。

    下面的示例很容易说明这个问题。
    Lambda表达式没有访问“外部类”的非静态 field/method,可以看到并没有持有“外部类的引用”:

    public synthetic constructor <init>()V
    

    Lambda表达式访问了“外部类”的非静态 field/method,可以看到持有了“外部类”的引用。

    private synthetic val$Lcom/rikkatheworld/hotfix/Main;  /*合成的变量 val$this, “外部类”的引用 */
    public synthetic constructor <init>(.....)
    

    所以此时存在这样的情况:基线APK中Lambda表达式中没有访问非静态field/method,修复后的APK中的Lambda表达式中访问非静态 field/methid.name会导致发现新增field,此时热部署也会失败。

    最后进行小结:

    • 增加或减少一个Lambda表达式会导致类方法比较错乱,所以会导致热部署失败
    • 修改一个Lambda表达式,可能导致新增field,所以此时也会导致热部署失败。

    9 访问权限检查对热替换的影响

    • 类加载阶段父类/实现接口访问检查
      一个类的加载阶段包括resolve->link->init三个阶段,父类/实现接口 权限检查在link阶段,dvmLinkClass()中依次对父类/实现接口进行dvmCheckClassAcess()权限检查,如果父类/实现接口是非public然后进行检查当前类和它是否是相同的ClassLoader。热修复补丁类是新classLoader加载的,所在会报父类不允许访问的错误。
    • 类校验阶段访问权限检查
      补丁类如果引用了非public类,而父类/实现接口 都是public的,是可以通过类加载阶段的。
      但是在运行时会爆出crash异常~~
      补丁类在单个dex文件中,加载dex肯定要进行dexopt,再dexopt过程中会dvmVerifyClass校验dex每个类。在校验过程中会检查补丁类所引用类的访问权限(提前dvmResolveClass加载被调用类)。还会校验调用方法的访问权限,public修饰直接返回。protected的话,先检查当前类和被调用方法所属类是否是父子类关系,不是的话会调用dvmIsSamePackage,这里会判断是否是相同的classLoader。

    10 <clinit>方法

    由于补丁热部署模式的特殊性----不允许类结构变更以及不允许变更 <clinit>方法,所以补丁工具如果发现了这几种限制情况,那么此时只能走冷启动重启生效,冷启动几乎是无任何限制的,可以做到任何场景的修复。

    可能有时候再源码层上来看并没有新增或减少 method和field,但是实际上由于要满足Java各种语法特性的需求,所以编译器会在编译期间自动合成一些method和field,最后就有可能触发了这几个限制情况。

    展开全文
  • smali语法快速入门

    2020-05-18 11:55:21
    Samli文件详解 通过反编译工具反编译出来每个.smali,都对应与java中的一个类,每个smali文件都是Davilk指令组成的,并遵循一定的结构.smali存在很多的指令用于描述对应的java文件,所有的指令都以”.”开头,常用的...
  • Dalvik smali语法

    2021-08-14 17:36:24
    1.dalvik寄存器:32位,所有类型,<=32 一个寄存器 64:使用两个相邻寄存器...流程 :java编译成.class在编译成.dex 最后反编译得到samli文件 .java →.class → .dex → smali dx.jar: .class打包.dex dx --de...
  • smali的文件语法格式

    2018-09-20 10:05:53
    无论是普通类、抽象类、接口类或者内部类,在反编译出来的代码中,他们都以单独的smali文件来存放。 一、smali文件的头3行描述了当前类的一些信息,格式如下: .class &lt;访问权限&gt;[修饰关键字]&...
  • 打包的流程就是需要拿到游戏的母包,对这个母包进行反编译,然后替换assets文件,lib文件,资源文件,修改对应的包名,还有需要替换掉部分代码,合并资源文件以及AndroidManifest.xml文件,在进行回编译签名出包,...
  • 此高亮配置文件是完全免费的。 本人在Ubuntu Gedit下阅读apktool生成的smali文件十分吃力,并且网上找不到已有的Gedit Smali高亮文件,所以就简单的写了此配置... "查看" -> "突出显示模式" -> "源代码" -> "Samli"。
  • 以下方法仅供学习使用,不得随意反编译apk文件并使用 方法一: 直接使用文件夹中的Androidby 文件夹中的Android反编译工具(64)位.exe或者使用Android反编译工具.exe,双击后选择需要反编译的apk文件即可。但是该...
  • 最近,成功在一些市场上的apk文件中植入自己的短信侦听的代码(非恶意,反对通过此手段做一些不好的事情)比如:神庙逃亡2,fruit monkey等...一个apk文件反编译成的samli格式的文件,很难对它做修改,因为基本上看不懂
  • 9200内核文件

    2014-05-12 21:52:27
    9200内核文件 用于手机内核安装 内核 - KERNEL-CHN-I9200ZCUAMF4-1371799721.t
  • 使用Apktool反编译apk 文件后,会在反编译工程目录下生成一个smali 文件夹,里面存放着所有反编译出的smali 文件,这些文件会根据程序包的层次结构生成相应的目录,程序中所有的类都会在相应的目录下生成独立的smali...
  • Android逆向世界之一:smali文件

    万次阅读 多人点赞 2017-07-15 20:45:05
    APK文件android工程编译完成会得到我们想要的apk安装包,apk文件其实是一个压缩包,可以直接用解压缩软件解压,解压后的文件如下图所示: assets文件夹保存一些额外的资源文件,如游戏的声音文件,字体文件等等,...
  • mac 10.12显示隐藏文件

    2017-01-22 10:00:00
    macOS Sierra 10.12版本 显示隐藏文件 1、显示隐藏文件  打开Terminal 输入:defaultswritecom.apple.finderAppleShowAllFiles-booltrue  再输入:killallFinder 2、隐藏隐藏文件  打开Terminal ...
  • Smali反编译Java文件

    万次阅读 2016-04-27 00:24:26
    我们在开发 安卓中,有时需要对其他apk文件进行反编译,以便我们方便进行学习交流研究使用。我们使用 Android SDK 默认生成的工程会自动添加一些类到我们的工程中。这些类在程序发布后会仍然保留在apk 文件中。我们...
  • 1. java文件编译成dex文件 新建一个名为“Hello.java”的文件,将以下内容保存。 public class Hello{ public int foo(int a,int b){ return (a+b)*(a-b); } public static void main(String[] argc){ Hello hello=...
  • 我国邮政大数据Hadoop平台投标文件技术部分.pdf
  • 我国邮政大数据Hadoop平台投标文件技术部分.docx
  • smali文件语法

    2015-11-01 17:37:22
    smali文件语法参考 - 在路上... - BlogJava http://www.blogjava.net/midea0978/archive/2012/01/04/367847.html Smali语法:数据类型、方法和字段 - 柳志超博客 http://liuzhichao.com/p/912.html Smali--Dalvik...
  • 高精地图文件数据存储格式定义_V1.0_20180412.docx
  • 高精地图文件数据存储格式定义-V1.0-20180412.docx
  • 1、使用文本编辑器阅读baksmali反编译生成的smali文件 (1)解压apk包 unzip xxx.apk (2)用baksmali进行对解压出来的dex文件反编译 java -jar baksmali-2.0.3.jar classes.dex 2、使用IDA Pro分析dex文件 ...
  • apk文件通过apktool反编译出来的都有一个smali文件夹,里面都是以.smali结尾的文件。 smali语言是Davlik的寄存器语言,语法上和汇编语言相似,Dalvik VM与JVM的最大的区别之一就是Dalvik VM是基于寄存器的。基于...

空空如也

空空如也

1 2 3 4 5 ... 10
收藏数 199
精华内容 79
关键字:

samli文件