有没有一种聪明的方法来确定 Java 字节码指令的长度?

Is there a clever way to determine the length of Java bytecode instructions?

我正在为 Java 创建一个静态分析工具,并且有一些关于我正在分析的程序的信息,如果我可以从 [=11= 中的字节码中获取这些信息,将会更容易获得] 个文件。

我不关心 class 文件中可能存在的每个 instructions。例如,我可能只需要查看是否有任何 getfield 指令。

问题是因为每条指令的长度都是可变的,所以在一般情况下,我需要(在我的代码中)指定每个操作码的长度,然后才能确定(例如)getfield 指令开始和结束。

对于其他一些指令集(如x86),有像"any opcode below 0x0F is 1 byte, anything equal to or greater than 0x0F is two bytes."

这样的规则

Java 字节码指令中是否有像这样方便的模式?

The JVM spec指令集比较清楚:

The number and size of the operands are determined by the opcode.

您可以尝试利用现有的字节码库,例如 Apache Commons BCEL,并使用关于在那里定义的操作码的元数据为您的应用程序构建一个单独的数据结构。例如,it contains a GETFIELD class 以及表示 JVM 指令的 getLength() 方法。

字节码设计中没有这样的特性。操作码只是 grouped by their meaning. JVM implementations I've seen use table lookup for the bytecode length, with the special handling of wide, tableswitch and lookupswitch 字节码。

Such table 非常小:只有 202 个字节码。

注意长度不仅取决于操作码本身,还取决于字节码的位置tableswitchlookupswitch 由于对齐要求具有可变长度填充。

如果您尝试将指令操作码映射到指令大小,您会收到以下令人沮丧的消息 table:

0 - 15       1 bytes
16           2 bytes
17           3 bytes
18           2 bytes
19 - 20      3 bytes
21 - 25      2 bytes
26 - 53      1 bytes
54 - 58      2 bytes
59 - 131     1 bytes
132          3 bytes
133 - 152    1 bytes
153 - 168    3 bytes
169          2 bytes
170 - 171    special handling
172 - 177    1 bytes
178 - 184    3 bytes
185 - 186    5 bytes
187          3 bytes
188          2 bytes
189          3 bytes
190 - 191    1 bytes
192 - 193    3 bytes
194 - 195    1 bytes
196          special handling
197          4 bytes
198 - 199    3 bytes
200 - 201    5 bytes

换句话说,指令的数值及其位模式中没有编码的大小信息,但还有另一个 属性,您可以考虑某种模式:在定义的 ~200 之外指令,大约 150 条指令有一个字节的大小,只剩下大约 50 条指令需要任何处理。即使是这一小组指令也可以进一步细分为逻辑组,大多数占用三个字节,第二大组占用两个字节。

所以快速通过指令的方法代码可能如下所示:

static void readByteCode(ByteBuffer bb) {
    while(bb.hasRemaining()) {
        switch(bb.get()&0xff) {
            case BIPUSH: // one byte embedded constant
            case LDC:    // one byte embedded constant pool index
            // follow-up: one byte embedded local variable index
            case ILOAD:  case LLOAD:  case FLOAD:  case DLOAD:  case ALOAD:
            case ISTORE: case LSTORE: case FSTORE: case DSTORE: case ASTORE: case RET:
            case NEWARRAY: // one byte embedded array type
                bb.get();
                break;

            case IINC: // one byte local variable index, another one for the constant
            case SIPUSH: // two bytes embedded constant
            case LDC_W: case LDC2_W: // two bytes embedded constant pool index
            // follow-up: two bytes embedded branch offset
            case IFEQ: case IFNE: case IFLT: case IFGE: case IFGT: case IFLE:
            case IF_ICMPEQ: case IF_ICMPNE: case IF_ICMPLT: case IF_ICMPGE:
            case IF_ICMPGT: case IF_ICMPLE: case IF_ACMPEQ: case IF_ACMPNE:
            case GOTO: case JSR: case IFNULL: case IFNONNULL:
            // follow-up: two bytes embedded constant pool index to member or type
            case GETSTATIC: case PUTSTATIC: case GETFIELD: case PUTFIELD:
            case INVOKEVIRTUAL: case INVOKESPECIAL: case INVOKESTATIC: case NEW:
            case ANEWARRAY: case CHECKCAST: case INSTANCEOF:
                bb.getShort();
                break;

            case MULTIANEWARRAY:// two bytes pool index, one byte dimension
                bb.getShort();
                bb.get();
                break;

            // follow-up: two bytes embedded constant pool index to member, two reserved
            case INVOKEINTERFACE: case INVOKEDYNAMIC:
                bb.getShort();
                bb.getShort();
                break;

            case GOTO_W: case JSR_W:// four bytes embedded branch offset
                bb.getInt();
                break;

            case LOOKUPSWITCH:
                // special handling left as an exercise for the reader...
                break;
            case TABLESWITCH:
                // special handling left as an exercise for the reader...
                break;
            case WIDE:
                int widened=bb.get()&0xff;
                bb.getShort(); // local variable index
                if(widened==IINC) {
                    bb.getShort(); // constant offset value
                }
                break;
            default: // one of the ~150 instructions taking one byte
        }
    }
}

我故意将一些指令分开,后续字节数相同,但含义不同。毕竟,你想在某些地方插入一些实际的逻辑,我猜。

请注意,两个 switch 字节码指令的处理被省略,它们需要填充,其实现需要了解缓冲区内的代码对齐,这由调用者控制。所以这取决于你的具体应用。参考lookupswitch and tableswitch.

的文档

当然,将所有单字节指令处理为 default 意味着代码不会捕获未知或无效指令。如果你想要安全,你必须插入案例......