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

标签 java jvm java-bytecode-asm

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

人们可能还出于其他原因想要对常量池进行排序:例如,通过将常量池按规范顺序放置来使一组 .class 文件更可压缩,测试使用 .class 文件的工具,使用顺序作为软件水印,或混淆实现不当的反编译器/反混淆器。

我 grep 了 ASM guide对于“常量”,但除了对常量池是什么的一般解释和“希望 ASM 隐藏与常量池相关的所有细节,这样你就不必为它烦恼”之外,没有任何有用的点击,这是反的-在这种情况下很有帮助。

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

最佳答案

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

一些不起作用的东西

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

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 本身相同的许可下。

关于java - 如何使用 ASM 控制常量池条目的顺序?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29079192/

相关文章:

java - 面板重绘方法未更新

java - 从mysql中提取unicode文本到java

memory-management - 分配后TLAB分配的对象是否共享?

Java,比较3个整数,排列最大,中值和最小

java - 有没有一种轻量级的方法可以在Java 9+中添加安全点

java - 如何使用 ASM 库将二进制指令映射回语句或表达式?

java - 我们如何将 play.libs.concurrent.HttpExecutionContext 传递给 Java 8 中的并行流?

java - 在网站中嵌入 DWG 文件

java - 无法理解在 Java 中使用 ASM 字节码重命名的方法

java - stackmap 帧的使用及其如何帮助字节码验证?