基于BTrace拦截的Java编译参数信息获取方法及实现

maijun 发表于 2021/05/15 11:17:02 2021/05/15
【摘要】 Java编译参数获取,在很多应用场景下都可能会需要用到,例如软件成分分析、依赖分析、Java静态代码分析等。基于获取到的Java的编译参数,可以将参数作为后续的步骤的输入,完成分析工作。本文将介绍一种基于exec函数族命令拦截和BTrace方法参数拦截的Java编译参数的获取的方法。

Java编译参数获取,在很多应用场景下都可能会需要用到,例如软件成分分析、依赖分析、Java静态代码分析等。基于获取到的Java的编译参数,可以将参数作为后续的步骤的输入,完成分析工作。本文将介绍一种Java编译参数的获取的方法,并实现了demo(可进一步扩展,满足自己的业务需要)。

1. Java代码编译方式

Java代码,主要有两种编译方式,一种是通过javac编译命令,一种是通过调用 Java API编译源代码。

1.1 javac编译命令

我们大家最熟悉的,应该也是 javac 编译命令,刚刚学习 java 的时候,就已经学会了使用 javac 命令,将 java 代码编译为 class 文件,然后使用 java 命令解释执行 class 文件。这里对这个命令不详细介绍,因为大部分 java 开发人员对这个命令足够熟悉,完整的命令,可以通过查看本文最后的 reference 1) 了解,也可以执行 javac -help 了解。在我电脑上执行 javac -help 如下:

maijun@LAPTOP-52NNQJ8V MINGW64 ~
$ javac -help
用法: javac <options> <source files>
其中, 可能的选项包括:
  -g                         生成所有调试信息
  -g:none                    不生成任何调试信息
  -g:{lines,vars,source}     只生成某些调试信息
  -nowarn                    不生成任何警告
  -verbose                   输出有关编译器正在执行的操作的消息
  -deprecation               输出使用已过时的 API 的源位置
  -classpath <路径>            指定查找用户类文件和注释处理程序的位置
  -cp <路径>                   指定查找用户类文件和注释处理程序的位置
  -sourcepath <路径>           指定查找输入源文件的位置
  -bootclasspath <路径>        覆盖引导类文件的位置
  -extdirs <目录>              覆盖所安装扩展的位置
  -endorseddirs <目录>         覆盖签名的标准路径的位置
  -proc:{none,only}          控制是否执行注释处理和/或编译。
  -processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
  -processorpath <路径>        指定查找注释处理程序的位置
  -parameters                生成元数据以用于方法参数的反射
  -d <目录>                    指定放置生成的类文件的位置
  -s <目录>                    指定放置生成的源文件的位置
  -h <目录>                    指定放置生成的本机标头文件的位置
  -implicit:{none,class}     指定是否为隐式引用文件生成类文件
  -encoding <编码>             指定源文件使用的字符编码
  -source <发行版>              提供与指定发行版的源兼容性
  -target <发行版>              生成特定 VM 版本的类文件
  -profile <配置文件>            请确保使用的 API 在指定的配置文件中可用
  -version                   版本信息
  -help                      输出标准选项的提要
  -A关键字[=值]                  传递给注释处理程序的选项
  -X                         输出非标准选项的提要
  -J<标记>                     直接将 <标记> 传递给运行时系统
  -Werror                    出现警告时终止编译
  @<文件名>                     从文件读取选项和文件名

1.2 Java API编译源代码

Java API编译源代码,有很多API可以调用,下面介绍一个非常简单的API:

使用ToolProvider.getSystemJavaCompiler来得到一个JavaCompiler接口的实例。JavaCompiler中最核心的方法是run()。通过这个方法能编译java源代码。

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int run(InputStream in, OutputStream out, OutputStream err, String... arguments);

如上面的介绍,我们更关心的是 run 方法的 arguments 参数,可以传递的参数,可以完全参考 javac 命令的参数传递,下面举一个简单的例子说明:

package zmj.test;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

public class JavaCompilerTest {
    public static void main(String[] args) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        System.out.println(compiler.getClass().getName());
        int result = compiler.run(null, null, null, "D:\\test\\Hello.java");
        System.out.println("result: " + result);
    }
}

首先看输出:

我们可以看到两点信息:

1) JavaCompiler 是一个接口,通过该方法,创建的 JavaCompiler 实例是 com.sun.tools.javac.api.JavacTool 类型;

2) 编译成功后,返回值为0,此时看 Hello.java 所在的目录,的确可以看到编译得到的 Hello.class。

当然,还支持其他的API,当然,我们捕获参数,只需要识别最底层调用API即可,可参考 github 上 demo 中 java-capture-agent 中的API定义(如果上面没有列举完整,可以自行添加)。

我们通常比较熟悉的Ant、Maven、Gradle等,包括当前开始抬头的Bazel,可以实现比较方便的Java源码编译,其实这些构建工具在编译源代码的时候,不外乎就是上面的两种方式,在 fork 方式下,调用 javac 命令,在非 fork 方式下,调用 Java API 编译。

2. Java源码编译参数拦截捕获实现

2.1 Java源码编译参数拦截实现原理

1) javac 编译命令捕获

我们知道,在Linux系统中,如果一个进程执行,要进入系统调用,都需要通过exec函数族调用才行。javac命令执行,也是需要通过 execve 调用执行才可以,因此可以重写 exec 函数族方式拦截 javac 命令,实现特定的 javac 参数的捕获(这一部分,我们通过参考 clang 编译参数获取框架 Bear 实现)。

2) Java API 编译方式

Java API 方式编译 Java 源代码,其实也是 Java 应用程序的执行。当前,我们可以提供字节码增强的方式,Java 有提供一种 基础字节码增强的 java agent 方式,可以实现对 字节码 的修改,这样,在执行到特定的 java 方法时,修改 该方法的 字节码,插入我们自己的 捕获参数的 代码,从而实现 Java API 编译方式的拦截捕获。

这里,我没有采用基础的 java agent 去重复实现,而是直接采用了一个非常优秀的 Java 动态分析跟踪工具 BTrace,BTrace 基于 字节码修改 技术实现,可以很好地满足我们的需求。

这里,对 BTrace 的使用,我们和基础的使用方式有两点不同:

① 基于非安全方式,安全方式的 BTrace使用方式,有太多限制,几乎什么都不能做,只能获取一些参数信息。当然这种限制,是为了解决生产环境正在运行的java程序问题,保证源程序的正常执行设置的,这里我们并不是解决生产环境问题,所以安全方式也没什么大用;

②基于 javaagent 方式使用,BTrace 使用有两种方式,一种是针对已经运行的 java 程序,通过 pid attach到运行的程序中,另外一种,是在程序运行开始时,在 java 命令中,使用 -javaagent 参数,将 BTrace 脚本设置上去。我们采用的是 javaagent 方式。

2.2 Java源码编译参数拦截实现

实现主要包含三个模块:拦截器,BTrace拦截 和 参数收集,如下所示:

(1) 拦截器

通过重写 exec 函数族实现,参考 Bear 的实现。在 拦截器中,判断执行的命令类型,如果命令是 javac 命令,则直接将参数传递给 参数收集 模块,如果命令是 java 命令,则在 java 命令中,将 BTrace 脚本通过 javaagent 方式添加到命令中继续执行,如果是其他命令,则不做修改,继续执行;

(2) BTrace拦截器

主要实现了一个 BTrace 脚本,对执行编译的 Java API 进行拦截捕获,如果拦截到编译参数,则将参数传递给 参数收集 模块;

(3) 参数收集

主要实现了对参数的处理操作(阻塞执行),可以包含如下的功能:

① 编译命令参数的记录;

② 参数去重处理,因为在不同的编译,调用的 Java API 层次不同,此时可能不同 API 捕获到了相同的参数,因此需要实现去重操作;

③ 源数据的拷贝:可以实现将 源代码、依赖、结果文件等,收集到特定的收集目录下。

3. 源码及实现

参考实现代码demo:https://github.com/zhangmaijun/java-capture

当前对部分场景做了测试,大部分(包括Ant、Maven、Gradle,及相互嵌套调用编译场景,及在脚本中调用 javac 命令的场景)可以适配,主要有一种场景:如果直接调用 javac,无法捕获,将 javac 放到脚本里面调用,就可以捕获,等我调通之后再来修改。

4. 参考

1) https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javac.html

2) https://github.com/btraceio/btrace

3) https://github.com/rizsotto/Bear/tree/2.4.4

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。