为什么 proguard 不混淆方法体?
因为它不能。
编译时根本不存储方法参数和局部变量的名称。
您看到的名称是由您的反编译器生成的。
对于编译后的代码,有两种方法可以在本地(即在方法内)存储数据:
操作数栈实际上只是一个栈。
请参阅 Java VM 规范中的 Table 7.2 以了解堆栈运算符。
您可以弹出值 (pop)、复制顶部值 (dup)、交换顶部两个值 (swap) 以及稍微改变行为 (pop2, dup_x1, dup_x2, dup2, dup2_x1, dup2_x2)。
大多数(如果不是所有)产生返回值的指令都会将该值放入堆栈。
对于这个问题,重要的是如何引用堆栈中的内容,这与任何其他堆栈一样:
相对于顶部位置,并基于使用的指令。
没有指定的数字或名称,它只是当前存在的任何东西。
现在,对于所谓的“局部变量”:
将它们更多地视为ArrayList,而不是 Java 中的变量。
因为这正是您访问它们的方式:按索引。
对于变量 0 到 3,有特殊指令(即单字节),因为它们经常使用,所有其他变量只能通过双字节指令访问,其中第二个字节是索引。
再次参见Table 7.2,“加载”和“存储”。
两个表中的前五个条目是每个数据类型的宽(双字节)存储/加载指令(请注意,对于单个值,boolean、char、byte 和 short 都转换为int,只留下int、float和Object作为单槽值,long和double作为双槽值),接下来的20条指令是直接访问寄存器0的指令到 3,最后 8 条指令用于访问数组索引(请注意,在数组内部,boolean、byte、char 和 short不 转换为 int,以不要浪费空间,这就是为什么还有三个指令(不是四个,因为 byte 和 char 具有相同的大小)。
最大堆栈大小和局部变量的数量都是有限的,并且必须在每个方法的Code 属性的标题中给出,如Section 4.7.3(max_stack 和max_locals)中定义的那样。
不过,关于局部变量的有趣之处在于,它们可以兼作方法参数,这意味着局部变量的数量永远不会低于方法参数的数量。
请注意,在为 Java VM 计算值时,long 和 double 类型的变量被视为两个值,因此需要两个“槽”。
另请注意,对于非静态方法,参数 0 将是 this,这需要另一个“插槽”。
话虽如此,让我们看一些代码!
例子:
class Test
{
public static void main(String[] myArgs) throws NumberFormatException
{
String myString = "42";
int myInt = Integer.parseInt(myString);
double myDouble = (double)myInt * 42.0d;
System.out.println(myDouble);
}
}
这里我们有三个局部变量myString、myInt和myDouble,加上一个参数myArgs。
另外,我们还有两个常量"42"和42.0d,还有很多外部引用:
-
java.lang.String[] - 类
-
java.lang.NumberFormatException - 类
-
java.lang.String - 类
-
java.lang.Integer.parseInt - 方法
-
java.lang.System.out - 字段
-
java.io.PrintStream.println - 方法
还有一些导出:Test 和 main,以及编译器将为我们生成的默认构造函数。
所有常量、引用和导出都将导出到Constant Pool - 局部变量和参数名称不会。
编译和反汇编类(使用javap -c Test)产生:
Compiled from "Test.java"
class Test {
Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.NumberFormatException;
Code:
0: ldc #2 // String 42
2: astore_1
3: aload_1
4: invokestatic #3 // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I
7: istore_2
8: iload_2
9: i2d
10: ldc2_w #4 // double 42.0d
13: dmul
14: dstore_3
15: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
18: dload_3
19: invokevirtual #7 // Method java/io/PrintStream.println:(D)V
22: return
}
除了默认构造函数,我们可以看到我们的main方法,一步一步来。
请注意如何使用astore_1 和aload_1 访问myString,使用istore_2 和iload_2 访问myInt,以及使用dstore_3 和dstore_3 和myDouble 访问myDouble。
@987654385可以在任何地方访问,因此也没有字节码处理它,但是在方法的开头,对 String 数组的引用将位于局部变量 1 中,该变量很快就会被对 "42" 的引用覆盖。
javap 也会显示常量池,如果你将-v 标志传递给它,但它并没有真正为输出添加任何值,因为来自常量池的所有相关信息都显示在 cmets 中。
但是现在,让我们看看反编译器产生了什么!
JD-GUI 0.3.5(JD-Core 0.6.2):
import java.io.PrintStream;
class Test
{
public static void main(String[] paramArrayOfString)
throws NumberFormatException
{
String str = "42";
int i = Integer.parseInt(str);
double d = i * 42.0D;
System.out.println(d);
}
}
南河 0.5.28:
class Test
{
public static void main(final String[] array) throws NumberFormatException {
System.out.println(Integer.parseInt("42") * 42.0);
}
}
注意导出到常量池的所有内容是如何保持不变的,而 JD-GUI 只是为局部变量选择一些名称,而 Procyon 会完全优化它们。
参数的名称 - paramArrayOfString 与 array(与原始的 myArgs 相比)是一个完美的例子,它表明不再有“正确”的名称,反编译器只需要依赖一些取名字的模式。
我不知道反编译代码中的“真实”名称来自何处,但我相当肯定它们不包含在 jar 文件中。
可能是您的 IDE 的功能?