最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

Java ASM詳解:MethodVisitor與Opcode(三)標(biāo)簽,條件結(jié)構(gòu),循環(huán)結(jié)構(gòu),棧幀

2021-11-06 23:02 作者:Nickid2018  | 我要投稿

在之前的文章中,我們已經(jīng)知道了基礎(chǔ)的字節(jié)碼。但是,這些字節(jié)碼只能構(gòu)建起一個(gè)簡單的結(jié)構(gòu),不能實(shí)現(xiàn)循環(huán)條件等高級結(jié)構(gòu)。這篇文章將討論關(guān)于程序流程結(jié)構(gòu)的字節(jié)碼。

一.標(biāo)簽

標(biāo)簽(Label)是用來劃明一部分字節(jié)碼的標(biāo)識(通常意義上標(biāo)簽就是一個(gè)標(biāo)記點(diǎn),但是為了接下來的講述就用它代表一塊字節(jié)碼)。一個(gè)標(biāo)簽下的字節(jié)碼塊,應(yīng)該從操作??臻_始到操作棧被清空結(jié)束——也就是說,一個(gè)標(biāo)簽代表的字節(jié)碼塊反編譯之后應(yīng)該是完整的一或多條語句。

在通常情況下,javac編譯器會把每條單獨(dú)語句都分配一個(gè)標(biāo)簽,這么做的目的是為了輸出行號和局部變量名稱。

標(biāo)簽也可以在我們使用Java時(shí)自己定義,下面的LABEL就是一個(gè)標(biāo)簽:

在字節(jié)碼中,標(biāo)簽代表的字節(jié)碼塊是從這個(gè)標(biāo)簽寫入開始到下一個(gè)標(biāo)簽寫入或該方法的字節(jié)碼讀取完畢的一部分字節(jié)碼。

在ASM庫中,標(biāo)簽用org.objectweb.asm.Label進(jìn)行表示,構(gòu)造方法如下:

寫入一個(gè)Label,需要用到MethodVisitor的方法,方法如下:

正如前面所說,兩個(gè)標(biāo)簽的寫入之間的字節(jié)碼可以看作這個(gè)標(biāo)簽代表的一塊字節(jié)碼塊。因此,兩個(gè)visitLabel之間的語句也可以被看作前一個(gè)Label代表的一部分字節(jié)碼區(qū)域。

那么標(biāo)簽有什么用呢?

首先它可以保存代碼的行號,這就用到了MethodVisitor::visitLineNumber這個(gè)方法了。

第一個(gè)參數(shù)代表了這條語句的行號,第二個(gè)參數(shù)就是這條語句的標(biāo)簽。標(biāo)簽必須先于行號被寫入,否則就會拋出IllegalArgumentException。

其次,它可以保存局部變量的名稱。局部變量有作用域,而作用域可以用兩個(gè)標(biāo)簽指定。在這兩個(gè)標(biāo)簽之內(nèi)且在指定局部變量槽位上的變量就是我們要命名的局部變量。寫入局部變量的名稱使用MethodVisitor::visitLocalVariable。

參數(shù)的意義分別是:名稱、描述符、泛型簽名、開始的標(biāo)簽、結(jié)束的標(biāo)簽、局部變量槽位。在javac編譯生成的類文件中,局部變量名稱的寫入都要在最后寫入。

最后,標(biāo)簽的最重要意義就是它可以用于跳轉(zhuǎn)字節(jié)碼上。

二.跳轉(zhuǎn)字節(jié)碼

用于跳轉(zhuǎn)的字節(jié)碼都使用了visitJumpInsn方法:

第一個(gè)就是字節(jié)碼,第二個(gè)是跳轉(zhuǎn)的目標(biāo)。字節(jié)碼決定了是否進(jìn)行跳轉(zhuǎn),標(biāo)簽決定了跳轉(zhuǎn)的目的地。

跳轉(zhuǎn)字節(jié)碼分為兩種——比較跳轉(zhuǎn)和無條件跳轉(zhuǎn)。

無條件跳轉(zhuǎn),也就是goto字節(jié)碼:

goto字節(jié)碼是當(dāng)程序運(yùn)行到這里時(shí),就直接跳轉(zhuǎn)到對應(yīng)的標(biāo)簽繼續(xù)執(zhí)行,通常都是用在循環(huán)內(nèi)部的。

比較跳轉(zhuǎn),也是大多數(shù)條件結(jié)構(gòu)和循環(huán)結(jié)構(gòu)使用的字節(jié)碼,它有四套字節(jié)碼,分別對應(yīng)了int比較、int與0比較、對象比較和對象空檢測:

1. if_icmp<cond>

<cond> = eq/ne/lt/ge/gt/le

這六個(gè)字節(jié)碼分別對應(yīng)了兩個(gè)int數(shù)據(jù)進(jìn)行相等、不相等、小于、大于等于、大于、小于等于的比較測試。如果比較成功,就跳轉(zhuǎn)到指定的標(biāo)簽處運(yùn)行。如果比較不成功,就沿著當(dāng)前的流程繼續(xù)運(yùn)行。

2. if<cond>

<cond> = eq/ne/lt/ge/gt/le

這六個(gè)字節(jié)碼分別對應(yīng)了一個(gè)int數(shù)據(jù)進(jìn)行等于0、不等于0、小于0、大于等于0、大于0、小于等于0的比較測試。測試結(jié)果和跳轉(zhuǎn)方式和上文相同。

3. if_acmp<cond>

<cond> = eq/ne

這兩個(gè)字節(jié)碼分別對應(yīng)兩個(gè)對象相等和不相等。這個(gè)字節(jié)碼比較的是對象的引用,而不是內(nèi)部的值——也就是說,即使兩個(gè)String對象內(nèi)部存儲字符串一樣,也不能保證它們的檢測結(jié)果為真?。ɡ馐鞘褂昧薙tring::intern,它會把字符串放進(jìn)常量池,并返回一個(gè)固定的引用)所以判斷字符串相等必須使用equals方法而不是==。

4. ifnull/ifnonnull

這兩個(gè)字節(jié)碼分別測試對象是空還是非空。執(zhí)行流程和之前3個(gè)一樣。

可以看到,這幾個(gè)字節(jié)碼指針對了int和對象引用的情況,而沒有考慮long、float、double的情況。于是Java加入了下面幾個(gè)字節(jié)碼用于比較它們,獲取到值后就可以傳遞給各個(gè)IF字節(jié)碼判斷:

1. lcmp

它用于比較兩個(gè)long的大?。喝绻谝粋€(gè)數(shù)字比第二個(gè)小,返回-1;如果第二個(gè)數(shù)字比第一個(gè)小,返回1;如果相等,返回0。

2. xcmp<op>

x=f/d, <op>=l/g

這套字節(jié)碼和lcmp的邏輯差不多:如果第一個(gè)數(shù)小于第二個(gè)數(shù),返回-1;如果第二個(gè)數(shù)小于第一個(gè)數(shù),返回1;如果相等,返回0。但是,如果其中一方是NaN,<op>就決定了它們返回的值:l版本返回-1,而g版本返回1。

接下來,我們將用這22個(gè)字節(jié)碼實(shí)現(xiàn)程序的復(fù)雜流程結(jié)構(gòu)。

三.條件結(jié)構(gòu)

在Java中,條件結(jié)構(gòu)類似于下面:

在編譯期間,這種代碼可以被看為:

也就是說,這種結(jié)構(gòu)就是由一個(gè)一個(gè)的if...else結(jié)構(gòu)組合形成的。一個(gè)簡單的if...else結(jié)構(gòu)用字節(jié)碼寫入后可以表示為這樣的流程:

if..else結(jié)構(gòu)字節(jié)碼寫入順序和執(zhí)行流程

if判斷條件通常都使用了返回boolean的表達(dá)式(除了特殊字節(jié)碼指定的比較方式外都需要這樣傳入),而boolean值的true是1,false是0,使用IFEQ字節(jié)碼相當(dāng)于被反向判斷。相類似的,javac在編譯時(shí)經(jīng)常將字節(jié)碼操作反轉(zhuǎn)保證if塊先于else塊寫入。

返回boolean值傳入if中的選擇結(jié)構(gòu)類似于這樣:

在跳轉(zhuǎn)之后,我們的操作棧和局部變量表會和跳轉(zhuǎn)之前相等。同時(shí),visitMaxs的參數(shù)變成了所有分支下最大的局部變量表大小和最大的操作棧深度。

下面舉一個(gè)例子。要生成這樣的Java代碼:

我們的字節(jié)碼應(yīng)該像下面這樣寫:

條件結(jié)構(gòu)可以被簡化為三元運(yùn)算符,三元運(yùn)算符的字節(jié)碼也類似于if...else。

下面是一個(gè)使用三元運(yùn)算符的例子:

字節(jié)碼寫入:

四.循環(huán)結(jié)構(gòu)

循環(huán)結(jié)構(gòu)都比較類似,都是流程返回到之前的代碼部分。先從while語句開始,它寫入類似于下面這樣:

while結(jié)構(gòu)的字節(jié)碼形式和流程,Y/N代表的是未反轉(zhuǎn)的結(jié)果,以便于理解流程

接下來用一段Java代碼寫一個(gè)例子:

這是用字節(jié)碼的方式寫入:

類似于while,do...while結(jié)構(gòu)也用了和它基本一致的思路,只不過是循環(huán)條件寫到了后面,并且循環(huán)條件不用反轉(zhuǎn)

do...while結(jié)構(gòu)的字節(jié)碼寫入流程和執(zhí)行流程

do...while循環(huán)就不再舉例子了,下面來看看for循環(huán)。

for循環(huán)有兩種:普通的for語句和for-each語句。普通的for循環(huán)語句在定義時(shí)包括了三條語句:一條初始化、一條條件和一條循環(huán)結(jié)束執(zhí)行語句。它的流程類似于while多加了一些部分:

普通for循環(huán)的字節(jié)碼寫入流程和執(zhí)行流程

接下來就是一個(gè)例子,使用for循環(huán):

在字節(jié)碼里面要這樣寫:

另一種for循環(huán),即for-each循環(huán),它的實(shí)現(xiàn)和for循環(huán)很不一樣。

for-each需要一個(gè)Iterable的對象才能使用,它的原理就是通過iterator進(jìn)行迭代。下面這兩種形式是等價(jià)的:

也就是說,for-each本質(zhì)是while循環(huán)。由于沒有講泛型,所以就不細(xì)講此處。

五.棧幀

我相信你已經(jīng)把上面的例子都跑了一遍(沒跑也沒事,我默認(rèn)已經(jīng)跑了),可是這些東西在你嘗試運(yùn)行它們的時(shí)候都會報(bào)錯(cuò)。它們報(bào)的錯(cuò)無一例外都是VerifyError,這是出了什么毛?。窟@就有關(guān)于棧幀了。

Java中執(zhí)行方法時(shí),JVM會分配給當(dāng)前線程一個(gè)棧幀,棧幀和方法綁定,它的內(nèi)部就是現(xiàn)在的局部變量表和操作棧數(shù)據(jù)(這在第三篇文章說過)。棧幀內(nèi)的局部變量表大小和操作棧大小來自visitMaxs。棧幀在方法開始執(zhí)行時(shí)創(chuàng)建,在方法返回時(shí)(包括拋出異常)銷毀。

但是在類文件中,我們不能保證一個(gè)類它的數(shù)據(jù)是不是異常的——有可能它規(guī)定的棧幀局部變量表或者操作棧小于真正運(yùn)行時(shí)的大小。所以Java引入了類的驗(yàn)證階段,檢查類內(nèi)部數(shù)據(jù)。其中有一項(xiàng)就是檢查方法棧幀——檢查方法字節(jié)碼是否正確排序、變量類型是不是一致等。但是這種驗(yàn)證很耗費(fèi)時(shí)間,所以JVM驗(yàn)證器引入了StackMapTable進(jìn)行輔助,這樣就能在線性的運(yùn)行下檢查。但是每一行都加入棧幀映射(stack map frame)實(shí)在是太浪費(fèi)空間了,所以JVM做了優(yōu)化,規(guī)定每個(gè)跳轉(zhuǎn)目標(biāo)之后都必須有一個(gè)映射用于表示棧幀變化。

棧幀映射中并不是一個(gè)真的局部變量表和操作棧類型表,它是以一種和前面的映射比較的方式保存——比如這個(gè)映射要比前面的映射少兩個(gè)元素等。第一個(gè)映射前面并沒有別的映射,所以它和空的操作棧與參數(shù)列表組成的局部變量表的棧幀比較。

(可以看看https://stackoverflow.com/questions/25109942/what-is-a-stack-map-frame下面的評論)

所以引發(fā)異常的真正原因我們找到了——看來驗(yàn)證器沒有檢查到方法內(nèi)部跳轉(zhuǎn)指令后的棧幀映射,導(dǎo)致了驗(yàn)證失敗拋出異常。

那么怎么寫入棧幀映射呢?

MethodVisitor提供了一個(gè)方法,叫visitFrame。它就是用于寫入當(dāng)前棧幀數(shù)據(jù)變化的方法。這個(gè)方法需要在每個(gè)跳轉(zhuǎn)目標(biāo)的visitLabel后面去寫,不是用于跳轉(zhuǎn)的標(biāo)簽不需要visitFrame。

visitFrame的方法原型如下:

它有5個(gè)參數(shù),指明了這個(gè)映射和前面的映射的比較方式和數(shù)據(jù)。先講后面的參數(shù),最后再講第一個(gè)參數(shù)。

第三個(gè)參數(shù)是一個(gè)代表局部變量變化的一個(gè)數(shù)組,長度應(yīng)該為第二個(gè)參數(shù)。數(shù)組內(nèi)的取值分為這幾種:

  • 如果變量是一個(gè)沒有初始化的對象,那么這個(gè)值是指向這個(gè)對象NEW字節(jié)碼的標(biāo)簽對象。

  • 如果變量是this并且在調(diào)用父類構(gòu)造函數(shù)之前被調(diào)用,這個(gè)值是UNINITIALIZED_THIS。

  • 如果變量類型不是基本類型,值就應(yīng)該是它的類的全限定名/描述符字符串。

  • 如果是基本類型,那么取值是固定的:int用INTEGER代替,float用FLOAT代替,long用LONG代替,double用DOUBLE代替,空用NULL代替。long和double即使需要占兩個(gè)槽位也不需要寫兩遍,byte、short、char、boolean要用INTEGER代替。

  • 如果這個(gè)局部變量槽位上暫時(shí)是空位(注意不是空對象),用TOP代替。

第五個(gè)參數(shù)類似,是表示操作棧變化的一個(gè)數(shù)組,長度是第四個(gè)參數(shù)。

下面是重點(diǎn)——第一個(gè)參數(shù)的意義。它的不同取值和意義如下:

  • F_NEW,只能在Java 6使用(或者ClassWriter被指定擴(kuò)展棧幀映射),它的寫入和之后的版本不一樣(其實(shí)是類似F_FULL,寫入和之前的棧幀信息無關(guān))。這篇文章不會介紹Java 6的棧幀映射寫入。

  • F_SAME,代表這里的局部變量表和之前的棧幀信息相比沒有變化,numLocal和numStack為0,兩個(gè)數(shù)組都為null。(即使不是null也不會寫入)

  • F_SAME1,代表這里的局部變量表和之前的棧幀信息一樣,而操作棧上有一個(gè)變量。numLocal是0,numStack是1,local是null,stack是一個(gè)數(shù)組,內(nèi)部只有一個(gè)元素,代表現(xiàn)在棧上對象的類型。

  • F_APPEND,代表現(xiàn)在的局部變量表和之前的棧幀信息一樣,但是會多出1-3個(gè)新的局部變量。numLocal是新增加的局部變量的數(shù)量,local是一個(gè)長度為numLocal的數(shù)組,存儲新增加的局部變量的類型。numStack是0,stack為null。

  • F_CHOP,代表現(xiàn)在的局部變量表要比之前的棧幀信息少1-3個(gè)局部變量。numLocal就是局部變量缺少的數(shù)量,numStack是0,local和stack都是null。

  • F_FULL,這代表現(xiàn)在的棧幀和之前的棧幀沒有關(guān)系,相當(dāng)于復(fù)寫了棧幀的信息。numLocal是局部變量數(shù)量,local是局部變量類型數(shù)組,numStack是操作棧深度,stack是操作棧類型數(shù)組。當(dāng)現(xiàn)在的棧幀比之前的棧幀多/少3個(gè)以上的局部變量,或者操作棧上有變量(除非局部變量表不變且棧深度為1可以使用F_SAME1對應(yīng)),都需要用這個(gè)標(biāo)志重新寫入。

在編譯時(shí),編譯器會盡量減少F_FULL的出現(xiàn)次數(shù),保證類文件不會因?yàn)轭~外棧幀信息變得臃腫。在我們自己生成字節(jié)碼時(shí),也盡量不要用F_FULL。

接下來,我們來回顧我們報(bào)錯(cuò)的代碼:

按照之前的代碼,我們要在每個(gè)跳轉(zhuǎn)目標(biāo)上加上棧幀信息:

為了方便用戶操作,asm自己加了一個(gè)計(jì)算棧幀信息的標(biāo)識:COMPUTE_FRAMES。在ClassWriter構(gòu)造函數(shù)中使用。

使用這個(gè)后,所有的visitFrame和visitMaxs都不需要我們自己寫。ClassWriter會根據(jù)字節(jié)碼推斷棧幀信息等并寫入,代價(jià)是增加近一倍的運(yùn)行時(shí)間。

六.實(shí)戰(zhàn)

下面,我們將用字節(jié)碼寫出一個(gè)簡單的階乘程序,使用for循環(huán)計(jì)算階乘并且用if判斷是否溢出。對應(yīng)的Java代碼如下:

首先計(jì)劃一下程序標(biāo)簽的位置:

下面我們用不開啟COMPUTE_FRAMES的ClassWriter進(jìn)行寫入:

然后我們對生成的類進(jìn)行測試:

得到下面的輸出:

這就代表成功了!

全部源代碼:https://paste.ubuntu.com/p/Gyhn3wHMQ3/


這篇專欄到這里就結(jié)束了,下一期:Java ASM詳解:MethodVisitor與Opcode(四)其他流程結(jié)構(gòu)

這篇文章一共講了22個(gè)字節(jié)碼,加上以前講過的一共186個(gè)。

這篇文章也同步到了博客上,也可以去那里閱讀。(排版有些不同,但是內(nèi)容一樣)

有錯(cuò)誤可以在評論區(qū)指出~

Java ASM詳解:MethodVisitor與Opcode(三)標(biāo)簽,條件結(jié)構(gòu),循環(huán)結(jié)構(gòu),棧幀的評論 (共 條)

分享到微博請遵守國家法律
大竹县| 武定县| 普兰店市| 且末县| 名山县| 旌德县| 务川| 阿拉善左旗| 尼玛县| 北票市| 中西区| 龙泉市| 龙游县| 辉南县| 苗栗市| 尚义县| 兴义市| 军事| 许昌市| 南城县| 南郑县| 砀山县| 崇信县| 大荔县| 江北区| 广南县| 县级市| 昆山市| 栖霞市| 雅江县| 屯昌县| 新丰县| 上犹县| 望江县| 彝良县| 小金县| 石狮市| 义马市| 宁河县| 南京市| 饶平县|