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