Android 无埋点从入门到放弃:了解 Java 字节码

                                                                                      

作者:李嘉辉

GrowingIO 大前端开发工程师。负责 GrowingIO Android 和 iOS 无埋点 SDK 的设计和开发,目前专注在 Android AOP 开发。

 

引言

 

Android 无埋点的本质是通过技术手段来改写 Java 字节码,达到“自动写代码”的目的,从而实现业务目标,简单来说就是一种 Hook 形式。但是在进行之前,我们需要了解下 Java 字节码,以免出现错误的修改,导致无埋点变成“crash 点”。这就好比我们写文档前,需要先识字一样。

 

本文讲述的内容虽然比较难懂和枯燥,但是我们尽量用生动的对话形式和例子来描述。如果您一次阅读后无法消化,请收藏,之后可以多次阅读,跟着文章中的步骤动手操作。相信这篇文章会对您接触 Android 无埋点的实践有所帮助。

 

本文涉及到的工具如下(mac/windows)

  • IntelliJ IDEA

  • jclasslib Bytecode viewer

  • 010 Editor

 

场景还原

 

今天是小 O 到 G 公司上班的第一天,公司决定让老 I 带小 O 熟悉公司的代码。

 

老 I:可以先熟悉一下公司的代码。

 

小 O:好的,我这就去。

 

1.字节码

 

小 O:我看到代码中很多常量都来自于 ASM,这个框架有什么作用呢?

 

int ACC_PUBLIC = 0x0001; // class, field, methodint ACC_PRIVATE = 0x0002; // class, field, methodint ACC_PROTECTED = 0x0004; // class, field, methodint ACC_STATIC = 0x0008; // field, methodint ACC_FINAL = 0x0010; // class, field, method, parameterint ACC_SUPER = 0x0020; // class...

 

老 I:ASM 是一个通用的 Java 字节码操控和分析框架。

 

小 O:操作 Java 字节码?

 

老 I:用《Java 虚拟机规范》上的说法,Java 字节码就是编译后被 Java 虚拟机所执行的代码,使用了一种平台中立(不依赖特定硬件及操作系统)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式称为 class 文件格式。

 

小 O:那我要学会 ASM,应该先了解字节码吗?

 

老 I:是的,我先跟你介绍一下字节码的基本结构,以下面的代码为例。​​​​​​​

 

public class Main{  public static void main(String[] args) {    System.out.println("Hello World!");  }}

 

老 I:一个标准的 ClassFile 结构如下:​​​​​​

 

ClassFile {        u4              magic;        u2              minor_version;        u2              major_version;        u2              constant_pool_count;        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;        filed_info      fields[fields_count];        u2              methods_count;        method_info     methods[methods_count];        u2              attributes_count;        attribute_info  attributes[attributes_count]; }

 

老 I:让我们通过一个更加易懂的方式继续,我们通过 010Editor 打开上述代码编译后的 class 文件可以看到如下信息。

 

 

2.magic

 

小 O:这个开头的数字 0xCAFEBABE 很有意思,是有某种特殊含义吗?

 

老 I:这是一个魔数,用于判断是否是一个 class 文件,是一个固定值,也就是 0xCAFEBABE(魔数背后的故事 https://en.wikipedia.org/wiki/Java_class_file),至于为什么使用这个魔数,可以看一下文章里 James Gosling 的描述。

 

3.minor_version

 

小 O:紧跟着魔数的 0x00000034 看图好像分为了 minor 和 major,是表示版本号吗?

 

老 I:对的,minor 表示本地 java 环境副版本号,你可以通过 java -version 查看本地 java 版本,比如我的本地版本,1.8 为主版本号,0 为副版本号,如图所示 0x0000,171 为 update。

 

 

4.major_version

 

老 I:major 表示本地 java 环境主版本号,0x0034 即 52 对应 1.8,0x0033 即 51 对应 1.7,依此类推。

 

5.constant_pool_count

 

小 O:下面这个 constant_pool_count 有 34 个, 010Editor 中显示的 0-32 就是他的成员了吗,那为什么 count 是 34 个呢?

 

老 I:这是因为 010Editor 默认使用的 class 解析模版(template)是从 0-32,但是实际上常量池使用 1 ~ constant_pool_count-1 作为索引,count 则为常量池中成员数 +1,0 用于保留作为其他用途,这个可以使用 IDEA 的 jclasslib Bytecode viewer 插件查看,它是符合从 1 开始计数的规范的,并且提供了对各种类型的过滤。

 

                                                       

 

小 O:那 0 的保留用途具体是什么呢?

 

老 I:这是一个好问题,0 的保留用途可以参照一些例子, 比如说:

  • java/lang/Object时的superclass

  • 表示捕获所有异常(等价于catch java/lang/Throwable)时的catch_type

  • 类为顶层类、顶层接口、本地类、匿名类时的outer_class_info_index

  • 匿名类时的inner_name_index

  • 当前类不是直接包含在某个方法或构造器时的method_index

     

6.constant_pool[constant_pool_count-1]

 

小 O:那后面紧跟着的就是所谓的常量池成员了吗?

 

老 I:对的,常量池包含所有字符串常量、类或接口名、字段名和其它常量,常量池数组中不同元素类型不同,结构不同,但是均使用第一个字节作为类型标记(tag byte),涉及的内容比较多,我们先整体上了解一下,跳过常量池继续往下看。

 

 

7.access_flags

 

小 O:这个 flag 是 0x0021,这是什么意思呢?

 

老 I:还记得最初你在 ASM 框架中看到的常量吗,这个 flag 定义如下。

 

              标志名                 值                含义

ACC_PUBLIC

0x0001

public
ACC_FINAL0x0010final
ACC_SUPER0x0020

JDK1.0.2 前需要对父类方法特殊处理,1.0.2 后带有以区分

ACC_INTERFACE0x0200interface
ACC_ABSTRACT0x0400abstract
ACC_SYNTHETIC0x1000synthetic,非 Java 源代码生成
ACC_ANNOTATION0x2000annotation
ACC_ENUM0x4000enum

   

老 I:你图中看到的 0x0021 即 ACC_SUPER & ACC_PUBLIC。

 

8.this_class

 

小 O:那这个 this_class(0x0005) 也是某种常量吗?

 

老 I:不是的,这里的 0x0005 是一个指向常量池中某个成员的有效索引,常量池中该索引处成员必须为 CONSTANT_Class_info 类型结构体,表示这个 class 文件所定义的类或接口。

 

老 I:在图中可以看到 0x0005 指向常量池中如下位置,你应该还记得 010Editor中template 解析常量池数组的 name 显示是从 0 开始,而实际常量池的索引从 1 开始,所以 0x0005 在 010Editor 中对应 constant_pool[4]),即下面图中这个位置。

 

 

老 I:然后根据该常量池成员结构的 name_index,继续查找常量池中对应 index,0x001A。

 

 

老 I:如果觉得 010Editor 不够清晰或者不好查找,可以使用 Intellij 的插件 jclasslib Bytecode viewer,支持直接跳转,方便快速查找。

 

 

9.super_class

 

小 O:那后面这个 0x0006 也是一个指向常量池的索引吗?

 

老 I:对的,父类索引,常量池中该索引处成员必须为 CONSTANT_Class_info 类型结构,表示这个 class 文件所定义的类的直接超类,如果为 0,即当前类为 java/lang/Object。

 

老 I:由图中可以看到 0x0006 指向常量池中如下位置。

 

 

老 I:根据 name_index,继续查找常量池中对应 index,0x001B。

 

 

老 I:这里我们可以通过 jclasslib 插件查看,支持直接跳转查看父类,还记得我们之前说过常量池 0 的保留用途吗?

 

 

老 I:从这里我们可以看到 Object 的 super_class 指向常量池中 0 这个位置(即无效索引)。

 

10.interfaces_count

 

小 O:我知道了,那这个 0x0000,表示当前类或接口的直接超接口数量,即 interfaces 表中成员个数是 0。

 

老 I:没错,就是这个道理。

 

11.interfaces[interfaces_count]

 

老 I:因为我们之前的类是没有超接口的,所以在原 class 文件增加两个接口,则可以看到:

 

老 I:在这个数组中,每个成员的值必须是一个有效索引,常量池中该索引处成员必须为 CONSTANT_Class_info 类型结构体,表示这个 class 文件所定义的类或接口的直接超接口,与源码定义接口从左往右顺序一致。

 

12.fields_count

 

小 O:那后面这个 0x0000 表示当前 class 文件 fields表中成员个数为 0。

 

老 I:对的。

 

13.fields[fields_count]

 

老 I:在这个数组中,每一个成员都是由 filed_info 结构所定义。​​​​​​​

 

field_info {    u2             access_flags;    u2             name_index;    u2             descriptor_index;    u2             attributes_count;    attribute_info attributes[attributes_count]; }

 

老 I:之前的代码没有定义成员,所以我们使用如下代码来看这个结构。

​​​​​​​

public class Main {    int i = 0;}

 

(1)access_flags

小 O:我知道,这个 flag 也是一些常量标识。

 

老 I:对的,这些常量标识的定义如下。

 

              标志名                 值                含义
ACC_PUBLIC0x0001public
ACC_PRIVATE0x0002private
ACC_PROTECTED0x0004protected
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_VOLATILE0x0040volatile
ACC_TRANSIENT0x0080transient
ACC_SYNTHETIC0x1000字段由编译器生成
ACC_ENUM0x4000enum

 

老 I:因为我们代码中使用的是默认 access_flags,所以在 010Editor 中显示为 0x0000。

 

(2)name_index

小 O:那这个结构中的 name_index、descriptor_index ,带着 index,应该都是指向常量池中某个成员的索引吧。

 

老 I:对,name_index 是一个有效索引,常量池中该索引处成员必须为 CONSTANT_Utf8_info 结构, 根据图中索引为 0x0005,找到常量池如下位置。

 

 

(3)descriptor_index

老 I:descriptor_index 也是一个有效索引,常量池中该索引处成员必须是 CONSTANT_Utf8_info 结构,为字段的描述符,根据图中索引 0x0006,找到常量池如下位置。

 

 

(4)attributes_count

小 O:那接下来的这个 attributes 是做什么的呢? 为什么这里是 0x0000?

 

老 I:这个表示当前字段的附加属性表中成员的个数。

 

(5)attributes[attributes_count]

老 I:属性表的成员,每一个成员为 attribute_info 结构,举个例子,可以在字段上增加 @Deprecated,如下图所示,具体的可以在之后看 attribute_info 时分析。

 

 

14.methods_count

 

小 O:这个应该是表示当前 class 文件 methods 表成员个数,最初那个例子中,0x0002 表示有两个函数,但是我们不是只定义了 main 函数吗?

 

老 I:对的,在最开始使用的例子中,0x0002 表示有两个函数。

 

15.methods[methods_count]

 

老 I:紧跟着 methods_count 的是方法表的成员,每一个成员为 method_info 结构,用于表示当前类或接口中某个方法的完整描述,结构如下。

​​​​​​​

method_info {    u2             access_flags;    u2             name_index;    u2             descriptor_index;    u2             attributes_count;    attribute_info attributes[attributes_count];}

 

老 I:我们使用如下代码来看这个结构,也顺道解释为什么之前的代码虽然只定义了一个 main 函数,count 却是 0x0002。

​​​​​​​

public class Main {}

 

老 I:我们可以通过 jclasslib 来看,比较清晰。实际上会有一个编译器提供的默认构造函数。

 

 

(1)access_flags

小 O:嗯,这个 method_info 跟 field_info 很接近,让我来试着解释一下。

 

老 I:好的,请开始你的表演,这个 flag 的定义我先列出来。

 

              标志名                 值                含义
ACC_PUBLIC0x0001public
ACC_PRIVATE0x0002private
ACC_PROTECTED0x0004protected
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_SYNCHRONIZED0x0020synchronized
ACC_BRIDGE0x0040bridge 方法,由编译器产生
ACC_VARARGS0x0080表示方法带有变长参数
ACC_NATIVE0x0100native
ACC_ABSTRACT0x0400abstract
ACC_STRICT0x0800strictfp,使用 FP-strict 浮点模式
ACC_SYNTHETIC0x1000字段由编译器生成

 

小 O:因为默认生成的构造函数是 public,所以对应 010Editor 中显示为 0x0001,即 ACC_PUBLIC。

 

(2)name_index

小O:name_index 是一个有效索引,常量池中该索引处成员必须为 CONSTANT_Utf8_info 结构,表示一个特殊方法名或有效非限定名,根据图中索引为 0x0004, 就可以找到常量池在如下位置。

 

 

(3)descriptor_index

小O:descriptor_index 也是一个有效索引,常量池中该索引处成员必须为 CONSTANT_Utf8_info 结构,表示方法的描述符,根据图中索引为 0x0004,就可以找到常量池在如下位置。

 

 

(4)attributes_count

小 O:这个 attributes_count 表示当前方法的附加属性表中成员的个数。

 

(5)attributes[attributes_count]

小 O:属性表的成员,每一个成员应该也为 attribute_info 结构。

 

老 I:嗯,对的,厉害了。这部分我补充个例子,比如 Code_attribute,可以看到如下图所示,具体在之后看 attribute_info 时分析。

 

 

16.attributes_count

 

小 O:这个 attributes_count 应该表示的是当前 class 文件属性表的成员个数。

 

老 I:嗯,没错。

 

17.attributes[attributes_count]

 

老I:这个文件属性表成员,每项都必须是attribute_info 结构,以 SourceFile_attribute 为例,我们通过如下代码来看。

​​​​​​​

public class Main {}

 

老 I:如下图所示。

 

 

老 I:其结构如下:​​​​​​​

 

SourceFile_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 sourcefile_index;}

 

(1)attribute_name_index

小 O:这个 index 肯定是一个有效索引。

 

老 I:对的,常量池中该索引处成员必须为 CONSTANT_Utf8_info 结构,表示字符串 "SourceFile"。

 

(2)attribute_length

老 I:SourceFile_attribute 的 length 为固定值 2,即图中 0x00000002。

 

(3)sourcefile_index

老 I:最后的 index 也是一个有效索引,常量池中该索引处成员必须为 CONSTANT_Utf8_info 结构,表示被编译的 class 文件的源文件名。

 

等你先熟练掌握今天介绍的内容,我再给你继续讲解其他的结构。常量池、属性(filed, method 中的 attribute_info 也将放在一起)涉及较多内容,掌握字节码对后续了解及使用 ASM(一个通用的 Java 字节码操控和分析框架)有很大帮助,也可以让你更快熟悉代码。

 

小 O:好的,让我再分析几个 class 文件熟悉一下。

 

小结

 

理解 Java 字节码并不单单对了解无埋点插桩( ASM 等框架)有帮助,在一些常见的情境下它也能发挥自己的作用。所以对 Java 字节码的掌握有利于我们更加深入地了解 Java 的各种机制,比如说很常见的面试题(可以看看自己是否能够回答上来)

  1. String 在编译期与运行期最大长度

  2. synchronized 语义

  3. new 语句中 dup 指令作用

 

参考文档:

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

  • 《Java虚拟机规范 Java SE 8版》

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值