如何使用ASM控制常量池条目的顺序?

时间:2015-03-16 14:22:39

标签: java jvm java-bytecode-asm

我正在实现一个转换,它会从.class文件中删除未使用的元素以减小它们的大小。因为一些常量池条目将被使用,我让ASM重新计算常量池,而不是从输入中复制它。但是,转换后的.class文件有时比原始文件大,因为ASM的常量池排序需要使用ldc_w指令(带有2字节索引),其中输入.class文件使用ldc(带有1) - 指数)。我想手动对常量池进行排序,以便ldc引用的常量首先出现。

有人可能也想要出于其他原因对常量池进行排序:例如,通过将其常量池按规范顺序排列,使一组.class文件更易于压缩,以测试使用.class文件的工具,以便使用作为软件水印的顺序,或混淆执行不佳的反编译器/反混淆器。

我在ASM guide中搜索“常量”,但除了常量池的常规解释之外没有任何有用的命中,并且“希望ASM隐藏与常量池相关的所有细节,所以你会不必为此烦恼。“,在这种情况下,这是有帮助的。

如何控制ASM发出常量池条目的顺序?

1 个答案:

答案 0 :(得分:7)

ASM没有提供干净的方法来执行此操作,但如果您愿意在org.objectweb.asm包中定义新类(或使用反射来访问包私有成员),则可能会这样做。这并不理想,因为它引入了对ASM实现细节的依赖,但它是我们能做的最好的。 (如果您知道非黑客的方法,请将其添加为另一个答案。)

有些不起作用的东西

ClassWriter公开newConst(以及其他常量池条目类型的变体)以允许实现自定义属性。由于ASM将重用常量池条目,因此您可以假设您可以通过调用newConst和朋友来按预期顺序预填充常量池。但是,许多常量池条目引用其他常量池条目(特别是由String和Class条目引用的Utf8条目),并且这些方法将自动添加引用的条目(如果尚未存在)。因此,例如,不可能在它引用的Utf8之前放置一个String常量。可以覆盖这些方法,但这样做无济于事,因为这种行为被烘焙到它们委托给的包私有或私有方法中。

This post建议在重载的visitEnd中对ClassWriter的内部数据结构进行排序。这不起作用有两个原因。首先,visitEnd是最终的(也许它不是在2005年写的那篇文章时)。其次,ClassWriter在访问期间发出类字节,因此在调用visitEnd时,常量池已经被写为字节,而常量池索引已经被编码为代码字节。

解决方案

解决方案需要两轮课堂写作。首先我们将正常编写类(包括其他转换),然后使用另一个带有预填充常量池的ClassWriter来解析和重写第一轮的结果。因为ClassWriter构建了常量池字节,所以我们必须在开始第二次解析和写入之前手动完成。我们将第二个解析/写入封装在第一个ClassWriter的toByteArray方法中。

这是代码。实际排序发生在sortItems方法中;这里我们按出现次数排序为ldc / ldc_w操作数(由MethodVisitor收集;请注意visitMethod是最终的,因此它必须是独立的)。 如果您要实施其他排序,更改sortItems并添加字段以存储您的排序所依据的任何内容。

package org.objectweb.asm;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

public class ConstantPoolSortingClassWriter extends ClassWriter {
    private final int flags;
    Map<Item, Integer> constantHistogram; //initialized by ConstantHistogrammer
    public ConstantPoolSortingClassWriter(int flags) {
        super(flags);
        this.flags = flags;
    }

    @Override
    public byte[] toByteArray() {
        byte[] bytes = super.toByteArray();

        List<Item> cst = new ArrayList<>();
        for (Item i : items)
            for (Item j = i; j != null; j = j.next) {
                //exclude ASM's internal bookkeeping
                if (j.type == TYPE_NORMAL || j.type == TYPE_UNINIT ||
                        j.type == TYPE_MERGED || j.type == BSM)
                    continue;
                if (j.type == CLASS) 
                    j.intVal = 0; //for ASM's InnerClesses tracking
                cst.add(j);
            }

        sortItems(cst);

        ClassWriter target = new ClassWriter(flags);
        //ClassWriter.put is private, so we have to do the insert manually
        //we don't bother resizing the hashtable
        for (int i = 0; i < cst.size(); ++i) {
            Item item = cst.get(i);
            item.index = target.index++;
            if (item.type == LONG || item.type == DOUBLE)
                target.index++;

            int hash = item.hashCode % target.items.length;
            item.next = target.items[hash];
            target.items[hash] = item;
        }

        //because we didn't call newFooItem, we need to manually write pool bytes
        //we can call newFoo to find existing items, though
        for (Item i : cst) {
            if (i.type == UTF8)
                target.pool.putByte(UTF8).putUTF8(i.strVal1);
            if (i.type == CLASS || i.type == MTYPE || i.type == STR)
                target.pool.putByte(i.type).putShort(target.newUTF8(i.strVal1));
            if (i.type == IMETH || i.type == METH || i.type == FIELD)
                target.pool.putByte(i.type).putShort(target.newClass(i.strVal1)).putShort(target.newNameType(i.strVal2, i.strVal3));
            if (i.type == INT || i.type == FLOAT)
                target.pool.putByte(i.type).putInt(i.intVal);
            if (i.type == LONG || i.type == DOUBLE)
                target.pool.putByte(i.type).putLong(i.longVal);
            if (i.type == NAME_TYPE)
                target.pool.putByte(i.type).putShort(target.newUTF8(i.strVal1)).putShort(target.newUTF8(i.strVal2));
            if (i.type >= HANDLE_BASE && i.type < TYPE_NORMAL) {
                int tag = i.type - HANDLE_BASE;
                if (tag <= Opcodes.H_PUTSTATIC)
                    target.pool.putByte(HANDLE).putByte(tag).putShort(target.newField(i.strVal1, i.strVal2, i.strVal3));
                else
                    target.pool.putByte(HANDLE).putByte(tag).putShort(target.newMethod(i.strVal1, i.strVal2, i.strVal3, tag == Opcodes.H_INVOKEINTERFACE));
            }
            if (i.type == INDY)
                target.pool.putByte(INDY).putShort((int)i.longVal).putShort(target.newNameType(i.strVal1, i.strVal2));
        }

        //parse and rewrite with the new ClassWriter, constants presorted
        ClassReader r = new ClassReader(bytes);
        r.accept(target, 0);
        return target.toByteArray();
    }

    private void sortItems(List<Item> items) {
        items.forEach(i -> constantHistogram.putIfAbsent(i, 0));
        //constants appearing more often come first, so we use as few ldc_w as possible
        Collections.sort(items, Comparator.comparing(constantHistogram::get).reversed());
    }
}

这是ConstantHistogrammer,位于org.objectweb.asm,因此它可以引用Item。此实现特定于ldc排序,但它演示了如何根据.class文件中的信息执行其他自定义排序。

package org.objectweb.asm;

import java.util.HashMap;
import java.util.Map;

public final class ConstantHistogrammer extends ClassVisitor {
    private final ConstantPoolSortingClassWriter cw;
    private final Map<Item, Integer> constantHistogram = new HashMap<>();
    public ConstantHistogrammer(ConstantPoolSortingClassWriter cw) {
        super(Opcodes.ASM5, cw);
        this.cw = cw;
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return new CollectLDC(super.visitMethod(access, name, desc, signature, exceptions));
    }
    @Override
    public void visitEnd() {
        cw.constantHistogram = constantHistogram;
        super.visitEnd();
    }
    private final class CollectLDC extends MethodVisitor {
        private CollectLDC(MethodVisitor mv) {
            super(Opcodes.ASM5, mv);
        }
        @Override
        public void visitLdcInsn(Object cst) {
            //we only care about things ldc can load
            if (cst instanceof Integer || cst instanceof Float || cst instanceof String ||
                    cst instanceof Type || cst instanceof Handle)
                constantHistogram.merge(cw.newConstItem(cst), 1, Integer::sum);
            super.visitLdcInsn(cst);
        }
    }
}

最后,以下是您如何一起使用它们的方法:

byte[] inputBytes = Files.readAllBytes(input);
ClassReader cr = new ClassReader(inputBytes);
ConstantPoolSortingClassWriter cw = new ConstantPoolSortingClassWriter(0);
ConstantHistogrammer ch = new ConstantHistogrammer(cw);
ClassVisitor s = new SomeOtherClassVisitor(ch);
cr.accept(s, 0);
byte[] outputBytes = cw.toByteArray();

SomeOtherClassVisitor应用的转化只会在第一次访问时发生,而不会在cw.toByteArray()内的第二次访问时发生。

没有针对此的测试套件,但我将上述排序应用于Oracle JDK 8u40中的rt.jar,而NetBeans 8.0.2通常使用转换后的类文件,因此它至少大部分都是正确的。 (转换保存了12684个字节,这本身就不值得。)

代码为available as a Gist,与ASM本身具有相同的许可。