`
zhaohaolin
  • 浏览: 984014 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

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

阅读更多

大多Java 程序员 知道他们的程序通常不会被编译为本机代码而是被编译为由java虚拟机(JVM)执行的字节码格式。然而,很少有java程序员曾经看过字节码因为他们的工具不鼓励他们去看。大多Java 调试工具不允许单步执行字节码,它们要么显示源代码行,要么什么也不显示。

  幸运的是JDK提供了javap,一个命令行工具,它使得查看字节码很容易。让我们看一个范例:

  public class ByteCodeDemo {
   public static void main(String[] args) {
    System.out.println("Hello world");
   }
  }

  在编译这个类后,你可以用十六进制编辑器打开.class文件然后参照虚拟机规范翻译字节码。幸运的是有更简单的方法。JDK包含一个命令行的 反汇编器:javap,它可以转换字节码为一种可读的助记符形式,可以像下面这样通过传递'-c'参数给javap得到字节码列表:

  javap -c ByteCodeDemo

  你应该会看到输出类似这样:

  public class ByteCodeDemo extends java.lang.Object {
   public ByteCodeDemo();
   public static void main(java.lang.String[]);
  }
  Method ByteCodeDemo()
  0 aload_0
  1 invokespecial #1
  4 return
  Method void main(java.lang.String[])
  0 getstatic #2
  3 ldc #3
  5 invokevirtual #4
  8 return

  仅仅从这个短小的列表你可以学到很多字节码的知识。从main方法的第一个指令开始:

  0 getstatic #2

  开始的整数是方法中的指令的偏移值,因此第一个指令以0开始。紧随偏移量是指令的助记符(mnemonic)。在这个范例中,'getstatic' 指令将一个静态成员压入一个称为操作数堆栈的数据结构 ,后续的指令可以引用这个数据结构中的成员。getstatic 指令后是要压入的成员。在这个例子中,要压入的成员是"#2 " 。如果你直接检查字节码,你会看到成员信息没有直接嵌入指令而是像所有由java类使用的常量那样存储 在一个共享池中。将成员信息存储在一个常量池中可以减小字节码指令的大小,因为指令只需要存储常量池中的一个索引而不是整个常量。在这个例子中,成员信息位于常量池中的#2处。常量池中的项目的顺序是和编译器相关的,因此在你的环境中看到的可能不是'#2' 。

  分析完第一个指令后很容易猜到其它指令的意思。'ldc' (load constant) 指令将常量"Hello, World."压入操作数栈。'invokevirtual'指令调用println方法,它从操作数栈弹出它的两个参数。不要忘记一个像println 这样的实例方法有两个参数:上面的字符串,加上隐含的'this'引用。

  字节码如何预防内存错误

  Java语言经常被吹捧为开发互联网 软件的"安全 的"语言。表面上和c++如此相似的代码如何体现安全呢?它引入的一个重要的安全概念是防止内存相关的错误。计算机罪犯利用内存错误在其它情况下安全的程序中插入自己的恶意的代码。Java字节码是第一个可以预防这种攻击的,像下面的范例展示的:

  public float add(float f, int n) {
   return f + n;
  }

  如果你将这个方法加入上面的范例中,重新编译它,然后运行javap,你将看到的字节码类似这个:

  Method float add(float, int)
  0 fload_1
  1 iload_2
  2 i2f
  3 fadd
  4 freturn

  在方法的开始,虚拟机将方法的参数放入一个称为局部变量表的数据结构中。将像名字暗示的那样,局部变量表也包含了你声明的任何局部变量。在这个 例子中,方法以三个局部变量表的项开始,这些都是add方法的参数,位置0保存this引用,而位置1和2分别保存float和int参数。

  为了实际的操作这些变量,它们必须被加载(压入)到操作数栈。第一个指令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检查你的代码以期提高性能和捕获特殊的不易察觉的错误时是没有用的。

分享到:
评论

相关推荐

    WIN XP蓝屏代码大全

    ◆错误分析:这个错误产生的原因很难判断, 不过Windows内存管理出了问题很可能会导致这个停机码的出现. ◇解决方案:如果是内存管理的缘故, 通常增加内存会解决问题. 7、0x0000002EATA_BUS_ERROR ◆错误分析:系统...

    电脑蓝屏对照码

    ◆错误分析:这个错误产生的原因很难判断, 不过Windows内存管理出了问题很可能会导致这个停机码的出现. ◇解决方案:如果是内存管理的缘故, 通常增加内存会解决问题. 7、0x0000002EATA_BUS_ERROR ◆错误分析:系统...

    JAVA上百实例源码以及开源项目源代码

    Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机的流程及操作:获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,并将当前的计数器初始化,调用每一个...

    JAVA上百实例源码以及开源项目

    EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机的流程及操作:获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,并将当前的计数器初始化,调用每一个EJB对象的count()方法,保证Bean...

    java源码包---java 源码 大量 实例

    EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机的流程及操作:获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,并将当前的计数器初始化,调用每一个EJB对象的count()方法,保证Bean...

    java源码包2

    EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机的流程及操作:获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,并将当前的计数器初始化,调用每一个EJB对象的count()方法,保证...

    黑客反汇编揭秘(第二版).part2.rar

    11.4.1 X码概念及其他约定 186 11.4.2 X码的目的与任务 187 11.4.3 对X码的要求 188 11.4.4 插入 189 11.5 总结 215 第12章 在Linux与BSD中反汇编ELF文件 216 12.1 所需要的工具 216 12.2 ELF文件结构 217 ...

    黑客反汇编揭秘(第二版).part1.rar

    11.4.1 X码概念及其他约定 186 11.4.2 X码的目的与任务 187 11.4.3 对X码的要求 188 11.4.4 插入 189 11.5 总结 215 第12章 在Linux与BSD中反汇编ELF文件 216 12.1 所需要的工具 216 12.2 ELF文件结构 217 ...

    java源码包3

    EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机的流程及操作:获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,并将当前的计数器初始化,调用每一个EJB对象的count()方法,保证...

    java源码包4

    EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机的流程及操作:获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,并将当前的计数器初始化,调用每一个EJB对象的count()方法,保证...

    成百上千个Java 源码DEMO 4(1-4是独立压缩包)

    Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机的流程及操作:获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,并将当前的计数器初始化,调用每一个...

    成百上千个Java 源码DEMO 3(1-4是独立压缩包)

    Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机的流程及操作:获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,并将当前的计数器初始化,调用每一个...

    计算机应用技术(实用手册)

    同理,数值小性能高,但是对内存的质量也要求严格! DRAM RAS# to CAS# Delay: 这个项目可控制DRAM作用指令与读取/写入指令之间的延迟时间,有2,3,4几种选择。数值越小,性能越好。 DRAM RAS# Precharge: 这...

    《计算机应用基础》客观题附答案.doc

    A、EBCDIC码 B、ASCII码 C、国标码 D、BCD码 9. 域名MH.BIT.EDU.CN中主机名是___A___。 A、MH B、EDU C、CN D、BIT 10. Modem是计算机通过电话线接入Internet时所必需的硬件,它的功能是___D___。 A、只将数字信号...

    C语言程序设计标准教程

    下表列出了Turbo C中各类整型量所分配的内存字节数及数的表示范围。 类型说明符 数的范围 分配字节数 int -32768~32767 ■■ short int -32768~32767 ■■ signed int -32768~32767 ■■ unsigned int 0~65535 ■...

    《计算机应用基础》客观题附答案.docx

    A、EBCDIC码 B、ASCII码 C、国标码 D、BCD码 9. 域名MH.BIT.EDU.CN中主机名是___A___。 A、MH B、EDU C、CN D、BIT 10. Modem是计算机通过电话线接入Internet时所必需的硬件,它的功能是___D___。 A、只将数字信号...

    会计理论考试题

    A、可以减少屏幕损耗 B、可以节省计算机内存 C、可以保障系统安全 D、可以增加动感 36.记录在存储介质上的一组相关信息的集合称为 __D_ 。 A、程序 B、磁盘 C、软件 D、文件 37.在资源管理器中,双击扩展名为“.TXT...

    网管教程 从入门到精通软件篇.txt

    Chkdsk 命令还可列出并纠正磁盘上的错误。  含有下列参数的 chkdsk 命令仅在使用故障恢复控制台时才可用。可在命令提示符下使用带有不同参数的 chkdsk 命令。  vol [drive:] [ chkdsk [drive:] [/p] [/r]  ...

    C++MFC教程

    这样做的目的主要是增加对语言的熟悉程度,同时也训练自己的思维和熟悉一些在编程中常犯的错误。更重要的是理解并能运用C++的各种特性,这些在以后的开发中都会有很大的帮助,特别是利用MFC进行开发的朋友对C++一定...

Global site tag (gtag.js) - Google Analytics