Java 9里开始支持模块化,以一个独立的开源项目jigsaw而来, 具体可以参考链接,
https://openjdk.java.net/projects/jigsaw/ 同时也可以参考JSR376标准
在模块化的时候,需要构建module_info.java来声明模块之间的关系,
在Module声明里面定义了requires ,exports, opens, uses, provides 5种类型,关于这几种类型的作用就不在这里详细介绍了。
在字节码种并没有单独的opcode,只是用一种固定的顺序进行定义,以requires ,exports, opens, uses, provides这个顺序来排序
我们来看一下以下的一段bytecode
2 // requires #8,0 // pointto #0 #10,8000 // "java.base" ACC_MANDATED #0 0 // exports 0 // opens 1 // uses #12 // testRequire/service 1 // provides #12 // testRequire/service with ... 1 #14 // ... with testRequire/ServiceImpl |
第一个2表示requires,同时表示有几个requires,下面的缩进行代表着有哪2个,#10,8000表示的是8000表示的是16进制的ACC_Flag 修饰符
其中requires的ACC独有的修饰符(TRANSITIVE,STATIC)
SYNTHETIC,MANDATED是共用的ACC修饰符
每个Flag定义的值为
TRANSITIVE |
0x20 |
STATIC |
0x40 |
SYNTHETIC |
0x1000 |
MANDATED |
0x8000 |
其本质上是通过bit位来设置不同的值的合集
第二个0代表着exports,第三个0代表着opens第四个1代表着uses,第五个1代表着provides,当值为非0的时候下一行就会缩进,代表着共有几个这样的类型,格式比较简单。但是我们从这种结构看出只能增加新的类型,同时类型顺序无法修改和删除,否则很难向下兼容。
这里简单的提一下这几个关键字
uses, provides,这个主要是对Java的SPI机制
Opens 主要是对Java的类反射机制,都是很有针对性的对Java提供的原生态的能力进行了管控。
Soot 的原来主版本目前并不支持模块分析,在Soot的Java9的branch孵化,目前已经合到主版本
Soot 需要显示的设置模块的相关信息
Options.v().set_soot_modulepath(path); |
Soot会通过设置的模块的路径去加载类
需要注意的是:基础模块java.base
你不需要设置基础模块的地址,Soot里面会去自己寻找基础模块,如何寻找请参考下面的章节。
Soot 本身是使用运行的jdk 加载基础模块的,那就是说Soot的加载基础模块是绑定你的运行的jdk的,你无法通过设置模块相关参数来设置基础模块,通常基础模块都被打包成了jmod文件,Soot解析jmod文件是通过运行soot的jdk来解析的,jmod基础类的读取就的依赖运行jdk的能力,如果你想用一个运行jdk的版本来分析不同的版本jdk基础类,就会变的困难,不过好在java的基础类是向下兼容的,确保升级到最新的版本,可以确保能分析低版本的基础类。Soot内部会添加VIRTUAL_FS_FOR_JDK这个值到modulepath中去,用来标示基础类的路径。
如果你运行的Soot的jdk是低版本的,或许还有条路可以试试,解开jmod文件,设置到你的模块路径下,这种方式适合在不高于jdk8的版本方式上,同时你还的设置:
Options.v().set_prepend_classpath(false); |
ModulePathSourceLocator是查找模块路径和类文件的核心模块:
通过ModulePathSourceLocator.
FoundFile lookUpInModulePath(String filename)方法去查找你想要获取的模块里的class
3. 深度解析该模块所在路径下的module-info 文件,分析requires的部分,构建模块间的依赖,分析依赖模块下的module-info,细节可参考5.2.5注:在Java虚拟机编译中,会对自定义的module-info的类,在编译过程中自动添加java.base的模块依赖,默认依赖java.base的基本模块。在这种场景下,soot会分析基础模块java.base的module-info, 通过查找基础模块的路径(重复步骤a,b), 找到java.base的module-info进行解析
4. 找到模块所对应的class文件,将FoundFile返回,如何找到所对应的模块请参考下面
Java使用模块的方式进行类的管控,Soot里在LoadClass的时候需要准确的找到模块里的类,以确定该类是否能被模块加载,核心代码在ModuleUtil里findModuleThatExports方法,其原则如下:
Java里的module管控是基于Package的,只需要鉴权Package就足够。
通过ModulePathSourceLocator可以获取模块下的class文件,这里涉及到模块的两种表示方式
1. 自定义的模块:打包成jar或者直接class,这和原来获取jar下的class一样,区别主要是需要解析module-info.class类,获取类的目的是为了获取模块的依赖关系,同时也需要加载require的模块。
2. Java的基础类:清楚Java 模块的知道Java9将原来的rt.jar 替换成了模块的方式,在java_home下会有一个jmods的目录,里面保存着将rt.jar 拆解成多个不同的jmod文件,在Java下面我们可以使用jmod的命令来查看jmod文件的内容。Soot 并没有使用jmod去解析默认的jdk下的基础的这些jmod文件,而是通过jrt的方式去加载jmod,也就是
jrt:/ |
的方式通过NIO File 去解析文件,其本质是通过lib下的jrt-fs.jar去解析,所以Soot会先去判断一下该文件是否存在。如果存在,Soot会将jrt:/加到需要分析的模块逻辑中。
注意:如果是jar的方式,分析module只需要直接设置路径就可以了,而如果是jmod 的方式,需要jrt:/的方式去分析
jrt的访问方式默认的起始路径是以modules,也就是对应的jmods目录
jrt:/modules/ |
访问JRT的方式可以使用NIO的Path,Path是支持jrt的方式,而传统的File是不支持jrt的路径访问方式,这里要特别注意。
在前面的段落里提到过Soot的核心函数Scene,对模块的解析,Soot提供了新的ModuleScene 来支持,当加载class的时候,在模块的方式下需要使用ModuleScene来加载类,代码如下:
public static SootClass loadClass(String name, boolean main, String module) { SootClass c = ModuleScene.v().loadClassAndSupport(name, Optional.of(module)); c.setApplicationClass(); if (main) Scene.v().setMainClass(c); return c; } |
需要使用name和模块名字来加载class,而原来的方式只需要提供class的名字
private static SootClass loadClass(String name, boolean main) { SootClass c = Scene.v().loadClassAndSupport(name); c.setApplicationClass(); if (main) Scene.v().setMainClass(c); return c; } |
解析模块下的类,Soot 沿用原来的框架思路通过Scene来进行加载,本质上使用SootResolver来进行class解析,而不同的是定义了新ModuleScene类来进行加载,使用SootModuleResolver 对模块下的class解析,思路类似:先构建class的引用,然后加到工作队列中,最后获取工作队列进行类的解析(ResolveClass),具体流程可以参考下面的图:
在分析Module-info的类的bytecode的时候,Soot继续沿用了objweb的asm解析框架,使用opcode v7.1的版本,可以支持最高java13的版本。和解析普通的类不一样的是Soot在SootClassBuilder通过构建SootModuleInfoBuilder 去实现asm提供的ModuleVisitor的接口。对Module-info.class里单独定义了SootModuleInfo类,而不是常见的SootClass,当然SootModuleInfo继承SootClass。
可见下图:
在上图可以看到SootModuleInfo里只是封装了export,open,require 这3种类型,具体的实现是在SootModuleInfoBuilder的代码中,Soot目前不支持uses, provides这2种类型,就是SPI的模式。
关键字requires里,是支持version有多个版本,在代码中是无法声明的,但是在编译的字节码里是声明的。
Module: #5,0 // "com.example.hello" #0 1 // requires #6,8000 // "java.base" ACC_MANDATED #7 // 11.0.5 1 // exports #8,0 // com/example/hello 0 // opens 0 // uses 0 // provides |
上面的例子里#7 11.0.5 代表的就是requires的版本,虽然require是有版本的,但是java里并不是必须的,我们可以从openjdk的项目声明看出:
http://openjdk.java.net/projects/jigsaw/spec/reqs/02#version-selection
虽然module里requires有明确的版本,但并没有希望在module里去严格执行版本管控,而是依赖于构建工具。模块系统只需要校验所选的模块集合满足每个模块的依赖即可,无需关注版本。在这个思路上Soot是一致的,Soot并没有对module-info中提供的版本构建版本的依赖分析,也就是并没有对依赖进行版本管控。
export 代表着允许被外部模块访问的package,同时exports 也和to搭档进行外部模块的指定
在上图中,在soot里exportPackage是个列表,每个列表里的key是packageName,而value是指定的多个模块名字。当没有没有指定to的时候,value里面soot保存的是EVERYONE_MODULE代表着任何模块都可以使用。