jarjar原理分析

前言

之前使用jarjar.jar(官网)来替换第三方jar包的包名,这里来研究一下实现原理。主要内容:jarjar的使用方式、jarjar与替换包名有关的源码、ASM的介绍、Class文件格式、修改class文件。

jarjar的使用

这里以eventbus的jar包做例子,用jd-gui查看jar结构:

再点击图示的类,利用自带的反编译可以看到这里import了org.greenrobot.eventbus.meta包下的一些类,注意这个import。

然后编写jarjar所需的rule规则,创建rule.txt,内容如下:

rule org.greenrobot.** com.mezzsy.@1

最后运行jar包:

java -jar jarjar-1.4.jar process rule.txt original-eventbus-3.0.0.jar changed-eventbus-3.0.0.jar

结果:

结果显示,包名org.greenrobot换成了com.mezzsy,并且不仅仅只是改包的名字,类里面引用到的类也跟着改了。

以上是基本使用,下面介绍更详细的使用方式。

rule替换规则

官方文档原文:

Rules file format
The rules file is a text file, one rule per line. Leading and trailing whitespace is ignored. There are three types of rules:

rule <pattern> <result> zap <pattern> keep <pattern>

The standard rule (rule) is used to rename classes. All references to the renamed classes will also be updated. If a class name is matched by more than one rule, only the first one will apply.

<pattern> is a class name with optional wildcards. ** will match against any valid class name substring. To match a single package component (by excluding . from the match), a single * may be used instead.

<result> is a class name which can optionally reference the substrings matched by the wildcards. A numbered reference is available for every * or ** in the <pattern>, starting from left to right: @1, @2, etc. A special @0 reference contains the entire matched class name.

The zap rule causes any matched class to be removed from the resulting jar file. All zap rules are processed before renaming rules.

The keep rule marks all matched classes as "roots". If any keep rules are defined all classes which are not reachable from the roots via dependency analysis are discarded when writing the output jar. This is the last step in the process, after renaming and zapping.

大致意思如下:

rule文件需要用txt格式写, 一行一条规则。有3种形式的指令:

rule <pattern> <result> 
zap <pattern> 
keep <pattern>
  1. rule
    用来替换类名,所有用到被替换类的类都会跟着被改变。

  2. zap
    用来移除指定的类,在rule之前执行。

  3. keep
    只会保留指定的Package的名称,在rule之后执行。

  4. pattern
    pattern是需要匹配的名称。**是替换所有匹配的,如上面的例子。*是只替换包下的类,包下的包的内容不替换,例子见下。

  5. result
    result是取代后的名称,可以使用@1@2这类的符号表示要使用第几个pattern的***所代表的字串。

使用例子

rule.txt:

rule org.greenrobot.eventbus.* com.mezzsy.@1

效果:

其它用法不再举例,因为官方文档看不明白,而且用的也比较少,这里略过,有兴趣的可以自行研究。常用用法就是上文的rule org.greenrobot.** com.mezzsy.@1表示org.greenrobot.换成com.mezzsy.

jarjar源码分析

类Main是jarjar的入口文件,会调用其Main方法,替换包名用的是process命令,会调用Main的process方法。

public void process(File rulesFile, File inJar, File outJar) throws IOException {
  if (rulesFile == null || inJar == null || outJar == null) {
    throw new IllegalArgumentException("rulesFile, inJar, and outJar are required");
  }
  List<PatternElement> rules = RulesFileParser.parse(rulesFile);
  boolean verbose = Boolean.getBoolean("verbose");
  boolean skipManifest = Boolean.getBoolean("skipManifest");
  MainProcessor proc = new MainProcessor(rules, verbose, skipManifest);
  StandaloneJarProcessor.run(inJar, outJar, proc);
  proc.strip(outJar);
}

传入的三个参数就是在命令行中输入的三个文件。

根据传进来的rule文件来解析其中的规则,分别封装成和指令对应的Rule、Zap、Keep类,它们继承自PatternElement,然后放入列表rules中。

verbose和skipManifest是获取系统属性,均为false。

然后创建MainProcessor对象,下面是MainProcessor的构造方法:

public MainProcessor(List<PatternElement> patterns, boolean verbose, boolean skipManifest) {
    this.verbose = verbose;
    List<Zap> zapList = new ArrayList<Zap>();
    List<Rule> ruleList = new ArrayList<Rule>();
    List<Keep> keepList = new ArrayList<Keep>();
    //提取各种rule规则
    for (PatternElement pattern : patterns) {
        if (pattern instanceof Zap) {
            zapList.add((Zap) pattern);
        } else if (pattern instanceof Rule) {
            ruleList.add((Rule) pattern);
        } else if (pattern instanceof Keep) {
            keepList.add((Keep) pattern);
        }
    }
	
    PackageRemapper pr = new PackageRemapper(ruleList, verbose);
    kp = keepList.isEmpty() ? null : new KeepProcessor(keepList);

    List<JarProcessor> processors = new ArrayList<JarProcessor>();
    if (skipManifest)
        processors.add(ManifestProcessor.getInstance());
    if (kp != null)
        processors.add(kp);
    processors.add(new ZapProcessor(zapList));
    processors.add(new JarTransformerChain(new RemappingClassTransformer[]{ new RemappingClassTransformer(pr) }));
    processors.add(new ResourceProcessor(pr));
    chain = new JarProcessorChain(processors.toArray(new JarProcessor[processors.size()]));
}

先是提取各个指令放入列表中,依次放入processors。
然后创建PackageRemapper对象,PackageRemapper继承自ASM库的Remapper,负责重新映射类型和名称。在构造方法里,会将Rule语句转换成Java能识别的表达式。
接着把这些Rule规则装进processors中。这里注意一个类,RemappingClassTransformer,它继承自ASM库的RemappingClassAdapter,是一个重新映射类型的ClassVisitor,ClassVisitor用于访问Java class文件。
然后这些指令封装成chain,注意这个chain,后面会用到。

回到Main类的process方法。创建完MainProcessor对象后,再执行StandaloneJarProcessor的run方法:

public static void run(File from, File to, JarProcessor proc) throws IOException {
    byte[] buf = new byte[0x2000];

    JarFile in = new JarFile(from);
    final File tmpTo = File.createTempFile("jarjar", ".jar");
    JarOutputStream out = new JarOutputStream(new FileOutputStream(tmpTo));
    Set<String> entries = new HashSet<String>();
    try {
        EntryStruct struct = new EntryStruct();
        Enumeration<JarEntry> e = in.entries();
        while (e.hasMoreElements()) {
            JarEntry entry = e.nextElement();
            struct.name = entry.getName();
            struct.time = entry.getTime();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            IoUtil.pipe(in.getInputStream(entry), baos, buf);
            struct.data = baos.toByteArray();
            if (proc.process(struct)) {
                if (entries.add(struct.name)) {
                    entry = new JarEntry(struct.name);
                    entry.setTime(struct.time);
                    entry.setCompressedSize(-1);
                    out.putNextEntry(entry);
                    out.write(struct.data);
                } else if (struct.name.endsWith("/")) {
                    // TODO(chrisn): log
                } else {
                    throw new IllegalArgumentException("Duplicate jar entries: " + struct.name);
                }
            }
        }

    }
    finally {
        in.close();
        out.close();
    }

     // delete the empty directories
    IoUtil.copyZipWithoutEmptyDirectories(tmpTo, to);
    tmpTo.delete();

}

先创建原始jar包的副本,然后遍历副本jar所有的entry,entry是指jar包中的文件,然后把每个entry封装成EntryStruct并对其调用proc的process方法。EntryStruct包含entry的文件流,文件名还有时间。

proc就是在Main中传入的MainProcessor,其process方法如下:

public boolean process(EntryStruct struct) throws IOException {
        String name = struct.name;
        boolean keepIt = chain.process(struct);//注释1
        if (keepIt) {
            if (!name.equals(struct.name)) {
                if (kp != null)
                    renames.put(name, struct.name);
                if (verbose)
                    System.err.println("Renamed " + name + " -> " + struct.name);
            }
        } else {
            if (verbose)
                System.err.println("Removed " + name);
        }
        return keepIt;
}

注释1处调用了chain的process方法:

public boolean process(EntryStruct struct) throws IOException{

    for (JarProcessor aChain : chain)
    {
        if (!aChain.process(struct))
        {
            return false;
        }
    }
    return true;
}

代码中的chain是一个JarProcessor数组,JarProcessor上文讲过,Zap、Rule、Keep是JarProcessor的实现类。那么以上的代码就可以总结为,遍历jar中每个entry,对其应用所有定义的rule规则。

这里只分析与rule有关的,zap和keep略,rule的是在JarTransformer的process方法中。

//JarTransformer.java
public boolean process(EntryStruct struct) throws IOException {
    if (struct.name.endsWith(".class")) {
        ClassReader reader;
        try {
            reader = new ClassReader(struct.data);
        } catch (Exception e) {
            return true; // TODO?
        }
        GetNameClassWriter w = new GetNameClassWriter(ClassWriter.COMPUTE_MAXS);
        reader.accept(transform(w), ClassReader.EXPAND_FRAMES);
        struct.data = w.toByteArray();
        struct.name = pathFromName(w.getClassName());
    }
    return true;
}

先判断当前文件是否是class文件。

然后通过调用ClassReader的accept完成包名替换,这部分是ASM来完成的,ASM的部分不分析,这里对出现的类作一个简单的介绍:
ClassVisitor上面讲过,用于访问Java class文件,也可以进行操作。
ClassReader是Java类解析器,配合ClassVisitor访问现有类。该类解析符合Java类文件格式的字节数组(也就是构造方法中的byte数组),并为遇到的每个字段,方法和字节码指令调用给定ClassVisitor的visit方法。
GetNameClassWriter是Jarjar中的类,继承自ClassVisitor,和ASM库的ClassWriter相似,用于生成更改后的字节流。

ASM的介绍

以下是官方原文:

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

大致意思就是,ASM是一个Java字节码操作和分析框架,它可以直接以二进制的形式修改现有类或者动态生成类。

ASM的源码分析和使用暂时略过,下面介绍Class文件格式。

Class文件格式

这里参考了周志明《深入理解Java虚拟机》的Class类文件结构的内容,只列出部分,详细的内容可以见书。

测试代码源码:

package com.mezzsy.javademo;

import java.util.ArrayList;
import java.util.List;

public class Main {
    private static int sInt = 1;

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(sInt);
        System.out.println(list);
    }
}

输入javac Main.java将Main.java编译成class文件。

可以使用010Editor来查看class二进制文件,完整内容如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QGRMytPR-1576218845297)(8.jpg)]

也可以用javap命令查看解析后的,输入javap -v -p -l Main.class,完整内容如下:

Classfile /Users/shengyuan/Projects/Java/JavaDemo/src/main/java/com/mezzsy/javademo/Main.class
  Last modified 2019-12-11; size 685 bytes
  MD5 checksum 77ccd9bf319852a53a2674d9237cf1af
  Compiled from "Main.java"
public class com.mezzsy.javademo.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #10.#22        // java/lang/Object."<init>":()V
   #2 = Class              #23            // java/util/ArrayList
   #3 = Methodref          #2.#22         // java/util/ArrayList."<init>":()V
   #4 = Fieldref           #9.#24         // com/mezzsy/javademo/Main.sInt:I
   #5 = Methodref          #25.#26        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #6 = InterfaceMethodref #27.#28        // java/util/List.add:(Ljava/lang/Object;)Z
   #7 = Fieldref           #29.#30        // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Methodref          #31.#32        // java/io/PrintStream.println:(Ljava/lang/Object;)V
   #9 = Class              #33            // com/mezzsy/javademo/Main
  #10 = Class              #34            // java/lang/Object
  #11 = Utf8               sInt
  #12 = Utf8               I
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               <clinit>
  #20 = Utf8               SourceFile
  #21 = Utf8               Main.java
  #22 = NameAndType        #13:#14        // "<init>":()V
  #23 = Utf8               java/util/ArrayList
  #24 = NameAndType        #11:#12        // sInt:I
  #25 = Class              #35            // java/lang/Integer
  #26 = NameAndType        #36:#37        // valueOf:(I)Ljava/lang/Integer;
  #27 = Class              #38            // java/util/List
  #28 = NameAndType        #39:#40        // add:(Ljava/lang/Object;)Z
  #29 = Class              #41            // java/lang/System
  #30 = NameAndType        #42:#43        // out:Ljava/io/PrintStream;
  #31 = Class              #44            // java/io/PrintStream
  #32 = NameAndType        #45:#46        // println:(Ljava/lang/Object;)V
  #33 = Utf8               com/mezzsy/javademo/Main
  #34 = Utf8               java/lang/Object
  #35 = Utf8               java/lang/Integer
  #36 = Utf8               valueOf
  #37 = Utf8               (I)Ljava/lang/Integer;
  #38 = Utf8               java/util/List
  #39 = Utf8               add
  #40 = Utf8               (Ljava/lang/Object;)Z
  #41 = Utf8               java/lang/System
  #42 = Utf8               out
  #43 = Utf8               Ljava/io/PrintStream;
  #44 = Utf8               java/io/PrintStream
  #45 = Utf8               println
  #46 = Utf8               (Ljava/lang/Object;)V
{
  private static int sInt;
    descriptor: I
    flags: ACC_PRIVATE, ACC_STATIC

  public com.mezzsy.javademo.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: getstatic     #4                  // Field sInt:I
        12: invokestatic  #5                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        15: invokeinterface #6,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        20: pop
        21: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: aload_1
        25: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        28: return
      LineNumberTable:
        line 10: 0
        line 11: 8
        line 12: 21
        line 13: 28

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: putstatic     #4                  // Field sInt:I
         4: return
      LineNumberTable:
        line 7: 0
}
SourceFile: "Main.java"

Class文件介绍

Java的Class文件是一组以8位字节为基础单位的二进制流,字节顺序采用大端序。

大端序是最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据。

如:最高位字节是0x0A 存储在最低的内存地址处。下一个字节0x0B存在后面的地址处。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w8w8xrQt-1576218845305)(5.jpg)]

和右撇子书写差不多。

Class文件采用的数据结构只有两种类型:无符号数和表。

无符号数属于基本的数据类型,以u1、u2、u4、 u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,一般以_info结尾。

整个Class文件本质上就是一张表,由如下的数据项构成:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RYimDp3K-1576218845306)(6.jpg)]

下面按照这个表的顺序介绍Class文件格式。

魔数和版本

每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虛拟机接受的Class文件。值为cafebabe。(可能这就是Java商标为咖啡的原因)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-woqxKxEu-1576218845307)(7.jpg)]

minor_version是次版本号,major_version是主版本号。

常量池

constant_pool_count是表示常量池的个数,这个值是比实际的常量数多1的,因为常量池计数是从1开始的,0空出来表示不引用任何常量池的内容。

什么是引用常量池

以上文解析的片段为例

Constant pool:
   #1 = Methodref          #10.#22        // java/lang/Object."<init>":()V
   #2 = Class              #23            // java/util/ArrayList
   #3 = Methodref          #2.#22         // java/util/ArrayList."<init>":()V

常量1引用了常量10、常量22,常量2引用了常量23,等等。

010Editor从0开始下标,可以忽略这一点,后文也是一样的

常量池主要存放两大类常量:字面量和符号引用。

字面量接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
符号引用包括了下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池是一个表的数据结构,其中每个常量也是表的数据结构,这些表的第1位是一个u1类型的标记位,代表当前常量是哪个类型。共有如下几种类型:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R0ZMOymW-1576218845308)(9.jpg)]

它们的结构如下:

常量项目类型描述
CONSTANT_Utf8_infotagu1值为1
lengthu2UTF-8编码的字符串占用的字节数
bytesu1长度为length的UTF-8编码的字符串
CONSTANT_Integer_infotagu1值为3
bytesu4按照高位在前存储的int值
CONSTANT_Float_infotagu1值为4
bytesu4按照高位在前存储的float值
CONSTANT_Long_infotagu1值为5
bytesu8按照高位在前存储的long值
CONSTANT_Double_infotagu1值为6
bytesu8按照高位在前存储的double值
CONSTANT_Class_infotagu1值为7
indexu2指向全限定名常量项的索引
CONSTANT_String_infotagu1值为8
indexu2指向字符串字面量的索引
CONSTANT_Fieldref_infotagu1值为9
indexu2指向声明字段的类或者接口描述符
indexu2指向字段描述符CONSTANT_NameAndType的索引项
CONSTANT_Methodref_infotagu1值为10
indexu2指向声明方法的类描述符CONSTANT_Class_info 的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_
InterfaceMethodref_info
tagu1值为11
indexu2指向声明方法的接口描述符CONSTANT_Class_info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_ NameAndType_infotagu1值为12
indexu2指向该字段或方法名称常量项的索引
indexu2指向该字段或方法描述符常量项的索引
CONSTANT_MethodHandle_infotagu1值为15
reference_kindu1值必须在1~9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_indexu2值必须是对常量池的有效索引
CONSTANT_MethodType_infotagu1值为16
descriptor_indexu2值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
CONSTANT_InvokeDynamic_infotagu1值为18
bootstrap_
method_attr_index
u2值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_
type_index
u2值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_
NameAndType_info结构,表示方法名和方法描述符

例子

来看一下测试用例中的第一个常量:

索引1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ab1Fa9Kq-1576218845309)(10.jpg)]

标记位的值为10,也就是Methodref,再看一下javap的解析:

 #1 = Methodref          #10.#22

内容一致。内容为方法的类描述符CONSTANT_Class_info的索引项是10,名称及类型描述符CONSTANT_NameAndType的索引项是22。

索引10

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suIMY0Gl-1576218845310)(11.jpg)]

索引10的tag是7,即CONSTANT_Class_info,指向全限定名常量项的索引的是34。

索引22

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EnKVeRw6-1576218845311)(12.jpg)]

索引22的tag是12,即CONSTANT_NameAndType_info,指向该字段或方法名称常量项的索引是13,指向该字段或方法描述符常量项的索引是14。

索引34

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pPTN8pQf-1576218845312)(14.jpg)]

索引34的tag是1,即CONSTANT_Utf8_info,表示一个字符串,长度为16,值为下面的byte数组,这里提取了内容,打印看看是什么:

public static void main(String[] args) {
    byte[] bytes = new byte[]{
            0x6A, 0x61, 0x76, 0x61,
            0x2F, 0x6C, 0x61, 0x6E,
            0x67, 0x2F, 0x4F, 0X62,
            0x6A, 0x65, 0x63, 0x74};
    String s = new String(bytes, Charset.forName("UTF-8"));
    System.out.println(s);
}

输出:

java/lang/Object

指向的类是Object。

索引13

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sDrJ99U5-1576218845312)(15.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1kiYKQs8-1576218845313)(16.jpg)]

内容是长度为6的字符串,看一下是什么内容:

public static void main(String[] args) {
    byte[] bytes = new byte[]{
            0x3C, 0x69, 0x6E, 0x69, 0x74, 0x3E};
    String s = new String(bytes, Charset.forName("UTF-8"));
    System.out.println(s);
}

//output
<init>
索引14

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FjidWPJ2-1576218845313)(17.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6vzxyFCx-1576218845313)(18.jpg)]

长度为3的字符串:

public static void main(String[] args) {
    byte[] bytes = new byte[]{
            0x28, 0x29, 0x56};
    String s = new String(bytes, Charset.forName("UTF-8"));
    System.out.println(s);
}

//output
()V
小结

把以上的内容合起来,就是索引1是表示一个方法,内容是java/lang/Object."<init>":()V,这个方法表示类的默认构造方法。

上面的分析比较复杂,因为是从字节码角度出发的,用javap解析会更直观一点:

#1 = Methodref          #10.#22        // java/lang/Object."<init>":()V
#10 = Class              #34            // java/lang/Object
#22 = NameAndType        #13:#14        // "<init>":()V
#13 = Utf8               <init>
#14 = Utf8               ()V

这里分析了一个索引,后面的也是一样的道理,这里就不分析了。

访问标志

紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

具体的标志位以及标志的含义如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eQkb5m5j-1576218845314)(19.jpg)]

测试用例是一个pubilc的类,且用高于JDK1.0.2的编译器编译的,所以内容应该为0x0021,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iUglGsiW-1576218845314)(20.jpg)]

结果也的确如此。

类索引、父类索引与接口索引集合

类索弥(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系。

类索引用于确定这个类的全限定名。
父类索引用于确定这个类的父类的全限定名,由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果
这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到在排列在接口索引集合中。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。下面是字段表的结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kG9s34dd-1576218845315)(21.jpg)]

access_flags

access_flags是字段修饰符。标志值如下:

name_index

name_index是字段简单名称的索引值。

descriptor_index

descriptor_index是字段和方法描述符的索引值。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。描述符标识字符含义如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9JA2HTwu-1576218845315)(24.jpg)]

对于数组类型,每一维度将使用一个前置的[字符来描述。
如一个定义为java.lang.String[][]"类型的二维数组,将被记录为[[Ljava/lang/String;, 一个整型数组int[]将被记录为[I

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()之内。
如方法void inc()的描述符为()V,方法java.lang.String toString()的描述符为()Ljava/lang/String;,方法int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target, int targetOffsct, int targetCount, int fromIndex)的描述
符为([CII[CIII)I

测试用例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MkiYYoPu-1576218845316)(22.jpg)]

fields_count值为1,只有一个字段。
access_flags为0x000A,即private static
name_index指向的索引是11,在常量池中表示的是一个字符串,其值为sInt。
descriptor_index指向的索引是12,在常量池中表示的是一个字符串,其值为I,表示基本类型int。
完整意思就是private static int sInt,再看测试用例的源码:

private static int sInt = 1;

一致。

赋值操作是在static块中执行的,这里只是声明字段。

注:字段表集合中不会列出从超类或者父接口中继承而来的字段。

方法表集合

方法表与字段表类似,结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HBKG0Weo-1576218845316)(25.jpg)]

access_flags

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pclSB2dQ-1576218845316)(26.jpg)]

其余与字段表相同。

属性表集合

虚拟机规范预定义的属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bv8w3g9l-1576218845317)(27.jpg)]

从字节码修改包名

根据上面的Class文件格式,从字节码的角度,模拟ASM手动修改包名。

代码还是用上面的测试代码:

package com.mezzsy.javademo;

import java.util.ArrayList;
import java.util.List;

public class Main {
    private static int sInt = 1;

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(sInt);
        System.out.println(list);
    }
}

测试代码并不太好,不方便替换包名,这里就把ArrayList换一个包名,并替换成自己的ArrayList。代码如下:

package com.mezzsy.javademo;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;

public class ArrayList implements List<Integer> {
    @Override
    public String toString() {
        return "ArrayList已经被替换";
    }
		//...省略List的方法,都是默认值,而且没有用到。
}

主要是修改toString方法,System.out.println(list)会调用这个方法。

从上面介绍的Class文件格式可以知道,涉及到类名的是常量池中字符串的值。这里配合javap的解析找到ArrayList的值:

#23 = Utf8               java/util/ArrayList

是索引23,值为java/util/ArrayList,需要替换成com/mezzsy/javademo/ArrayList,长度为29,UTF-8编码为:636f6d2f6d657a7a73792f6a61766164656d6f2f41727261794c697374。

这里class文件用Sublime打开,直接更改二进制数据。

把0100 136a 6176 612f 7574 696c 2f41 7272 6179 4c69 7374换成01001d636f6d2f6d657a7a73792f6a61766164656d6f2f41727261794c697374。

开头01是tag,1字节。001d是长度,2字节,后面就是UTF-8编码。

然后运行,输入命令:

java com/mezzsy/javademo/Main

这里要进入java目录下,或者是com的父目录下,否则会找不到或无法加载主类 Main.class

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VZpvyXYw-1576218845318)(28.jpg)]

可以看到,这里已经替换了包名并使用了自己的类。

总结

jarjar替换包名的思路很简单,就是遍历jar包然后利用ASM修改class文件。ASM以后会进行分析,这里先略过。

另外,对Class文件的操作不仅仅只是替换包名那么简单,它还可以修改方法的字节码(在属性表中),在线上就可以修复bug。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值