天天看點

Java位元組碼(.class檔案)的代碼解析

java二進制指令代碼以以下格式緊湊排列(opcode占一個位元組):

opcode operand*

除了tableswitch和lookupswitch兩條指令中間存在填充位元組以外,其他指令都沒有填充位元組,即使在兩條指令之間也沒有。因而在讀取指令的時候,要根據指令的定義讀取。 

通過對上面java指令集的分析可以知道,java指令集中很大一部分沒有操作數,因而對這部分指令,隻需要讀取一個位元組的操作碼,将操作碼映射成助記符即可。 

而對其他帶操作數的指令,則需要根據不同類型分析(由于apache中的bcel(binary code engineering library)對位元組碼的支援,操作碼和助記符的映射可以用com.sun.org.apache.bcel.internal.constats中提供的映射表數組來完成)。

1.       處理兩條特殊的指令tableswitch和lookupswitch指令。

對這兩條指令,首先都要去掉填充字元以使defaultbyte1索引号是字對齊的。

    private static void make4bytealignment(bytesequence codes) {

       int usedbytes = codes.getindex() % 4;

       int paddingbytes = (usedbytes == 0) ? 0 : 4 - usedbytes;

       for(int i = 0;i < paddingbytes;i++) {

           codes.readbyte();

       }

}

對tableswitch指令,讀取defaultoffset值,最小項的值,最大項的值以及在最小項和最大項之間每一項的offset值。并且将讀取到的offset值和目前指令的基位址相加:

           int defaultoffset1 = baseoffset + codes.readint();

           builder.append("\tdefault = #" + defaultoffset1);

           int low = codes.readint();

           int high = codes.readint();

           int npair1 = high - low + 1;

           builder.append(", npairs = " + npair1 + "\n");

           for(int i = low;i <= high;i++) {

              int match = i;

              offset = baseoffset + codes.readint();

              builder.append(string.format("\tcase %d : #%d\n", match, offset));

        }

對lookupswitch指令,讀取defaultoffset值,鍵值對數值(npairs),以及npairs對的鍵值對,将得到的offset值和目前指令的基位址相加:

           int defaultoffset2 = baseoffset + codes.readint();

           builder.append("\tdefault = #" + defaultoffset2);

           int npairs2 = codes.readint();

           builder.append(", npairs = " + npairs2 + "\n");

           for(int i = 0;i < npairs2;i++) {

              int match = codes.readint();

2.       所有條件跳轉指令都有兩個位元組的偏移量操作數(if<cond>, if_icmp<cond>, ifnull, ifnonnull, if_acmp<cond>)。無條件跳轉指令goto和子例程跳轉指令jsr也都是兩個位元組的偏移量作為操作數。

offset = baseoffset + codes.readshort();

builder.append(string.format("\t\t#%d\n", offset));

3.       對寬偏移量的跳轉指令goto_w和子例程跳轉指令jsr_w的操作數是四個位元組的偏移量。

offset = baseoffset + codes.readint();

4.       wide指令,則繼續讀取下一條指令,并将wide參數設定為true。

bytecodetostring(codes, pool, verbose, true);

5.       還有一些指令值以一個位元組的局部變量索引号作為操作數的,如果有wide修飾,則用兩個位元組作為操作數,代表局部變量索引号。這樣的指令有:aload, iload, fload, lload, dload, astore, istore, fstore, lstore, dstore, ret。

if(wide) {

    index = codes.readunsignedshort();

} else {

    index = codes.readunsignedbyte();

builder.append(string.format("\t\t%%%d\n", index));

6.       iinc指令,以一個位元組的局部變量索引号和一個自己的常量作為參數;如果以wide修飾,則該指令的局部變量索引号和常量都占兩個位元組。

    if(wide) {

       index = codes.readunsignedshort();

       constvalue = codes.readshort();

    } else {

       index = codes.readunsignedbyte();

       constvalue = codes.readbyte();

    }

builder.append(string.format("\t\t%d %d\n", index, constvalue));

7.       對象操作指令,它們的操作數都是常量池中的索引,長度為兩個位元組。指向constant_class_info類型的結構,這些指令有new, checkcast, instanceof, anewarray。

index = codes.readunsignedshort();

builder.append("\t\t" + pool.getclassinfo(index).toinstructionstring(verbose) + "\n");

8.       所有字段操作指令,它們的操作數都是常量池中的索引,長度為兩個位元組。指向constant_fieldref_info類型結構,這些指令有getfield, putfield, getstatic, putstatic。

builder.append("\t\t" + pool.getfieldrefinfo(index).toinstructionstring(verbose) + "\n");

9.       非接口方法調用指令,也都是以兩個位元組的索引号作為操作數,指向常量池中的constant_methodref_info類型結構,這些指令有invokespecial, invokevirtual, invokestatic。

builder.append("\t\t" + pool.getmethodrefinfo(index).toinstructionstring(verbose) + "\n");

10.   接口方法調用指令invokeinterface,它有四個位元組的操作數,前兩個位元組為常量池的索引号,指向constant_interfacemethodref_info類型,第三個位元組為count,表示參數的位元組數,最後一個位元組為0值。

int nargs = codes.readunsignedbyte(); //historical, redundant

builder.append("\t\t" + pool.getinterfacemethodrefinfo(index).toinstructionstring(verbose));

builder.append(" : " + nargs + "\n");

codes.readunsignedbyte(); //reserved should be zero

11.   基本類型的數組建立指令newarray,它的操作數為一個位元組的類型辨別。

string type = constants.type_names[codes.readbyte()];

builder.append(string.format("\t\t(%s)\n", type));

12.   多元數組的建立指令multianewarray,它有三個位元組的操作數,前兩個位元組為索引号,指向constant_class_info類型,表示數組的類型,最後一個位元組指定數組的次元。

int dimensions = codes.readunsignedbyte();

builder.append(string.format("\t\t%s (%d)\n", pool.getclassinfo(index).getname(), dimensions));

13.   常量入棧指令ldc,以一個位元組的索引号作為參數,指向constant_integer_info、constant_float_info、constant_string_info、constant_class_info類型,表示要入棧的常量值(int類型值、float類型值、string引用類型值或對象引用類型值)。

index = codes.readunsignedbyte();

builder.append("\t\t" + pool.getpoolitem(index).toinstructionstring(verbose) + "\n");

14.   寬索引的常量入棧指令ldc_w,以兩個位元組的索引号作為參數,指向constant_integer_info、constant_float_info、constant_string_info、constant_class_info類型,表示要入棧的常量值(int類型值、float類型值、string引用類型值或對象引用類型值)。

15.   寬索引的常量入棧指令ldc2_w,以兩個位元組的索引号作為參數,指向constant_long_info、constant_double_info類型,表示要入棧的常量值(long類型值、double類型值)。

16.   bipush指令,以一個位元組的常量作為操作數。

byte constbyte = codes.readbyte();

builder.append(“\t” + constbyte);

17.   sipush指令,以兩個位元組的常量作為操作數。

short constshort = codes.readshort();

builder.append(“\t” + constshort);

以上還有一些沒有完成的代碼,包括字段(方法)的簽名和描述符沒有解析,有一些解析的格式還需要調整等。不管怎麼樣,總體的結構就是這樣了,其它的都是細節問題,這裡不讨論了。 

參見bcel項目的org.apache.bcel.classfile.utility類.