Java ASM詳解:MethodVisitor與Opcode(一)基本操作與運算

前文我們說到了很多Visitor,它們用于給類中定義類型,添加字段,附上注釋。但是對于一個語言來說,最重要的那一部分我們還沒有說到——那就是:方法。
一.方法定義
在ClassVisitor中,我們看到了有一個方法名為visitMethod,參數(shù)是(int,String,String,String,String[]),按照參數(shù)列表的順序,它們分別指訪問標志,方法名,方法描述符,泛型簽名和拋出異常列表,返回一個MethodVisitor。(關(guān)于方法描述符,請看此系列的第一篇;關(guān)于訪問標志,請看第二篇)
對于方法名,有下面的規(guī)則:
1.方法名不能是關(guān)鍵字或保留字(goto)
2.方法名不能以數(shù)字開頭
3.可以為<init>和<clinit>
其中,<init>是構(gòu)造函數(shù),一個類可以有不止一個構(gòu)造函數(shù)。而<clinit>每個類最多有一個,并且方法描述符必須為()V,它在類初始化階段被JVM調(diào)用。(包括調(diào)用這個類的成員和Class.forName,但不包括Class.forName的initialize參數(shù)為false時的調(diào)用)
若方法名不正確,在嘗試加載這個類的時候會拋出java.lang.ClassFormatError: Illegal method name。
拋出異常列表中,所有的類名稱都為全限定名。
二.操作棧(Operand Stack)
操作棧是一個方法被調(diào)用時JVM分配出來的一個??臻g,它用于存儲方法內(nèi)加載的數(shù)據(jù)和進行字節(jié)碼指令操作。當JVM接收到一個字節(jié)碼指令(例如iadd),就會取出棧頂?shù)膸醉椩兀▽τ趇add來說,就是棧頂?shù)膬身棧谶M行操作之后,將計算或獲得的數(shù)據(jù)放回棧頂(比如iadd計算棧頂兩個int的加和之后會放回加和數(shù)字)。
對于普通的對象,只會占用一個棧元素。但對于long或double這種對象,會占用兩個棧元素。這有關(guān)于之后要介紹的visitMaxs。
如果一個字節(jié)碼需要超過了現(xiàn)在操作棧內(nèi)的元素數(shù)量的元素,那么在調(diào)用生成的方法時會拋出`java.lang.VerifyError: Unable to pop operand off an empty stack`。
如果一個字節(jié)碼需要的類型與現(xiàn)在操作棧中元素類型不同,那么在調(diào)用生成的方法時拋出`java.lang.VerifyError: Register <slot> contains wrong type`或`java.lang.VerifyError: Bad type on operand stack`。
在之后的講解中,我們會大量的使用這個名詞,在接下來的編寫中,操作棧的變化將會像下面這樣寫:
三.局部變量表(Local Variable Table)
局部變量表在方法調(diào)用中分配的另一個空間,用于存儲現(xiàn)在方法內(nèi)所有的局部變量,表中的數(shù)據(jù)可以被編號為0-n,叫Slot。普通的元素只會占用一個Slot,但long和double這種數(shù)據(jù)會占用兩個。關(guān)于這個的詳細使用,請看下面的xload和xstore的字節(jié)碼介紹。
當這個方法為靜態(tài)方法時,局部變量表會將參數(shù)列表中的變量按順序放入局部變量表中。
當這個方法不是靜態(tài)方法,局部變量表的0位是this,之后才會將參數(shù)列表變量依次放入表中。
如果局部變量表大小超過了256,那么字節(jié)碼將會發(fā)生變化,xload、xstore等都會受到影響(需要以wide字節(jié)碼輔助才能進行正常的局部變量讀取寫入)。但是ASM9中不提供wide字節(jié)碼,因為MethodWriter中有ASM庫自己的處理,所以在用戶層編寫ASM是無影響的。
四.MethodVisitor的方法
在說完操作棧的概念之后,我們來看看MethodVisitor中都定義了哪些有關(guān)于字節(jié)碼和執(zhí)行的方法。
下面這些方法第一個參數(shù)都為字節(jié)碼。
visitInsn(int):訪問一個零參數(shù)要求的字節(jié)碼指令,如ACONST_NULL
visitIntInsn(int, int):訪問一個需要零操作棧要求但需要有一個int參數(shù)的字節(jié)碼指令,如BIPUSH
visitVarInsn(int, int):訪問一個有關(guān)于局部變量的字節(jié)碼指令,如ALOAD
visitTypeInsn(int, String):訪問一個有關(guān)于類型的字節(jié)碼指令,如CHECKCAST
visitFieldInsn(int, String, String, String):訪問一個有關(guān)于字段的字節(jié)碼,如PUTFIELD
visitMethodInsn(int, String, String, String, boolean):訪問一個有關(guān)于方法調(diào)用的字節(jié)碼,如INVOKESPECIAL
visitJumpInsn(int, Label):訪問跳轉(zhuǎn)字節(jié)碼,如IFEQ
之后,是一些被包裝好的字節(jié)碼訪問方法,這些方法都基于最基本的字節(jié)碼指令,但是不需要我們自己用上面提到的那些方法直接調(diào)用字節(jié)碼。
visitInvokeDynamicInsn(String, String, Handle, Object...):基于INVOKEDYNAMIC,動態(tài)方法調(diào)用,會在lambda表達式和方法引用里面說到
visitLdcInsn(Object):基于LDC、LDC_W和LDC2_W,將一個常量加載到操作棧用(詳細見下文)
visitIincInsn(int, int):基于IINC、IINC_W,自增/減表達式
visitTableSwitchInsn(int, int, Label, Label...):基于TABLESWITCH,用于進行table-switch操作
visitLookupSwitchInsn(Label, int[], Label[]):基于LOOKUPSWITCH,用于進行l(wèi)ookup-switch操作
visitMultiANewArrayInsn(String, int):基于MULTIANEWARRAY,用于創(chuàng)建多重維度數(shù)組,如int[][]
在下文說到它們時,會以下面的方式表達:
到這里,所有有關(guān)于字節(jié)碼指令的方法就結(jié)束了。塊級結(jié)構(gòu)的方法會在下一篇說。
最后,說一下每個方法都要在最后調(diào)用的方法:visitMaxs(int, int)。它第一個參數(shù)是操作棧的最大大小,第二個是局部變量的個數(shù)。如果你調(diào)用這個方法時局部變量數(shù)量寫小了,就會在生成方法調(diào)用時拋出`java.lang.ClassFormatError: Arguments can't fit into locals`,如果操作棧大小寫小了,在生成方法調(diào)用時會拋出`java.lang.VerifyError: Stack size too large`
那么下面,我們將逐系列逐條講解所有的字節(jié)碼。這篇專欄先講基本的操作棧加載存儲等操作、常量獲取和運算操作。
注意:接下來的x可以為a(針對對象)、i(針對int)、l(針對long)、f(針對float)、d(針對double)、b(針對byte)、c(針對char)、s(針對short),它代表了操作對象的類型。有些時候沒有針對于byte和short的專用字節(jié)碼,這是因為在JVM中,byte和short在被計算時會被強制拉長為int,所以它們使用的和int一樣。char和int能互相轉(zhuǎn)換。boolean類似,它們也需要使用int的字節(jié)碼,而且boolean值的false就是int值0,而true就是int值1。
[啥事都不干的字節(jié)碼:nop]
這個字節(jié)碼啥都不干,在實際開發(fā)中可以當做代碼插入點使用。
[加載字節(jié)碼:xload與xload_n]
x=a/i/l/f/d
如果在調(diào)用此字節(jié)碼時對應(yīng)位置沒有初始化變量(原先為參數(shù)或已經(jīng)用xstore進行值的放入被視為該位置被初始化),在生成方法調(diào)用時會拋出`java.lang.VerifyError: Accessing value from uninitialized register <slot>`。
如果要進行加載的對象位置小于等于3,可以用對應(yīng)的xload_n版本代替(注意,ASM9的Opcodes中已經(jīng)不存在xload_n版本的字節(jié)碼常量,但是在javap反匯編時可以看到此條),例如aload_2。
[存儲字節(jié)碼:xstore與xstore_n]
x=a/i/l/f/d
存儲對象的位置規(guī)則與加載相同。與加載規(guī)則不同的是,xstore可以指定到一個未初始化的位置,并將這個位置初始化。有意思的一點是,你可以不遵循初始化位置的連續(xù)性,也就是說,假如2、3位置都未初始化,你可以通過xstore將對象放入3中并初始化它,這時位置2變?yōu)榱宋炊x的狀態(tài),它在被xload加載時都會拋出`java.lang.VerifyError: Register <slot> contains wrong type`,即使你用的加載指令與放入指令類型相同。這時你只能通過另一次xstore將對象放入位置2,才能使這個位置類型固定。
和xload一樣,xstore也有xstore_n版本,但ASM9已經(jīng)不支持直接寫入它們了。
[返回字節(jié)碼:(x)return]
x=a/i/l/f/d
返回字節(jié)碼是每個方法必有的,包括void無返回值方法。如果一個方法沒有寫任何的返回字節(jié)碼指令,在調(diào)用這個生成的方法時就會拋出`java.lang.VerifyError: Falling off the end of the code`。
返回字節(jié)碼無視操作棧內(nèi)剩余的所有值,只會將棧頂元素返回,并清除操作棧。
在這個方法為同步方法的前提下,所在線程不是已經(jīng)鎖定的監(jiān)視器對象所有者時,這條指令會拋出`IllegalMonitorStateException`。這種情況在普通狀況下根本無法發(fā)生,只有當這個同步方法上在其同步對象上使用了monitorexit卻沒有使用monitorenter時可能發(fā)生。
[復(fù)制棧頂字節(jié)碼:dup家族]
這個字節(jié)碼是用于復(fù)制棧頂元素并插入到棧中的字節(jié)碼,可以節(jié)省xload和xstore的使用量。在這里,...指棧頂下的其他元素。
DUP家族的名稱規(guī)律是:DUP后緊接著的數(shù)字代表了復(fù)制數(shù)量,Xn代表插入到棧頂下第幾層。
[彈出棧頂字節(jié)碼:pop,pop2]
這個字節(jié)碼也是用于操作操作棧的。它的使用情況舉一個例子:調(diào)用了一個有返回值的方法但返回值我們不需要,就可以采用POP。
[交換元素字節(jié)碼:swap]
這個字節(jié)碼可以交換棧頂?shù)膬蓚€操作數(shù)。
[常量池常量讀取字節(jié)碼:ldc(ldc_w, ldc2_w)]
常量池(Constant Pool)中,含有以下幾種數(shù)據(jù):整數(shù)Integer、浮點數(shù)Float、字符串字面值String、類的引用Type、句柄Handle或動態(tài)常量值ConstantDynamic,所以LDC值可能有這些。
在JVM中,如果常量值是Integer或Float,就會直接將它們放到操作棧頂;如果為String,將String類的引用放到操作棧頂;若為Type,將對應(yīng)的類型初始化,并將其Class實例引用放到操作棧頂;對于Handle,將java.lang.invoke.MethodHandle/MethodType的引用至于操作棧頂。
在解析類型的引用期間(Type),這條指令可能會拋出有關(guān)于類加載的異常;同樣的,解析有關(guān)于句柄(Handle)的時候也有可能拋出和句柄有關(guān)的異常。
[空值常量字節(jié)碼:aconst_null]
當程序中使用了null,就可以用這個字節(jié)碼。
[普通數(shù)字常量字節(jié)碼:xconst_n]
x=i/l/f/d;對于iconst,n=m1,0,1,2,3,4,5;對于lconst、dconst,n=0,1;對于fconst,n=0,1,2
當數(shù)字較小時,獲得數(shù)字常量可以不使用LDC,可以直接用這些字節(jié)碼代替(節(jié)省常量池空間)。
[整數(shù)常量字節(jié)碼:bipush和sipush]
當一個數(shù)字沒有超過這兩個字節(jié)碼規(guī)定的范圍,我們都可以使用這兩個字節(jié)碼獲取整數(shù)常量。在編譯中,屬于這個范圍的數(shù)字都是用它們進行獲取整數(shù)(除非是-1~5),而更大/小的整數(shù)都是用LDC。
說完了基本的加載存儲常量指令,下面來看看程序的最基本功能:計算。
[取反運算字節(jié)碼:xneg]
x=i/l/f/d
這個字節(jié)碼用于計算取反(-x)。注意:如果計算時數(shù)字溢出、下溢或精度丟失,這個字節(jié)碼也不會反饋任何警告。
對于整數(shù)(int和long),計算規(guī)則就是(~x)+1,當它們處于MIN_VALUE時,取反結(jié)果仍為MIN_VALUE。
對于浮點數(shù)(double和float),這個字節(jié)碼運算為:
取反與從零減去不等價,若x為+0.0,0.0-x結(jié)果為+0.0,而-x為-0.0
若數(shù)字為NaN(Not A Number,float的0x7fc00000或double的0x7ff8000000000000L),結(jié)果也為NaN
若數(shù)字為無窮大(float正0x7f800000負0xff800000,double正0x7ff0000000000000L負0xfff0000000000000L),結(jié)果為相反符號的無窮大
若數(shù)字為0,結(jié)果為相反符號的0
[加法運算字節(jié)碼:xadd]
x=i/l/f/d
這個字節(jié)碼用于計算加法(a+b)。注意:如果計算時數(shù)字溢出、下溢或精度丟失,這個字節(jié)碼也不會反饋任何警告。
對于浮點數(shù)(double和float),這個字節(jié)碼運算為:
如果兩個數(shù)都為NaN,結(jié)果是NaN。
2. 如果兩個數(shù)為相反符號的無窮大,和為NaN
同一符號的無窮大結(jié)果是該符號的無窮大
有限值與無窮大的和還是無窮大
相反符號的兩個0(+0和-0)結(jié)果為+0
相同符號的兩個0和為該符號的0
0與非零值的和為非零值
符號相反,絕對值相等的有限值和為+0
若不屬于上面的情況,結(jié)果將以IEEE 754舍入到最近可表示的浮點值。如果結(jié)果太大無法表示為浮點數(shù)(超過最大表示范圍“溢出”,也就是絕對值超過float的3.4028235e+38f或double的1.7976931348623157e+308),結(jié)果為對應(yīng)符號的無窮大;如果結(jié)果太小無法表示為浮點數(shù)(超過最小表示范圍“下溢”,也就是絕對值小于float的1.4e-45f或double的4.9e-324),結(jié)果是對應(yīng)符號的0。
[減法運算字節(jié)碼:xsub]
x=i/l/f/d
這個字節(jié)碼用于計算減法(a-b),等價于a+(-b)。注意:如果計算時數(shù)字溢出、下溢或精度丟失,這個字節(jié)碼也不會反饋任何警告。
浮點數(shù)運算法則請同時參照xadd與xneg。
[乘法運算字節(jié)碼:xmul]
x=i/l/f/d
這個字節(jié)碼用于計算乘法(a*b)。注意:如果計算時數(shù)字溢出、下溢或精度丟失,這個字節(jié)碼也不會反饋任何警告。
對于浮點數(shù)(double和float),這個字節(jié)碼運算為:
兩個數(shù)字中有一個是NaN,結(jié)果為NaN
無窮大乘以一個0,結(jié)果為NaN
無窮大與有限值相乘,結(jié)果為無窮大,符號取決于兩個數(shù)字的符號是否相同,相同為正,相反為負
其余情況為IEEE 754規(guī)定,在xadd那里有完整說明
[除法運算字節(jié)碼:xdiv]
x=i/l/f/d
這個字節(jié)碼用于計算除法(a/b)。注意:如果計算時數(shù)字溢出、下溢或精度丟失,這個字節(jié)碼也不會反饋任何警告。
對于整數(shù)(int和long),這個字節(jié)碼只會保留商的整數(shù)部分。如果除數(shù)為0,這個字節(jié)碼會拋出`java.lang.ArithmeticException: / by zero`
對于浮點數(shù)(double和float),這個字節(jié)碼運算為:
兩個數(shù)字中有一個是NaN,結(jié)果為NaN
無窮大除以無窮大,結(jié)果為NaN
無窮大除以有限值,結(jié)果為無窮大,符號取決于兩個數(shù)字的符號(規(guī)則見xmul)
有限值除以無窮大,結(jié)果為0,符號同上
0除以0為NaN
0除以有限值為0,符號同上
有限值除以0為無窮大,符號同上
其余情況為IEEE 754規(guī)定,在xadd那里有完整說明
[取余運算字節(jié)碼:xrem]
這個字節(jié)碼用于計算取余操作(a%b)。注意:如果計算時數(shù)字溢出、下溢或精度丟失,這個字節(jié)碼也不會反饋任何警告。
對于浮點數(shù)(double和float),這個字節(jié)碼運算為:
兩個數(shù)字中有一個是NaN,結(jié)果為NaN
符號取決于被除數(shù)
被除數(shù)為無窮大或除數(shù)為0,結(jié)果為NaN
被除數(shù)為有限值而除數(shù)為無窮大,結(jié)果為被除數(shù)
被除數(shù)為0,結(jié)果為0
其余情況為IEEE 754規(guī)定,在xadd那里有完整說明
[自增字節(jié)碼:iinc(iinc_w)]
自增字節(jié)碼是適用于int的字節(jié)碼,在以下情境中會用到:
i++或i--或++i或--i
i+=x或i-=x
自增字節(jié)碼可以使用負數(shù)。
[按位且運算字節(jié)碼:xand]
x=i/l
這個字節(jié)碼用于計算按位且操作(a&b)。
[按位或運算字節(jié)碼:xor]
x=i/l
這個字節(jié)碼用于計算按位或操作(a|b)。
[按位異或運算字節(jié)碼:xxor]
x=i/l
這個字節(jié)碼用于計算按位或操作(a^b)。
同時,這個字節(jié)碼還可以用于計算按位取反(這也是JVM的實現(xiàn)):~x=x^(-1)。
[按位左移運算字節(jié)碼:xshl]
x=i/l
這個字節(jié)碼用于計算按位左移操作(a<<b)。如果左移位數(shù)超過了32(int)或64(long)位,系統(tǒng)只會采取最低的5(int)或6(long)位進行左移操作。
[按位右移運算字節(jié)碼:xshr]
x=i/l
這個字節(jié)碼用于計算按位右移操作(a>>b)。如果右移位數(shù)超過了32(int)或64(long)位,系統(tǒng)只會采取最低的5(int)或6(long)位進行右移操作。
[按位無符號右移運算字節(jié)碼:xushr]
x=i/l
這個字節(jié)碼用于計算按位無符號右移操作(a>>>b)。如果右移位數(shù)超過了32(int)或64(long)位,系統(tǒng)只會采取最低的5(int)或6(long)位進行無符號右移操作。
運算字節(jié)碼說完之后,最后,來看看數(shù)字轉(zhuǎn)換的字節(jié)碼。
[轉(zhuǎn)換為float的字節(jié)碼:x2f]
x=i/l/d
轉(zhuǎn)換為float采取了IEEE 754的取值規(guī)律,詳見xadd。雖然對于int,float轉(zhuǎn)換是由低級拓寬范圍,但是由于float值不能取到所有int可表示的數(shù)字(float僅有24位精確數(shù)字,其他為指數(shù)和符號位),所以此轉(zhuǎn)換仍然不精確。
[轉(zhuǎn)換為double的字節(jié)碼:x2d]
x=i/l/f
轉(zhuǎn)換為double采取了IEEE 754的取值規(guī)律,詳見xadd。對于int,這種轉(zhuǎn)換是完全精確的。對于float,如果這個方法是FP-Strict,也就是采取了ACC_STRICT修飾(Java中的strictfp),這個計算就是精確的;如果不是,這個計算可能進行舍入。對于long,由于double值不能取到long表示的所有數(shù)字(double僅有53位精確數(shù)字,其他為指數(shù)和符號位),所以計算不精確。
[轉(zhuǎn)換為int的字節(jié)碼:x2i]
x=d/l/f
由于int在四種數(shù)字中級別最低,long轉(zhuǎn)換為它時都有可能丟失精度(甚至符號位),float和double會使用IEEE 754“向零舍入”。特殊情況下,如果浮點數(shù)的NaN轉(zhuǎn)換為int,值為0;如果浮點數(shù)超出int最大范圍,則為相應(yīng)符號下的最大值。
[轉(zhuǎn)換為long的字節(jié)碼:x2l]
x=i/d/f
由于long級別大于int,int轉(zhuǎn)換為long不丟失精度。在浮點數(shù)下,long與int的轉(zhuǎn)換規(guī)則類似。
[int轉(zhuǎn)換為其他基本類型的字節(jié)碼:i2x]
x=b/c/s
這三個字節(jié)碼能分別將int縮小轉(zhuǎn)換為byte(-128~127)、short(-32768~32767)和char(0~65535)。由于是縮小變換,可能丟失精度甚至符號位。
[下面是使用例子:計算平方和]
Java代碼如下:
使用ASM寫入,如下:

這篇專欄到這里就結(jié)束了,下一期:Java ASM詳解:MethodVisitor與Opcode(二)類、數(shù)組與調(diào)用
這篇文章一共講了130個字節(jié)碼呢~
有錯誤可以在評論區(qū)指出~