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

Jef*_*oom 6 java jvm java-bytecode-asm

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

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

我在ASM指南中搜索了"常量",但除了常量池的常规解释之外没有任何有用的命中,并且"希望ASM隐藏与常量池相关的所有细节,因此您不必为此烦恼",在这种情况下,这是有帮助的.

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

Jef*_*oom 8

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

有些东西不起作用

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

这篇文章建议在重载中对ClassWriter的内部数据结构进行排序visitEnd.这不起作用有两个原因.首先,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());
    }
}
Run Code Online (Sandbox Code Playgroud)

这是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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,这是你如何一起使用它们:

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();
Run Code Online (Sandbox Code Playgroud)

所应用的转换SomeOtherClassVisitor仅在第一次访问时发生,而不是在第二次访问中发生cw.toByteArray().

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

该代码作为Gist提供,与ASM本身具有相同的许可.