一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

字節(jié)碼解析

 且看且珍惜 2014-12-10

看完這篇文章,你需要準備足夠多的勇氣,因為它像王大娘的裹腳一樣又臭又長。

字節(jié)碼是java源代碼經(jīng)編譯后生成的二進制數(shù)據(jù),通俗點說class文件中保存的就是字節(jié)碼。

字節(jié)碼處理也有很多成熟且越來越笨重的工具,比如說用得非常廣泛的cglib,而cglib又用到了另一個成熟且越來越笨重的工具,這就是asm。

網(wǎng)上抄來抄去的關(guān)于aop的實現(xiàn),都會提到cglib,但很少會去深究asm,更少會去深層次的剖析字節(jié)碼。因此,我覺得有必要體現(xiàn)自己的2B之處,就是要通俗易懂的揭開字節(jié)碼的面紗。

字節(jié)碼是由字節(jié)組成的,1個字節(jié)就是一個byte(不是bit,1個byte有8個bit),說到字節(jié)碼,這里得提一點:java中的byte是有符號的,也就是-128到127之間(包含),而實際上字節(jié)碼中byte是無符號的,也就是0到255之間(包含),特別是字節(jié)碼中經(jīng)常有2個byte來表示數(shù)據(jù)長度的時候,一定要注意換算為無符號int型,因為長度不可能為負數(shù)。

下面開始分析字節(jié)碼(class)文件的結(jié)構(gòu):

4個字節(jié),每個字節(jié)碼(class)文件的開頭,都有4個字節(jié)組成的被稱作“魔數(shù)(magic)”的東東,它們是固定值:[0xCA,0xFE,0xBA,0xBE]。

2個字節(jié),組成一個整數(shù),表示次版本號(minor version),如[0,0]轉(zhuǎn)換int后是0。

2個字節(jié),組成一個整數(shù),表示主版本號(major version),如[0,45]轉(zhuǎn)換int后是45。

2個字節(jié),組成一個整數(shù),表示常量池(constant pool)的大小,如[0,31]轉(zhuǎn)換int后是31,表示后面會有一個31個元素的數(shù)組,姑且叫它數(shù)組吧,這個數(shù)組把后面經(jīng)常會用到的一些常量按順序放進來,后面需要用到的某個常量的時候,直接使用一個下標索引值,就指向了這個數(shù)組中的常量,這樣做的目的在于減小文件的體積。

雖然定義了一個31個元素的數(shù)組,但是,后面的數(shù)據(jù)并不是從0下標開始填充的(這算一個坑吧),而是跳過0下標,從1下標開始填充,也就是說,sonstant_pool[0]只是純粹打醬油的,后面如果誰指向了0,或者指向了大于30的下標,說明這個class是不符合要求的,或者解析錯了吧。

常量池這個數(shù)組中的元素之間,是不需要額外的特殊字節(jié)來分隔的,它們一個挨一個,排列得非常緊湊。每一個元素的起始,都有1個字節(jié)的數(shù)字來表示自己是什么類型,類型只有以下這么幾種,如果出現(xiàn)了其他數(shù)字,說明class有問題,或者解析有問題。

1代表1個字符串,這個字符串是utf-8編碼的。(utf)

3代表1個int數(shù)。(int)

4代表1個float數(shù)。(float)

5代表1個long數(shù)。(long)

6代表1個double數(shù)。(double)

7代表1個class引用,其值也是1個數(shù)字,指向1個1類型的下標。(class_ref)

8代表1個string引用,其值也是1個數(shù)字,指向1個1類型的下標。(string_ref)

9代表1個成員變量引用,其中包含2個數(shù)字,一個指向1個7類型下標(類),另一個指向1個12類型的下標(成員變量)。(field_ref)

10代表1個方法引用,其中包含2個數(shù)字,一個指向1個7類型下標(類),另一個指向1個12類型的下標(方法)。(method_ref)

11代表1個接口方法引用,其中包含2個數(shù)字,一個指向1個7類型下標(類),另一個指向1個12類型的下標(方法)。(interface_method_ref)

12代表1個帶描述的名稱的引用,其中包含2個數(shù)字,一個指向1個1類型下標(名稱),另一個指向1個1類型的下標(描述)。(name_and_type)

為什么沒有boolean、byte、char、short?因為這幾種類型都是按int類型存放的。

如果判斷出這個元素是1類型(utf),會緊跟著2個字節(jié),表示這個字符串以utf-8編碼后的字節(jié)的長度,根據(jù)這個長度,去讀取后面的這么多字節(jié),就將這個字符串取出來了。

如果判斷出這個元素是3類型(int),會緊跟著4個字節(jié),表示這個int型的值。

如果判斷出這個元素是4類型(float),會緊跟著4個字節(jié),表示這個float型的值。

如果判斷出這個元素是5類型(long),會緊跟著8個字節(jié),表示這個long型的值,遇到這種情況的話,后面的元素不能填充到接下來的數(shù)組下標中,必須跳1格,這是網(wǎng)上所有的抄襲者都不會告訴你的(好大一個坑)。

如果判斷出這個元素是6類型(double),會緊跟著8個字節(jié),表示這個double型的值,遇到這種情況的話,后面的元素不能填充到接下來的數(shù)組下標中,必須跳1格,這也是網(wǎng)上所有的抄襲者都不會告訴你的(這個和long一樣,巨坑)。

如果判斷出這個元素是7類型(class_ref),會緊跟著2個字節(jié),把這2個字節(jié)轉(zhuǎn)換為int值,就得到了一個下標,拿這個下標到常量池里面去取,就是一個1類型的字符串,比如說當前類名、繼承的父類名、實現(xiàn)的接口名等,都是這種形式。

如果判斷出這個元素是8類型(string_ref),會緊跟著2個字節(jié),把這2個字節(jié)轉(zhuǎn)換為int值,就得到了一個下標,拿這個下標到常量池里面去取,就是一個1類型的字符串,是不是有種脫了褲子放屁的感腳?

如果判斷出這個元素是9類型(field_ref),會緊跟著2個字節(jié),把這2個字節(jié)轉(zhuǎn)換為int值,就得到了一個下標,拿這個下標到常量池里面去取,就是一個7類型,然后再拿這個7類型的值去取一個1類型的字符串,就得到了類名啦,還沒完,后面還緊跟了2個字節(jié),把這2個字節(jié)轉(zhuǎn)換為int值,又得到一個下標,拿這個下標到常量池取回來一個12類型的我擦,再拿12類型的指向的下標去取,才能取回來屬性名和描述,繞這么大一個彎是要作死的節(jié)奏啊。

如果判斷出這個元素是10類型(method_ref),和9雷同,只不過對應的是方法。

如果判斷出這個元素是11類型(interface_method_ref),和10雷同,只不過對應的是接口方法,誰這么設(shè)計,一定有他的道理。

如果判斷出這個元素是12類型(name_and_type),會緊跟著2個字節(jié),把這2個字節(jié)轉(zhuǎn)換為int值,就得到了一個下標,拿這個下標到常量池里面去取,就是一個1類型的字符串,就得到屬性名或方法名了,然后緊跟著的2個字節(jié),同樣原理去找,找到一個1類型的字符串,就得到描述了。

把這些理解清楚了,再大的事都不是事了。

切記,常量池相當于一個數(shù)組,但這個數(shù)組中某些位置是空著的(挖的坑)。

常量池繞完了,就該進入正文了。

首先遇到的是2個字節(jié),這2個字節(jié)學問大,代表了這個類的各種修飾符的組合,比如說public、abstract、final等等等等,我們并不需要知道每個值代表什么修飾符,因為有多少種組合,我們也不需要關(guān)心,我們可以用Modifier.isAbstract(這個值)來判斷這個類是不是抽象類。

然后又是2個字節(jié),這2個字節(jié)轉(zhuǎn)換為int值,指向的是常量池中1個7類型(class_ref)的下標,再繞到1類型(utf)的時候,其實對應的就是這個類的名稱,不過是用/分隔了包名,如test/Hello。

然后又是2個字節(jié),這2個字節(jié)轉(zhuǎn)換為int值,指向的是常量池中1個7類型(class_ref)的下標,再繞到1類型(utf)的時候,其實對應的就是這個類繼承的父類的名稱,不過是用/分隔了包名,如java/lang/Object,因為java是單繼承嘛,所以這里連一點退路都沒給,直接就只能是1個父類。

然后又是2個字節(jié),為什么和2這么有緣呢?這2字節(jié)轉(zhuǎn)換為int值,表示這個類直接實現(xiàn)的接口數(shù)量,如果為0,那么后面就跳過了,如果不是0,那么接下來就相當于來了一個接口的數(shù)組。

這個數(shù)組很簡單,沒有常量池那么多招數(shù),每個元素都是2字節(jié),這2字節(jié)轉(zhuǎn)換為int值,指向的是常量池中1個7類型(class_ref)的下標,再繞到1類型(utf)的時候,其實對應的就是這個接口的名稱,還是用/分隔了包名。

接口完了,就該是成員變量了。

首先,按理有2個字節(jié),轉(zhuǎn)換為int值,表示成員變量的數(shù)量。

到了成員變量內(nèi)部,最開始會有2個字節(jié),表示變量的修飾符,和類的修飾符一樣的道理。

接著有2個字節(jié),轉(zhuǎn)換為int值,指向常量池中1類型(utf)的下標,取回來變量名稱。

接著有2個字節(jié),轉(zhuǎn)換為int值,指向常量池中1類型(utf)的下標,取回來變量描述。

為什么不直接指向9類型(field_ref)?估計開發(fā)編譯器的人腦殼也整暈了吧。

成員變量還會有一些屬性(attribute),所以接下來又來一個數(shù)組,首當其沖的是2個字節(jié),轉(zhuǎn)換為int值,表示attribute數(shù)組的大小。

attribute數(shù)組也沒有多大的坑,每個元素也是緊密挨著。

每個元素的開頭都有2個字節(jié),轉(zhuǎn)換為int值,指向常量池,表示屬性名稱。

接下來有4個字節(jié),轉(zhuǎn)換為int值,表示屬性的數(shù)據(jù)大?。ňo跟著的字節(jié)數(shù)),然后就是屬性的數(shù)據(jù),因為屬性也分很多種情況,像剝洋蔥,一層一層剝進去,數(shù)據(jù)格式都大同小異,如果有需要你就讀出來吧,沒需要直接skip。

屬性數(shù)組讀完了,成員變量也就讀完了。

接下來該是方法了,方法和成員變量如出一轍,先是2個字節(jié)表示方法的數(shù)量,然后一個挨一個的方法數(shù)據(jù),沒什么好說的,太細節(jié)的也就是屬性(Attribute),而屬性又分很多種,不說個三天兩夜也說不完。

方法完了,還有很多類的屬性(Attribute),也可以看作一個數(shù)組。

所以,按理,也有2個字節(jié),轉(zhuǎn)換為int值,表示類屬性(Attribute)的數(shù)量。

有了數(shù)量,開始遍歷,每個屬性開頭都有2個字節(jié),轉(zhuǎn)換為int值,指向常量池中1類型(utf)或7類型(class_ref)的下標,最終都能取出一個字符串值,包括SourceFile、InnerClasses等等。

緊跟著有4個字節(jié),轉(zhuǎn)換為int值,表示屬性的數(shù)據(jù)大小(緊跟著的字節(jié)數(shù)),然后就是屬性的數(shù)據(jù)。

根據(jù)屬性名稱的不同,分為不同的屬性類型,每種屬性類型占用的空間數(shù)量不等。

比如說SourceFile這種代表源代碼,屬性數(shù)據(jù)就只有2字節(jié),這2字節(jié)轉(zhuǎn)換為int值,執(zhí)行常量池中1類型(utf)的下標,取回來一個字符串值,就是源代碼文件的名稱(不含路徑),如Hello.java。

而InnerClasses則代表內(nèi)部類,里面有多組數(shù)據(jù),不僅指向自己的類名,如test/Hello$1.class、test/Hello$2.class,還指向自己所在外部類的類名,如test/Hello等,這里就不多說了,能看懂這篇文章的,細節(jié)的東西參考網(wǎng)上的資料,也能解析好了。

類的屬性讀完,整個class也就戛然而止,有點意猶未盡的感腳。

這篇文章主要說了字節(jié)碼的主體結(jié)構(gòu)和一些比較坑的地方,細節(jié)還需自行打磨,一步一步按照上面所述的思路,我相信你也能寫代碼將字節(jié)碼解析出來,但是這一步解析,并不是一步到位反編譯成源代碼,更多的是了解字節(jié)碼的機制,字節(jié)碼已經(jīng)了然于胸,還有什么事不能干?

比如說掃描所有的class文件,通過解析字節(jié)碼,獲取該class實現(xiàn)了哪些接口,然后當我們看到某個接口的時候,能自動列出來這個接口有哪些實現(xiàn)類,這就是eclipse里Ctrl+T的功能。

反射也可以做這個事情,但是反射慢,反射還會觸發(fā)類中靜態(tài)代碼塊的執(zhí)行,而很多時候我們不希望這些靜態(tài)代碼塊這么早的被執(zhí)行。

要做好字節(jié)碼的解析,最重要的一步,就是把常量池給解析正確,否則,稍微有一點差錯,就是失之毫厘謬以千里。

最后用一個簡單的例子來分析:

源代碼文件名:Hello.java

源代碼:

package test;

public class Hello{

  public void say(){

    System.out.println("hello");

  }

}

解析(16進制數(shù)據(jù) //備注):

CAFEBABE 魔數(shù)

0000 //次版本號 0

0032 //主版本號 50

001F //常量池大小 31

//下邊是常量池,#代表下標

     //#1

07   //類型7 

0002 //指向#2


     //#2

01   //類型1 

000A //字符串長度:10

746573742F48656C6C6F //字符串:test/Hello


     //#3

07   //類型7 

0004 //指向#4


     //#4

01   //類型1 

0010 //字符串長度:16

6A6176612F6C616E672F4F626A656374 //字符串:java/lang/Object


     //#5

01   //類型1 

0006 //字符串長度:6

3C696E69743E //字符串:<init>


     //#6

01   //類型1 

0003 //字符串長度:3

282956 //字符串:()V


     //#7

01   //類型1 

0004 //字符串長度:4

436F6465 //字符串:Code


     //#8

0A   //類型10 

0003 //指向#3

0009 //指向#9


     //#9

0C   //類型12 

0005 //指向#5

0006 //指向#6


     //#10

01   //類型1 

000F //字符串長度:15

4C696E654E756D6265725461626C65 //字符串:LineNumberTable


     //#11

01   //類型1 

0012 //字符串長度:18

4C6F63616C5661726961626C655461626C65 //字符串:LocalVariableTable


     //#12

01   //類型1 

0004 //字符串長度:4

74686973 //字符串:this


     //#13

01   //類型1 

000C //字符串長度:12

4C746573742F48656C6C6F3B //字符串:Ltest/Hello;


     //#14

01   //類型1 

0003 //字符串長度:3

736179 //字符串:say


     //#15

09   //類型9 

0010 //指向#16

0012 //指向#16


     //#16

07   //類型7 

0011 //指向#17


     //#17

01   //類型1 

0010 //字符串長度:16

6A6176612F6C616E672F53797374656D //字符串:java/lang/System


     //#18

0C   //類型12 

0013 //指向#19

0014 //指向#20


     //#19

01   //類型1 

0003 //字符串長度:3

6F7574 //字符串:out


     //#20

01   //類型1 

0015 //字符串長度:21

4C6A6176612F696F2F5072696E7453747265616D3B //字符串:Ljava/io/PrintStream;


     //#21

08   //類型8 

0016 //指向#22


     //#22

01   //類型1 

0005 //字符串長度:5

68656C6C6F //字符串:hello


     //#23

0A   //類型10 

0018 //指向#24

001A //指向#26


     //#24

07   //類型7 

0019 //指向#25


     //#25

01   //類型1 

0013 //字符串長度:19

6A6176612F696F2F5072696E7453747265616D //字符串:java/io/PrintStream


     //#26

0C   //類型12 

001B //指向#27

001C //指向#28


     //#27

01   //類型1 

0007 //字符串長度:7

7072696E746C6E //字符串:println


     //#28

01   //類型1 

0015 //字符串長度:21

284C6A6176612F6C616E672F537472696E673B2956 //字符串:(Ljava/lang/String;)V


     //#29

01   //類型1 

000A //字符串長度:10

536F7572636546696C65 //字符串:SourceFile


     //#30

01   //類型1 

000A //字符串長度:10

48656C6C6F2E6A617661 //字符串:Hello.java


0021 //類訪問修飾符

0001 //本類指向#1 結(jié)果字符串:test/Hello

0003 //父類指向#:3 結(jié)果字符串:java/lang/Object

0000 //接口數(shù)量:0

0000 //成員變量數(shù)量:0

0002 //方法數(shù)量:2

     //方法0

0001 //訪問修飾符

0005 //方法名指向#5 結(jié)果字符串:<init>

0006 //方法描述指向#6

0001 //屬性數(shù)量為:1

     //屬性0

0007 //屬性名指向#7 實際字符串:Code

0000002F //屬性數(shù)據(jù)長度:47

00010001000000052AB70008B100000002000A00000006000100000002000B0000000C000100000005000C000D0000 //屬性數(shù)據(jù)略


     //方法1

0001 //訪問修飾符

000E //方法名指向#14 結(jié)果字符串:say

0006 //方法描述指向#6

0001 //屬性數(shù)量為:1

     //屬性0

0007 //屬性名指向#7 實際字符串:Code

00000037 //屬性數(shù)據(jù)長度:55

0002000100000009B2000F1215B60017B100000002000A0000000A00020000000400080005000B0000000C000100000009000C000D0000 //屬性數(shù)據(jù)略


0001 //類屬性數(shù)量:1

     //屬性0

001D //屬性名指向#29 實際字符串:SourceFile

00000002 //屬性數(shù)據(jù)長度:2

001E //屬性數(shù)據(jù),指向#30 實際字符串:Hello.java


//完畢。

最后說一句,你可以使用javap命令來校驗你的解析是否正確,尤其是常量池的下標是否對應。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    开心激情网 激情五月天| 夜夜嗨激情五月天精品| 久久精品中文扫妇内射| 不卡视频在线一区二区三区| 中文字幕日韩一区二区不卡| 日韩精品第一区二区三区| 亚洲午夜av久久久精品| 色婷婷成人精品综合一区| 在线亚洲成人中文字幕高清| 国产精品十八禁亚洲黄污免费观看 | 色婷婷在线视频免费播放| 最近最新中文字幕免费| 国产亚洲精品一二三区| 黄片免费播放一区二区| 中文字幕高清免费日韩视频| 久久人妻人人澡人人妻| 亚洲精品一区三区三区| 99久久精品免费精品国产| 久久精品伊人一区二区| 日本一区二区三区久久娇喘| 五月婷婷六月丁香亚洲| 高清国产日韩欧美熟女| 亚洲一区在线观看蜜桃| 国产精品一区二区香蕉视频 | 91日韩在线视频观看| 国产精品日韩精品最新| 日韩精品在线观看完整版| 久久黄片免费播放大全| 国产伦精品一区二区三区高清版| 亚洲国产精品av在线观看| 免费人妻精品一区二区三区久久久| 日韩不卡一区二区在线| 尤物天堂av一区二区| 免费黄色一区二区三区| 亚洲国产精品av在线观看 | 久草视频这里只是精品| 加勒比系列一区二区在线观看| 一区二区三区免费公开| 日韩精品一区二区亚洲| 日韩av生活片一区二区三区| 午夜国产福利在线播放|