ฉันจะควบคุมลำดับของรายการพูลคงที่โดยใช้ ASM ได้อย่างไร

ฉันกำลังใช้การแปลงที่จะลบองค์ประกอบที่ไม่ได้ใช้ออกจากไฟล์ .class เพื่อลดขนาด เนื่องจากรายการพูลคงที่บางรายการจะไม่ได้ใช้ ฉันจึงให้ ASM คำนวณพูลค่าคงที่ใหม่ แทนที่จะคัดลอกจากอินพุต อย่างไรก็ตาม ไฟล์ .class ที่ถูกแปลงบางครั้งอาจมีขนาดใหญ่กว่าไฟล์ต้นฉบับ เนื่องจากการเรียงลำดับพูลคงที่ของ ASM ต้องใช้คำสั่ง ldc_w (พร้อมดัชนี 2 ไบต์) โดยที่ไฟล์ .class อินพุตใช้ ldc (พร้อมดัชนี 1 ไบต์) ฉันต้องการเรียงลำดับพูลค่าคงที่ด้วยตนเองโดยให้ค่าคงที่ที่อ้างอิงโดย ldc มาก่อน

เราอาจต้องการเรียงลำดับพูลคงที่ด้วยเหตุผลอื่นๆ เช่นกัน ตัวอย่างเช่น เพื่อให้ชุดของไฟล์ .class บีบอัดได้มากขึ้นโดยการวางพูลคงที่ตามลำดับแบบบัญญัติ เพื่อทดสอบเครื่องมือที่ใช้ไฟล์ .class เพื่อใช้ลำดับเป็น ลายน้ำของซอฟต์แวร์ หรือเพื่อสร้างความสับสนให้กับตัวถอดรหัส/ตัวถอดรหัสที่สร้างความสับสนซึ่งมีการใช้งานไม่ดี

ฉันเข้าไปดู คำแนะนำ ASM สำหรับ "คงที่" แต่ไม่มี Hit ที่มีประโยชน์นอกจากคำอธิบายทั่วไปว่าพูลคงที่คืออะไร และ "หวังว่า ASM จะซ่อนรายละเอียดทั้งหมดที่เกี่ยวข้องกับพูลคงที่ ดังนั้นคุณจะไม่ต้อง รบกวนด้วย" ซึ่งไม่เป็นประโยชน์ในกรณีนี้

ฉันจะควบคุมลำดับที่ ASM ส่งรายการพูลคงที่ได้อย่างไร


person Jeffrey Bosboom    schedule 16.03.2015    source แหล่งที่มา
comment
จุดที่น่าสนใจ ฉันคิดว่าถ้าปรัชญาของ ASM คือโปรแกรมเมอร์ไม่จำเป็นต้องดูแล ก็ควรพยายามหลีกเลี่ยงดัชนีกว้างที่ไม่จำเป็น ดังนั้นการยื่นคำขอคุณลักษณะอาจเป็นวิธีที่ถูกต้อง...   -  person Holger    schedule 16.03.2015


คำตอบ (1)


ASM ไม่ได้จัดเตรียมวิธีที่สะอาดตาในการทำเช่นนี้ แต่เป็นไปได้หากคุณยินดีที่จะกำหนดคลาสใหม่ในแพ็คเกจ org.objectweb.asm (หรือใช้การสะท้อนกลับเพื่อเข้าถึงสมาชิกแพ็คเกจส่วนตัว) สิ่งนี้ไม่เหมาะเพราะจะทำให้ต้องอาศัยรายละเอียดการใช้งานของ ASM แต่เป็นสิ่งที่ดีที่สุดที่เราสามารถทำได้ (หากคุณทราบวิธีที่ไม่แฮ็ก โปรดเพิ่มเป็นคำตอบอื่น)

บางสิ่งที่ไม่ได้ผล

ClassWriter เปิดเผย newConst (และตัวแปรสำหรับประเภทรายการพูลคงที่อื่นๆ) เพื่ออนุญาตให้นำแอตทริบิวต์ที่กำหนดเองไปใช้ เนื่องจาก ASM จะนำรายการพูลคงที่กลับมาใช้ใหม่ คุณอาจถือว่าคุณสามารถเติมพูลคงที่ล่วงหน้าตามลำดับที่คุณต้องการได้โดยการโทร newConst และเพื่อนๆ อย่างไรก็ตาม รายการพูลคงที่จำนวนมากอ้างอิงถึงรายการพูลคงที่อื่นๆ (โดยเฉพาะรายการ Utf8 ซึ่งอ้างอิงโดยรายการสตริงและคลาส) และวิธีการเหล่านี้จะเพิ่มรายการอ้างอิงโดยอัตโนมัติหากไม่มีอยู่ ดังนั้นจึงเป็นไปไม่ได้ที่จะใส่ค่าคงที่ String ก่อน Utf8 ที่อ้างอิงถึง เป็นต้น วิธีการเหล่านี้สามารถแทนที่ได้ แต่การทำเช่นนี้ไม่ได้ช่วยอะไร เนื่องจากลักษณะการทำงานนี้ถูกอบเข้าสู่วิธีการแพ็คเกจแบบส่วนตัวหรือแบบส่วนตัวที่พวกเขามอบหมายให้

โพสต์นี้ แนะนำให้เรียงลำดับโครงสร้างข้อมูลภายในของ ClassWriter ในการโอเวอร์โหลด visitEnd. สิ่งนี้ใช้ไม่ได้ด้วยเหตุผลสองประการ ประการแรก visitEnd ถือเป็นที่สิ้นสุด (บางทีอาจไม่ได้ย้อนกลับไปในปี 2548 เมื่อมีการเขียนโพสต์นั้น) ประการที่สอง ClassWriter ปล่อยไบต์ของคลาสในระหว่างการเยี่ยมชม ดังนั้นเมื่อถึงเวลาเรียก visitEnd พูลคงที่จะถูกเขียนเป็นไบต์แล้ว และดัชนีพูลคงที่จะถูกรวมเข้าเป็นไบต์ของโค้ดแล้ว

การแก้ไขปัญหา

การแก้ปัญหานี้ต้องใช้การเขียนในชั้นเรียนสองรอบ ขั้นแรก เราจะเขียนคลาสตามปกติ (รวมถึงการแปลงอื่นๆ) จากนั้นใช้ ClassWriter อื่นที่มีพูลคงที่ที่เติมไว้ล่วงหน้าเพื่อแยกวิเคราะห์และเขียนผลลัพธ์ของรอบแรกใหม่ เนื่องจาก ClassWriter สร้างไบต์พูลคงที่ในขณะที่ทำงาน เราจึงต้องดำเนินการด้วยตนเองก่อนที่จะเริ่มแยกวิเคราะห์และเขียนครั้งที่สอง เราจะสรุปการแยกวิเคราะห์/เขียนครั้งที่สองในเมธอด toByteArray ของ ClassWriter ตัวแรก

นี่คือรหัส การเรียงลำดับจริงเกิดขึ้นในวิธี 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()

ไม่มีชุดทดสอบสำหรับสิ่งนี้ แต่ฉันใช้การเรียงลำดับข้างต้นกับ rt.jar จาก Oracle JDK 8u40 และ NetBeans 8.0.2 ทำงานได้ตามปกติโดยใช้ไฟล์คลาสที่แปลงแล้ว ดังนั้นอย่างน้อยที่สุดก็ถูกต้องเป็นส่วนใหญ่ (การแปลงบันทึกได้ 12,684 ไบต์ ซึ่งแทบจะไม่คุ้มค่าเลย)

รหัส มีให้ใช้งานในรูปแบบ Gist ภายใต้ใบอนุญาตเดียวกันกับ ASM เอง

person Jeffrey Bosboom    schedule 16.03.2015