关于Hook相关知识的学习一

接着上一篇文章对对某app通信协议加密字段的一次分析(IDA Pro动态调试、Frida和Hook)
想学习下Xposed和Frida原理以及两者之间的区别(当然还有针对so层的hook工具Cydia,以后有机会再学),写篇文章总结总结,有新的见解了持续更新。之后对App保护策略以及动静态测试进行学习,例如反调试、加壳啥的,努力学习!
hook也分为好几种类型的hook,java层的Davlik hook、ART hook,native层的基于.got.plt节区GOT hook、针对汇编指令级别的Inline hook、基于动态库加载劫持的LD_PRELOAD hook,这里先简单学习针对java层的hook方法。
先看下Xposed工具的介绍:
Xposed框架简介
Xposed框架是一款可以在不修改APK的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作”,在这个框架下,我们可以加载很多插件App,这些插件App可以直接或间接劫持、篡改、伪造一些信息。
从网上截取的一段Xposed的精简介绍: 在这里插入图片描述
这里涉及到了一些概念:app_process、Zygote、XposedBridge.jar、/init.rc等,就一一捋下它们之间与Xposed框架的关系。
一、安卓系统启动
安卓系统按下电源键之后发生了这么几件事:
1)固化在 ROM 中预定位置的 Bootloader 将会被加载到内存中,Bootloader 初始化完软硬件环境后将 Linux 内核启动起来,Linux内核启动完成后会启动 init 进程(安卓系统启动后执行的第一个进程,pid=1)。
2)init 进程会初始化并启动属性服务,并且读取配置文件/init.rc并执行/init.rc文件中配置的任务:
init程序源码如图所示,初始化并启动属性服务,并读取了配置文件/init.rc在这里插入图片描述 /init.rc配置文件中,又启动了一些重要的进程,例如提到的zygote进程,还有servicemanager 进程,/init.rc配置文件中重要的进程启动如下图所示(zygote和servicemanager):
在这里插入图片描述
在这里插入图片描述
这里先看zygote进程,如上图所示,init.rc文件导入了init.zygote64_32.rc这个配置文件(同样要执行),init.zygote64_32.rc配置文件文件内容如下:
在这里插入图片描述
根据图片所示,init.zygote64_32.rc配置文件又会通过执行app_process程序继而启动(产生)zygote进程,app_process程序源码如下(先只用关注红方框):
在这里插入图片描述
如上图所示,app_process程序调用runtime.start(…)方法创建zygote进程。那么start(…)具体传入的参数是什么意思呢,怎么着调用了runtime.start(…)这个方法就创建了zygote进程了? Android.Runtime(…)方法的源码注释如下图所示:
在这里插入图片描述
根据注释,AndroidRuntime::start()方法的功能是:启动dalvik虚拟机(DVM),并且调用"className"这个参数表示的Java类的"static void main(String args)"方法(也就是上一步图中看到的传入的“com.android.internal.os.ZygoteInit”的main()方法)。
Android.Runtime(…)方法的start()方法如下图所示,如下图,通过startVm的方式完成注释中说的启动DVM,之后通过调用startReg()来注册JNI函数(简单来说就是native层c/c++与Java层相互联系的一个函数,不知道JNI的可以去看下ndk开发相关的知识),然后通过JNI方式调用注释中说到的ZygoteInit.main(),第一次进入Java世界:
在这里插入图片描述
在这里插入图片描述
如上图所示,通过JNI方式调用com.android.internal.os.ZygoteInit的main()方法,那么Java世界的ZygoteInit的main()方法又进行了什么操作呢?ZygoteInit的main()方法如下图所示:
在这里插入图片描述
如上图红框所示,第一次进入Java世界后,ZygoteInit的main()主要完成这么几件事:
(1)注册Zygote使用的socket,zygote作为通信的服务端,用于响应客户端请求(可以理解为接收应用子进程创建的请求,当收到请求时,zygote执行一系列操作,最后通过fork创建子进程);
(2)为Java世界预加载类和资源(预加载一次类后,在通过fork创建子进程时,只需要做一个复制即可,这样便加快了子进程的启动速度)
(3)通过调用startSystemServer来启动system_server进程来为Java世界服务
system_server进程是zygote进程创建的第一个进程,也就是“太子”,其中驻留着Android系统多个重要的服务,比如WindowManagerService、ActivityManagerService等。由这个“太子”作为自己的代理人去处理Java世界的繁杂事务。
(4)通过调用runSelectLoopMode()方法,进入无限循环,等待客户端的连接请求,并处理请求。
先看下比较重要的第3)步startSystemServer(…)方法如下图所示,之后讨论下zygote如何接受子进程任务请求并fork子进程,这两者之间关系密切:
在这里插入图片描述
所谓的fork:它是一个系统调用,调用它之后,可以创建一个与当前进程一模一样的进程,包括相同的进程上下文、堆栈地址、内存信息、PCB等。调用fork的进程称为父进程,fork将返回子进程的pid,而新的进程称为子进程,子进程将从fork()处开始执行,并且fork将返回0。

在startSystemServer(…)方法里先创建了 system_server 的进程而后执行了handleSystemServerProcess 方法,handleSystemServerProcess 方法里调用了RuntimeInit的 zygoteInit 方法:
在这里插入图片描述
RuntimeInit的 zygoteInit 方法里又执行了nativeZygoteInit()方法、applicationInit()方法:
在这里插入图片描述
如上图,nativeZygoteInit()方法的作用是使得system_server进程可以使用Binder与其他进程通信,applicationInit(…)方法如下图所示,调用了invokeStaticMain(…)方法:
在这里插入图片描述
invokeStaticMain(…)方法如下:
在这里插入图片描述
SystemServer类的main()方法如下:
在这里插入图片描述
综上,zygote的main()方法的第三步,system_server 进程在启动过程中完成的工作包括:
1)启动 Binder 线程池,使进程可以通过 Binder 与其他进程进程通信
2)创建 SystemServiceManager
3)使用 SystemServiceManager 对各种系统服务进行创建、启动和生命周期管理

至此,我们大致分析完了init.rc配置文件中一些程序的执行流程。

3)应用进程的创建
这里不讨论源码,只从理论角度分析下应用进程的创建。
从网上截取的一个图片(参考链接:https://www.jianshu.com/p/be7e933927ed)
在这里插入图片描述
大致步骤如下:
1)APP进程发起进程创建请求
APP进程可以通过startActivity等系统API请求SystemServer进程创建进程。
2)SystemServer作为中间人接受请求
SystemServer进程把创建进程的请求先交给了Process.start,这个就是创建进程的入口。紧接着SystemServer进程通知ZygoteState创建LocalSocket,因为SystemServer要通过socket与zygote通信,因此SystemServer进程先要创建LocalSocket,以便可以通过LocalSocket 使用socket方式发送/接收数据了。
3)zygote接受中间人SystemServer传来的请求
前文提到过ZygoteInit的main()方法中第一步已经创建了LocalServerSocket,由LocalServerSocket接收SystemServer传来的请求,最后一步runSelectLoop开启了一个循环一直accept客户端的连接。
之后LocalServerSocket调用forkAndSpecialize(…)来fork子进程,forkAndSpecialize(…)方法如下图所示,进程fork完成之后,返回结果给system_server进程的ActivityManagerService:
在这里插入图片描述
到此,Zygote进程把进程fork出来了,之后需要做进程的初始化操作,比如设置进程异常的捕获方式,开始Binder线程池等等,最后进入了ActivityThread的main方法,一个进程就正式被启动了。要特别说明的是每当zygote孵化一个新的应用程序进程时,都会将它启动过程中创建的Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使得每一个应用程序进程都有一个独立的Dalvik虚拟机实例,并且每个子进程还会与Zygote一起共享Java运行时库(预加载的运行时库)。这也是Xposed选择替换app_process,并能 将XposedBridge这个jar包加载到每一个Android应用程序中的原因,这之后再讨论。

二、APP的启动
前面大致提到了App进程的创建,接下来详细分析下APP的启动。
参考(https://blog.csdn.net/oheg2010/article/details/82826415
https://blog.csdn.net/xiangzhihong8/article/details/79986612
https://www.jianshu.com/p/08855d69c0bf
https://blog.csdn.net/hzwailll/article/details/85339714)
先放张比较经典的图,后续根据图进行解释:
在这里插入图片描述
Launcher
我们常见的放置各种应用图标的手机桌面也是一个App,叫做Launcher。Launcher这个App是在前面提到的zygote.main()方法的第三步创建 SystemServiceManager时,由于之后会使用SystemServiceManager创建启动各种系统服务,其中就包含了ActivityManagerService,由ActivityManagerService来启动的。(ActivityManagerService,简称AMS,具有管理Activity行为、控制activity的生命周期、派发消息事件、内存管理等功能,并且它不光是管理Activity的,其实四大组件都归它管)

Binder
Binder是Android跨进程通信(IPC)的一种方式,也是Android系统中最重要的特性之一,android 四大组件以及不同的App都运行在不同的进程,它则是各个进程的桥梁将不同的进程粘合在一起

1)上图第1、2、3、4小步:启动app的某个Activity到创建App进程并执行ActivityThread.main()方法
点击Launcher的某个App图标后,Launcher通过Binder进程间通信机制通知ActivityManagerService,它要启动一个Activity。ActivityManagerService通过Binder进程间通信机制通知Launcher进入Paused状态;Launcher通过Binder进程间通信机制通知ActivityManagerService,它已经准备就绪进入Paused状态, 于是ActivityManagerServicey就去看要启动的应用是否已经在后台运行,如果在后台运行就直接启动;否则通过前文中“应用进程的创建”中的方式先process.start()接收请求,通过socket方式通知zygote,让zygote fork()创建一个新的进程,用来启动一个ActivityThread实例, 即将要启动的Activity就是在这个ActivityThread实例中运行。

2)上图第5、6小步:绑定应用进程到ActivityManagerService(AMS)并启动Activity
运行要启动应用进程的ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象mAppThread传递给ActivityManagerService,这个Binder对象mAppThread相当于应用进程在ActivityManagerService(AMS)的一个代理对象,这样就把这个应用进程与ActivityManagerService(AMS)绑定了起来,以便以后ActivityManagerService能够通过这个Binder代理对象和应用进程自身进行通信,控制应用进程。
之后ActivityManagerService通过Binder进程间通信机制(代理对象)通知ActivityThread,现在一切准备就绪,它可以创建主Activity的实例,并执行它的生命周期方法,也就是诸如OnCreate()、OnResume()等方法,完成Activity的启动。
下面对ActivityThread类进行分析,看下它是怎样把代理对象与AMS绑定起来的:
在这里插入图片描述
ActivityThread类的一些成员变量如上图所示,前面提到的代理对象mAppThread就是在ActivityThread类里面定义的,同时,它还定义了内部类 H(上图最后一行),H继承于Handler 用于跨进程通信切换线程。这里,ActivityThread通过代理对象mAppThread与AMS通信,AMS返回相应请求命令到mAppThread后,mAppThread又通过这个H与ActivityThread主线程进行通信(Handler机制)。
ActivityThread的main()方法如下图所示,这是App的真正入口:
在这里插入图片描述
main()方法中的thread.attach(false)的作用就是把ActivityThread类定义的代理对象:ApplicationThread类的对象mAppThread绑定到AMS,attach()方法如下图所示:
在这里插入图片描述
在thread.attach()方法中,AMS接收到attachApplication的请求后,就调用attachApplication方法,将ApplicationThread对象mAppThread绑定到AMS自身,ApplicationThread类实现了IBinder接口,用于ActivityThread和ActivityManagerService的所在进程间通信。attachApplicationLocked()方法如下图所示,用于向App进程发送调用onCreate方法以及创建启动Activity的请求:
在这里插入图片描述
bindApplication()方法最后通过handleBindApplication方法初始化Application并调用onCreate方法:
在这里插入图片描述
attachApplicationLocked(app)最后通过调用scheduleLaunchActivity方法创建启动Activity:
在这里插入图片描述
至此,完成了下图红框的部分:
在这里插入图片描述
App进程的binder线程(ApplicationThread mAppThread)在收到AMS的调用onCreate方法(bindApplication)以及创建启动Activity的请求(LaunchActivity)后,通过前面提到的H(handler机制)向主线程ActivityThread发送bind_application和Launch_Activity消息,这里注意的是AMS和主线程并不直接通信,而是AMS和主线程的内部类ApplicationThread通过Binder通信,ApplicationThread再和主线程通过Handler消息交互。(这里猜测这样的设计意图可能是为了统一管理主线程与AMS的通信,并且不向AMS暴露主线程中的其他公开方法,猜测参见学也不知义 )。主线程Handler在接收到APP进程的Binder进程的bind_application和Launch_Activity请求后创建Application并调用onCreate方法,再通过反射机制创建目标Activity,并回调Activity.onCreate()等方法,到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染后显示APP主界面。
三、APK文件的生成
放一张经典的官方图(参考https://www.cnblogs.com/xunbu7/p/7345912.html):在这里插入图片描述
1.打包资源文件,生成R.java文件
打包资源的工具是aapt,在这个过程中,项目中的AndroidManifest.xml文件和布局文件XML都会编译,然后生成相应的R.java,另外AndroidManifest.xml会被aapt编译成二进制。存放在APP的res目录下的资源,该类资源在APP打包前大多会被编译,变成二进制文件,并会为每个该类文件赋予一个resource id。对于该类资源的访问,应用层代码则是通过resource id进行访问的。Android应用在编译过程中aapt工具会对资源文件进行编译,并生成一个resource.arsc文件,resource.arsc文件相当于一个文件索引表,记录了很多跟资源相关的信息。
2. 处理aidl文件,生成相应的Java文件
这一过程中使用到的工具是aidl,即Android接口描述语言,aidl工具解析接口定义文件然后生成相应的Java代码接口供程序调用。如果在项目没有使用到aidl文件,则可以跳过这一步。
3. 编译项目源代码,生成class文件
项目中所有的Java代码,包括R.java和.aidl文件,都会通过java编译器(javac)编译成.class文件。
4.转换所有的class文件,生成classes.dex文件
使用的工具为dx,任何第三方的libraries和.class文件都会被转换成.dex文件。dx工具的主要工作是将Java字节码转成成Dalvik字节码、压缩常量池、消除冗余信息等。
5.打包生成APK文件
所有没有编译的资源,如images、assets目录下资源(该类文件是一些原始文件,APP打包时并不会对其进行编译,而是直接打包到APP中,对于这一类资源文件的访问,应用层代码需要通过文件名对其进行访问);编译过的资源和.dex文件都会被apkbuilder工具打包到最终的.apk文件中。
6. 对APK文件进行签名
7. 对签名后的APK文件进行对齐处理
对APK进行对齐处理,用到的工具是zipalign,对齐的主要过程是将APK包中所有的资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时的速度会更快。对齐的作用就是减少运行时内存的使用。
这里重点关注第3、4步,.java文件生成.class文件以及.class文件被转换成.dex文件的过程。
1).java文件生成.class文件
(参考https://www.jianshu.com/p/2c106b682cfb、https://www.cnblogs.com/chenyangyao/p/5240079.html)

.class文件格式如下图所示:
在这里插入图片描述
1.Demo源码

首先,编写一个简单的Java源码:
在这里插入图片描述
这段代码包括一个成员变量num和一个方法add()。

2.字节码
通过运行命令:javac Demo.java,生成Demo.class文件。打开.class文件如下:
在这里插入图片描述
3.class文件反编译java文件

在分析class文件之前,我们先来看下将这个Demo.class反编译回Demo.java的结果,如下图所示:
在这里插入图片描述
可以看到,回编译的源码比编写的代码多了一个空的构造函数和this关键字,具体下文解释。

4.字节码结构
从上面的字节码文件中我们可以看到,里面就是一堆的16进制字节。那么该如何解读呢?我们先来看一张.class文件格式表:
在这里插入图片描述

这是一张.class文件总的结构表,我们按照上面的顺序逐一进行解读就可以了。

首先,我们来说明一下:class文件只有两种数据类型:无符号数和表。如下表所示:
在这里插入图片描述
由于表没有固定长度,所以通常会在其前面加上个数说明。
好了,现在我们开始对那一堆的16进制进行解读。
4.1 魔数
从上面的总的结构图中可以看到,开头的4个字节表示的是魔数,其值为0XCAFE BABE:
在这里插入图片描述
魔数就是用来区分文件类型的一种标志,一般都是用文件的前几个字节来表示。比如0XCAFE BABE表示的是class文件,那么为什么不是用文件名后缀来进行判断呢?因为文件名后缀容易被修改,所以为了保证文件的安全性,将文件类型写在文件内部可以保证不被篡改。

4.2 版本号

紧跟着魔数后面的4位就是版本号了,同样也是4个字节,其中前2个字节表示副版本号,后2个字节
表示主版本号。再来看看我们Demo字节码中的值:
3.字节码-版本号.png
前面两个字节是0x0000,也就是其值为0;
后面两个字节是0x0034,也就是其值为52.
所以上面的代码就是52.0版本来编译的,也就是jdk1.8.0。
4.3 常量池

4.3.1 常量池容量计数器

接下来就是常量池了。由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。Demo的值为:
4.字节码-常量池容量计数值.png
其值为0x0013,也就是19。
需要注意的是,这实际上只有18项常量。为什么呢?
通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。
Class文件中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。

4.3.2 字面量和符号引用

在对这些常量解读前,我们需要搞清楚几个概念。
常量池主要存放两大类常量:字面量和符号引用。如下表:
在这里插入图片描述
4.3.2.1 全限定名

com/april/test/Demo这个就是类的全限定名,仅仅是把包名的".“替换成”/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。

4.3.2.2 简单名称

简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()方法和num字段的简单名称分别是add和num。

4.3.2.3 描述符

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表:
在这里插入图片描述
对于数组类型,每一维度将使用一个前置的[字符来描述,如一个定义为java.lang.String [][]类型的二维数组,将被记录为:[[Ljava/lang/String;,,一个整型数组int[]被记录为[I。

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“( )”之内。如方法java.lang.String toString()的描述符为( ) LJava/lang/String;,方法int abc(int[] x, int y)的描述符为([II) I。

4.3.3 常量类型和结构

常量池中的每一项都是一个表,其项目类型共有14种,如下表格所示:
在这里插入图片描述
这14种类型的结构各不相同,如下表格所示:

常量池中常量项的结构总表.png
从上面的表格可以看到,虽然每一项的结构都各不相同,但是他们有个共同点,就是每一项的第一个字节都是一个标志位,标识这一项是哪种类型的常量。

4.3.4 常量解读

好了,我们进入这18项常量的解读,首先是第一个常量,看下它的标志位是啥:
在这里插入图片描述
其值为0x0a,即10,查上面的表格可知,其对应的项目类型为CONSTANT_Methodref_info,即类中方法的符号引用。其结构为:
在这里插入图片描述
即后面4个字节都是它的内容,分别为两个索引项:
6.字节码-第一个常量的项目.png
其中前两位的值为0x0004,即4,指向常量池第4项的索引;
后两位的值为0x000f,即15,指向常量池第15项的索引。
至此,第一个常量就解读完毕了。
后面还有17项常量就不一一去解读了,因为整个常量池还是挺长的:
实际上,我们只要敲一行简单的命令:
javap -verbose Demo.class
其中部分的输出结果为下图所示,正是常量池内容:
在这里插入图片描述

4.4 访问标志

常量池后面就是访问标志,用两个字节来表示,其标识了类或者接口的访问信息,比如:该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final等等。各种访问标志如下所示:
在这里插入图片描述
再来看下我们Demo字节码中的值:
在这里插入图片描述
其值为:0x0021,是0x0020和0x0001的并集,即这是一个Public的类。

4.5 类索引、父类索引、接口索引

访问标志后的两个字节就是类索引;
类索引后的两个字节就是父类索引;
父类索引后的两个字节则是接口索引计数器。
通过这三项,就可以确定了这个类的继承关系了。

4.5.1 类索引

我们直接来看下Demo字节码中的值:
在这里插入图片描述
类索引的值为0x0003,即为指向常量池中第三项的索引。你看,这里用到了常量池中的值了。
我们回头翻翻常量池中的第三项:
#3 = Class #17 // com/april/test/Demo
通过类索引我们可以确定到类的全限定名。

4.5.2 父类索引

从上图看到,父类索引的值为0x0004,即常量池中的第四项:
#4 = Class #18 // java/lang/Object
这样我们就可以确定到父类的全限定名。
可以看到,如果我们没有继承任何类,其默认继承的是java/lang/Object类。
同时,由于Java不支持多继承,所以其父类只有一个。

4.5.3 接口计数器
从上图看到,接口索引个数的值为0x0000,即没有任何接口索引,我们demo的源码也确实没有去实现任何接口。

4.5.4 接口索引集合
由于我们demo的源码没有去实现任何接口,所以接口索引集合就为空了,不占地方。
可以看到,由于Java支持多接口,因此这里设计成了接口计数器和接口索引集合来实现。

4.6 字段表

接口计数器或接口索引集合后面就是字段表了。
字段表用来描述类或者接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。

4.6.1 字段表计数器

同样,其前面两个字节用来表示字段表的容量,看下demo字节码中的值:
在这里插入图片描述
其值为0x0001,表示只有一个字段。

4.6.2 字段表访问标志

我们知道,一个字段可以被各种关键字去修饰,比如:作用域修饰符(public、private、protected)、static修饰符、final修饰符、volatile修饰符等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。字段的访问标志有如下这些:
在这里插入图片描述
4.6.3 字段表结构

字段表作为一个表,同样有他自己的结构:
在这里插入图片描述
4.6.4 字段表解读

我们先来回顾一下我们demo源码中的字段:
private int num = 1;
由于只有一个字段,还是比较简单的,直接看demo字节码中的值:
在这里插入图片描述
访问标志的值为0x0002,查询上面字段访问标志的表格,可得字段为private;
字段名索引的值为0x0005,查询常量池中的第5项,可得:
#5 = Utf8 num
描述符索引的值为0x0006,查询常量池中的第6项,可得:
#6 = Utf8 I
属性计数器的值为0x0000,即没有任何的属性。

至此,字段表解读完成。

4.6.5 注意事项
字段表集合中不会列出从父类或者父接口中继承而来的字段。
内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
在Java语言中字段是无法重载的,两个字段的数据类型,修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的.

4.7 方法表

字段表后就是方法表了。

4.7.1 方法表计数器

前面两个字节依然用来表示方法表的容量,看下demo字节码中的值:
在这里插入图片描述
其值为0x0002,即有2个方法。

4.7.2 方法表访问标志
跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同,方法表的具体访问标志如下:
在这里插入图片描述
4.7.3 方法表结构
方法表的结构实际跟字段表是一样的,方法表结构如下:
在这里插入图片描述
4.7.4 属性解读

还是先回顾一下Demo中的源码:

public int add() {
    num = num + 2;
    return num;
}

只有一个自定义的方法。但是上面方法表计数器明明是2个,这是为啥呢?
这是因为它包含了默认的构造方法,我们来看下下面的分析就懂了,先看下Demo字节码中的值:
在这里插入图片描述
这是第一个方法表,我们来解读一下这里面的16进制:
访问标志的值为0x0001,查询上面字段访问标志的表格,可得字段为public;
方法名索引的值为0x0007,查询常量池中的第7项,可得:
#7 = Utf8
这个名为的方法实际上就是默认的构造方法了。

描述符索引的值为0x0008,查询常量池中的第8项,可得:
#8 = Utf8 ()V
注:描述符不熟悉的话可以回头看看4.3.2.3的内容。

属性计数器的值为0x0001,即这个方法表有一个属性。
属性计数器后面就是属性表了,由于只有一个属性,所以这里也只有一个属性表。
由于涉及到属性表,这里简单说下,下一节会详细介绍。
属性表的前两个字节是属性名称索引,这里的值为0x0009,查下常量池中的第9项:
#9 = Utf8 Code
即这是一个Code属性,我们方法里面的代码就是存放在这个Code属性里面。
先跳过属性表,我们再来看下第二个方法:
在这里插入图片描述
访问标志的值为0x0001,查询上面字段访问标志的表格,可得字段为public;
方法名索引的值为0x000b,查询常量池中的第11项,可得:
#11 = Utf8 add
描述符索引的值为0x000c,查询常量池中的第12项,可得:
#12 = Utf8 ()I
属性计数器的值为0x0001,即这个方法表有一个属性。
属性名称索引的值同样也是0x0009,即这是一个Code属性。
可以看到,第二个方法表就是我们自定义的add()方法了。

4.7.5 注意事项
如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现父类的方法。
编译器可能会自动添加方法,最典型的便是类构造方法(静态构造方法)方法和默认实例构造方法方法。
在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。

4.8 属性表
前面说到了属性表,现在来重点看下。属性表不仅在方法表有用到,字段表和Class文件中也会用得到。本篇文章中用到的例子在字段表中的属性个数为0,所以也没涉及到;在方法表中用到了2次,都是Code属性;至于Class文件,在末尾时会讲到,这里就先不说了。

4.8.1 属性类型
属性表实际上可以有很多类型,上面看到的Code属性只是其中一种,下面这些是虚拟机中预定义的属性:

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量池
Deprecated 类,方法,字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部便狼描述
StackMapTable Code属性 JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature 类,方法表,字段表 用于支持泛型情况下的方法签名
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 用于存储额外的调试信息
Synthetic 类,方法表,字段表 标志方法或字段为编译器自动生成的
LocalVariableTypeTable 类 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类,方法表,字段表 为动态注解提供支持
RuntimeInvisibleAnnotations 表,方法表,字段表 用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation 方法表 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotation 方法表 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
AnnotationDefault 方法表 用于记录注解类元素的默认值
BootstrapMethods 类文件 用于保存invokeddynamic指令引用的引导方式限定符

4.8.2 属性表结构

属性表的结构比较灵活,各种不同的属性只要满足以下结构即可:

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u2 attribute_length 1 属性长度
u1 info attribute_length 属性表
即只需说明属性的名称以及占用位数的长度即可,属性表具体的结构可以去自定义。

4.8.3 部分属性详解

下面针对部分常见的一些属性进行详解

4.8.3.1 Code属性

前面我们看到的属性表都是Code属性,我们这里重点来看下。
Code属性就是存放方法体里面的代码,像接口或者抽象方法,他们没有具体的方法体,因此也就不会有Code属性了。

4.8.3.1.1 Code属性表结构

先来看下Code属性表的结构,如下图:
在这里插入图片描述
可以看到:Code属性表的前两项跟属性表是一致的,即Code属性表遵循属性表的结构,后面那些则是他自定义的结构。

4.8.3.1.2 Code属性解读

同样,解读Code属性只需按照上面的表格逐一解读即可。
我们先来看下第一个方法表中的Code属性:
在这里插入图片描述
属性名索引的值为0x0009,上面也说过了,这是一个Code属性;
属性长度的值为0x00000026,即长度为38,注意,这里的长度是指后面自定义的属性长度,不包括属性名索引和属性长度这两个所占的长度,因为这哥俩占的长度都是固定6个字节了,所以往后38个字节都是Code属性的内容;
max_stack的值为0x0002,即操作数栈深度的最大值为2;
max_locals的值为0x0001,即局部变量表所需的存储空间为1;max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。
code_length的值为0x00000000a,即字节码指令的10;
code的值为0x2a b7 00 01 2a 04 b5 00 02 b1,这里的值就代表一系列的字节码指令。一个字节代表一个指令,一个指令可能有参数也可能没参数,如果有参数,则其后面字节码就是他的参数;如果没参数,后面的字节码就是下一条指令。
这里我们来解读一下这些指令,文末最后的附录附有Java虚拟机字节码指令表,可以通过指令表来查询指令的含义。
2a 指令,查表可得指令为aload_0,其含义为:将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
b7 指令,查表可得指令为invokespecial,其含义为:将操作数栈顶的reference类型的数据所指向的对象作为方法接受者,调用此对象的实例构造器方法、private方法或者它的父类的方法。其后面紧跟着的2个字节即指向其具体要调用的方法。
00 01,指向常量池中的第1项,查询上面的常量池可得:#1 = Methodref #4.#15 // java/lang/Object.""😦)V 。即这是要调用默认构造方法。
2a 指令,同第1个。
04 指令,查表可得指令为iconst_1,其含义为:将int型常量值1推送至栈顶。
b5 指令,查表可得指令为putfield,其含义为:为指定的类的实例域赋值。其后的2个字节为要赋值的实例。
00 02,指向常量池中的第2项,查询上面的常量池可得:#2 = Fieldref #3.#16 // com/april/test/Demo.num:I。即这里要将num这个字段赋值为1。
b5 指令,查表可得指令为return,其含义为:返回此方法,并且返回值为void。这条指令执行完后,当前的方法也就结束了。
所以,上面的指令简单点来说就是,调用默认的构造方法,并初始化num的值为1。
同时,可以看到,这些操作都是基于栈来完成的。

实际上,只要一行命令,就能将这样字节码转化为指令了,还是javap命令,截取部分输出结果::
在这里插入图片描述
看看,那是相当的简单。关于字节码指令,就到此为止了。继续往下看。

exception_table_length的值为0x0000,即异常表长度为0,所以其异常表也就没有了;

attributes_count的值为0x0001,即code属性表里面还有一个其他的属性表,后面就是这个其他属性的属性表了;
所有的属性都遵循属性表的结构,同样,这里的结构也不例外。
前两个字节为属性名索引,其值为0x000a,查看常量池中的第10项:
#10 = Utf8 LineNumberTable
即这是一个LineNumberTable属性。LineNumberTable属性先跳过,具体可以看下一小节。
再来看下第二个方法表中的的Code属性:

17.字节码-Code属性表2.png
属性名索引的值同样为0x0009,所以,这也是一个Code属性;
属性长度的值为0x0000002b,即长度为43;
max_stack的值为0x0003,即操作数栈深度的最大值为3;
max_locals的值为0x0001,即局部变量表所需的存储空间为1;
code_length的值为0x00000000f,即字节码指令的15;
code的值为0x2a 2a b4 20 02 05 60 b5 20 02 2a b4 20 02 ac,使用javap命令,可得:
在这里插入图片描述
可以看到,这就是我们自定义的add()方法;
exception_table_length的值为0x0000,即异常表长度为0,所以其异常表也没有;
attributes_count的值为0x0001,即code属性表里面还有一个其他的属性表;
属性名索引值为0x000a,即这同样也是一个LineNumberTable属性。

4.8.3.2 LineNumberTable属性

LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系。

4.8.3.2.1 LineNumberTable属性表结构
在这里插入图片描述
line_number_info(行号表),其长度为4个字节,前两个为start_pc,即字节码行号;后两个为line_number,即Java源代码行号。

4.8.3.2.2 LineNumberTable属性解读

前面出现了两个LineNumberTable属性,先看第一个:
在这里插入图片描述
attributes_count的值为0x0001,即code属性表里面还有一个其他的属性表;
属性名索引值为0x000a,查看常量池中的第10项:
#10 = Utf8 LineNumberTable
即这是一个LineNumberTable属性。
attribute_length的值为0x00 00 00 0a,即其长度为10,后面10个字节的都是LineNumberTable属性的内容;
line_number_table_length的值为0x0002,即其行号表长度长度为2,即有两个行号表;
第一个行号表其值为0x00 00 00 07,即字节码第0行对应Java源码第7行;
第二个行号表其值为0x00 04 00 08,即字节码第4行对应Java源码第8行。

同样,使用javap命令也能看到:

  LineNumberTable:
    line 7: 0
    line 8: 4

第二个LineNumberTable属性为:
在这里插入图片描述

这里就不逐一看了,同样使用javap命令可得:
LineNumberTable:
line 11: 0
line 12: 10
所以这些行号是有什么用呢?当程序抛出异常时,我们就可以看到报错的行号了,这利于我们debug;使用断点时,也是根据源码的行号来设置的。

4.8.3.2 SourceFile属性

前面将常量池、字段集合、方法集合等都解读完了。最终剩下的就是一些附加属性了。
先来看看剩余还未解读的字节码:
在这里插入图片描述
同样,前面2个字节表示附加属性计算器,其值为0x0001,即还有一个附加属性。
最后这一个属性就是SourceFile属性,即源码文件属性。
先来看看其结构:

4.8.3.2.1 SourceFile属性结构
在这里插入图片描述

4.8.3.2.2 SourceFile属性解读

属性名索引的值为0x000d,即常量池中的第13项,查询可得:
#13 = Utf8 SourceFile
属性长度的值为0x00 00 00 02,即长度为2;
源码文件索引的值为0x000e,即常量池中的第14项,查询可得:
#14 = Utf8 Demo.java
所以,我们能够从这里知道,这个Class文件的源码文件名称为Demo.java。同样,当抛出异常时,可以通过这个属性定位到报错的文件。
至此,上面的字节码就完全解读完毕了。

2).class文件被转换成.dex文件
Java类文件中通常有多个方法签名,如果其他类文件中引用了该类文件中的方法,相应的方法签名也会被复制到其他类文件中,也就是说如果多个不同的类同时包含相同的方法签名,大量的字符串常量会被多个类文件重复使用。这些冗余的信息造成的文件体积增大严重影响了虚拟机解析文件的效率。针对这个问题,dx工具对所有Java类文件中的常量池进行了分解,消除了其中的冗余信息,然后将它们重新组合成一个常量池,并让所有类文件共享这个常量池。简而言之,这个过程是基于于dx对常量池的压缩,相同的字符串和常量在DEX文件中只会出现一次(文件的体积会随之缩小)。DEX文件格式参考(https://blog.csdn.net/sinat_18268881/article/details/55832757)。
其余过程参考: https://shuwoom.com/?p=269
https://www.jianshu.com/p/b29a21a162ad

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值