Java方法签名
学习bytecode、dalvik bytecode甚至JNI时,都会用到方法的签名,因此这里先给大家介绍下。
使用javap(将在2.2章中介绍)工具可生成方法的签名,如:
方法定义:
1 | public native int add(long a, String b); |
对应的签名:
1 | (JLjava/lang/String;)I |
我们可以看到,在签名中,返回值在最后,参数使用括号括住。
下面我来详细解释一下它们的关系:
基本数据类型都有一一对应的签名:
数据类型 | 签名 |
---|---|
void | V |
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
float | F |
long | J |
double | D |
对象与数组比较特殊。
对象均以L开头,以;结束,中间为包名+类名,以/分隔,如类java.lang.String的签名为Ljava/lang/String;
数组使用[来表示,一个[代表一维。如String[]的签名为[Ljava/lang/String;,String[][]的签名为[[Ljava/lang/String;,如果是int[][][]三维数组,则其签名为[[[I
bytecode介绍
关于bytecode的介绍,可参考http://en.wikipedia.org/wiki/Java_bytecode
学习bytecode与学习汇编有些相似,没学过汇编的童鞋或许会觉得汇编很难,其实汇编语法不难,只是使用汇编写复杂程序难。而bytecode相对来说比汇编还要简单些,这里大概介绍bytecode的语法。
bytecode是JVM规范中所定义的指令集,由JVM识别并运行。
bytecode是基于栈的,指令所用到的数据均取自于栈,而指令的操作结果,则会压入栈顶。对于栈不理解的,请先看下数据结构中栈的定义。另外,参数与临时变量都是存放在本地变量中,可以把本地变量和栈都理解为一个固定的int型数组。
这里先举个简单的例子:
下面是一个简单的方法:
1 | int add(int a, int b) { |
这个方法中,需要把两个参数中的数据进行加法运算,加法运算的指令为iadd,上面说了指令所用到的数据均来自于栈,因此就需要先把参数a和参数b加载到栈中。
我们看下相应的字节码:
1 | int add(int a, int b) { |
为了书写方便,以后均用locals代表本地变量,locals[i]代表第i个本地变量。
iload是用来从locals中加载内容的,但为什么是1呢?或许有人会说locals的计数是从1开始的。但其实不是,这里的计数与java语法中数组的计数一样,都是从0开始的。
大家有没注意到,这个方法是个成员方法,我们知道成员方法是有this指针的,所以locals[0]就是this指针。当然如果是静态方法,就没this指针了,那么locals[0]就是第1个参数了。
也就是说,参数是按顺序放在locals的最前面的,而临时变量就是放在locals的参数后面。这个特性非常重要,大家要记住,后面会提到。
bytecode指令可以分为几类,这里是我综合对ASM(后缀文章会介绍到)与维基百科的理解进行分类,相比维基百科所述的分类复杂一点,但更详细更好理解:
1)常量赋值
对象常量(aconst_null)、基本数据类型常量(iconst_、lconst_、fconst_、dconst_、bipush、sipush)、另外ldc指令适用于所有常量。
需要注意的是,dconst_*泛指了dconst_0与dconst_1,其它类似,后面也有类似的写法。
2)数组操作
创建(newarray/anewarray)、读取(xaload)、写入(xastore)、取长度(arraylength)。
3)栈操作
压入栈(pop/pop2)、从栈中拷贝(dup*)、交换栈中的值(swap)
4)算术运算
加(add)、减(sub)、乘(mul)、除(div)、取模(rem)、取反(neg)、左移(shl)、算术右移(shr)、逻辑右移(ushr)、与(and)、或(or)、异或(xor)
5)复杂类型相关
强制类型转换(2、checkcast)、创建(new)、是否某个类的实例(instanceof)
6)比较或控制跳转
比较指令(cmp),控制跳转指令(if*、goto、jsr)
7)返回(*return/ret)
8)异常(athrow)
9)本地变量操作
读取(load)、写入(store)
10)成员变量操作
读取(getfield/getstatic)、写入(putfield/putstatic)
11)方法调用(invoke*)
12)switch语句(*switch)
指令的详细说明请参考:
http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
由于指令非常多,不便于一一细说,大家可通过eclipse的bytecode插件来对照着看,此插件会在下一章中介绍。
另外有一点需要注意的是,long和double都是占8个字节的,而上面说过本地变量和栈都可当作int型数组,因为Java中int型占4个字节,所以long和double都占两个位置,而其它类型(boolean、byte、char、short、int、float、引用)都占4个字节。
举个例子,如果有如下方法定义void a(long a, char b),假设我们想加载参数b在栈中,需要怎么写呢?我们看到这是一个成员方法,成员方法含this指针,因此locals[0]为this指针。而参数a为long型,占两个字节,因为locals[1~2]为参数a,那么参数b就是locals[3]了。相应的指令为iload 3。
细心的童鞋会发现,bytecode的大部分操作都没有boolean、byte、char、short相应的指令,只有与int相关的指令,因为它们都直接作为int来处理了。
dalvik bytecode介绍
官方文档:https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html
dalvik bytecode与bytecode相似,都是一套指令集,由相应的虚拟机来识别运行。但不同的是,davlik bytecode是dalvik VM的指令集,且它是基于寄存器的,也就是本地变量是保存在寄存器中,且指令的数据来源于寄存器。
与上面相同的例子,我们看下dalvik bytecode是怎样的
1 | int add(int a, int b) { |
根据注释可以知道,v2和v3分别是分别是参数a和参数b,而v0则是临时变量c。而这个方法又是成员方法,所以v1就是this指针了。
这里反映了dalvik bytecode的一个特点:临时变量在最前面的寄存器,参数在临时变量之后,寄存器计数从0开始。
有学过smali(后面会介绍)语法的童鞋或许会有疑问,参数不是应该从p0开始,而临时变量则是从v0开始么?实际上smali的写法只是为了方便大家而已,它生成dex时会针对性地做转换处理。
指令的分类和bytecode大同小异,这里不作区分,建议大家先把bytecode学好,这样学dalvik bytecode自然会事半功倍。
细心的童鞋会发现,很多指令对寄存器的范围以及常量有限制,例如const/4 vA, #+B中的A或B这类字母都代表4bit,而vA代表目的寄存器,#+B代表常量,因此目的寄存器需要在v0v15之间,而常量要在00xF之间。
bytecode与dalvik bytecode对比
bytecode是基于栈的,其参数与临时变量均存放在locals中,而指令所需要的数据,都存放在stack中。
dalvik bytecode是基于寄存器的,其参数、临时变量及指令所需要的数据,均存放在寄存器中。
bytecode有专门的指令(aconst_null)来代表空对象指针。
dalvik bytecode空对象没作区分,直接使用常量0来表示,也就意味着当一个寄存器被赋值为0,我们需要根据下文才能识别它是对象还是基本数据类型。
bytecode指令的执行结果会被JVM自动压入栈顶中,无须我们关心。
dalvik bytecode有部分指令的执行结果需要使用move-result*这一系列的指令来指定存放在某个寄存器中。
总结
本文并没深入介绍bytecode与dalvik bytecode,但介绍的知识点都是一些关键的点,对于入门来说已经足够了,建议有兴趣学习bytecode与dalvik bytecode的童鞋能够对照着代码来学,多看看。
关于如何对照着代码来
阅读bytecode,可参见2.2章ASM bytecode online插件部分,而dalvik bytecode的阅读,请参见2.3章asmdex,smali/baksmali。