Java逆向基礎之條件跳轉位運算循環
條件跳轉的例子,絕對值
public class abs { public static int abs(int a) { if (a<0) return -a; return a; } }
編譯
javac abs.java
反編譯
javap -c -verbose abs.class
public static int abs(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: ifge 7 4: iload_0 5: ineg 6: ireturn 7: iload_0 8: ireturn
其中ifge 7 意思是,當棧頂的值大於等於0的時候跳轉到偏移位7,任何的ifXX指令都會將棧中的值彈出用於進行比較
其中ineg 意思是將棧頂int型數值取負並將結果壓入棧頂
再看一個例子,取最小值
public class min { public static int min (int a, int b) { if (a>b) return b; return a; } }
反編譯
public static int min(int, int); descriptor: (II)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=2 0: iload_0 1: iload_1 2: if_icmple 7 5: iload_1 6: ireturn 7: iload_0 8: ireturn
if_icmple會從棧中彈出兩個值進行比較,如果第二個(a,iload_0)小於或者等於第一個(b,iload_1),那麽跳轉到偏移位7
從上面例子可以看出在Java代碼中if條件中的測試與在字節碼中是完全相反的
max函數例子
public class max { public static int max (int a, int b) { if (a>b) return a; return b; } }
反編譯
public static int max(int, int); descriptor: (II)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=2 0: iload_0 1: iload_1 2: if_icmple 7 5: iload_0 6: ireturn 7: iload_1 8: ireturn
代碼和min的差不多,唯一的區別是最後兩個iload指令(偏移位5和偏移位7)互換了
更復雜的例子
public class cond { public static void f(int i) { if (i<100) System.out.print("<100"); if (i==100) System.out.print("==100"); if (i>100) System.out.print(">100"); if (i==0) System.out.print("==0"); } }
反編譯
public static void f(int); descriptor: (I)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: iload_0 1: bipush 100 3: if_icmpge 14 6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 9: ldc #3 // String <100 11: invokevirtual #4 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 14: iload_0 15: bipush 100 17: if_icmpne 28 20: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 23: ldc #5 // String ==100 25: invokevirtual #4 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 28: iload_0 29: bipush 100 31: if_icmple 42 34: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 37: ldc #6 // String >100 39: invokevirtual #4 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 42: iload_0 43: ifne 54 46: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 49: ldc #7 // String ==0 51: invokevirtual #4 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 54: return
if_icmpge 14 棧頂彈出兩個值,並且比較兩個數值,如果第的二個值大於或等於第一個,跳轉到偏移位14
if_icmpne 28 棧頂彈出兩個值,並且比較兩個數值,如果第的二個值不等於第一個,跳轉到偏移位28
ifne 54 當棧頂int型數值不等於0時跳轉到偏移位54
傳參例子
public class minmax { public static int min (int a, int b) { if (a>b) return b; return a; } public static int max (int a, int b) { if (a>b) return a; return b; } public static void main(String[] args) { int a=123, b=456; int max_value=max(a, b); int min_value=min(a, b); System.out.println(min_value); System.out.println(max_value); } }
反編譯
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=5, args_size=1 0: bipush 123 2: istore_1 3: sipush 456 6: istore_2 7: iload_1 8: iload_2 9: invokestatic #2 // Method max:(II)I 12: istore_3 13: iload_1 14: iload_2 15: invokestatic #3 // Method min:(II)I 18: istore 4 20: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 23: iload 4 25: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 28: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 31: iload_3 32: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 35: return
istore_1 將棧頂元素彈出並存到本地變量數組1號元素,因為0號給了this
位操作
public class bitop { public static int set (int a, int b) { return a | 1<<b; } public static int clear (int a, int b) { return a & (~(1<<b)); } }
反編譯
public static int set(int, int); descriptor: (II)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=2 0: iload_0 1: iconst_1 2: iload_1 3: ishl 4: ior 5: ireturn LineNumberTable: line 5: 0 public static int clear(int, int); descriptor: (II)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=2 0: iload_0 1: iconst_1 2: iload_1 3: ishl 4: iconst_m1 5: ixor 6: iand 7: ireturn LineNumberTable: line 10: 0
set函數的指令解釋
0: iload_0 //載入第0個參數即a,壓入棧
1: iconst_1 //數字1壓入棧
2: iload_1 ///載入第1個參數即b,壓入棧
3: ishl //彈出棧頂兩個元素,將int型數值左移位指定位數並將結果壓入棧頂,第一個彈出值(b)為左移的位數,第二個彈出的值(1)為要對其進行左移的數,即對1左移b位
4: ior //將棧頂兩int型數值作“按位或”並將結果壓入棧頂
5: ireturn //返回棧頂值
clear函數中iconst_m1指令將-1壓入棧,-1就是16進制的0xFFFFFFFF, ixor是進行異或操作,操作結果相當於取反
將上面的例子擴展成long類型
public class longbitop { public static long lset (long a, int b) { return a | 1<<b; } public static long lclear (long a, int b) { return a & (~(1<<b)); } }
反編譯
public static long lset(long, int); descriptor: (JI)J flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=3, args_size=2 0: lload_0 1: iconst_1 2: iload_2 3: ishl 4: i2l 5: lor 6: lreturn LineNumberTable: line 5: 0 public static long lclear(long, int); descriptor: (JI)J flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=3, args_size=2 0: lload_0 1: iconst_1 2: iload_2 3: ishl 4: iconst_m1 5: ixor 6: i2l 7: land 8: lreturn LineNumberTable: line 9: 0
需要註意的指令
i2l 將棧頂int型數值強制轉換成long型數值並將結果壓入棧頂
32位需要升級為64位值時,會使用i21指令把整型擴展成64位長整型.
循環
看個簡單的例子
public class Loop { public static void main(String[] args) { for (int i = 1; i <= 10; i++) { System.out.println(i); } } }
反編譯
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: iconst_1 1: istore_1 2: iload_1 3: bipush 10 5: if_icmpgt 21 8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_1 12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 15: iinc 1, 1 18: goto 2 21: return
指令解釋
0: iconst_1 //常數1壓入棧
1: istore_1 //棧頂彈出存入本地變量數組1號元素
2: iload_1 //本地變量1號元素壓入棧頂
3: bipush 10 //常數10壓入棧頂
5: if_icmpgt 21 // 比較棧頂兩int型數值大小,當結果大於0時跳轉到偏移位21,比較都是拿第二個彈出值與第一個比較即i與10比較
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_1
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V //8~12這裏輸出變量i
15: iinc 1, 1 //將指定int型變量增加指定值(常用於i++,i--,i+=2) ,這裏指本地變量1號元素加1
18: goto 2 //跳轉到偏移位2
21: return
多說一句,我們調用println打印數據類型是整型,我們看註釋,“(I)V”,I的意思是整型,V的意思是返回void
再看個復雜的,斐波那契數列
public class Fibonacci { public static void main(String[] args) { int limit = 20, f = 0, g = 1; for (int i = 1; i <= limit; i++) { f = f + g; g = f - g; System.out.println(f); } } }
反編譯
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=5, args_size=1 0: bipush 20 2: istore_1 3: iconst_0 4: istore_2 5: iconst_1 6: istore_3 7: iconst_1 8: istore 4 10: iload 4 12: iload_1 13: if_icmpgt 37 16: iload_2 17: iload_3 18: iadd 19: istore_2 20: iload_2 21: iload_3 22: isub 23: istore_3 24: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 27: iload_2 28: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 31: iinc 4, 1 34: goto 10 37: return
拿幾個指令說一下
if_icmpgt 37 比較棧頂兩int型數值大小,當結果大於0時跳轉到偏移位37,比較都是拿棧的第二個彈出值(本地變量4號)與第一個比較(本地變量1號)
isub 棧頂的兩個元素相減並將結果壓入棧頂,誰減誰,仍然是棧頂彈出的第二個減第一個
iinc 4, 1 表示是本地變量4號元素加1,結果存在本地變量4號
這裏的英文作者又吐槽了一下
8: istore 4
10: iload 4
這兩句中的iload 4,感覺多余。。
switch例子
public class tableswitch { public static void f(int a) { switch (a) { case 0: System.out.println("zero"); break; case 1: System.out.println("one\n"); break; case 2: System.out.println("two\n"); break; case 3: System.out.println("three\n"); break; case 4: System.out.println("four\n"); break; default: System.out.println("something unknown\n"); break; } } }
反編譯
public static void f(int); descriptor: (I)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: iload_0 1: tableswitch { // 0 to 4 0: 36 1: 47 2: 58 3: 69 4: 80 default: 91 } 36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 39: ldc #3 // String zero 41: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 44: goto 99 47: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 50: ldc #5 // String one\n 52: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 55: goto 99 58: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 61: ldc #6 // String two\n 63: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 66: goto 99 69: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 72: ldc #7 // String three\n 74: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 77: goto 99 80: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 83: ldc #8 // String four\n 85: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 88: goto 99 91: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 94: ldc #9 // String something unknown\n 96: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 99: return
其中需要註意的是tableswitch是在JVM級別上直接支持switch 語句,非常有意思的是,編譯器編譯的時候,會根據case條件的不同翻譯成tableswitch或者lookupswitch
1: tableswitch { // 0 to 4
0: 36
1: 47
2: 58
3: 69
4: 80
default: 91
}
的意思,將棧頂元素與大括號內冒號前的元素逐個比較,若相等,則跳轉到右邊對應的偏移塊號
break對應其中的goto 99,如果沒有這句,棧頂元素會繼續和後面的大括號內後續元素進行比較
Java逆向基礎之條件跳轉位運算循環