是否可以用 switch 语句无限循环?

Is it possible to loop infinitely with a switch statement?

我正在编写代码质量工具。我正在扫描源代码并编译 classes 搜索潜在的无限循环。

我想不出源代码 switch 语句可以无限循环的方法。我错了吗?

Switch 语句编译为 lookupswitchtableswitch 操作码。出于安全原因,我将需要检查编译 classes,并且在质量控制程序处理编译的 classes 之前允许修改字节码。 话虽如此,是否可以通过修改 class 或使用汇编程序生成它来仅使用那些操作码来无限循环?

我已经处理了所有其他分支指令和语句。

非常感谢您的帮助。

编辑: 结论:

正如我所怀疑的那样,根据此处提供的答案,源代码中的 switch 语句只能向前分支,但字节码中的任何分支指令都可能向后跳转(假设字节码修改)。

Having say that, is there a possible way of looping infinitely by using only those opcodes by modifying a class or generating it with assembler?

要有一个无限循环,你必须在某个地方倒退。如果您修改字节码,这可能会在您添加或更改跳转以返回的任何地方发生。如果不是,它就不能是一个循环,无限或其他。

在字节码级别,基本上一切都是gotos。 tableswitch 或 lookupswitch 指令只是要跳转到的偏移量列表。如果你愿意,你可以让它向后跳。你不能让它直接跳转到它自己,但这只是因为它每次都会从堆栈中弹出一个 int。如果你在它前面加上一个 int push,你可以有一个 2 指令循环。

考虑以下源代码:

public static void main(String... arg) {
    loop: for(;;) switch(arg.length) {
        case 0: continue;
        default: break loop;
    }
}

用Oracle的javac(jdk1.8)编译时,会得到

public static void main(java.lang.String...)
  Code:
     0: aload_0
     1: arraylength
     2: lookupswitch  { // 1
                   0: 20
             default: 23
        }
    20: goto          0
    23: goto          26
    26: return

这显然是一个直截了当的翻译,但这个结果并不是强制性的。最后的 goto 实际上已经过时了,通过使用 Eclipse 4.4.2 进行编译,我得到:

public static void main(java.lang.String...) t
  Code:
     0: aload_0
     1: arraylength
     2: tableswitch   { // 0 to 0
                   0: 20
             default: 23
        }
    20: goto          0
    23: return

所以这个编译器已经省略了这些过时的 gotos 之一。但是可以想象,另一个编译器甚至可以在不改变语义的情况下也消除另一个 goto

public static void main(java.lang.String...) t
  Code:
     0: aload_0
     1: arraylength
     2: tableswitch   { // 0 to 0
                   0: 0
             default: 20
        }
    20: return

还可以想象,字节码优化工具能够采用前一种结果并将其转换为第三种变体。由于这一切都没有改变代码的语义,它仍然反映了上面显示的有效 Java 源代码。

所以有一个 switch 字节码指令产生一个循环并不一定代表在 Java 源代码中不可重现的逻辑。它只是一个编译器实现依赖 属性 当他们从不产生这样的结构而是产生更多冗余代码时。请始终记住,while/for 循环和 switch 语句都是源代码工件,并不强制要求特定的字节代码形式。

有趣的是,您可以使用字节码版本 1.6 (50) 执行此操作,但不能使用字节码版本 1.7 (51),因为验证失败。此代码(需要 ASM5)工作正常并具有无限循环:

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;

public class LookupTest {
    public static void main(String[] args) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        new ClassLoader() {
            @Override
            protected Class<?> findClass(String name)
                    throws ClassNotFoundException {
                ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS
                        | ClassWriter.COMPUTE_FRAMES);
                // Create public class extending java.lang.Object
                cw.visit(V1_6, ACC_PUBLIC | ACC_SUPER, name, null,
                        "java/lang/Object", null);
                // Create default constructor
                MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V",
                        null, null);
                mv.visitCode();
                // Call superclass constructor (this is required)
                mv.visitVarInsn(ALOAD, 0);
                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>",
                        "()V", false);
                // Create branch target
                Label target = new Label();
                mv.visitLabel(target);
                // System.out.println("Hello");
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
                        "Ljava/io/PrintStream;");
                mv.visitLdcInsn("Hello");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream",
                        "println", "(Ljava/lang/String;)V", false);
                // switch(0) {
                mv.visitInsn(ICONST_0);
                // default: goto target;
                // }
                mv.visitLookupSwitchInsn(target, new int[0], new Label[0]);
                mv.visitMaxs(-1, -1);
                mv.visitEnd();
                cw.visitEnd();
                byte[] bytes = cw.toByteArray();
                return defineClass(name, bytes, 0, bytes.length);
            }
        }.loadClass("LookupGotoTest").newInstance();
    }
}

但是,如果您将 V1_6 替换为 V1_7,它将失败并出现以下错误:

Exception in thread "main" java.lang.VerifyError: Bad instruction
Exception Details:
  Location:
    LookupGotoTest.<init>()V @13: lookupswitch
  Reason:
    Error exists in the bytecode
  Bytecode:
    0x0000000: 2ab7 0008 b200 0e12 10b6 0016 03ab 0000
    0x0000010: ffff fff7 0000 0000                    
  Stackmap Table:
    full_frame(@4,{Object[#2]},{})

    at java.lang.Class.getDeclaredConstructors0(Native Method)
    at java.lang.Class.privateGetDeclaredConstructors(Class.java:2658)
    at java.lang.Class.getConstructor0(Class.java:3062)
    at java.lang.Class.newInstance(Class.java:403)
    at LookupTest.main(LookupTest.java:46)

但是,如果我改为向前跳转并添加 goto 指令,即使使用 1.7 字节码也能正常工作:

Label target2 = new Label();
// switch(0) {
mv.visitInsn(ICONST_0);
// default: goto target2;
// }
mv.visitLookupSwitchInsn(target2, new int[0], new Label[0]);
mv.visitLabel(target2);
// goto target
mv.visitJumpInsn(GOTO, target);

由于验证程序不同而出现差异:Java 类 之前 Java 1.6 没有 StackMapTable 并通过 Type Inference, while classes with version 1.7 or higher are verified by Type Checking which has separate strict rules for individual instructions including lookupswitch.

验证

目前我不清楚这样的指令是否在 1.7+ 中实际上是不允许的,或者 ASM 只是生成了不正确的 StackMapTable。


正如@Holger 和@apangin 指出的那样,这可能是一个 ASM 错误,可以解决通过 mv.visitLookupSwitchInsn(target, new int[]{1}, new Label[]{target}); 添加至少一个 case 分支的问题。所以总而言之:是的,您可以使用任何字节码版本在 switch 中生成后向分支。