提高代码质量及字节码如何防止内存错误

发表于:2008-5-07 16:19

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:未知    来源:网络转载

  为了实际的操作这些变量,它们必须被加载(压入)到操作数栈。第一个指令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字节码的内存和安全保护无论我们是否注意都是存在地,那么我们为什么还费心查看字节码呢?在很多情况下,知道编译器如何将你的代码转换为字节码可以帮助你写出更高效的代码,而且在某些情况下可以防止不易发觉的错误。考虑下面的例子:


//返回 str1+str2 的串连

String concat(String str1, String str2) {

return str1 + str2;

}


//将 str2 附加到 str1

void concat(StringBuffer str1, String str2) {

str1.append(str2);

}


猜猜每个方法需要多少个方法调用。现在编译这些方法并且运行javap,你会得到类似下面的输出:


Method java.lang.String concat1(java.lang.String, java.lang.String)

0 new #5

3 dup

4 invokespecial #6

7 aload_1

8 invokevirtual #7

11 aload_2

12 invokevirtual #7

15 invokevirtual #8

18 areturn

Method void concat2(java.lang.StringBuffer, java.lang.String)

0 aload_1

1 aload_2

2 invokevirtual #7

5 pop

6 return

concat1方法执行了5个方法调用s: new, invokespecial和三个invokevirtuals,这比concat2方法执行了更多的工作,后者只执行了一个invokevirtual调用。大多Java程序员已经得到过警告,因为String是不可变的,而使用StringBuffer进行字符串连接效率更高。使用javap分析这个使得这点变得很生动。如果你不能肯定两个语言构造在性能上是否相等,你应该使用javap分析字节码。然而,对just-in-time (JIT)编译器要小心,因为JIT编译器将字节码重新编译为本机代码而能执行一些javap不能揭示的附加优化。除非你有你的虚拟机的源代码,否则你应该补充你的字节码的基准性能分析。

最后的一个范例展示了检查字节码如何帮助防止程序中的错误。像下面那样创建两个类,确保它们在独立的文件中。


public class ChangeALot {

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,你会得到信息:


EternallyConstant beginning execution.


现在试着编辑ChangeALot,修改debug和log变量的值为true(两个都为true)。只重新编译ChangeALot。再次运行EternallyConstant,你将看到下面的输出:


EternallyConstant beginning execution

Logging mode is on


debug变量怎么了?即使你将debug设置为true,信息"Debug mode is on"并没有出现。答案在字节码中。对 EternallyConstant运行javap你会看到:


Method void main(java.lang.String[])

0 getstatic #2

3 ldc #3

5 invokevirtual #4

8 getstatic #5

11 ifeq 22

14 getstatic #2

17 ldc #6

19 invokevirtual #4

22 return


  惊奇吧!在log成员上有一个'ifeq'检查,而代码根本没有检查debug成员。因为debug成员被标记为final类型,编译器知道debug成员在运行时永远不会改变,因此它通过移除'if'声明进行优化。这确实是一个非常有用的优化,因为它允许你在程序中嵌入调试代码而在将它设置为false时不用付出运行时的代价。不幸的是这个优化能够导致主要的编译时混乱。如果你改变一个final成员,你必须记住重新编译任何可能引用该成员的类。这是因为这个'reference'可能已经经过优化了。Java开发环境不能总是发现这个微妙的相关性,一些能导致非常奇怪的错误。因此,古老的C++格言对于java环境仍然有效:"When in doubt, rebuild all."(有疑问,重新编译所有的代码)。

  知道一些字节码的知识对于使用java编程的程序员都是有价值的。javap工具使得查看字节码很容易。有时候,使用javap检查你的代码以期提高性能和捕获特殊的不易察觉的错误时是没有用的。

22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号