簡體   English   中英

是否可以使用switch語句無限循環?

[英]Is it possible to loop infinitely with a switch statement?

我正在編寫一個代碼質量工具。 我正在掃描源和編譯的類,搜索潛在的無限循環。

我不能認為源代碼切換語句的方式可以無限循環。 我錯了嗎?

Switch語句編譯為lookupswitchtableswitch操作碼。 出於安全原因,我需要檢查編譯類,並且在質量控制程序處理編譯的類之前還允許字節碼修改。 話雖如此,是否有可能通過修改類或使用匯編程序生成類來僅使用那些操作碼來無限循環?

我已經處理了所有其他分支指令和聲明。

非常感謝您的幫助。

編輯:結論:

正如我所懷疑的那樣,通過這里提供的答案,源代碼中的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 1.6之前的Java類沒有StackMapTable並且通過類型推斷進行驗證,而版本為1.7或更高版本的類通過類型檢查進行驗證, 類型檢查對包括lookupswitch在內的各個指令具有單獨的嚴格規則。

目前我不清楚這種指令是否實際上不允許在1.7+或ASM中生成錯誤的StackMapTable。


正如@Holger和@apangin所指出的,這可能是一個ASM錯誤,可以通過mv.visitLookupSwitchInsn(target, new int[]{1}, new Label[]{target});添加至少一個案例分支mv.visitLookupSwitchInsn(target, new int[]{1}, new Label[]{target}); 總而言之:是的,您可以使用任何字節碼版本在交換機中生成向后分支。

話雖如此,是否有可能通過修改類或使用匯編程序生成類來僅使用那些操作碼來無限循環?

要擁有一個無限循環,你必須向后某個地方。 如果修改字節代碼,則可以在添加或更改跳轉的位置執行此操作。 如果不是它不能是一個循環,無限或其他。

在字節碼級別,一切都是基本的。 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 s實際上是過時的,通過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

所以這個編譯器已經省略了其中一個過時的goto 但是可以想象另一個編譯器甚至可以在不改變語義的情況下消除其他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語句都是源代碼工件,而不是強制要求特定的字節代碼形式。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM