看完這篇文章,你需要準備足夠多的勇氣,因為它像王大娘的裹腳一樣又臭又長。 字節(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命令來校驗你的解析是否正確,尤其是常量池的下標是否對應。 |
|
來自: 且看且珍惜 > 《動態(tài)代理》