# ASMTEST **Repository Path**: DDjason/ASMTEST ## Basic Information - **Project Name**: ASMTEST - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2019-11-25 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ASM jvm字节码分析和修改工具 学习分享 [TOC] ## 一、ASM概述 ### ASM简述 [ASM官网](https://asm.ow2.io/) https://asm.ow2.io/ [ASM文档](https://asm.ow2.io/asm4-guide.pdf) https://asm.ow2.io/asm4-guide.pdf ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。 ### 用途 - 程序分析,既可能只是简单的语法分析(syntaxic parsing),也可能是完整的语义分析 (sematic analysis),可用于查找应用程序中的潜在 bug、检测未被用到的代码、对代码 实施逆向工程,等等。 - 程序生成,在编译器中使用。这些编译器不仅包括传统编译器,还包括用于分布式程序 设计的 stub 编译器或 skeleton 编译器,以及 JIT(即时)编译器,等等。 - 程序转换可,用于优化或混淆(obfuscate)程序、向应用程序中插入调试或性能监视代 码,用于面向切面的程序设计(AOP),等等 ASM 已经被广泛应用于一系列 Java 项目:AspectWerkz、AspectJ、BEA WebLogic、IBM AUS、OracleBerkleyDB、Oracle TopLink、Terracotta、RIFE、EclipseME、Proactive、Speedo、Fractal、EasyBeans、BeanShell、Groovy、Jamaica、CGLIB、dynaop、Cobertura、JDBCPersistence、JiP、SonarJ、Substance L&F、Retrotranslator 等。Hibernate 和 Spring 也通过 cglib,另一个更高层一些的自动代码生成工具使用了 ASM ### ASM库的使用时机 既然ASM是一个java class文件 字节码的层面的工具,那么它的**使用时机可以在什么时候?** ![ASM使用时机](./ClassAutoCreateLoad.png) android开发时,我们都知道,在打包过程中,是先将java文件编译成class文件,之后再把class文件编译成dex文件。如果我们想要对代码进行一些修改操作的话,可以在class文件打包成dex文件的过程中,扫描代码,找出我们想要修改的代码进行修改。ASM可以帮助我们重新生成class文件,把处理后的代码生成class文件,之后打包成dex文件。 在apk安装到android设备运行时**能不能修改或生成新的class字节码**然后在 dalvik、art虚拟机里加载运行 ### 能生成并产生新的字节码的方式举例 1. Javassist Javassist是一个开源的分析、编辑和创建Java字节码的类库[Javassist](https://github.com/jboss-javassist/javassist) 2. JDK的动态代理 3. cglib CGLIB(Code Generator Library)是一个强大的、高性能的代码生成库 ... 上述中的实现中cglib的实现是依靠ASM。 ![cglib](./Cglib.png) #### ASM是一种可以面向切面的工具 ASM既然可以在class字节码中插入想要执行的代码,那么也可以实现面向切面的编程 比较其他面向切面的方式的使用时机 ![ASM 图标](./APT_AspectJ_JavaSSiit.png) 1. 利用 APT 修改java文件 ,可以在编译期生成java源代码 代表框架:DataBinding,Dagger2 2. AspectJ在java --> class阶段,修改java代码; Android性能调优工具Hugo [Hugo](https://github.com/JakeWharton/hugo) 3. Javassist和asm,都是修改的.class 代表框架:热修复框架HotFix **不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。** ## Class字节码和JVM指令码 ### 1. Class结构解析 一个class文件的格式 | 类型 | 描述 | 备注 | |-----------------|------------------------------------------|------------------| | u4 | magic | 魔数:0xCAFEBABE | | u2 | minor_version | 小版本号 | | u2 | major_version | 主版本号 | | u2 | constant_pool_count | 常量池大小,从1开始 | | cp_info | constant_pool [constant_pool_count-1] | 常量池信息 | | u2 | access_flags | 访问标志 | | u2 | this_class | 类索引 | | u2 | super_class | 父类索引 | | u2 | interfaces_count | 接口个数 | | u2 | interfaces [interfaces_count] | 接口类索引信息 | | u2 | fields_count | 字段数 | | field_info | fields [fields_count] | 字段表信息 | | u2 | methods _count | 方法数 | | method_info | methods [methods_count] | 方法表信息 | | u2 | attributes_count | 属性个数 | | attribute_info | attributes [attributes_count] | 属性表信息 | 一个标准的class文件就是上表的文件格式,按照以8位字节为基础单位的二进制流进行存储。 1. 魔数:0xCAFEBABE 表示这个文件的类型是一个class文件 2. 小版本号和主版本号 与编译的javac jdk版本号有关 3. 在常量池大小后面紧跟的就是常量池的具体的信息 4. 常量池中每一个常量都是一个表,jdk1.7之后一共有14种类型的常量,他们对应着14个不同结构的表,但这14个表都有一个共同特点:那就是表开始的第一位是一个u1类型的标志位,代表当前常量属于哪种常量类型。 常量池的容量计数不是从0开始的,而是从1开始的,这是因为0有它的特殊用用途,那就是为了表达在特殊情况下需要表达“不引用任何一个常量池项目”的含义。在Class文件结构中只有常量池的容量计数是从1开始的,对于其他集合,包括接口索引集合、字段集合、方法集合等的容量计数都是从0开始的。 其取值和含义如下表所示: 常量类型表 ![常量池表](./常量池表.png) 常量结构表 ![常量结构表](./常量池的变量结构.png) 通过上述的表可以将整个常量池分析出来 5. 访问标志 紧接着常量池之后的两个字节表示访问标志,主要是用来标记类或者接口层次的一些属性。目标之定义了16个标志位中的8位,没有使用到的一律为0。 具体标志位如下表: ![访问标志](./访问标志.png) 6. 类索引、父类索引和接口索引集合 在访问标志之后,有3个用来确定一个类的继承关系的数据,按先后顺序分别是:类索引(用于确定类的全限定名)、 父类索引(用于确定父类的全限定名)、 接口索引(用于描述类实现了哪些接口)。其索引的数值指向常量池中 7. 字段表集合 在接口索引之后是字段表集合,字段表用来描述接口或者类中声明的变量。它包括类级变量和实例级变量,但是不包括局部变量以及从父类和接口中继承而来的字段。字段表的格式如下: ![字段表集合.png](./字段表集合.png) 7.1. 字段修饰符 字段修饰符与类中的访问标志很类似,用来描述字段的一些属性: ![字段修饰符](./字段修饰符.png) 7.2. 全限定名 把类全路径中的.替换为/,同时在最后加入一个;即可。 7.3. 简单名称 简单名称指的是没有类型和修饰符的字段或者方法名称。 7.4. 描述符 描述符用来描述字段的数据类型、方法的参数列表和返回值。其中基本类型字段的描述符用一个大写字母来表示,而对象类型则用字符L加上对象类型的全限定名来表示。具体如下表: ![字段描述符.png](./字段描述符.png) 8. 方法表集合 对方法描述的方式与对字段描述的方式基本一致,方法表的结构也与字段表的结构完全一致,不同之处在于方法的访问标志与字段的访问标志有所区别。例如volatile与transient不能修饰方法,但是方法却有synchronized、native、strictfp和abstract等属性。其具体访问标志如下: ![方法表集合.png](./方法表集合.png) 9. 属性表集合 属性表集合用于描述某些场景的专有信息,它一共有21个属性,属性表结构如下: ![属性表.png](./属性表.png) ### 2. JVM指令码 [jdk8指令码](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5) Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。 固定格式 **Opcode+Operands** ``` java 指令码 助记符 说明 0x00 nop 无操作 0x01 aconst_null 将null推送至栈顶 0x02 iconst_m1 将int型-1推送至栈顶 0x03 iconst_0 将int型0推送至栈顶 0x04 iconst_1 将int型1推送至栈顶 0x05 iconst_2 将int型2推送至栈顶 0x06 iconst_3 将int型3推送至栈顶 0x07 iconst_4 将int型4推送至栈顶 0x08 iconst_5 将int型5推送至栈顶 0x09 lconst_0 将long型0推送至栈顶 0x0a lconst_1 将long型1推送至栈顶 0x0b fconst_0 将float型0推送至栈顶 0x0c fconst_1 将float型1推送至栈顶 0x0d fconst_2 将float型2推送至栈顶 0x0e dconst_0 将double型0推送至栈顶 0x0f dconst_1 将double型1推送至栈顶 0x10 bipush 将单字节的常量值(-128~127)推送至栈顶 0x11 sipush 将一个短整型常量值(-32768~32767)推送至栈顶 0x12 ldc 将int, float或String型常量值从常量池中推送至栈顶 0x13 ldc_w 将int, float或String型常量值从常量池中推送至栈顶(宽索引) 0x14 ldc2_w 将long或double型常量值从常量池中推送至栈顶(宽索引) 0x15 iload 将指定的int型本地变量推送至栈顶 0x16 lload 将指定的long型本地变量推送至栈顶 0x17 fload 将指定的float型本地变量推送至栈顶 0x18 dload 将指定的double型本地变量推送至栈顶 0x19 aload 将指定的引用类型本地变量推送至栈顶 0x1a iload_0 将第一个int型本地变量推送至栈顶 0x1b iload_1 将第二个int型本地变量推送至栈顶 0x1c iload_2 将第三个int型本地变量推送至栈顶 0x1d iload_3 将第四个int型本地变量推送至栈顶 0x1e lload_0 将第一个long型本地变量推送至栈顶 0x1f lload_1 将第二个long型本地变量推送至栈顶 0x20 lload_2 将第三个long型本地变量推送至栈顶 0x21 lload_3 将第四个long型本地变量推送至栈顶 0x22 fload_0 将第一个float型本地变量推送至栈顶 0x23 fload_1 将第二个float型本地变量推送至栈顶 0x24 fload_2 将第三个float型本地变量推送至栈顶 0x25 fload_3 将第四个float型本地变量推送至栈顶 0x26 dload_0 将第一个double型本地变量推送至栈顶 0x27 dload_1 将第二个double型本地变量推送至栈顶 0x28 dload_2 将第三个double型本地变量推送至栈顶 0x29 dload_3 将第四个double型本地变量推送至栈顶 0x2a aload_0 将第一个引用类型本地变量推送至栈顶 0x2b aload_1 将第二个引用类型本地变量推送至栈顶 0x2c aload_2 将第三个引用类型本地变量推送至栈顶 0x2d aload_3 将第四个引用类型本地变量推送至栈顶 0x2e iaload 将int型数组指定索引的值推送至栈顶 0x2f laload 将long型数组指定索引的值推送至栈顶 0x30 faload 将float型数组指定索引的值推送至栈顶 0x31 daload 将double型数组指定索引的值推送至栈顶 0x32 aaload 将引用型数组指定索引的值推送至栈顶 0x33 baload 将boolean或byte型数组指定索引的值推送至栈顶 0x34 caload 将char型数组指定索引的值推送至栈顶 0x35 saload 将short型数组指定索引的值推送至栈顶 0x36 istore 将栈顶int型数值存入指定本地变量 0x37 lstore 将栈顶long型数值存入指定本地变量 0x38 fstore 将栈顶float型数值存入指定本地变量 0x39 dstore 将栈顶double型数值存入指定本地变量 0x3a astore 将栈顶引用型数值存入指定本地变量 0x3b istore_0 将栈顶int型数值存入第一个本地变量 0x3c istore_1 将栈顶int型数值存入第二个本地变量 0x3d istore_2 将栈顶int型数值存入第三个本地变量 0x3e istore_3 将栈顶int型数值存入第四个本地变量 0x3f lstore_0 将栈顶long型数值存入第一个本地变量 0x40 lstore_1 将栈顶long型数值存入第二个本地变量 0x41 lstore_2 将栈顶long型数值存入第三个本地变量 0x42 lstore_3 将栈顶long型数值存入第四个本地变量 0x43 fstore_0 将栈顶float型数值存入第一个本地变量 0x44 fstore_1 将栈顶float型数值存入第二个本地变量 0x45 fstore_2 将栈顶float型数值存入第三个本地变量 0x46 fstore_3 将栈顶float型数值存入第四个本地变量 0x47 dstore_0 将栈顶double型数值存入第一个本地变量 0x48 dstore_1 将栈顶double型数值存入第二个本地变量 0x49 dstore_2 将栈顶double型数值存入第三个本地变量 0x4a dstore_3 将栈顶double型数值存入第四个本地变量 0x4b astore_0 将栈顶引用型数值存入第一个本地变量 0x4c astore_1 将栈顶引用型数值存入第二个本地变量 0x4d astore_2 将栈顶引用型数值存入第三个本地变量 0x4e astore_3 将栈顶引用型数值存入第四个本地变量 0x4f iastore 将栈顶int型数值存入指定数组的指定索引位置 0x50 lastore 将栈顶long型数值存入指定数组的指定索引位置 0x51 fastore 将栈顶float型数值存入指定数组的指定索引位置 0x52 dastore 将栈顶double型数值存入指定数组的指定索引位置 0x53 aastore 将栈顶引用型数值存入指定数组的指定索引位置 0x54 bastore 将栈顶boolean或byte型数值存入指定数组的指定索引位置 0x55 castore 将栈顶char型数值存入指定数组的指定索引位置 0x56 sastore 将栈顶short型数值存入指定数组的指定索引位置 0x57 pop 将栈顶数值弹出 (数值不能是long或double类型的) 0x58 pop2 将栈顶的一个(long或double类型的)或两个数值弹出(其它) 0x59 dup 复制栈顶数值并将复制值压入栈顶 0x5a dup_x1 复制栈顶数值并将两个复制值压入栈顶 0x5b dup_x2 复制栈顶数值并将三个(或两个)复制值压入栈顶 0x5c dup2 复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶 0x5d dup2_x1 复制栈顶的一个或两个值,将其插入栈顶那两个或三个值的下面 0x5e dup2_x2 复制栈顶的一个或两个值,将其插入栈顶那两个、三个或四个值的下面 0x5f swap 将栈最顶端的两个数值互换(数值不能是long或double类型的) 0x60 iadd 将栈顶两int型数值相加并将结果压入栈顶 0x61 ladd 将栈顶两long型数值相加并将结果压入栈顶 0x62 fadd 将栈顶两float型数值相加并将结果压入栈顶 0x63 dadd 将栈顶两double型数值相加并将结果压入栈顶 0x64 isub 将栈顶两int型数值相减并将结果压入栈顶 0x65 lsub 将栈顶两long型数值相减并将结果压入栈顶 0x66 fsub 将栈顶两float型数值相减并将结果压入栈顶 0x67 dsub 将栈顶两double型数值相减并将结果压入栈顶 0x68 imul 将栈顶两int型数值相乘并将结果压入栈顶 0x69 lmul 将栈顶两long型数值相乘并将结果压入栈顶 0x6a fmul 将栈顶两float型数值相乘并将结果压入栈顶 0x6b dmul 将栈顶两double型数值相乘并将结果压入栈顶 0x6c idiv 将栈顶两int型数值相除并将结果压入栈顶 0x6d ldiv 将栈顶两long型数值相除并将结果压入栈顶 0x6e fdiv 将栈顶两float型数值相除并将结果压入栈顶 0x6f ddiv 将栈顶两double型数值相除并将结果压入栈顶 0x70 irem 将栈顶两int型数值作取模运算并将结果压入栈顶 0x71 lrem 将栈顶两long型数值作取模运算并将结果压入栈顶 0x72 frem 将栈顶两float型数值作取模运算并将结果压入栈顶 0x73 drem 将栈顶两double型数值作取模运算并将结果压入栈顶 0x74 ineg 将栈顶int型数值取负并将结果压入栈顶 0x75 lneg 将栈顶long型数值取负并将结果压入栈顶 0x76 fneg 将栈顶float型数值取负并将结果压入栈顶 0x77 dneg 将栈顶double型数值取负并将结果压入栈顶 0x78 ishl 将int型数值左移位指定位数并将结果压入栈顶 0x79 lshl 将long型数值左移位指定位数并将结果压入栈顶 0x7a ishr 将int型数值右(符号)移位指定位数并将结果压入栈顶 0x7b lshr 将long型数值右(符号)移位指定位数并将结果压入栈顶 0x7c iushr 将int型数值右(无符号)移位指定位数并将结果压入栈顶 0x7d lushr 将long型数值右(无符号)移位指定位数并将结果压入栈顶 0x7e iand 将栈顶两int型数值作“按位与”并将结果压入栈顶 0x7f land 将栈顶两long型数值作“按位与”并将结果压入栈顶 0x80 ior 将栈顶两int型数值作“按位或”并将结果压入栈顶 0x81 lor 将栈顶两long型数值作“按位或”并将结果压入栈顶 0x82 ixor 将栈顶两int型数值作“按位异或”并将结果压入栈顶 0x83 lxor 将栈顶两long型数值作“按位异或”并将结果压入栈顶 0x84 iinc 将指定int型变量增加指定值(i++, i--, i+=2) 0x85 i2l 将栈顶int型数值强制转换成long型数值并将结果压入栈顶 0x86 i2f 将栈顶int型数值强制转换成float型数值并将结果压入栈顶 0x87 i2d 将栈顶int型数值强制转换成double型数值并将结果压入栈顶 0x88 l2i 将栈顶long型数值强制转换成int型数值并将结果压入栈顶 0x89 l2f 将栈顶long型数值强制转换成float型数值并将结果压入栈顶 0x8a l2d 将栈顶long型数值强制转换成double型数值并将结果压入栈顶 0x8b f2i 将栈顶float型数值强制转换成int型数值并将结果压入栈顶 0x8c f2l 将栈顶float型数值强制转换成long型数值并将结果压入栈顶 0x8d f2d 将栈顶float型数值强制转换成double型数值并将结果压入栈顶 0x8e d2i 将栈顶double型数值强制转换成int型数值并将结果压入栈顶 0x8f d2l 将栈顶double型数值强制转换成long型数值并将结果压入栈顶 0x90 d2f 将栈顶double型数值强制转换成float型数值并将结果压入栈顶 0x91 i2b 将栈顶int型数值强制转换成byte型数值并将结果压入栈顶 0x92 i2c 将栈顶int型数值强制转换成char型数值并将结果压入栈顶 0x93 i2s 将栈顶int型数值强制转换成short型数值并将结果压入栈顶 0x94 lcmp 比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶 0x95 fcmpl 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 0x96 fcmpg 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 0x97 dcmpl 比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 0x98 dcmpg 比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 0x99 ifeq 当栈顶int型数值等于0时跳转 0x9a ifne 当栈顶int型数值不等于0时跳转 0x9b iflt 当栈顶int型数值小于0时跳转 0x9c ifge 当栈顶int型数值大于等于0时跳转 0x9d ifgt 当栈顶int型数值大于0时跳转 0x9e ifle 当栈顶int型数值小于等于0时跳转 0x9f if_icmpeq 比较栈顶两int型数值大小,当结果等于0时跳转 0xa0 if_icmpne 比较栈顶两int型数值大小,当结果不等于0时跳转 0xa1 if_icmplt 比较栈顶两int型数值大小,当结果小于0时跳转 0xa2 if_icmpge 比较栈顶两int型数值大小,当结果大于等于0时跳转 0xa3 if_icmpgt 比较栈顶两int型数值大小,当结果大于0时跳转 0xa4 if_icmple 比较栈顶两int型数值大小,当结果小于等于0时跳转 0xa5 if_acmpeq 比较栈顶两引用型数值,当结果相等时跳转 0xa6 if_acmpne 比较栈顶两引用型数值,当结果不相等时跳转 0xa7 goto 无条件跳转 0xa8 jsr 跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶 0xa9 ret 返回至本地变量指定的index的指令位置(一般与jsr, jsr_w联合使用) 0xaa tableswitch 用于switch条件跳转,case值连续(可变长度指令) 0xab lookupswitch 用于switch条件跳转,case值不连续(可变长度指令) 0xac ireturn 从当前方法返回int 0xad lreturn 从当前方法返回long 0xae freturn 从当前方法返回float 0xaf dreturn 从当前方法返回double 0xb0 areturn 从当前方法返回对象引用 0xb1 return 从当前方法返回void 0xb2 getstatic 获取指定类的静态域,并将其值压入栈顶 0xb3 putstatic 为指定的类的静态域赋值 0xb4 getfield 获取指定类的实例域,并将其值压入栈顶 0xb5 putfield 为指定的类的实例域赋值 0xb6 invokevirtual 调用实例方法 0xb7 invokespecial 调用超类构造方法,实例初始化方法,私有方法 0xb8 invokestatic 调用静态方法 0xb9 invokeinterface 调用接口方法 0xba invokedynamic 调用动态链接方法 0xbb new 创建一个对象,并将其引用值压入栈顶 0xbc newarray 创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶 0xbd anewarray 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶 0xbe arraylength 获得数组的长度值并压入栈顶 0xbf athrow 将栈顶的异常抛出 0xc0 checkcast 检验类型转换,检验未通过将抛出ClassCastException 0xc1 instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶 0xc2 monitorenter 获得对象的锁,用于同步方法或同步块 0xc3 monitorexit 释放对象的锁,用于同步方法或同步块 0xc4 wide 扩大本地变量索引的宽度 0xc5 multianewarray 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶 0xc6 ifnull 为null时跳转 0xc7 ifnonnull 不为null时跳转 0xc8 goto_w 无条件跳转 0xc9 jsr_w 跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶 ============================================ 0xca breakpoint 调试时的断点标记 0xfe impdep1 为特定软件而预留的语言后门 0xff impdep2 为特定硬件而预留的语言后门 最后三个为保留指令 ``` 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。 在一个方法的调用中需要了解栈帧的中局部变量表和操作数栈是怎么配合行动的 ### 3. 局部变量表 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。 在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。 一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务(用于实现finally块,java5版本及之前纯在)的,目前已经很少使用了。 虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的变量表来存储。 在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非 static 的方法),那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字 “this” 来访问到这个隐含的参数。其余参数则按照方法参数表顺序排列,占用从 1 开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。 为了尽可能节省栈帧空间,局部变量中的 Slot 是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码 PC 计数器的值已经超出了某个变量的作用域,那这个变量对应的 Slot 就可以交给其他变量使用。 **不使用的对象应手动赋值为 null** ``` java public static void main(String[] args) { byte[] placeholder = new byte[64 * 1024 * 1024]; System.gc(); } public static void main(String[] args) { { byte[] placeholder = new byte[64 * 1024 * 1024]; } System.gc(); } public static void main(String[] args) { { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 0; System.gc(); } ``` ### 4. 操作数栈 操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。 操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。 当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。 ``` java begin iload_0 // push the int in local variable 0 onto the stack iload_1 // push the int in local variable 1 onto the stack iadd // pop two ints, add them, push result istore_2 // pop int, store into local variable 2 end ``` ![local+operand.png](./local+operand.png) ``` java int i = 2; i = i++; System.out.println(i); ``` 操作数栈最大值的计算 局部便量表的计算 常量池 ### ASM框架 ## ASM原理 asm分为如下包 核心API - **org.objectweb.asm**包 - **org.objectweb.asm.signature**包 定义了基于事件的API,提供了类分析器和写入器组件。它们包含在asm.jar文件中。 - **org.objectweb.asm.util**包 位于 asm-util.jar存档文件中,提供各种基于核心 API 的工具,可以在开发和调试 ASM 应用程序时使用。 - **org.objectweb.asm.commons**包 提供了几个很有用的预定义类转换器,它们大多是基于核心 API 的。这个包包含在 asm-commons.jar 存档文件中。 - **org.objectweb.asm.tree**包 位于 asm-tree.jar 存档文件中,定义了基于对象的 API,并提供了一些工具,用于在基于事件和基于对象的表示方法之间进行转换。 - **org.objectweb.asm.tree.analysis**包 提供了一个类分析框架和几个预定义的类分析器,它们以树 API 为基础。这个包包含在 asm-analysis.jar 存档文件中。 ASM框架中的核心类有以下几个: 1. ClassReader:该类用来解析编译过的class字节码文件。 2. ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。 3. ClassAdapter:该类也实现了ClassVisitor接口,它将对它的方法调用委托给另一个ClassVisitor对象。 可以通过IDEA插件 __ASM Bytecode Viewer__ 来查看class来辅助编ASM编写。 #### ASM 框架的执行过程 ![reader_visitor_writer](./read_visitor_writer.png) ### ASM框架读取.class文件 #### 1. 怎么读取 因为字节码文件有着严格的结构,我们就可以利用它来写出标准的解析方法,甚至是生成我们指定的class类。ASM通过将这些解析和生成的方法封装,就可以解析java字节码文件。 ![ASM读取class.png](./ASM读取class.png) ASM 内部采用 访问者模式 将 .class 类文件的内容从头到尾扫描一遍,每次扫描到类文件相应的内容时,都会调用ClassVisitor内部相应的方法。 比如: - 扫描到类文件时,会回调ClassVisitor的visit()方法; ![class_visitor](./ClassVisit.png) - 扫描到类成员时,会回调ClassVisitor的visitField()方法; - 扫描到类方法时,会回调ClassVisitor的visitMethod()方法; ![field_method_visitor](./field_method_visitor.png) - ······ ![classVisitor](./classVisitor.png) 扫描到相应结构内容时,会回调相应方法,该方法会返回一个对应的字节码操作对象(比如,visitMethod()返回MethodVisitor实例),通过修改这个对象,就可以修改class文件相应结构部分内容,最后将这个ClassVisitor字节码内容覆盖原来.class文件就实现了类文件的代码切入。 ASM 中提供一个ClassReader类,这个类可以直接由字节数组或者class文件间接的获得字节码数据。它会调用accept()方法,接受一个实现了抽象类ClassVisitor的对象实例作为参数,然后依次调用ClassVisitor的各个方法。字节码空间上的偏移被转成各种visitXXX方法。使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移。 这个过程中ClassReader可以看作是一个事件生产者,ClassWriter继承自ClassVisitor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件的消费者。 #### 2. 怎么访问 一、ASM库提供了两类API接口模型来产生或者修改类字节码: (1)核心API: 基于事件,每个事件代表类的一个元素,如头事件、方法事件、字段事件等。特点是更快耗费更少的内存。 - 生成类 ClassVisitor 通过这个类调用 ``` java package pkg; public interface Comparable extends Mesurable { int LESS = -1; int EQUAL = 0; int GREATER = 1; int compareTo(Object o); } cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "pkg/Comparable", null, "java/lang/Object", new String[] { "pkg/Mesurable" }); cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, new Integer(-1)).visitEnd(); cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, new Integer(0)).visitEnd(); cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, new Integer(1)).visitEnd(); cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null).visitEnd(); cw.visitEnd(); byte[] b = cw.toByteArray(); ``` - 转换类 增加和删除类成员 - 方法生成 同ClassVisitor ,转换方法 通过methodVisitor这个类 - 转换方法 无状态转换 有状态转换 (2)树型API: 基于对象树状结构,字段方法等都可以看做对象树的一部分。使用相对简单,但耗费内存,是在基于事件的基础上的。 树形API的核心是`ClassNode `类 ``` java public class ClassNode ... { public int version; public int access; public String name; public String signature; public String superName; public List interfaces; public String sourceFile; public String sourceDebug; public String outerClass; public String outerMethod; public String outerMethodDesc; public List visibleAnnotations; public List invisibleAnnotations; public List attrs; public List innerClasses; public List fields; public List methods; } ``` - 生成类 通过classNode 生成一个class信息 - 添加和删除类成员 添加和删除类就是在 ClassNode 对象的 fields 或 methods 列表中添加或删除元素。 - 方法的生成 类似于ClassNode 有一个methodNode类用于生成方法 ``` java MethodNode mn = new MethodNode(...); InsnList il = mn.instructions; il.add(new VarInsnNode(ILOAD, 1)); LabelNode label = new LabelNode(); il.add(new JumpInsnNode(IFLT, label)); il.add(new VarInsnNode(ALOAD, 0)); il.add(new VarInsnNode(ILOAD, 1)); il.add(new FieldInsnNode(PUTFIELD, "pkg/Bean", "f", "I")); LabelNode end = new LabelNode(); il.add(new JumpInsnNode(GOTO, end)); il.add(label); il.add(new FrameNode(F_SAME, 0, null, 0, null)); il.add(new TypeInsnNode(NEW, "java/lang/IllegalArgumentException")); il.add(new InsnNode(DUP)); il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/IllegalArgumentException", "", "()V")); il.add(new InsnNode(ATHROW)); il.add(end); il.add(new FrameNode(F_SAME, 0, null, 0, null)); il.add(new InsnNode(RETURN)); mn.maxStack = 2; mn.maxLocals = 2; ``` #### 基于树形API ASM库提供了用于方法分析的API 方法分析主要分为两块 1. 数据流分析 目标是对于所有可能出现的参数值,模拟一个方法中的所有可能执行路径。 **org.objectweb.asm.tree.analysis**包中提供正向数据流分析框架 2. 控制流分析 #### 3. 怎么重新生成 ClassWriter重新构建java字节码是和ClassReader解析字节码相对应。当ClassWriter被调用visit方法,按照调用的有序的visit事件,将事件内容按照字节码的规则回编会字节流最后输出字节码class文件。 **删除或插入代码等修改后不影响原代码逻辑的执行么?** ASM框架中怎么重新计算局部变量表和操作数栈 - LocalVariablesSorter 提供的工具将对出现的局部变量重新编号并计算局部变量表大小 - ClassWriter 1. 在使用 new ClassWriter(0)时,不会自动计算任何东西。必须自行计算帧、局部变 量与操作数栈的大小。 2. 在使用 new ClassWriter(ClassWriter.COMPUTE_MAXS)时,将为你计算局部变量 与操作数栈部分的大小。还是必须调用 visitMaxs,但可以使用任何参数:它们将被 忽略并重新计算。使用这一选项时,仍然必须自行计算这些帧。 3. 在 new ClassWriter(ClassWriter.COMPUTE_FRAMES)时,一切都是自动计算。 不再需要调用 visitFrame,但仍然必须调用 visitMaxs(参数将被忽略并重新计算) ## 用 Transform API + ASM 实现 Android的埋点