上一章节讲述字节码的解析,只要知道了calss的文件结构,我们就可以操作字节码里面的数据,从而改变其代码结构,完成动态代理,AOP AapectJ此类框架的功能。当然精力有限,只能是弄一个小案例,弄懂其中原理。
动态的往class字节码中添加字段。效果如下,MainHello是原始的class文件,MainHello111是操作之后的class,可以看到 test_wuzhi , char_name , book_name 三个字段已经成功写入。
首先完成这个功能,我们需要知道class结构中的 constant 表 、 fields表 两个结构表。
constant 常量表如下,其他可以不看,但是其中UTF8类型 的值是不是很眼熟,
#8中的 main_type #9的Ljava/long/String #12 的name 以及 #24的 I(代表int) ,简单的说就是class结构中所有的字符串常量、类型 、字段名称、 方法名称calss中的UTF8这个类型里面。
想要增加字段,大概思路有这么几步。我们先讲思路,然后继续讲解详细的读取方式。
1、需要增加字段名称以及字段类型,所以需要增加常量池个数大小,也就是十六进制中JDK主版本号后面的【0025】。以及其中fields的个数。对应的结构体为constant_pool_count 和fields_count 。都需要 +1;
2、往constatn_pool区插入 字段名称、字段类型(如果有则不需要,直接饮用其索引项即可) ,以test_wuzhi为例作为字段名称。
按照常量Constant_Utf8_info 的结构规则为 tag(类型 1bit) + length(字符串所占长度 2bit) +bytes(字符串) 这里的length的意思是,后续字节码中有length的长度是我当前Utf8的身体,你们不要解析分割错了。
main_wuzh十六进制字节码为746573745F77757A6869
于是得出完整的main_wuzh 常量的完整字节码:01(tag)000a(length)746573745F77757A6869(bytes)
在class字节码中constatn_pool 数据结尾处插入main_wuzh 的字节码即可。如何确定那里是constatn_pool 的结尾?于是解析class时,需要记录下class结构表中每一个结构体的开始于结束指针位。也就是在class字节码下班1058的位置,insert入我们准备的字节码即可。
字段类型亦是如此,就不过多讲解。
3、 fields表 其中属性如下。
类型 | 描述 | 备注 |
---|---|---|
u2 | access_flags | 记录字段的访问标志 |
u2 | name_index | 常量池中的索引项,指定字段的名称 |
u2 | descriptor_index | 常量池中的索引项,指定字段的描述符 |
u2 | attributes_count | attributes包含的项目数 |
attribute_info | attributes[attributes_count] |
我们以 private String main_wuzh 这个字段作为例子。
从下图中,我们得出字节码 0002(access_flags)002c(name_index)0009(descriptor_index)0000(如果当前字段还有除开系统提供的其他属性,就回增加在attribute_info中,这里没有则为0)。
字段的增加也是和常量一样,解析class字节码时,记录下 fields表的开始与结束为,在结束位插入刚刚的字节码即可。
但是要注意的是往class字节码中插入新的数据时,后续的结构体的起始于结束位的偏移问题。当往字节码中插入长度为X的数据,则后续的结构体起始于结束的指针坐标,也要加上X,以修正class后续整体的结构位。
思路大概如上,改变constant_pool_count 和fields_count 的数码。在添加对应的常量数据与字段数据即可。
那么我在继续说说其中多结构体的读取方式,其中magic 、minor_version这类的单结构体,我们好理解,直接substring即可。那想constant_pool 、fields、methods之类的就比较复杂了,其中又包含了子结构体。这样的结构体,理解与实际解析就比较复杂了,于是我们来详细说道说道。我们就以constant_pool 为例。
简单的说,class其文件结构,不想json 、html、xml 都有想这样 : ‘’ <> 之类的分隔符,当解析到这些分隔符的时候,你就知道,从这里到这里是一个结构体,从那里到那里又是一个新的结构体。class的文件结构特殊之处在于,class其中的结构体,一个一个紧密的排列在一起,从0-8是一个结构体,8-12是一个结构体,12-16又是一个结构体,以此按照class结构表中各结构长度单位u1、u2、u3、u4代替json、xml中分隔符的作用,当指针位从0读取到文件下标位置8的时候,你就知道一个结构体已经读取完了,可以开始读取下一个结构体了,从指针位读取到12的时候,你就可以知道,这是jdk次版本号的内容,以此类推。
读取也很简单,class开头的第一个结构体是 magic 长度为U4,也就是占据了8个单位。那就是从class的开头,substring其中0-8的位置,返回的内容就是calss 其中魔数的十六进制内容,转换对应的字符串,就是我们想要的对应。
我们定义一个变量start_pointer,记录当前读取的位置, 然后把每个结构体的单位长度累加起来,就是下一个结构体的起始位,比如当前已经读取完magic ,那么start_pointer 值就为8,下一个结构体minor_version,长度为u2,也就是4个单位,那么 substring(start_pointer,start_pointer+4) 返回内容,在从十六进制转换成十进制,就是我们想要的内容。
class结构表
符号 | 中文名 | 结构 | 作用 | 规则 |
---|---|---|---|---|
magic | 魔数 | U4 | 所有的由Java编译器编译而成的class文件的前4个字节都是 “0xCAFEBABE” ,JVM用来判断是否是可加载的.class文件 | |
minor_version | 次版本号 | u2 | JVM | |
major_version | 主版本号 | u2 | JVM加载class文件的时候,判断是否可加载,如果JDK.Mj_VersionJDK1.0->45;1.7->51 |
|
constant_pool_count | 常量池中常量数量 | u2 | 记录了constatn_pool中constant_pool_info的数量 | index从1开始;index=0:某些指向常量池的索引值的数据在特定的情况下表达“不引用任何一个常量池项”。 |
constatn_pool | 常量池数据区 | constant_pool_info结构 | 包含Class文件结构及其子结构中引用的所有 字符串常量、类、接口、字段名和其它常量(字面量和符号引用) | tag bytes:第一个字节,用于识别哪种类型的常量。index=constant_pool_count - 1。 |
access_flags | 访问标志 | u2 | 表示某个类或者接口的访问权限及基础属性 | |
this_class | 类索引 | u2 | this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口 | |
super_class | 父类索引 | u2 | super_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的直接父类 | |
interfaces_count | 接口计数器 | u2 | 当前类或接口的直接父类接口数 | |
interfaces | 接口信息数据区(接口表) | u2 | interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引值, 它的长度为 interfaces_count | |
fields_count | 字段计数区 | u2 | fields_count的值表示当前Class文件 fields[] 数组的成员个数 | |
fields | 字段信息数据区(字段表) | field_info结构 | fields[]数组中的每个成员都必须是一个fields_info结构 的数据项,用于表示当前类或接口中某个字段的完整描述,但不包括从父类或父接口继承的部分 | |
methods_count | 方法计数器 | u2 | methods_count的值表示当前Class 文件 methods[]数组的成员个数 | |
methods | 方法信息数据区(方法表) | method_info 结构 | methods[] 数组中的每个成员都必须是一个 method_info 结构 的数据项,用于表示当前类或接口中某个方法的完整描述 | |
attributions_count | 属性计数器 | u2 | attributes_count的值表示当前 Class 文件attributes表的成员个数 | |
attributions | 属性信息数据区(属性表) | attribute_info结构 | attributes 表的每个项的值必须是attribute_info结构 | 在Java 7 规范里,Class文件结构中的attributes表的项包括下列定义的属性InnerClasses 、 EnclosingMethod 、 Synthetic 、Signature、SourceFile,SourceDebugExtension 、Deprecated、RuntimeVisibleAnnotations 、RuntimeInvisibleAnnotations以及BootstrapMethods属性。 |
常量的读取方式
同样的,根据class结构表 所示,主版本后,就是常量池数。读取方式也是substring(start_pointer,start_pointer+4) 返回 0025 ,十进制就是37,标识当前class文件中,有37个常量。开头我们说了,calss的内容结构,是没有分隔符的。如果解决?这里calss用一个常量计数器标识,标识后续的37个单位长度的内容里面,都是我常量区的小弟。那如何确定每个单位的长度是多少?如下图,我们要如何确定cp_info #1是从那里开始,到那里结束,#2又是才能够那里开始,那里结束。从下图常量类型表里面,我们看到14中常量里面有一个tag 标识。
于是我们画个草图,分析如下。
后续诸如 methods 、attributes 、interfaces表会在后续的字节码方法操作中讲解,这章节,就先讲讲关于class字节码中字段操作所需要的知识点。
代码连接 https://pan.baidu.com/s/1D6yi9Lss8_nSwEuRmpWucg ClazzAnalysis类
文章参考
https://blog.csdn.net/sinat_38259539/article/details/78248454
https://my.oschina.net/u/2246410/blog/1800670
https://www.jianshu.com/p/ae3f860499aa?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation