我正在实现一个转换,它会从.class文件中删除未使用的元素以减小它们的大小。因为一些常量池条目将被使用,我让ASM重新计算常量池,而不是从输入中复制它。但是,转换后的.class文件有时比原始文件大,因为ASM的常量池排序需要使用ldc_w
指令(带有2字节索引),其中输入.class文件使用ldc
(带有1) - 指数)。我想手动对常量池进行排序,以便ldc
引用的常量首先出现。
有人可能也想要出于其他原因对常量池进行排序:例如,通过将其常量池按规范顺序排列,使一组.class文件更易于压缩,以测试使用.class文件的工具,以便使用作为软件水印的顺序,或混淆执行不佳的反编译器/反混淆器。
我在ASM guide中搜索“常量”,但除了常量池的常规解释之外没有任何有用的命中,并且“希望ASM隐藏与常量池相关的所有细节,所以你会不必为此烦恼。“,在这种情况下,这是有帮助的。
如何控制ASM发出常量池条目的顺序?
答案 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本身具有相同的许可。