Java字节码asm – 如何创建一个仅更改类名的类的克隆?

huangapple go评论67阅读模式
英文:

Java bytecode asm - How can I create a clone of a class with only the class name changed?

问题

Java asm - 我如何创建一个类的克隆,只改变类名?

我知道使用 asm SimpleRemapper 可以简单地修改类名,但我只想更改外部类名,而不修改方法中使用的类名(请参见下面的示例)

基本上,如果我有一个目标类

public class Target {
  public Target clone(...) ...
  public int compare(another: Target) ...
}

我只想创建一个看起来像这样的克隆:

public class ClonedTarget {
  public Target clone(...) ...
  public int compare(another: Target) ...
}

(请注意,clone 的返回类型和 compare 的参数类型没有更改。这是我的用例有意为之)。

英文:

Java asm - How can I create a clone of a class with only the class name changed ?

I know that there's a simple way to modify the class name using asm SimpleRemapper, but I just want the outer class name changed without modifying the class names used in the methods. (please see below example)

Basically if I have a target class

public class Target {
  public Target clone(...) ...
  public int compare(another: Target) ...
}

I just wanted to create a clone that looks as:

public class ClonedTarget {
  public Target clone(...) ...
  public int compare(another: Target) ...
}

(Note that the return type of clone and arg type of compare hasn't changed. This is intentional for my use case).

答案1

得分: 5

使用ASM API克隆一个类并仅更改名称,即保留其他类引用不变,实际上非常容易。

ClassReader cr = new ClassReader(Target.class.getResourceAsStream("Target.class"));
ClassWriter cw = new ClassWriter(cr, 0);
cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
    @Override
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        super.visit(version, access, "ClonedTarget", signature, superName, interfaces);
    }
}, 0);
byte[] code = cw.toByteArray();

当将ClassReaderClassWriter链接在一起时,中间的ClassVisitor只需覆盖它想要更改的构件对应的方法。因此,为了仅更改名称而不做其他更改,我们只需要重写类声明的visit方法,并将不同的名称传递给super方法。

通过将类阅读器传递给类编写器的构造函数,我们甚至表明将只进行少量更改,从而启用转换过程的后续优化,即大部分常量池以及方法的代码将只是复制过来。


值得考虑的是,从字节码层面上看,构造函数具有特殊的名称<init>,因此它们在生成的类中仍然是构造函数,无论其名称如何。继续在生成的类中调用超类构造函数的普通构造函数可能会继续工作。

ClonedTarget对象上调用实例方法时,this引用的类型为ClonedTarget。这个基本属性不需要声明,因此在这方面不需要适应任何声明。

问题在于原始代码假定this的类型是Target,并且由于没有进行任何适应,复制的代码仍然错误地假定this的类型是Target,这可能以各种方式导致问题。

考虑以下情况:

public class Target {
  public Target clone() { return new Target(); }
  public int compare(Target t) { return 0;}
}

这看起来似乎不受影响。生成的默认构造函数只调用super(),并将继续工作。compare方法具有未使用的参数类型,保持不变。而clone()方法实例化Target(未更改)并返回它,与返回类型Target(未更改)相匹配。似乎没问题。

但在这里看不到的是,clone方法覆盖了从java.lang.Object继承的Object clone()方法,因此将生成一个桥接方法。这个桥接方法将具有Object clone()的声明,并只委托给Target clone()方法。问题在于这个委托是对this的调用,调用目标的假定类型编码在调用指令中。这将导致VerifierError

通常情况下,我们不能简单地区分哪些调用是在this上应用的,哪些是在未更改的引用上应用的,比如参数或字段。甚至不需要一个确定的答案。考虑以下情况:

public void method(Target t, boolean b) {
    (b? this: t).otherMethod();
}

隐式地假设this的类型为Target,它可以在this和来自其他源的Target实例之间互换使用。我们不能更改this的类型并保留参数类型,而不重写代码。

可见性方面还会出现其他问题。对于重命名的类,验证器将拒绝对原始类的未更改访问的私有成员。

除了出现VerifyError外,有问题的代码可能会通过,并在以后的某个时间引发问题。考虑以下情况:

public class Target implements Cloneable {
    public Target duplicate() {
        try {
            return (Target)super.clone();
        } catch(CloneNotSupportedException ex) {
            throw new AssertionError();
        }
    }
}

由于这个duplicate()方法没有覆盖超类方法,所以不会生成桥接方法,而且验证器从技术上来说认为所有未更改的Target使用都是正确的。

Objectclone()方法返回的不是Target的实例,而是重命名后的克隆中的this类,即ClonedTarget。因此,当执行时,这将失败并引发ClassCastException


这并不排除对已知内容的类的工作用例。但一般而言,这是非常脆弱的。

英文:

Cloning a class and changing the name and only the name, i.e. leave every other class reference as-is, is actually very easy with the ASM API.

ClassReader cr = new ClassReader(Target.class.getResourceAsStream(&quot;Target.class&quot;));
ClassWriter cw = new ClassWriter(cr, 0);
cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
    @Override
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        super.visit(version, access, &quot;ClonedTarget&quot;, signature, superName, interfaces);
    }
}, 0);
byte[] code = cw.toByteArray();

When chaining a ClassReader with a ClassWriter, the ClassVisitor in the middle only needs to overwrite those methods corresponding to an artifact it wants to change. So, to change the name and nothing else, we only need to override the visit method for the class’ declaration and pass a different name to the super method.

By passing the class reader to the class writer’s constructor, we’re even denoting that only little changes will be made, enabling subsequent optimizations of the transform process, i.e. most of the constant pool, as well as the code of the methods, will just get copied here.


It’s worth considering the implications. On the bytecode level, constructors have the special name &lt;init&gt;, so they keep being constructors in the resulting class, regardless of its name. Trivial constructors calling a superclass constructor may continue to work in the resulting class.

When invoking instance methods on ClonedTarget objects, the this reference has the type ClonedTarget. This fundamental property does not need to be declared and thus, there is no declaration that needs adaptation in this regard.

Herein lies the problem. The original code assumes that this is of type Target and since nothing has been adapted, the copied code still wrongly assumes that this is of type Target, which can break in various ways.

Consider:

public class Target {
  public Target clone() { return new Target(); }
  public int compare(Target t) { return 0;}
}

This looks like not being affected by the issue. The generated default constructor just calls super() and will continue to work. The compare method has an unused parameter type left as-is. And the clone() method instantiates Target (unchanged) and returns it, matching the return type Target (unchanged). Seems fine.

But what’s not visible here, the clone method overrides the method Object clone() inherited from java.lang.Object and therefore, a bridge method will be generated. This bridge method will have the declaration Object clone() and just delegate to the Target clone() method. The problem is that this delegation is an invocation on this and the assumed type of the invocation target is encoded within the invocation instruction. This will cause a VerifierError.

Generally, we can not simply tell apart which invocations are applied on this and which on an unchanged reference, like a parameter or field. It does not even need to have a definite answer. Consider:

public void method(Target t, boolean b) {
    (b? this: t).otherMethod();
}

Implicitly assuming that this has type Target, it can use this and a Target instance from another source interchangeably. We can not change the this type and keep the parameter type without rewriting the code.

Other issues arise with visibility. For the renamed class, the verifier will reject unchanged accesses to private members of the original class.

Besides failing with a VerifyError, problematic code may slip through and cause problems at a later time. Consider:

public class Target implements Cloneable {
    public Target duplicate() {
        try {
            return (Target)super.clone();
        } catch(CloneNotSupportedException ex) {
            throw new AssertionError();
        }
    }
}

Since this duplicate() does not override a superclass method, there won’t be a bridge method and all unchanged uses of Target are correct from the verifier’s perspective.

But the clone() method of Object does not return an instance of Target but of the this’ class, ClonedTarget in the renamed clone. So this will fail with a ClassCastException, only when being executed.


This doesn’t preclude working use cases for a class with known content. But generally, it’s very fragile.

huangapple
  • 本文由 发表于 2020年8月9日 03:53:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/63319665.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定