`
RednaxelaFX
  • 浏览: 3013740 次
  • 性别: 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"加载出错

相关推荐

Global site tag (gtag.js) - Google Analytics