手写 Class 字节码解析技术(二)字段的操作

          上一章节讲述字节码的解析,只要知道了calss的文件结构,我们就可以操作字节码里面的数据,从而改变其代码结构,完成动态代理,AOP AapectJ此类框架的功能。当然精力有限,只能是弄一个小案例,弄懂其中原理。

         动态的往class字节码中添加字段。效果如下,MainHello是原始的class文件,MainHello111是操作之后的class,可以看到 test_wuzhi  , char_name ,  book_name 三个字段已经成功写入。

手写 Class 字节码解析技术(二)字段的操作_第1张图片

          手写 Class 字节码解析技术(二)字段的操作_第2张图片         手写 Class 字节码解析技术(二)字段的操作_第3张图片

         

首先完成这个功能,我们需要知道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入我们准备的字节码即可。

手写 Class 字节码解析技术(二)字段的操作_第4张图片

         字段类型亦是如此,就不过多讲解。

       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后续整体的结构位。

 手写 Class 字节码解析技术(二)字段的操作_第5张图片

 

       思路大概如上,改变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_Version JDK1.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 标识。

   

  于是我们画个草图,分析如下。

 

手写 Class 字节码解析技术(二)字段的操作_第6张图片

   后续诸如 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

你可能感兴趣的:(android技术)