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

How can I control the order of constant pool entries using ASM?

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

人们可能还出于其他原因想要对常量池进行排序:例如,为了使一组 .class 文件更易于压缩,通过将它们的常量池按规范顺序排列,以测试消耗的工具.class 文件,将顺序用作软件水印,或混淆执行不佳的 decompilers/deobfuscators.

我在 ASM guide 中搜索了 "constant",但是除了对什么是常量池和 "Hopefully ASM hides all the details related to the constant pool, so you will not have to bother about it." 的一般解释外没有任何有用的命中,这对这种情况。

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

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

有些东西不起作用

ClassWriter 公开 newConst(以及其他常量池条目类型的变体)以允许实现自定义属性。因为 ASM 将重用常量池条目,您可能会假设可以通过调用 newConst 和朋友以您想要的顺序预填充常量池。然而,许多常量池条目引用其他常量池条目(特别是 Utf8 条目,它们被 String 和 Class 条目引用),如果引用的条目不存在,这些方法将自动添加。因此,例如,不可能将 String 常量放在它引用的 Utf8 之前。这些方法可以被重写,但这样做没有帮助,因为这种行为已经融入到它们委托给的包私有或私有方法中。

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

解决方案

解决方案需要两轮class写作。首先我们将正常写入 class(包括其他转换),然后使用另一个带有预填充常量池的 ClassWriter 来解析和重写第一轮的结果。因为 ClassWriter 构建常量池字节,所以我们必须在开始第二次解析和写入之前手动执行此操作。我们将把第二个 parse/write 封装在第一个 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 通常使用转换后的 class 文件运行,所以它至少大部分是正确的。 (该转换节省了 12684 个字节,这本身并不值得。)

代码是 available as a Gist,与 ASM 本身使用相同的许可证。