`
RednaxelaFX
  • 浏览: 3049554 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

JVM在校验阶段不检查接口的实现状况

    博客分类:
  • Java
阅读更多
作者:RednaxelaFX
主页:http://rednaxelafx.iteye.com
日期:2009-06-02

系列笔记:
一个通不过Java字节码校验的例子
数组协变带来的静态类型漏洞
以Python为例讨论高级编程语言程序的wire format与校验
CLR上的接口调用也是在运行时检查的
为什么JVM与CLR都不对接口方法调用做静态校验?

继续看到底要运行一个Java程序需要做的各种检查是在什么时候发生的。这次我们来看看接口调用的问题。

当前的JVM规范中,与方法调用相关的指令有4个:invokevirtual、invokeinterface、invokestatic与invokespecial。其中调用接口方法时使用的JVM指令是invokeinterface。这个指令与另外3个方法调用指令有一个显著的差异:它不要求JVM的校验器(verifier)检查被调用对象(receiver)的类型;另外3个方法调用指令都要求校验被调用对象。也就是说,使用invokeinterface时如果被调用对象没有实现指定的接口,则应该在运行时而不是链接时抛出异常;而另外3个方法调用指令都要求在链接时抛出异常。

看看JVM规范是怎么说的:
Java Virtual Machine Specification, 2nd Edition 写道
invokeinterface
...
Runtime Exceptions
...
if the class of objectref does not implement the resolved interface, invokeinterface throws an IncompatibleClassChangeError.

可以留意一下另外3个方法调用指令中“IncompatibleClassChangeError”都是Linking Exception而不是Runtime Exception。

这种规定对Java程序来说可见的行为就是:如果一个方法通不过校验,则整个方法都不会被执行;如果能通过校验而抛出运行时异常,则方法当中抛出异常之前的部分都会被执行。

当然,我们直接用Java语言写出来的程序很难引发这样的错误,因为Java编译器会做检查来保证一定程度的类型安全。但是Java的class文件,或者说Java字节码可以由Java编译器以外的别的方式生成,此时就得不到Java编译器对类型安全的保证,而要依赖于JVM对字节码的校验以及运行时的检查了。

================================================================

我是之前在读John Rose对JSR 292的invokedynamic的讲解时留意到invokeinterface的这个特点的。John特别提到invokedynamic就像invokeinterface一样,都不在校验时对被调用对象的类型做检查。不过之前一直没见过调用对一个没实现接口的对象调用接口方法实际是个什么样子。

好吧,这次就来看个例子。首先创建一个接口IFoo,一个实现了该接口的类FooImpl,和一个未实现该接口的类Bar:
IFoo.java:
public interface IFoo {
    void method();
}

FooImpl.java:
public class FooImpl implements IFoo {
    public void method() {
        System.out.println("FooImpl.method()");
    }
}

Bar.java:
public class Bar {
    public void anotherMethod() {
        System.out.println("Bar.anotherMethod()");
    }
}


接下来构造出一个能引发运行时异常的程序。大致的意思是这样的:
public class TestInterfaceCall {
    public static void main(String[] args) {
        IFoo f = new FooImpl();
        f.method();

        Bar b = new Bar();
        ((IFoo)b).method(); // << watch this
    }
}

注意第7行代码。如果就这么写然后编译的话,生成的字节码里会有一个checkcast指令将Bar类型的引用转换为IFoo类型的引用。如果有checkcast的话,运行时就会在该指令上报错,因为Bar没有实现IFoo。但这次我想引发的错误不是强制转换相关,而是接口调用相关:想达到的效果是以b为被调用对象,但调用IFoo.method()而不是Bar上已有的方法。所以要靠自己来生成字节码,避免checkcast指令。

上个月的两个相关帖里我使用了ObjectWeb的ASM库来生成Java字节码。这个库很实用,但写起来还是繁琐了些。这次我决定用Charles Nutter写的bitescript。使用该库需要JRuby 1.2.0或更高的版本,我这次用的是JRuby 1.3.0RC2。

安装bitescript只要用JRuby的gem就行:
gem install bitescript


然后编写生成字节码用的脚本:
test.rb:
require 'rubygems'
require 'bitescript'
include BiteScript

IFoo    = Java::IFoo
FooImpl = Java::FooImpl
Bar     = Java::Bar

fb = FileBuilder.build(__FILE__) do
  public_class 'TestInterfaceCall' do
    public_static_method 'main', void, string[] do
      # IFoo f = new FooImpl();
      new FooImpl
      dup
      invokespecial FooImpl, '<init>', [void]
      astore 1

      # f.method();
      aload 1
      invokeinterface IFoo, 'method', [void]
      
      # Bar b = new Bar();
      new Bar
      dup
      invokespecial Bar, '<init>', [void]
      astore 2
      
      # ((IFoo)b).method();
      aload 2
      ## checkcast IFoo # skip the cast to trigger IncompatibleClassChangeError
      invokeinterface IFoo, 'method', [void]
      returnvoid
    end
  end
end

fb.generate do |filename, class_builder|
  File.open(filename, 'w') do |file|
    file.write(class_builder.generate)
  end
end

可以对比一下直接用ASM时的代码,显然用bitescript要简洁易懂得多。Good job, Charles!

把前面的IFoo.class、FooImpl.class和Bar.class放在“当前目录”下,然后执行上述脚本,生成TestInterfaceCall.class。(注意,执行该脚本时,JRuby要能够找到前面几个.class文件,不然生成出来的代码有错误。留意底下的回复。)
接着,运行java TestInterfaceCall,
D:\sdk\jruby-1.3.0RC2\test_bitescript>java TestInterfaceCall
FooImpl.method()
Exception in thread "main" java.lang.IncompatibleClassChangeError
        at TestInterfaceCall.main(test.rb)

可以看到程序打印出了"FooImpl.method()"这句话,也就是说异常是在运行时而不是链接时抛出的。

如今用到Java的字节码改写/动态生成的工具已经很普遍了,如果在使用它们的时候不够小心,相信这里所提到的运行时异常也会有机会见到的 =v=

P.S. 我这次运行的环境是:
D:\sdk\jruby-1.3.0RC2\test_bitescript>java -version
java version "1.6.0_11"
Java(TM) SE Runtime Environment (build 1.6.0_11-b03)
Java HotSpot(TM) Client VM (build 11.0-b16, mixed mode, sharing)


=========================

更新:发现这么一篇论文:Using abstract interpretation to add type checking for interfaces in Java bytecode verification
abstract 写道
Java interface types support multiple inheritance. Because of this, the standard bytecode verifier ignores them, since it is not able to model the class hierarchy as a lattice. Thus, type checks on interfaces are performed at run time. We propose a verification methodology that removes the need for run-time checks. The methodology consists of: (1) an augmented verifier that is very similar to the standard one, but is also able to check for interface types in most cases; (2) for all other cases, a set of additional simpler verifiers, each one specialized for a single interface type. We obtain these verifiers in a systematic way by using abstract interpretation techniques. Finally, we describe an implementation of the methodology and evaluate it on a large set of benchmarks.
8
0
分享到:
评论
5 楼 Arbow 2009-06-08  
果然如此,需要用 jruby -J-cp ./ ./test.rb 运行
4 楼 Arbow 2009-06-07  
可能是这样,我运行jruby ./test.rb 的时候,没有加入 -cp . 了:)
3 楼 RednaxelaFX 2009-06-06  
嘿嘿,我知道原因了。不是因为你的脚本加了引号,你多半是直接用这帖里的脚本的对吧?那你在执行那段脚本的时候,IFoo.class、FooImpl.class和Bar.class肯定不在当前目录下。我刚才试了一下,这样执行得到的结果就跟你说的一样。这是因为JRuby在运行的时候没有找到合适的.class文件,所以Java::IFoo这样的名字就没有对应到一个实际的类,出来的结果就不对了。
多谢提醒,我得把帖子修改一下,免得误导了……一开始就得让那几个.class文件在当前目录下才行,或者是把那几个.class文件所在的位置放到JRuby执行的classpath上。
2 楼 RednaxelaFX 2009-06-06  
Arbow 写道
似乎引入了jruby中特有的class格式,不知道是否由于这样导致"Java::IfooJava_class"加载出错

奇怪……请问你运行的脚本是跟我的完全一样还是修改过?完全一样的话,生成的TestInterfaceCall.class也应该是一样的才对。但我这边生成出来的是这样的:
Compiled from "test.rb"
public class TestInterfaceCall extends java.lang.Object{
public static void main(java.lang.String[]);
  Code:
   0:   new     #9; //class FooImpl
   3:   dup
   4:   invokespecial   #13; //Method FooImpl."<init>":()V
   7:   astore_1
   8:   aload_1
   9:   invokeinterface #18,  1; //InterfaceMethod IFoo.method:()V
   14:  new     #20; //class Bar
   17:  dup
   18:  invokespecial   #21; //Method Bar."<init>":()V
   21:  astore_2
   22:  aload_2
   23:  invokeinterface #18,  1; //InterfaceMethod IFoo.method:()V
   28:  return

}


要注意我的脚本里,开头的Java::IFoo等的几行都是没有引号的,或许你的脚本里不小心加了引号?我只是随便猜猜,呵呵。另外你的IFoo写成/Ifoo了。
1 楼 Arbow 2009-06-05  
请教一个问题。我使用JRuby1.3生成的TestInterfaceCall.class,运行时候会提示ClassNotFound,但事实上IFoo等文件已经在同一目录下了


$ java -cp . TestInterfaceCall
Exception in thread "main" java.lang.NoClassDefFoundError: Java::IfooJava_class
Caused by: java.lang.ClassNotFoundException: Java::IfooJava_class
at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:320)
Could not find the main class: TestInterfaceCall.  Program will exit.


javap结果如下:
public class TestInterfaceCall extends java.lang.Object{
public static void main(java.lang.String[]);
  Code:
   0:	new	#9; //class "Java::FooImplJava_class"
   3:	dup
   4:	invokespecial	#13; //Method "Java::FooImplJava_class"."<init>":()V
   7:	astore_1
   8:	aload_1
   9:	invokeinterface	#18,  1; //InterfaceMethod "Java::IfooJava_class".method:()V
   14:	new	#20; //class "Java::BarJava_class"
   17:	dup
   18:	invokespecial	#21; //Method "Java::BarJava_class"."<init>":()V
   21:	astore_2
   22:	aload_2
   23:	invokeinterface	#18,  1; //InterfaceMethod "Java::IfooJava_class".method:()V
   28:	return

}


似乎引入了jruby中特有的class格式,不知道是否由于这样导致"Java::IfooJava_class"加载出错

相关推荐

    JVM.rar_jvm_jvm 实现

    在这个简单的JVM实现中,`JVM.java`文件很可能是模拟了JVM的执行引擎部分。执行引擎负责解析和执行字节码,其核心操作就是堆栈操作。在Java字节码中,每条指令都会对操作数栈产生影响,比如`iadd`指令用于将栈顶的两...

    揭秘Java虚拟机-JVM设计原理与实现

    《揭秘Java虚拟机-JVM设计原理与实现》这本书深入探讨了Java虚拟机(JVM)的工作原理及其在Java编程中的核心地位。Java虚拟机是Java平台的核心组成部分,它负责执行字节码,为开发者提供了跨平台的运行环境。以下是...

    java之jvm学习笔记十而(访问控制器的栈校验机制)-步骤2源码

    4. **异常处理**:如果校验失败,JVM会抛出`VerifyError`异常,表示字节码验证过程中发现了不符合Java语言规范的情况。这通常意味着编译器出现了问题,或者有恶意代码试图绕过类型检查。 5. **多线程同步**:在多...

    JVM Hotspot实现源码

    《OpenJDK中的JVM Hotspot实现源码解析》 在Java世界中,JVM(Java Virtual Machine)是运行Java程序的关键组件,它负责将字节码解释执行或即时编译为机器码,使得Java具备跨平台的能力。Hotspot是Oracle JDK和...

    jvmjava,java实现的JVM。.zip

    “jvmjava”项目提供了一个实际的JVM实现,开发者可以参与其中,修改和调试代码,观察JVM如何处理各种情况。这有助于深入理解JVM的生命周期管理、异常处理、多线程等复杂概念。 五、优化与进阶 掌握JVM的工作原理...

    java之jvm学习笔记十二(访问控制器的栈校验机制)

    在Java虚拟机(JVM)中,访问控制器的栈校验机制是确保程序安全执行的重要环节。本篇学习笔记将深入探讨这一主题,主要关注Java字节码的执行过程以及如何通过栈校验来防止非法操作。 Java字节码是由Java编译器生成...

    java之jvm学习笔记十而(访问控制器的栈校验机制)

    栈校验机制是JVM在执行字节码时进行的一种静态类型检查。当方法调用、字段访问或数组操作等指令被执行时,JVM会检查操作数栈中的数据类型是否符合Java语言规范的要求。这种检查是基于每个操作的字节码描述来完成的,...

    jdk,jvm源码

    在研究JVM源码时,可以参考OpenJDK项目,它是JDK的开源实现,提供了完整的JVM源代码。通过阅读源码,我们可以学习到更多关于类加载、内存管理、线程调度等底层细节,并且能够针对具体问题进行定制化开发。 个人网站...

    jvm 启动过程 JVM 原理

    这个阶段不是必需的,可以在类或接口被首次使用时动态解析。 5. **初始化**:最后,JVM执行类的初始化方法(),这包括静态变量的初始化和静态代码块的执行。当且仅当类被首次主动使用时,才会进行初始化。 6. **...

    对象在jvm中的存储情况

    java对象在jvm中的存储情况 jvm

    推荐一些JVM原理,JVM调优,JVM内存模型,JAVA并发 电子书1

    标题中提到了JVM原理、JVM调优、JVM内存模型和JAVA并发,这些都是Java虚拟机(JVM)相关的核心概念。JVM是运行Java字节码的虚拟计算机,为Java提供了一个跨平台的环境,确保Java程序可以在不同的操作系统上运行而...

    java 查看JVM中所有的线程的活动状况

    本文将详细讲解如何查看JVM中的线程活动情况,并提供相关示例代码。 首先,Java提供了`java.lang.management.ThreadMXBean`接口,它是管理JVM线程的管理接口。通过这个接口,我们可以获取线程的各种信息,包括线程...

    jvm特性与java特性

    3. 安全性:JVM提供了一个相对安全的执行环境,通过类加载器和字节码校验器来确保加载到JVM中的类是安全的,以及字节码在运行时不会对系统造成危害。类加载机制允许JVM在运行时动态加载类文件,并提供运行时代码的...

    NeatJVM,java中的jvm实现.zip

    《深入解析NeatJVM:Java中的JVM实现》 在Java编程领域,JVM(Java虚拟机)是每一个开发者必须了解的关键组成部分。NeatJVM是一个开源项目,旨在为开发者提供一个清晰、简洁的Java虚拟机实现,帮助我们更好地理解和...

    mini-jvm使用 Java 8 实现 jvm

    在计算机科学领域,Java虚拟机(JVM)是Java平台的核心组成部分,它负责执行Java字节码,使得Java应用程序可以在任何支持JVM的平台上运行,实现“一次编写,到处运行”的理念。本文将深入探讨一个特别的项目——...

    深入理解jvm虚拟机

    理解JVM如何通过字节码和本地接口实现平台无关性,是理解Java跨平台原理的关键。 8. 线程调度和同步:JVM提供了内置的多线程支持,允许Java程序创建多个线程。JVM在执行多线程时,通过线程调度器对线程进行调度。...

    jvm 详细介绍,了解jvm各个组成部分和功能

    在典型的 JVM 实现中,存在一个简单的 CPU 架构模型,包括以下几个主要部分: - **PC(Program Counter)**:程序计数器,用于记录当前正在执行的指令的位置。 - **Operand Stack Pointer**:操作数栈指针,用于...

    jvm视频及笔记

    1. **JVM架构**:JVM主要由类装载器、运行时数据区、执行引擎、本地方法接口和本地库组成。了解每个部分的功能对于优化程序性能至关重要。 2. **类装载机制**:包括加载、验证、准备、解析和初始化五个阶段,确保类...

    JVM调优篇.pdf

    初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。 类构造器 初始化阶段是执行类构造...

    JVM线程状态和Thread.sleep的实现原理探究.pdf

    然而,尽管两者在实现上是相互对应的,但在状态定义上,JVM分别对操作系统线程和Java线程定义了不同的状态。这些状态不仅帮助Java程序员理解程序的运行情况,同时也为JVM的设计和优化提供了理论基础。 操作系统线程...

Global site tag (gtag.js) - Google Analytics