当前位置:   article > 正文

逆向基础(十三) JAVA (一)

static double main(string[ ] args)

糖果 · 2015/07/06 15:17

翻译书籍:Reverse Engineering for Beginners

作者:Dennis Yurichev

翻译者:糖果

54.1介绍


大家都知道,java有很多的反编译器(或是产生JVM字节码) 原因是JVM字节码比其他的X86低级代码更容易进行反编译。

a).多很多相关数据类型的信息。

b).JVM(java虚拟机)内存模型更严格和概括。

c).java编译器没有做任何的优化工作(JVM JIT不是实时),所以,类文件中的字节代码的通常更清晰易读。

JVM字节码知识什么时候有用呢?

a).文件的快速粗糙的打补丁任务,类文件不需要重新编译反编译的结果。

b).分析混淆代码

c).创建你自己的混淆器。

d).创建编译器代码生成器(后端)目标。

我们从一段简短的代码开始,除非特殊声明,我们用的都是JDK1.7

反编译类文件使用的命令,随处可见:javap -c -verbase.

在这本书中提供的很多的例子,都用到了这个。

54.2 返回一个值

可能最简单的java函数就是返回一些值,oh,并且我们必须注意,一边情况下,在java中没有孤立存在的函数,他们是“方法”(method),每个方法都是被关联到某些类,所以方法不会被定义在类外面, 但是我还是叫他们“函数” (function),我这么用。

  1. #!java
  2. public class ret
  3. {
  4. public static int main(String[] args)
  5. {
  6. return 0;
  7. }
  8. }
  9. 复制代码

编译它。

  1. javac ret.java
  2. 复制代码

使用Java标准工具反编译。

  1. javap -c -verbose ret.class
  2. 复制代码

会得到结果:

  1. #!java
  2. public static int main(java.lang.String[]);
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=1, locals=1, args_size=1
  6. 0: iconst_0
  7. 1: ireturn
  8. 复制代码

对于java开发者在编程中,0是使用频率最高的常量。 因为区分短一个短字节的 iconst_0指令入栈0,iconst_1指令(入栈),iconst_2等等,直到iconst5。也可以有iconst_m1, 推送-1。

就像在MIPS中,分离一个寄存器给0常数:3.5.2 在第三页。

栈在JVM中用于在函数调用时,传参和传返回值。因此, iconst_0是将0入栈,ireturn指令,(i就是integer的意思。)是从栈顶返回整数值。

让我们写一个简单的例子, 现在我们返回1234:

  1. #!java
  2. public class ret
  3. {
  4. public static int main(String[] args)
  5. {
  6. return 1234;
  7. }
  8. }
  9. 复制代码

我们得到:

清单:

  1. 54.2:jdk1.7(节选)
  2. public static int main(java.lang.String[]);
  3. flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: sipush 1234 3: ireturn
  4. 复制代码

sipush(shot integer)如栈值是1234,slot的名字以为着一个16bytes值将会入栈。 sipush(短整型) 1234数值确认时候16-bit值。

  1. #!java
  2. public class ret
  3. {
  4. public static int main(String[] args)
  5. {
  6. return 12345678;
  7. }
  8. }
  9. 复制代码

更大的值是什么?

清单 54.3 常量区

  1. ...
  2. #2 = Integer 12345678
  3. ...
  4. 复制代码

5栈顶

  1. public static int main(java.lang.String[]);
  2. flags: ACC_PUBLIC, ACC_STATI
  3. Code:
  4. stack=1, locals=1, args_size=1
  5. 0: ldc #2 // int 12345678
  6. 2: ireturn
  7. 复制代码

操作码 JVM的指令码操作码不可能编码成32位数,开发者放弃这种可能。因此,32位数字12345678是被存储在一个叫做常量区的地方。让我们说(大多数被使用的常数(包括字符,对象等等车)) 对我们而言。

对JVM来说传递常量不是唯一的,MIPS ARM和其他的RISC CPUS也不可能把32位操作编码成32位数字,因此 RISC CPU(包括MIPS和ARM)去构造一个值需要一系列的步骤,或是他们保存在数据段中: 28。3 在654页.291 在695页。

MIPS码也有一个传统的常量区,literal pool(原语区) 这个段被叫做"lit4"(对于32位单精度浮点数常数存储) 和lit8(64位双精度浮点整数常量区)

布尔型

  1. #!java
  2. public class ret
  3. {
  4. public static boolean main(String[] args)
  5. {
  6. return true;
  7. }
  8. }
  9. 复制代码

  1. public static boolean main(java.lang.String[]);
  2. flags: ACC_PUBLIC, ACC_STATIC
  3. Code:
  4. stack=1, locals=1, args_size=1
  5. 0: iconst_1
  6. 复制代码

这个JVM字节码是不同于返回的整数学 ,32位数据,在形参中被当成逻辑值使用。像C/C++,但是不能像使用整型或是viceversa返回布尔型,类型信息被存储在类文件中,在运行时检查。

16位短整型也是一样。

  1. #!java
  2. public class ret
  3. {
  4. public static short main(String[] args)
  5. {
  6. return 1234;
  7. }
  8. }
  9. 复制代码

  1. public static short main(java.lang.String[]);
  2. flags: ACC_PUBLIC, ACC_STATIC
  3. Code:
  4. stack=1, locals=1, args_size=1
  5. 0: sipush 1234
  6. 3: ireturn
  7. 复制代码

还有char 字符型?

  1. #!java
  2. public class ret
  3. {
  4. public static char main(String[] args)
  5. {
  6. return 'A';
  7. }
  8. }
  9. 复制代码

  1. public static char main(java.lang.String[]);
  2. flags: ACC_PUBLIC, ACC_STATIC
  3. Code:
  4. stack=1, locals=1, args_size=1
  5. 0: bipush 65
  6. 2: ireturn
  7. 复制代码

bipush 的意思"push byte"字节入栈,不必说java的char是16位UTF16字符,和short 短整型相等,单ASCII码的A字符是65,它可能使用指令传输字节到栈。

让我们是试一下byte。

  1. #!java
  2. public class retc
  3. {
  4. public static byte main(String[] args)
  5. {
  6. return 123;
  7. }
  8. }
  9. 复制代码

  1. public static byte main(java.lang.String[]);
  2. flags: ACC_PUBLIC, ACC_STATIC
  3. Code:
  4. stack=1, locals=1, args_size=1
  5. 0: bipush 123
  6. 2: ireturn
  7. 909
  8. 复制代码

也许会问,位什么费事用两个16位整型当32位用?为什么char数据类型和短整型类型还使用char.

答案很简单,为了数据类型的控制和代码的可读性。char也许本质上short相同,但是我们快速的掌握它的占位符,16位的UTF字符,并且不像其他的integer值符。使用 short,为各位展现一下变量的范围被限制在16位。在需要的地方使用boolean型也是一个很好的主意。代替C样式的int也是为了相同的目的。

在java中integer的64位数据类型。

  1. #!java
  2. public class ret3
  3. {
  4. public static long main(String[] args)
  5. {
  6. return 1234567890123456789L;
  7. }
  8. }
  9. 复制代码

清单54.4常量区

  1. ...
  2. #2 = Long 1234567890123456789l
  3. ...
  4. public static long main(java.lang.String[]);
  5. flags: ACC_PUBLIC, ACC_STATIC
  6. Code:
  7. stack=2, locals=1, args_size=1
  8. 0: ldc2_w #2 // long ⤦
  9. Ç 1234567890123456789l
  10. 3: lreturn
  11. 复制代码

64位数也被在存储在常量区,ldc2_w 加载它,lreturn返回它。 ldc2_w指令也是从内存常量区中加载双精度浮点数。(同样占64位)

  1. #!java
  2. public class ret
  3. {
  4. public static double main(String[] args)
  5. {
  6. return 123.456d;
  7. }
  8. }
  9. 复制代码

清单54.5常量区

  1. ...
  2. #2 = Double 123.456d
  3. ...
  4. public static double main(java.lang.String[]);
  5. flags: ACC_PUBLIC, ACC_STATIC
  6. Code:
  7. stack=2, locals=1, args_size=1
  8. 0: ldc2_w #2 // double 123.456
  9. Ç d
  10. 3: dreturn
  11. dreturn 代表 "return double"
  12. 复制代码

最后,单精度浮点数:

  1. #!java
  2. public class ret
  3. {
  4. public static float main(String[] args)
  5. {
  6. return 123.456f;
  7. }
  8. }
  9. 复制代码

清单54.6 常量区

  1. ...
  2. #2 = Float 123.456f
  3. ...
  4. public static float main(java.lang.String[]);
  5. flags: ACC_PUBLIC, ACC_STATIC
  6. Code:
  7. stack=1, locals=1, args_size=1
  8. 0: ldc #2 // float 123.456f
  9. 2: freturn
  10. 复制代码

此处的ldc指令使用和32位整型数据一样,从常量区中加载。freturn 的意思是"return float"

那么函数还能返回什么呢?

  1. #!java
  2. public class ret
  3. {
  4. public static void main(String[] args)
  5. {
  6. return;
  7. }
  8. }
  9. 复制代码

  1. public static void main(java.lang.String[]);
  2. flags: ACC_PUBLIC, ACC_STATIC
  3. Code:
  4. stack=0, locals=1, args_size=1
  5. 0: return
  6. 复制代码

这以为着,使用return控制指令确没有返回实际的值,知道这一点就非常容易的从最后一条指令中演绎出函数(或是方法)的返回类型。

54.3 简单的计算函数


让我们继续看简单的计算函数。

  1. #!java
  2. public class calc
  3. {
  4. public static int half(int a)
  5. {
  6. return a/2;
  7. }
  8. }
  9. 复制代码

这种情况使用icont_2会被使用。

  1. public static int half(int);
  2. flags: ACC_PUBLIC, ACC_STATIC
  3. Code:
  4. stack=2, locals=1, args_size=1
  5. 0: iload_0
  6. 1: iconst_2
  7. 2: idiv
  8. 3: ireturn
  9. 复制代码

iload_0 将零给函数做参数,然后将其入栈。iconst_2将2入栈,这两个指令执行后,栈看上去是这个样子的。

  1. +---+
  2. TOS ->| 2 |
  3. +---+
  4. | a |
  5. +---+
  6. 复制代码

idiv携带两个值在栈顶, divides 只有一个值,返回结果在栈顶。

  1. +--------+
  2. TOS ->| result |
  3. +--------+
  4. 复制代码

ireturn取得比返回。 让我们处理双精度浮点整数。

  1. #!java
  2. public class calc
  3. {
  4. public static double half_double(double a)
  5. {
  6. return a/2.0;
  7. }
  8. }
  9. 复制代码

清单54.7 常量区

  1. ...
  2. #2 = Double 2.0d
  3. ...
  4. public static double half_double(double);
  5. flags: ACC_PUBLIC, ACC_STATIC
  6. Code:
  7. stack=4, locals=2, args_size=1
  8. 0: dload_0
  9. 1: ldc2_w #2 // double 2.0d
  10. 4: ddiv
  11. 5: dreturn
  12. 复制代码

类似,只是ldc2_w指令是从常量区装载2.0,另外,所有其他三条指令有d前缀,意思是他们工作在double数据类型下。

我们现在使用两个参数的函数。

  1. #!java
  2. public class calc
  3. {
  4. public static int sum(int a, int b)
  5. {
  6. return a+b;
  7. }
  8. }
  9. 复制代码

  1. #!bash
  2. public static int sum(int, int);
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=2, locals=2, args_size=2
  6. 0: iload_0
  7. 1: iload_1
  8. 2: iadd
  9. 3: ireturn
  10. 复制代码

iload_0加载第一个函数参数(a),iload_2 第二个参数(b)下面两条指令执行后,栈的情况如下:

  1. +---+
  2. TOS ->| b |
  3. +---+
  4. | a |
  5. +---+
  6. 复制代码

iadds 增加两个值,返回结果在栈顶。

  1. +--------+ TOS ->| result | +--------+
  2. 复制代码

让我们把这个例子扩展成长整型数据类型。

  1. #!java
  2. public static long lsum(long a, long b)
  3. {
  4. return a+b;
  5. }
  6. 复制代码

我们得到的是:

  1. #!java
  2. public static long lsum(long, long);
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=4, locals=4, args_size=2
  6. 0: lload_0
  7. 1: lload_2
  8. 2: ladd
  9. 3: lreturn
  10. 复制代码

第二个(load指令从第二参数槽中,取得第二参数。这是因为64位长整型的值占用来位,用了另外的话2位参数槽。)

稍微复杂的例子

  1. #!java
  2. public class calc
  3. {
  4. public static int mult_add(int a, int b, int c)
  5. {
  6. return a*b+c;
  7. }
  8. }
  9. 复制代码

  1. public static int mult_add(int, int, int);
  2. flags: ACC_PUBLIC, ACC_STATIC
  3. Code:
  4. stack=2, locals=3, args_size=3
  5. 0: iload_0
  6. 1: iload_1
  7. 2: imul
  8. 3: iload_2
  9. 4: iadd
  10. 5: ireturn
  11. 复制代码

第一是相乘,积被存储在栈顶。

  1. +---------+
  2. TOS ->| product |
  3. +---------+
  4. 复制代码

iload_2加载第三个参数(C)入栈。

  1. +---------+
  2. TOS ->| c |
  3. +---------+
  4. | product |
  5. +---------+
  6. 复制代码

现在iadd指令可以相加两个值。

54.4 JVM内存模型


X86和其他低级环境系统使用栈传递参数和存储本地变量,JVM稍微有些不同。

主要体现在: 本地变量数组(LVA)被用于存储到来函数的参数和本地变量。iload_0指令是从其中加载值,istore存储值在其中,首先,函数参数到达:开始从0 或者1(如果0参被this指针用。),那么本地局部变量被分配。

每个槽子的大小都是32位,因此long和double数据类型都占两个槽。

操作数栈(或只是"栈"),被用于在其他函数调用时,计算和传递参数。不像低级X86的环境,它不能去访问栈,而又不明确的使用pushes和pops指令,进行出入栈操作。

54.5 简单的函数调用


mathrandom()返回一个伪随机数,函数范围在「0.0...1.0)之间,但对我们来说,由于一些原因,我们常常需要设计一个函数返回数值范围在「0.0...0.5)

  1. #!java
  2. public class HalfRandom
  3. {
  4. public static double f()
  5. {
  6. return Math.random()/2;
  7. }
  8. }
  9. 复制代码

常量区

  1. ...
  2. #2 = Methodref #18.#19 // java/lang/Math.⤦
  3. Ç random:()D
  4. 6(Java) Local Variable Array
  5. #3 = Double 2.0d
  6. ...
  7. #12 = Utf8 ()D
  8. ...
  9. #18 = Class #22 // java/lang/Math
  10. #19 = NameAndType #23:#12 // random:()D
  11. #22 = Utf8 java/lang/Math
  12. #23 = Utf8 random
  13. public static double f();
  14. flags: ACC_PUBLIC, ACC_STATIC
  15. Code:
  16. stack=4, locals=0, args_size=0
  17. 0: invokestatic #2 // Method java/
  18. Ç lang/Math.random:()D
  19. 3: ldc2_w #3 // double 2.0d
  20. 6: ddiv
  21. 7: dreturn
  22. 复制代码

java本地变量数组 916 静态执行调用math.random()函数,返回值在栈顶。结果是被0.5初返回的,但函数名是怎么被编码的呢? 在常量区使用methodres表达式,进行编码的,它定义类和方法的名称。第一个methodref 字段指向表达式,其次,指向通常文本字符("java/lang/math") 第二个methodref表达指向名字和类型表达式,同时链接两个字符。第一个方法的名字式字符串"random",第二个字符串是"()D",来编码函数类型,它以为这两个值(因此D是字符串)这种方式1JVM可以检查数据类型的正确性:2)java反编译器可以从被编译的类文件中修改数据类型。

最后,我们试着使用"hello,world!"作为例子。

  1. #!java
  2. public class HelloWorld
  3. {
  4. public static void main(String[] args)
  5. {
  6. System.out.println("Hello, World");
  7. }
  8. }
  9. 复制代码

常量区

917 常量区的ldc行偏移3,指向"hello,world!"字符串,并且将其入栈,在java里它被成为饮用,其实它就是指针,或是地址。

  1. ...
  2. #2 = Fieldref #16.#17 // java/lang/System.⤦
  3. Ç out:Ljava/io/PrintStream;
  4. #3 = String #18 // Hello, World
  5. #4 = Methodref #19.#20 // java/io/
  6. Ç PrintStream.println:(Ljava/lang/String;)V
  7. ...
  8. #16 = Class #23 // java/lang/System
  9. #17 = NameAndType #24:#25 // out:Ljava/io/
  10. Ç PrintStream;
  11. #18 = Utf8 Hello, World
  12. #19 = Class #26 // java/io/
  13. Ç PrintStream
  14. #20 = NameAndType #27:#28 // println:(Ljava/
  15. Ç lang/String;)V
  16. ...
  17. #23 = Utf8 java/lang/System
  18. #24 = Utf8 out
  19. #25 = Utf8 Ljava/io/PrintStream;
  20. #26 = Utf8 java/io/PrintStream
  21. #27 = Utf8 println
  22. #28 = Utf8 (Ljava/lang/String;)V
  23. ...
  24. public static void main(java.lang.String[]);
  25. flags: ACC_PUBLIC, ACC_STATIC
  26. Code:
  27. stack=2, locals=1, args_size=1
  28. 0: getstatic #2 // Field java/
  29. Ç lang/System.out:Ljava/io/PrintStream;
  30. 3: ldc #3 // String Hello, ⤦
  31. Ç World
  32. 5: invokevirtual #4 // Method java/io⤦
  33. Ç /PrintStream.println:(Ljava/lang/String;)V
  34. 8: return
  35. 复制代码

常见的invokevirtual指令,从常量区取信息,然后调用pringln()方法,貌似我们知道的println()方法,适用于各种数据类型,我这种println()函数版本,预先给的是字符串类型。

但是第一个getstatic指令是干什么的?这条指令取得对象信息的字段的一个引用或是地址。输出并将其进栈,这个值实际更像是println放的指针,因此,内部的print method取得两个参数,输入1指向对象的this指针,2)"hello,world"字符串的地址,确实,println()在被初始化系统的调用,对象之外,为了方便,javap使用工具把所有的信息都写入到注释中。

54.6 调用beep()函数


这可能是最简单的,不使用参数的调用两个函数。

  1. #!java
  2. public static void main(String[] args)
  3. {
  4. java.awt.Toolkit.getDefaultToolkit().beep();
  5. };
  6. 复制代码

  1. #!bash
  2. public static void main(java.lang.String[]);
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=1, locals=1, args_size=1
  6. 0: invokestatic #2 // Method java/
  7. Ç awt/Toolkit.getDefaultToolkit:()Ljava/awt/Toolkit;
  8. 3: invokevirtual #3 // Method java/
  9. Ç awt/Toolkit.beep:()V
  10. 6: return
  11. 复制代码

首先,invokestatic在0行偏移调用javaawt.toolkit. getDefaultTookKit()函数,返回toolkit类对象的引用,invokedvirtualIFge指令在3行偏移,调用这个类的beep()方法。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/207415?site
推荐阅读
相关标签
  

闽ICP备14008679号