baksmali和smali源码分析

文章来源:http://sunzeduo.blog.51cto.com/2758509/1540085

最近有一个项目需要修改一些dex文件,提起修改dex文件,最有名的就是baksmali和smali项目,这个项目并且开源,所以一个思路就是通过修改这个原来来实现修改dex文件的目的。


首先是下载 baksmali源码:

下载地址 https://code.google.com/p/smali/
下载完成以后,将项目导入到eclipse工程中,本来笔者认为开源项目直接导入后就能够调试了,结果发现事情远远没有那么简单,这个项目依赖了众多的jar包。
没有办法笔者只好将需要的jar包一一下载下来,然后加入到建立的工程中,如图1:


wKioL1Psgc-DqsrsAAPHA_2Ep_4088.jpg


主要需要的jar包以及下载地址:


1 antlr-4.2.2-complete.jar  
一个开源的词法分析器的jar包,主要用于生成smali和baksmali的语法解释器的
java文件  生成的 smaliParser.java 和 smaliTreeWalker.java 这两个java文件加起来将近
1M了

下载地址 
http://www.antlr.org/download/antlr-4.2.2-complete.jar


2 guava-17.0.jar  
google的一个开源的库,英文单词的意思是石榴,里面实现了很多容器的类
下载地址
http://search.maven.org/remotecontent?filepath=com/google/guava/guava/17.0/guava-17.0.jar


3 jsr305-1.3.9.jar
一些用于判断对象类型是否可以为 NULL的jar包,据说将要进入JDK中
下载地址
http://www.java2s.com/Code/JarDownload/jsr305/jsr305-1.3.9.jar.zip


4 ST-4.0.8.jar
下载地址
http://www.stringtemplate.org/download/ST-4.0.8.jar


5 org.apache.commons.cli_1.2.0.v201105210650.jar
下载地址
http://dl.shiguanglu.com/jarfiles/o/org.apache.commons/org.apache.commons.cli_1.2.0.v201105210650.jar.zip

6 com.springsource.org.junit-4.7.0.jar
下载地址

http://dl.shiguanglu.com/jarfiles/c/com.springsource.org.junit/com.springsource.org.junit-4.7.0.jar.zip



将这些jar包加入到工程里面,这个时候还是会报错的,因为在源代码中放的smali语法分析器的文件形式不是java的文件形式,而是 antlr 这个语法分析器的文件形式,具体目录是在 smali/src/main/antlr3这个目录下,里面有 smaliParser.g 和 smaliTreeWalker.g 这两个文件,需要使用

java -jar antlr3.jar xx.g

这个命令将 这两个文件生成相应的java文件,然后将这两个java文件拷贝到 smali/src/main/java/org/jf/smali这个目录下

这个时候还有最后一步,就是要处理一下 smali/src/main/jflex 这个目录下的
smaliLexer.flex 这个文件了

这个文件需要下载一个叫做 jflex-1.6.0.jar 的jar包
下载地址在 http://jflex.de/download.html

然后运行
java -jar jflex-1.6.0.jar smaliLexer.flex
这个命令,生成了 smaliFlexLexer.java 这个文件

同样需要将 smaliFlexLexer.java 文件拷贝到 smali/src/main/java/org/jf/smali 这个目录下

这个时候如果你在建立的项目将源码中的test代码也导入的话,不出意外会出现一个错误,错误文件是 lexerTest.java 里面大概 163行吧,修改成下面的代码即可
        //modify by sunzeduo 2014-6-23
        InputStreamReader reader = new InputStreamReader(smaliStream);

就是有些jar包升级了,测试代码没有及时修改过来,导致的出现的问题,这个问题很好修改。


ok 解决完了编译问题,现在在eclipse中的样子就是如图2的样子了

wKioL1Pshe7g-FuvAANnPJ0lsXI245.jpg

我们经常在反编译android apk包的时候使用apktool这个工具,其实本身这个工具里面对于dex文件解析和重新生成就是使用的baksmali 和smali这两个jar包
其中 baksmali是将 dex文件转换成便于阅读的smali文件的,
具体使用命令如下:
java -jar baksmali.jar classes.dex -o myout
其中myout是输出的文件夹

而smali是将smali文件重新生成回 dex文件的
具体使用的命令如下:
java -jar smali.jar myout -o classes.dex

这两个工具在源码中分别对应两个文件夹,下面见图三,源码文件夹的样子

wKiom1PshsGQq0EDAAKEMieqepk811.jpg


在分析源码之前,这里我先将这些文件夹下的主要作用来说明一下,让大家有个总体的认识

baksmali     该文件夹下面主要实现的是将 dex转换成smali文件的代码,需要分析的重点

deodexerant  该文件夹下面的文件是一个可执行文件,笔者尝试用ndk编译了一下,能够编译出可执行文件根据里面的描述,是为了 拷贝出来dalvik虚拟机中的函数表,有兴趣的同学可以试试,就是用dlopen 将 libdvm.so 打开,然后得到 dvmGetInlineOpsTable 这个导出表,看看里面的内容

dexlib2      这个文件夹其实是dex和smali转换真正起到作用的代码,不管是baksmali还是smali都需要用到这个库中的接口和方法,才能解析和生成相应的smali文件,这个在后面会重点分析的

examples     一些有代表意义的smali文件

externaljar  这个是我为第三方jar建立的一个目录

gradle       管理工具,没有特别关注,跟源码分析无关

scripts      执行smali和baksmali的脚步文件,跟源码无关

smali        该文件夹下面主要实现的是将 smali文件转换成dex文件的代码,需要重点分析

smali-integration-tests   整合测试的一个文件夹,里面也是一些smali文件,跟源码无关

util         一些能够提取出来的辅助类的实现

baksmali 的源码分析

在baksmali进行源码分析之前,需要读者掌握一条主线,因为本身笔者只是由于项目需要用到这套源码,在工作之余的时间里面来进行学习也没有时间和精力熟读源码的每个文件每个方法,但是依据这条主线,至少能够猜出并且猜对baksmali里面的源码的文件大概的作用是什么,这样在修改问题和移植的时候才能做到游刃有余。


这条主线是,baksmali其实只是利用了dexlib2提供的接口,将dex文件读入到一块内存中,这块内存或者说数据结构开辟的大小是跟输入的dex文件相关的,而这块内存所映射的数据结构是一个列表形式的数组,以类为单位,将dex文件全部解析出来,可以简单理解为比如这个dex文件中有100个类,这个数组就有100个对象,每个对象数据结构相同,但大小,内容各异。


baksmali通过 dexlib2提供的接口获取到这个对象中以后,自己定义了一种文件组织规则,当然这种组织规则也绝非无中生有,而是在指令方面遵循 google的dalvik 指令规范,在文件组织方面遵循了 java jar包对于各个class的组织规范,自己在寄存器和函数组织方面有些自己的规则.


而这套规则也将来要跟smali这个生成dex文件的工具一一对应。通过这样的一种规则,baksmali 将获取的对象生成了一个一个的smali文件,让冰冷的二进制的dex文件变得栩栩如生,方便大家的阅读。


以这条主线,我们来看一下baksmali源码的文件组织结构

baksmali源码有50多个文件,分别是由图4 图5 图6 三副图表示


wKioL1PslLOBZiDVAAJBGibopXM042.jpg

                                                               图4


其中图4 下面直接的四个文件

baksmali.java
通过main函数得到dex文件的内存布局,将其生成smali文件

baksmaliOptions.java
生成smali文件的一些选项配置文件


dump.java
类似与dexdump这个工具的作用,是在控制台输出dex文件中的各个段的信息的函数,
本身和baksmali源码无关


main.java  
主程序入口文件,主要检测参数,通过调用
//Read in and parse the dex file
DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexFileFile, options.apiLevel);
将dex映射到内存中去,然后再调用 
baksmali.disassembleDexFile(dexFile, options);
这个函数来生成smali文件



                     wKioL1PslO6wrw_6AAM0r4EWmfk102.jpg

                                                         图5


图5 中文件众多

为了能够比较好的理解这些文件的作用,这里需要补充一点smali文件生成的规则,smali文件的生成其实
只有三种定义,对于一个smali文件而言只有类,成员变量,和方法,对应与文件来说就是 

ClassDefinition.java
FieldDefinition.java
MethodDefinition.java

不管再复杂的类,baksmali源码也认为一个类中只有这三种基本元素,当然生成一个类的时候是需要从

ClassDefinition.java 文件中的writeTo写起的,而在这过程中肯定有成员变量,而写成员变量的时候
就需要从 FieldDefinition.java文件中的 writeTo 写起

当写成员函数的时候就需要从 MethodDefinition.java 文件中的 writeTo 写起了

在理解了上面概念后,我用思维导图画了一下这个文件夹下的文件的组织形式,以供读者整体理解
如图7


wKiom1PslFvBrJAFAAJrxp8o2QI512.jpg

                                                                 图7



看完图5的文件夹了,再看图6中的文件,就很好理解了

wKioL1PslcCCsXViAAJZct85hWI375.jpg

                                                           图6



这个文件夹下的文件主要是实现生成smali文件的时候的一些基本数据类型的写入,比如

boolean类型的true和false

char 类型的写入等等


ok 理解完了整个源码的目录组织,以及大部分文件的作用,现在我们可以进入baksmali源码,开始进行分析了


baksmali 首先执行的第一个main 函数

1
2
3
4
5
6
7
8
9
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
     public  static  void  main(String[] args) throws IOException {
         Locale locale =  new  Locale( "en" "US" );
         Locale.setDefault(locale);
 
         CommandLineParser parser =  new  PosixParser();
         CommandLine commandLine;
 
         try  {
             commandLine = parser.parse(options, args);
         catch  (ParseException ex) {
             usage();
             return ;
         }
 
         baksmaliOptions options =  new  baksmaliOptions();
 
         boolean disassemble =  true ;               // 需要反编译
  
                ...
                
    //中间有一部分获取命令行参数的代码,暂时省略
 
         String[] remainingArgs = commandLine.getArgs();
         Option[] clOptions = commandLine.getOptions();
 
                ...
 
 
     //解析完成命令行参数
                 
     //首先判断机器cpu的个数,确定多个cpu能够同时工作,以提高解析效率
         if  (options.jobs <= 0) {
             options.jobs = Runtime.getRuntime().availableProcessors();
             if  (options.jobs > 6) {
                 options.jobs = 6;
             }
         }
 
 
     //判断api的版本号,当大于17的时候,设置检测包的私有访问属性
         if  (options.apiLevel >= 17) {
             options.checkPackagePrivateAccess =  true ;
         }
 
         String inputDexFileName = remainingArgs[0];
 
     //打开目标文件
         File dexFileFile =  new  File(inputDexFileName);
         if  (!dexFileFile.exists()) {
             System.err.println( "Can't find the file "  + inputDexFileName);
             System. exit (1);
         }
                 
                 
         //Read in and parse the dex file
         DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexFileFile, options.apiLevel);     // 重点1 
         
                 
       //主要判断odex文件的一些代码,省略
             ...
   
 
     //反汇编dex文件,生成一个又一个的smali文件
         boolean errorOccurred =  false ;
         if  (disassemble) {
             errorOccurred = !baksmali.disassembleDexFile(dexFile, options);            //  重点2
         }
 
         if  (doDump) {
             if  (dumpFileName == null) {
                 dumpFileName = commandLine.getOptionValue(inputDexFileName +  ".dump" );
             }
             dump.dump(dexFile, dumpFileName, options.apiLevel);
         }
 
         if  (errorOccurred) {
             System. exit (1);
         }
     }



关于main函数的分析主要有两点,需要重点研究一下,一个是

1
2
  //Read in and parse the dex file
DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexFileFile, options.apiLevel);     // 重点1

另外一个就是

1
errorOccurred = !baksmali.disassembleDexFile(dexFile, options);            //  重点2

我们首先看 DexFileFactory.loadDexFile(dexFileFile, options.apiLevel); 这个函数做了什么事情



    public static DexBackedDexFile loadDexFile(String path, int api) throws IOException {

        return loadDexFile(new File(path), new Opcodes(api));

    }

    

    其中 new Opcodes(api) 根据传入的api版本号 生成了 Opcodes 这个对象

    

    这个对象主要是将  dalvik 虚拟机所有的指令code 映射到 一张 hashmap中,索引是本身的指令名称

    

    比如 move-result-wide, if-ne ,invoke-static/range 这些指令,而结果是相应的枚举类,其实本身 Opcode 这个类将dalvik 虚拟机支持的指令进行了很好的代码诠释,在理解了整个代码框架以后,可以重点关注一下

      

    真正调用的 loadDexFile 函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
     public  static  DexBackedDexFile loadDexFile(File dexFile, @Nonnull Opcodes opcodes) throws IOException {
 
     ...
     //首先判断文件是否为一个压缩文件,如果是的话解压缩后提取dex文件进行解析
                 
         InputStream inputStream =  new  BufferedInputStream( new  FileInputStream(dexFile));
 
         try  {
             return  DexBackedDexFile.fromInputStream(opcodes, inputStream);              // 重点 1
         catch  (DexBackedDexFile.NotADexFile ex) {
             // just eat it
         }
 
      // Note: DexBackedDexFile.fromInputStream will reset inputStream back to the same position, if it fails
 
         try  {
             return  DexBackedOdexFile.fromInputStream(opcodes, inputStream);
         catch  (DexBackedOdexFile.NotAnOdexFile ex) {
             // just eat it
         }
 
         throw  new  ExceptionWithContext( "%s is not an apk, dex file or odex file." , dexFile.getPath());
     }



我们依然跟着重点1 进入到 DexBackedDexFile.fromInputStream(opcodes, inputStream);  这个函数

1
2
3
4
5
6
7
8
9
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
     public  static  DexBackedDexFile fromInputStream(@Nonnull Opcodes opcodes, @Nonnull InputStream is)
             throws IOException {
         if  (!is.markSupported()) {
             throw  new  IllegalArgumentException( "InputStream must support mark" );
         }
         is.mark(44);
         byte[] partialHeader =  new  byte[44];
         try  {
             ByteStreams.readFully(is, partialHeader);
         catch  (EOFException ex) {
             throw  new  NotADexFile( "File is too short" );
         } finally {
             is.reset();
         }
 
     //验证一下魔幻数和dex文件头部
         verifyMagicAndByteOrder(partialHeader, 0);
 
         byte[] buf = ByteStreams.toByteArray(is);
         return  new  DexBackedDexFile(opcodes, buf, 0,  false );    //继续跟踪下去
     }
     
     
     
     private  DexBackedDexFile(Opcodes opcodes, @Nonnull byte[] buf,  int  offset, boolean verifyMagic) {
         super(buf);
 
         this .opcodes = opcodes;
 
         if  (verifyMagic) {
             verifyMagicAndByteOrder(buf, offset);
         }
 
         stringCount = readSmallUint(HeaderItem.STRING_COUNT_OFFSET);
         stringStartOffset = readSmallUint(HeaderItem.STRING_START_OFFSET);
         typeCount = readSmallUint(HeaderItem.TYPE_COUNT_OFFSET);
         typeStartOffset = readSmallUint(HeaderItem.TYPE_START_OFFSET);
         protoCount = readSmallUint(HeaderItem.PROTO_COUNT_OFFSET);
         protoStartOffset = readSmallUint(HeaderItem.PROTO_START_OFFSET);
         fieldCount = readSmallUint(HeaderItem.FIELD_COUNT_OFFSET);
         fieldStartOffset = readSmallUint(HeaderItem.FIELD_START_OFFSET);
         methodCount = readSmallUint(HeaderItem.METHOD_COUNT_OFFSET);
         methodStartOffset = readSmallUint(HeaderItem.METHOD_START_OFFSET);
         classCount = readSmallUint(HeaderItem.CLASS_COUNT_OFFSET);
         classStartOffset = readSmallUint(HeaderItem.CLASS_START_OFFSET);
     }


    其实这个函数很简单,就是通过传入的文件流通过dex文件头找到了 dex 文件中的各个索引表的起始地址,索引数量等信息,然后返回一个实例对象给上层,以方便后面的调用

    

    注:这里需要对dex文件的格式有一定的了解,读者可以查阅相关的文档。



    分析完了

     DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexFileFile, options.apiLevel);

     

    这条语句,我们得到了 DexBackedDexFile 类的一个实例对象,这个对象里面包含什么东西,总结一下有以下内容

    

    <*>   private final Opcodes opcodes;  要解析dex文件的dalvik虚拟机的指令集合

    <*>   这个dex文件中各种索引的开始地址,索引个数等信息

          比如

                        private final int protoCount;

     private final int protoStartOffset;    

     这两个成员变量主要就是为后面的方法列表提供弹药,保存的是在这个dex文件中实现或者调用的方法信息的字段

     比如 你的dex里面有个这样的方法,

     int testcall(String test)

     那么在 这个表中一定有一个 IL  类型的函数原型,其中 I表示返回类型为 int,L 表示这个函数有一个参数,并且参数是一个对象类型

     具体是什么对象呢,在这个表中其实是根据偏移来保存对象的类型的,本身proto这个表中并不提供方法信息的,而是为方法提供函数调用原型,略为有点绕,不过习惯了就好。

    

    

    <*>   dex文件的文件流,以便再进行深入的查询


    ok,我们再回到main函数,看后面的一个关键调用

    

    errorOccurred = !baksmali.disassembleDexFile(dexFile, options);

    这个调用总体说来,就是完成了 将dex文件转换成一个一个smali文件的艰巨任务!

1
2
3
4
5
6
7
8
9
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
     public  static  boolean disassembleDexFile(DexFile dexFile, final baksmaliOptions options) {
 
     ...
                 
     //根据传入的文件夹路径创建文件夹
         File outputDirectoryFile =  new  File(options.outputDirectory);
         if  (!outputDirectoryFile.exists()) {
             if  (!outputDirectoryFile.mkdirs()) {
                 System.err.println( "Can't create the output directory "  + options.outputDirectory);
                 return  false ;
             }
         }
 
 
     //排序并生成dex文件中的所有 类定义实例到类定义的列表中
         //sort the classes, so that if we're on a case-insensitive file system and need to handle classes with file
         //name collisions, then we'll use the same name for each class, if the dex file goes through multiple
         //baksmali/smali cycles for some reason. If a class with a colliding name is added or removed, the filenames
         //may still change of course
         List<? extends ClassDef> classDefs = Ordering.natural().sortedCopy(dexFile.getClasses());    //  重点1 
 
         if  (!options.noAccessorComments) {
             options.syntheticAccessorResolver =  new  SyntheticAccessorResolver(classDefs);
         }
                 
     //生成文件的扩展名,为.smali
         final ClassFileNameHandler fileNameHandler =  new  ClassFileNameHandler(outputDirectoryFile,  ".smali" );
 
     //根据 options.jobs 的值来生成处理 smali文件的线程数量
         ExecutorService executor = Executors.newFixedThreadPool(options.jobs);
         List<Future<Boolean>> tasks = Lists.newArrayList();
 
         for  (final ClassDef classDef: classDefs) {
             tasks.add(executor.submit( new  Callable<Boolean>() {
                 @Override  public  Boolean call() throws Exception {
                     return  disassembleClass(classDef, fileNameHandler, options);     //回调的解析函数,重点2
                 }
             }));
         }
 
                 ...
     }


    可以看出来,这个函数主要做了这么几件事情

    

    <*>创建了要生成smali文件的文件夹目录

    <*>生成了解析dex文件所有的类实例

    <*>开启多线程运行的机制,以类为单位来生成一个又一个的 smali文件,当然文件的扩展名名.smali



    故事1 

    List<? extends ClassDef> classDefs = Ordering.natural().sortedCopy(dexFile.getClasses());   //  重点1 

    

    这个函数主要分为两部分

    

    dexFile.getClasses() 这个函数其实是调用的是 DexBackedDexFile  这个类的 getClasses 方法

    

    函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
     public  Set<? extends DexBackedClassDef> getClasses() {
         return  new  FixedSizeSet<DexBackedClassDef>() {
             @Nonnull
             @Override
             public  DexBackedClassDef readItem( int  index) {
                 return  new  DexBackedClassDef(DexBackedDexFile. this , getClassDefItemOffset(index));
             }
 
             @Override
             public  int  size() {
                 return  classCount;
             }
         };
     }


    其实就是返回一个 new FixedSizeSet<DexBackedClassDef>() 这个匿名类,然后

    Ordering.natural().sortedCopy(new FixedSizeSet<DexBackedClassDef>()),这个方法会在内部调用到

    

    new FixedSizeSet<DexBackedClassDef>() 这个类中的继承的两个方法 readItem 和 size,其中

    readItem 这个方法,根据传进来的index值来实例化 DexBackedClassDef 类,加入到    

    List<? extends ClassDef> classDefs 这个列表中去

    

    我们再来看 这条语句

    return new DexBackedClassDef(DexBackedDexFile.this, getClassDefItemOffset(index)); 

1
2
3
4
5
6
     public  int  getClassDefItemOffset( int  classIndex) {
         if  (classIndex < 0 || classIndex >= classCount) {
             throw  new  InvalidItemIndex(classIndex,  "Class index out of bounds: %d" , classIndex);
         }
         return  classStartOffset + classIndex*ClassDefItem.ITEM_SIZE;
     }



    很简单,就是从dex文件中找到 指定class的索引地址,dex文件中表示class的其实是个比较复杂的结构,需要好好理解一下。

官方文档对于dex中的class数据结构表示如下:



wKioL1P9xPGDNlQJAAksqfGnV24927.jpg



基本上就是这样了,再看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
     public  DexBackedClassDef( @Nonnull  DexBackedDexFile dexFile,
                              int  classDefOffset) {
         this .dexFile = dexFile;
         
         //classDefOffset 是这个类结构体在dex文件中的偏移地址。
         this .classDefOffset = classDefOffset;
 
                 //获取类的数据部分的偏移
         int  classDataOffset = dexFile.readSmallUint(classDefOffset + ClassDefItem.CLASS_DATA_OFFSET);
         if  (classDataOffset ==  0 ) {
             staticFieldsOffset = - 1 ;
             staticFieldCount =  0 ;
             instanceFieldCount =  0 ;
             directMethodCount =  0 ;
             virtualMethodCount =  0 ;
         else  {
         
             //如果不等于0,则要读取各种变量,方法的个数 保存到这个类的私有成员变量中,等到实际解析的时候
             //再来使用
             DexReader reader = dexFile.readerAt(classDataOffset);
             staticFieldCount = reader.readSmallUleb128();
             instanceFieldCount = reader.readSmallUleb128();
             directMethodCount = reader.readSmallUleb128();
             virtualMethodCount = reader.readSmallUleb128();
             staticFieldsOffset = reader.getOffset();
         }
 
     }


这里再列出来 dex文件关于 class类数据的格式说明,以方便读者对代码的理解


wKiom1P9xIGix6xKAAMrnde8ex8506.jpg


   ok 我们再回到

    List<? extends ClassDef> classDefs = Ordering.natural().sortedCopy(dexFile.getClasses());

    

   这条语句,通过对里面机制的了解,已经知道,其实这条语句完成以后, 

   List<? extends ClassDef> classDefs  这个变量已经保存了 dex文件中关于类的各种信息。



   故事2

1
2
3
4
5
6
7
8
9
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
57
58
59
60
61
62
63
64
65
66
67
68
       return  disassembleClass(classDef, fileNameHandler, options);
       
     private  static  boolean  disassembleClass(ClassDef classDef, ClassFileNameHandler fileNameHandler,
                                             baksmaliOptions options) {
  
                //获取类名
         String classDescriptor = classDef.getType();
 
         //validate that the descriptor is formatted like we expect
         if  (classDescriptor.charAt( 0 ) !=  'L'  ||
                 classDescriptor.charAt(classDescriptor.length()- 1 ) !=  ';' ) {
             System.err.println( "Unrecognized class descriptor - "  + classDescriptor +  " - skipping class" );
             return  false ;
         }
 
                 //生成相应要输入smali文件的位置信息
         File smaliFile = fileNameHandler.getUniqueFilenameForClass(classDescriptor);
 
         //create and initialize the top level string template
         ClassDefinition classDefinition =  new  ClassDefinition(options, classDef);   // 重点1 
 
         //write the disassembly
         Writer writer =  null ;
         try
         {
             File smaliParent = smaliFile.getParentFile();
             if  (!smaliParent.exists()) {
                 if  (!smaliParent.mkdirs()) {
                     // check again, it's likely it was created in a different thread
                     if  (!smaliParent.exists()) {
                         System.err.println( "Unable to create directory "  + smaliParent.toString() +  " - skipping class" );
                         return  false ;
                     }
                 }
             }
 
             if  (!smaliFile.exists()){
                 if  (!smaliFile.createNewFile()) {
                     System.err.println( "Unable to create file "  + smaliFile.toString() +  " - skipping class" );
                     return  false ;
                 }
             }
 
             BufferedWriter bufWriter =  new  BufferedWriter( new  OutputStreamWriter(
                     new  FileOutputStream(smaliFile),  "UTF8" ));
 
             writer =  new  IndentingWriter(bufWriter);
             classDefinition.writeTo((IndentingWriter)writer);     //重点2 
         catch  (Exception ex) {
             System.err.println( "\n\nError occurred while disassembling class "  + classDescriptor.replace( '/' '.' ) +  " - skipping class" );
             ex.printStackTrace();
             // noinspection ResultOfMethodCallIgnored
             smaliFile.delete();
             return  false ;
         }
         finally
         {
             if  (writer !=  null ) {
                 try  {
                     writer.close();
                 catch  (Throwable ex) {
                     System.err.println( "\n\nError occurred while closing file "  + smaliFile.toString());
                     ex.printStackTrace();
                 }
             }
         }
         return  true ;
     }    



    这个函数有两个重点

    

    ClassDefinition classDefinition = new ClassDefinition(options, classDef);  // 重点1 

    

    classDefinition.writeTo((IndentingWriter)writer);    //重点2 


    其实这两个重点调用完成以后,整个smali文件就已经生成了,所以我们就顺着前面的脚步跟进去,看看这两个重点到底做了什么事情

    

    1 构造函数将 classdef 传入到 ClassDefinition 这个类中

 

1
2
3
4
5
    public  ClassDefinition( @Nonnull  baksmaliOptions options,  @Nonnull  ClassDef classDef) {
         this .options = options;
         this .classDef = classDef;
         fieldsSetInStaticConstructor = findFieldsSetInStaticConstructor();
     }

    

    2 writeTo 将生成smali文件的各个元素给写入到 IndentingWriter writer 代表的smali文件中。

    

 

1
2
3
4
5
6
7
8
9
10
11
   public  void  writeTo(IndentingWriter writer)  throws  IOException {
         writeClass(writer);
         writeSuper(writer);
         writeSourceFile(writer);
         writeInterfaces(writer);
         writeAnnotations(writer);
         Set<String> staticFields = writeStaticFields(writer);
         writeInstanceFields(writer, staticFields);
         Set<String> directMethods = writeDirectMethods(writer);
         writeVirtualMethods(writer, directMethods);
     }

    

    到这里baksmali 源码的分析,大体框架已经完成。



    当然还有很多细节了,其实主要涉及在 public void writeTo(IndentingWriter writer) 这个函数里面

    

    我们举一个比较复杂的例子 Set<String> directMethods = writeDirectMethods(writer); 来代码跟踪一边,看看里面的做了什么,

    基本上就搞清楚 里面做的事情了

    

 

1
2
3
4
5
6
7
8
9
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
   private  Set<String> writeDirectMethods(IndentingWriter writer)  throws  IOException {
         boolean  wroteHeader =  false ;
         Set<String> writtenMethods =  new  HashSet<String>();
         Iterable<?  extends  Method> directMethods;
         if  (classDef  instanceof  DexBackedClassDef) {
             directMethods = ((DexBackedClassDef)classDef).getDirectMethods( false );            //重点1 
         else  {
             directMethods = classDef.getDirectMethods();
         }
         for  (Method method: directMethods) {
             if  (!wroteHeader) {
                 writer.write( "\n\n" );
                 writer.write( "# direct methods" );
                 wroteHeader =  true ;
             }
             writer.write( '\n' );
   ...
             MethodImplementation methodImpl = method.getImplementation();
             if  (methodImpl ==  null ) {
                 MethodDefinition.writeEmptyMethodTo(methodWriter, method, options);
             else  {
                 MethodDefinition methodDefinition =  new  MethodDefinition( this , method, methodImpl);   //重点2 
                 methodDefinition.writeTo(methodWriter);            //重点3 
             }
         }
         return  writtenMethods;
     }    
     
     
     这个函数有三个重点
     
     directMethods = ((DexBackedClassDef)classDef).getDirectMethods( false );            //重点1 
     
     
     public  Iterable<?  extends  DexBackedMethod> getDirectMethods( final  boolean  skipDuplicates) {
         if  (directMethodCount >  0 ) {
         
         //首先得到这个类中的 direct 方法的在dex文件中的偏移地址
             DexReader reader = dexFile.readerAt(getDirectMethodsOffset());
             final  AnnotationsDirectory annotationsDirectory = getAnnotationsDirectory();
             final  int  methodsStartOffset = reader.getOffset();
//返回 new Iterable<DexBackedMethod>()给上层的调用函数,并且继承实现了
//iterator() 这个函数
             return  new  Iterable<DexBackedMethod>() {
                 @Nonnull
                 @Override
                 public  Iterator<DexBackedMethod> iterator() {
                     final  AnnotationsDirectory.AnnotationIterator methodAnnotationIterator =
                             annotationsDirectory.getMethodAnnotationIterator();
                     final  AnnotationsDirectory.AnnotationIterator parameterAnnotationIterator =
                             annotationsDirectory.getParameterAnnotationIterator();
//返回了 new VariableSizeLookaheadIterator<DexBackedMethod>(dexFile, methodsStartOffset)
//这个对象,里面继承实现了 readNextItem 这个方法,这个方法通过传入的 方法开始偏移,从
//dex文件中 返回 DexBackedMethod 这个对象给上层
                     return  new  VariableSizeLookaheadIterator<DexBackedMethod>(dexFile, methodsStartOffset) {
                         private  int  count;
                         @Nullable  private  MethodReference previousMethod;
                         private  int  previousIndex;
                         @Nullable
                         @Override
                         protected  DexBackedMethod readNextItem( @Nonnull  DexReader reader) {
                             while  ( true ) {
                                 if  (++count > directMethodCount) {
                                     virtualMethodsOffset = reader.getOffset();
                                     return  null ;
                                 }
// 生成一个 method的对象
                                 DexBackedMethod item =  new  DexBackedMethod(reader, DexBackedClassDef. this ,
                                         previousIndex, methodAnnotationIterator, parameterAnnotationIterator);    //重点1
                                 MethodReference currentMethod = previousMethod;
                                 MethodReference nextMethod = ImmutableMethodReference.of(item);
                                 previousMethod = nextMethod;
                                 previousIndex = item.methodIndex;
                                 if  (skipDuplicates && currentMethod !=  null  && currentMethod.equals(nextMethod)) {
                                     continue ;
                                 }
                                 return  item;
                             }
                         }
                     };
                 }
             };
         else  {
             if  (directMethodsOffset >  0 ) {
                 virtualMethodsOffset = directMethodsOffset;
             }
             return  ImmutableSet.of();
         }
     }

    

    

    

    关于重点1 

    

    

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
         public  DexBackedMethod( @Nonnull  DexReader reader,
                            @Nonnull  DexBackedClassDef classDef,
                            int  previousMethodIndex,
                            @Nonnull  AnnotationsDirectory.AnnotationIterator methodAnnotationIterator,
                            @Nonnull  AnnotationsDirectory.AnnotationIterator paramaterAnnotationIterator) {
         this .dexFile = reader.dexBuf;
         this .classDef = classDef;
         // large values may be used for the index delta, which cause the cumulative index to overflow upon
         // addition, effectively allowing out of order entries.
         int  methodIndexDiff = reader.readLargeUleb128();
         this .methodIndex = methodIndexDiff + previousMethodIndex;
         this .accessFlags = reader.readSmallUleb128();
         this .codeOffset = reader.readSmallUleb128();
         this .methodAnnotationSetOffset = methodAnnotationIterator.seekTo(methodIndex);
         this .parameterAnnotationSetListOffset = paramaterAnnotationIterator.seekTo(methodIndex);
     }

    

    

    根据官方文档,encoded_method Format 这种格式的数据结构

    wKiom1P9xhHiMyFYAAJBV4J2iUw683.jpg


    

    其实这个构造函数就是将 数据结构中要求的索引从dex文件中找到,保存到自己的私有成员变量当中

    

    重点2 

 

1
2
3
4
5
6
7
   MethodImplementation methodImpl = method.getImplementation();
      public  DexBackedMethodImplementation getImplementation() {
         if  (codeOffset >  0 ) {
             return  new  DexBackedMethodImplementation(dexFile,  this , codeOffset);
         }
         return  null ;
     }

    

    

    重点3

1
2
3
4
5
6
7
8
9
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
57
58
59
60
61
62
     MethodDefinition methodDefinition =  new  MethodDefinition( this , method, methodImpl);
     
     public  MethodDefinition( @Nonnull  ClassDefinition classDef,  @Nonnull  Method method,
                             @Nonnull  MethodImplementation methodImpl) {
         this .classDef = classDef;
         this .method = method;
         this .methodImpl = methodImpl;
//这里传入的method其实是 DexBackedMethod
         try  {
             //TODO: what about try/catch blocks inside the dead code? those will need to be commented out too. ugh.
//methodImpl.getInstructions() 其实是调用的是 public Iterable<? extends Instruction> getInstructions()
// 在 DexBackedMethodImplementation 这个类中实现的,主要是根据前面的偏移从dex文件中读取相应的指令数据
//放在指令列表中
             instructions = ImmutableList.copyOf(methodImpl.getInstructions());
             
             
             methodParameters = ImmutableList.copyOf(method.getParameters());
             packedSwitchMap =  new  SparseIntArray( 0 );
             sparseSwitchMap =  new  SparseIntArray( 0 );
             instructionOffsetMap =  new  InstructionOffsetMap(instructions);
             for  ( int  i= 0 ; i<instructions.size(); i++) {
                 Instruction instruction = instructions.get(i);
//处理switch case 指令
                 Opcode opcode = instruction.getOpcode();
                 if  (opcode == Opcode.PACKED_SWITCH) {
                     boolean  valid =  true ;
                     int  codeOffset = instructionOffsetMap.getInstructionCodeOffset(i);
                     int  targetOffset = codeOffset + ((OffsetInstruction)instruction).getCodeOffset();
                     try  {
                         targetOffset = findSwitchPayload(targetOffset, Opcode.PACKED_SWITCH_PAYLOAD);
                     catch  (InvalidSwitchPayload ex) {
                         valid =  false ;
                     }
                     if  (valid) {
                         packedSwitchMap.append(targetOffset, codeOffset);
                     }
                 else  if  (opcode == Opcode.SPARSE_SWITCH) {
                     boolean  valid =  true ;
                     int  codeOffset = instructionOffsetMap.getInstructionCodeOffset(i);
                     int  targetOffset = codeOffset + ((OffsetInstruction)instruction).getCodeOffset();
                     try  {
                         targetOffset = findSwitchPayload(targetOffset, Opcode.SPARSE_SWITCH_PAYLOAD);
                     catch  (InvalidSwitchPayload ex) {
                         valid =  false ;
                         // The offset to the payload instruction was invalid. Nothing to do, except that we won't
                         // add this instruction to the map.
                     }
                     if  (valid) {
                         sparseSwitchMap.append(targetOffset, codeOffset);
                     }
                 }
             }
         } catch  (Exception ex) {
             String methodString;
             try  {
                 methodString = ReferenceUtil.getMethodDescriptor(method);
             catch  (Exception ex2) {
                 throw  ExceptionWithContext.withContext(ex,  "Error while processing method" );
             }
             throw  ExceptionWithContext.withContext(ex,  "Error while processing method %s" , methodString);
         }
     }

    

    

    重点4 

    

    methodDefinition.writeTo(methodWriter);

    这个函数其实也是十分复杂的一个函数,但是总体的思路,其实也是根据前面传递过来的数据,主要是索引值和偏移地址,来

    将method里面的数据写回到 smali文件中去

    

    

    由于篇幅的关系,这里就不在那么细节的分析 method的writeTo了,在看 method的writeTo方法的时候,需要

    仔细理解一下 parameterRegisterCount 这个局部变量的赋值情况。总体来说java代码中非静态方法会自动为该函数加入一个参数

    其实这个参数就相当于 this指针的作用,由于dalvik虚拟机中的寄存器都是32位的,所以对于 J和D也就是 long和Double类型的

    其实每个参数是用两个寄存器表示的。

    

    从下面的代码也能看出来

1
2
3
4
5
6
7
8
         for  (MethodParameter parameter: methodParameters) {
             String type = parameter.getType();
             writer.write(type);
             parameterRegisterCount++;
             if  (TypeUtils.isWideType(type)) {
                 parameterRegisterCount++;
             }
         }

    

    理解参数占用的寄存器数量是如何计算出来以后,就能很好的理解smali代码中关于p寄存器和v寄存器表示的规则了,并且为后续编写dex文件为函数添加寄存器的功能打下基础。

    

    总之,baksmali对于写方法来说,基本上是最复杂的操作了,在理解了写入方法的操作以后,前面的操作的理解基本上应该不成问题。

    

    

    到这里,基本上已经将baksmali的框架分析完成了。下一步 我们需要分析 smali框架了源代码了

     smali框架源码主要是对于baksmali的一个逆向过程,也就是其编译过程。本身包的文件很少,也就是13个java文件

 

 但是里面有几个有antlr3 和 jflex生成的词法分析器和解释器文件

 

 smaliParser.java

 smaliTreeWalker.java

 

 这两个文件时由 antlr3 生成的

 

 smaliFlexLexer.java

 这个文件是由 jflex生成的

 

 对于这两部分的生成,以及这两个工具的使用,笔者未做深入研究。而smali本身最核心的地方就是利用这几个词法分析器 来生成不同的label,instruction,field,method,class等对象,最后组装成dex文件,这几个语义解析的文件主要还是为smali提供弹药,而真正组装成最后我们看到的dex文件,其实是dexlib2做的事情。

 

 下一步我们需要好好看看dexlib2的源码框架。


其实前面描述了那么多,不管是baksmali还是smali工具,多是涉及到一些对于dex文件的外围io操作,而真正解析dex,解析dex各种指令,各种索引的操作是放在dexlib2这个目录下面的,下面就让我们走进dexlib2这个目录,好好分析一下这个库是如何解析android dex文件的吧。


还是老规矩,先介绍源码的目录结构已经关键代码文件的作用,给大家有个整体上的认识再逐步细化。见下图 dexlib2代码目录


wKiom1P9xpig58yvAADclCjCASw631.jpg



这个是dexlib2的目录,明显看出来比baksmali和smali代码量要多很多,这里先将核心目录给大家做一下介绍


analysis 这个暂时不知道具体作用


base     这个文件夹下面全部都是抽象类,主要是对于一些dex文件的一些基础数据结构的一些表示

              这里面重点要注意的是这个目录下面的 reference这个文件夹,里面分别有 field索引,

             method索引,string索引,以及type索引,这些都是跟dex文件本身组织结构息息相关的,

             这里如果不是太清楚的话,建议看一下dex文件的文件组织结构。里面有表示string type

             proto  class的这些段的。


value        目录下面的这些类都是跟类成员变量初始值相关的操作,比如 在某个类中的成员变量

                 String m_s = "hello world"  这个时候就要用到 BaseStringEncodedValue 这个类的操作

                 了


 base    主目录下的这几个文件,

             BaseAnnotation.java  BaseAnnotationElement.java 跟注释相关

             BaseExceptionHandler.java   try catch后的exceptionhandler相关

             BaseMethodParameter.java    函数参数相关

             BaseTryBlock                try catch块相关

         

         

builder  这个是为生成dex文件的一些组件文件,

         build/debug         下面是对于 dex文件中debug信息保存的类,为最后生成dex做准备

         build/instruction    对于dalvik虚拟机支持的所有指令的支持的类,格式很鲜明,基本上

                                        每种类型的dalvik虚拟机指令用一个类来表示                              

         build/                       这下面就放了一些对于debug信息,异常句柄,指令,swith case块,

                                          trycatch 块,函数builder的一些实现

         总之这个文件夹下是支撑将smali文件写回为dex文件的类库

         

         

dexbacked  这个目录其实是将输入的dex进行解析后接受的类库

           dexbacked/instruction   解析后的dex的所有指令存放的类,也是以某类指令建立类来接收的

           dexbacked/raw          

 对于dex文件结构的各个组件接收的类。比如typeid,stringid,classdef,protoid,mapitem,headeritem,这些对比dex文件的结构就能和这些类一一对应起来    

                              

           dexbacked/reference     dex文件中的成员变量索引,方法索引,字符串索引,类型索引的类

           dexbacked/util          一些小的工具的类集合

           dexbacked/value         还是跟初始值相关的类

           dexbacked               一些更加上层抽象的类,表示的信息量更多,比如DexBackedClassDef.java

                                   就表示一个类,但是这个类又是由n个成员变量,n个方法来表示的,对于

                                   成员变量又涉及到了初始值,权限,访问属性,本身定义等,

                                   对于成员方法就更复杂了,除了指令还有try catch信息 debug信息,注释信息等等


           总之, dexbacked这个类库,有java语言完整表达了整个dex文件的文件结构,细化到dex文件的每个细节,就是说这个文件夹下的类已经能够涵盖dex文件所有的东西,里面的每个类,每个方法,每条指令都能从这个文件夹下找到相应的类来表示。        



iface      这个就是一个接口定义类,前面提到的base抽象类都是继承与这个文件夹下面的类,主要是为了                利用java多态的特点,减少代码的编写量,让代码看起来更加专业。

           


immutable      

这个文件夹提供了为类,方法,成员变量,指令,各种索引的不可更改的常量定义,但是这些类本身是可以new出来的,当你要为dex文件添加类,方法,成员变量或者指令的 时候,这个文件夹下面的类就很有作用了,可以用这个文件夹下面的类轻松构造出来各种索引,达到更改dex文件的目的,一般要配合builder下的各种方法来使用

                     

           

rewriter       这个文件夹下面提供了对于写回dex文件各种函数的hook,包括写回类的hook,

                    写回方法的hook,甚至到写回每条指令的hook,这个文件夹其实起到的作用也是为了能够

                    通过hook方便的修改dex文件。

           


writer     写回dex文件启动的文件夹,前面的builder这些都是为其提供弹药的,这里整体的dex文件的生成

            ,所有组织都是通过这个文件夹下面的不同的类协同完成的。

           

           

.             直接根目录下的是更基础的访问权限,指令格式,操作格式,索引,值类型的一些基础类了



  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值