翻译:Cherami
开始使用JAVAP
大多Java程序员知道他们的程序通常不会被编译为本机代码而是被编译为由java虚拟机(JVM)执行的字节码格式。然而,很少有java程序员曾经看过字节码因为他们的工具不鼓励他们去看。大多Java 调试工具不允许单步执行字节码,它们要么显示源代码行,要么什么也不显示。
幸运的是JDK提供了javap,一个命令行工具,它使得查看字节码很容易。让我们看一个范例:
public static void main(String[] args) {
System.out.println("Hello world");
}
}
public ByteCodeDemo();
public static void main(java.lang.String[]);
}
Method ByteCodeDemo()
0 aload_0
1 invokespecial #1 <Method java.lang.Object()>
4 return
Method void main(java.lang.String[])
0 getstatic #2 <Field java.io.PrintStream out>
3 ldc #3 <String "Hello world">
5 invokevirtual #4 <Method void println(java.lang.String)>
8 return
分析完第一个指令后很容易猜到其它指令的意思。'ldc' (load constant) 指令将常量"Hello, World."压入操作数栈。'invokevirtual'指令调用println方法,它从操作数栈弹出它的两个参数。不要忘记一个像println这样的实例方法有两个参数:上面的字符串,加上隐含的'this'引用。
字节码如何预防内存错误
Java语言经常被吹捧为开发互联网软件的"安全的"语言。表面上和c 如此相似的代码如何体现安全呢?它引入的一个重要的安全概念是防止内存相关的错误。计算机罪犯利用内存错误在其它情况下安全的程序中插入自己的恶意的代码。Java字节码是第一个可以预防这种攻击的,像下面的范例展示的:
return f n;
}
0 fload_1
1 iload_2
2 i2f
3 fadd
4 freturn
为了实际的操作这些变量,它们必须被加载(压入)到操作数栈。第一个指令fload_1将位置1处的float压入操作数栈,第二个指令iload_2将位置2处的int压入操作数栈。这些指令的一个引起注意的事情是指令中的'i'和'f'前缀,这说明Java字节码指令是强类型的。如果参数的类型和字节码的类型不匹配,VM将该字节码作为不安全的而加以拒绝。更好的是,字节码被设计为只需在类被加载时执行一次这样的类型安全检查。
这个类型安全是如何加强安全的?如果一个攻击者能够欺骗虚拟机将一个int作为一个float或者相反,它就可以很容易的以一个预期的的方法破坏计算。如果这些计算涉及银行结余,那么隐含的安全性是很明显的。更危险的是欺骗VM将一个int作为一个Object引用。在大多情况下,这将导致VM崩溃,但是攻击者只需要找到一个漏洞。不要忘记攻击者不会手工搜索这个漏洞--写出一个程序产生数以亿计的错误字节码的排列是相当容易的,这些排列试图找到危害VM的幸运的那个。
字节码的另一个内存安全防护是数组操作。'aastore' 和 'aaload' 字节码操作Java数组并且它们总是检查数组边界。如果调用程序越过了数组尾,这些字节码将抛出一个ArrayIndexOutOfBoundsException。也许所有最重要的检查都使用分支指令,例如,以if开始的字节码。在字节码中,分支指令只能转移到同一方法中的其它指令。在方法外可以传递的唯一控制是使它返回:抛出一个异常或者执行一个'invoke'指令。这不仅关闭了很多攻击,同时也防止由于摇荡引用(dangling reference)或者堆栈冲突而引发的令人厌恶的错误。如果你曾经使用系统调试器打开你的程序并定位到代码中的一个随机的位置,那么你会很熟悉这些错误。
所有这些检查中需要记住的重要的一点是它们是由虚拟机在字节码级进行的而不是仅仅由编译器在源代码级进行的。一个例如c 这样的语言的编译器可能在编译时预防上面讨论的某些内存错误,但是这些保护只是在源代码级应用。操作系统将很乐意加载执行任何机器码,无论这些代码是由精细的c 编译器产生的还是心怀恶意的攻击者产生的。简单的讲,C 仅仅是在源代码级上面向对象而Java的面向对象的特性扩展到编译过的代码级。
分析字节码提升代码质量
Java字节码的内存和安全保护无论我们是否注意都是存在地,那么我们为什么还费心查看字节码呢?在很多情况下,知道编译器如何将你的代码转换为字节码可以帮助你写出更高效的代码,而且在某些情况下可以防止不易发觉的错误。考虑下面的例子:
String concat(String str1, String str2) {
return str1 str2;
}
//将 str2 附加到 str1
void concat(StringBuffer str1, String str2) {
str1.append(str2);
}
0 new #5 <Class java.lang.StringBuffer>
3 dup
4 invokespecial #6 <Method java.lang.StringBuffer()>
7 aload_1
8 invokevirtual #7 <Method java.lang.StringBuffer append(java.lang.String)>
11 aload_2
12 invokevirtual #7 <Method java.lang.StringBuffer append(java.lang.String)>
15 invokevirtual #8 <Method java.lang.String toString()>
18 areturn
Method void concat2(java.lang.StringBuffer, java.lang.String)
0 aload_1
1 aload_2
2 invokevirtual #7 <Method java.lang.StringBuffer append(java.lang.String)>
5 pop
6 return
最后的一个范例展示了检查字节码如何帮助防止程序中的错误。像下面那样创建两个类,确保它们在独立的文件中。
public static final boolean debug=false;
public static boolean log=false;
}
public class EternallyConstant {
public static void main(String [] args) {
System.out.println("EternallyConstant beginning execution");
if (ChangeALot.debug)
System.out.println("Debug mode is on");
if (ChangeALot.log)
System.out.println("Logging mode is on");
}
}
EternallyConstant beginning execution.
现在试着编辑ChangeALot,修改debug和log变量的值为true(两个都为true)。只重新编译ChangeALot。再次运行EternallyConstant,你将看到下面的输出:
Logging mode is on
0 getstatic #2 <Field java.io.PrintStream out>
3 ldc #3 <String "EternallyConstant beginning execution">
5 invokevirtual #4 <Method void println(java.lang.String)>
8 getstatic #5 <Field boolean log>
11 ifeq 22
14 getstatic #2 <Field java.io.PrintStream out>
17 ldc #6 <String "Logging mode is on">
19 invokevirtual #4 <Method void println(java.lang.String)>
22 return
知道一些字节码的知识对于使用java编程的程序员都是有价值的。javap工具使得查看字节码很容易。有时候,使用javap检查你的代码以期提高性能和捕获特殊的不易察觉的错误时是没有用的。
字节码和VM是相当的复杂的,这已经超过这个技巧可以涵盖的范围。要知道更多东西,看看Bill Venners写的《Inside the Java Virtual Machine》(译者注: http://www.artima.com/insidejvm/ed2/ 有该书的1、2、3、4、5、7、8、9和20章的免费html版,英文。)