0
点赞
收藏
分享

微信扫一扫

[Android][ASM]指令注入入门(二)——开发环境搭建与Hello World

双井暮色 2022-04-29 阅读 45
javaandroid

[Android][ASM]代码注入入门(二)—— 开发环境搭建与Hello World

前言

在上一篇中,我们见识了AOSP中使用asm实现的一个功能——lockedregioncodeinjection:其可以在编译时为services.core.unboosted.jar中指定类中的同步代码块添加try-catch代码块,并在代码块的首位追加boostPriorityForLockedSection()resetPriorityAfterLockedSection()方法调用,并将最后修改产物保存为services.core.priorityboosted.jar,供services模块后续继续编译使用;

透过浅析lockedregioncodeinjection工具本身实现的代码,我们领略了asm的强大之处。那么我们是否可以利用其特性,为自己实现一些自动化追加、修改代码的功能呢?

答案是肯定的,但是在那之前,我们还是从搭建开发环境、编写Hello World demo入手,循序渐进。

开发环境搭建

开发环境介绍

操作系统

为了调试方便,我这边使用Ubuntu(WSL2) 做展示,理论上平台是不限的;

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
Codename:       focal
$ uname -a
Linux xxxx 5.10.60.1-microsoft-standard-WSL2 #1 SMP Wed Aug 25 23:20:18 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

JAVA环境

为了快速调试,我不打算依附于AOSP的编译流程来搭建开发环境,而是基于Ubuntu(Linux)来搭建一个纯java的开发环境;

本次使用OpenJDK 8来搭建:

 $ dpkg -l | grep jdk
ii  openjdk-8-jdk:amd64            8u312-b07-0ubuntu1~20.04              amd64        OpenJDK Development Kit (JDK)
ii  openjdk-8-jdk-headless:amd64   8u312-b07-0ubuntu1~20.04              amd64        OpenJDK Development Kit (JDK) (headless)
ii  openjdk-8-jre:amd64            8u312-b07-0ubuntu1~20.04              amd64        OpenJDK Java runtime, using Hotspot JIT
ii  openjdk-8-jre-headless:amd64   8u312-b07-0ubuntu1~20.04              amd64        OpenJDK Java runtime, using Hotspot JIT (headless)

asm框架下载

lockedregioncodeinjection使用的是asm-6.0,为了保持行为一致,我们也下载6.0版本的来搭建环境;
官网Maven 提供多个版本、多个模块的下载链接,这里我们下载如下几个同版本号的文件:

  • asm-6.0.jar
  • asm-analysis-6.0.jar
  • asm-commons-6.0.jar
  • asm-tree-6.0.jar

注:本篇的demo只需要使用asm-6.0.jarasm-tree-6.0.jar,但后续更多功能会依赖另外两个,因此就一并下载了;

下载完成后放置的目录结构如下:

~/workspace $ tree ./
./
└── asm
    ├── asm-6.0.jar
    ├── asm-analysis-6.0.jar
    ├── asm-commons-6.0.jar
    └── asm-tree-6.0.jar

1 directory, 4 files

环境配置

Ubuntu若使用apt-get/apt安装OpenJDK,无需额外操作即可正常使用javac/java的等命令;

ryan ~/workspace $ java -version
openjdk version "1.8.0_312"
OpenJDK Runtime Environment (build 1.8.0_312-8u312-b07-0ubuntu1~20.04-b07)
OpenJDK 64-Bit Server VM (build 25.312-b07, mixed mode)
ryan ~/workspace $ javac -version
javac 1.8.0_312
ryan ~/workspace $ jar -version

此外,由于我们的demo包含两部分:

  1. 待修改的字节码文件;
  2. 调用asm来修改创建字节码的工具;
    因此在workspace目录下,srctools目录来存放,与上面的asm目录并列;
    同时创建一个名为out的目录,用于存放asm修改后的产物:
ryan ~/workspace $ tree
.
├── asm
│   ├── asm-6.0.jar
│   ├── asm-analysis-6.0.jar
│   ├── asm-commons-6.0.jar
│   └── asm-tree-6.0.jar
├── out
├── src
└── tools

4 directories, 4 files

Hello World

如上所说,这个demo需要两个部分的开发:

  1. 待修改的字节码文件,以下称为源码端;
  2. 调用asm来修改创建字节码的工具,以下称为工具端;

源码端

源码端我们先写个简单的:

//src/Test.java
public class Test {
    public static void main(String[] args) {
        String a = "Hi Test";
        System.out.println(a);
    }
}

编译、运行:

ryan ~/workspace/src $ javac Test.java
ryan ~/workspace/src $ java Test
Hi Test

我们的目标就定为:将输出结果从"Hi Test"改为"Hello World"

工具端

lockedregioncodeinjection不同,我们写demo,不需要那么多参数传入,直接先硬编码实现即可;

//tools/StringModifier.java
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodNode;

public class StringModifier {
    public static void main(String[] args) throws IOException {
        //输入src/Test.class
        FileInputStream fis = new FileInputStream("../src/Test.class");
        //输出out/Test.class
        FileOutputStream fos = new FileOutputStream("../out/Test.class");
        //输入输出流对接,对内容进行解析与修改
        convert(fis, fos);
        fos.flush();
        fis.close();
        fos.close();
    }

    private static void convert(InputStream in, OutputStream out)
            throws IOException {
        //ClassReader封装了类的解析
        ClassReader reader = new ClassReader(in);
        //ClassWriter则负责修改后的类的生成
        ClassWriter writer = new ClassWriter(0);
        //对解析过程的事件回调,是通过ClassVisitor及其子类来实现的,并将ClassWriter传入,以追踪所有改动
        StringModificationClassVisitor visitor = new StringModificationClassVisitor(writer);
        //ClassReader.accept()这是一个耗时操作,会解析整个类,并在特定阶段调用ClassVisitor的对应方法回调,用户可以通过创建子类集成ClassVisitor,并重写对应方法来实现截获、修改
        reader.accept(visitor, 0);
        byte[] data = writer.toByteArray();
        //将ClassWriter保存的信息,以二进制形式写到文件中;
        out.write(data);
    }

    private static class StringModificationClassVisitor extends ClassVisitor {

        private String currentClassName = null;

        private StringModificationClassVisitor(ClassWriter writer) {
            super(Opcodes.ASM6, writer);
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName,
                String[] interfaces) {
            //当解析到类声明头时调用
            currentClassName = name;
            super.visit(version, access, name, signature, superName, interfaces);
        }

        
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                String[] exceptions) {
            //当解析到方法时调用,包括构造方法
            MethodVisitor chain = super.visitMethod(access, name, desc, signature, exceptions);
            //由于我们需要关注Test类的main方法,因此在此硬编码条件过滤
            if ("Test".equals(currentClassName) && "main".equals(name)) {
                //返回自定义的一个MethodVisitor
                return new StringModificationMethodVisitor(currentClassName, chain);
            } else {
                //父类实现返回(不修改)
                return chain;
            }
        }

    }

    private static class StringModificationMethodVisitor extends MethodVisitor {

        private MethodVisitor chain;
        private String owner;

        private StringModificationMethodVisitor(String owner, MethodVisitor chain) {
            //构造一个MethodNode用于代理、保存MethodVisitor的解析结果
            super(Opcodes.ASM6, new MethodNode());
            this.owner = owner;
            this.chain = chain;
        }

        public void visitEnd() {
        	//方法解析到末尾时调用
            MethodNode mn = (MethodNode) mv;
            //成员变量mv实际就是构造方法传入的那个MethodNode
            //mv.instructions即为当前方法的所有指令集合
            InsnList instructions = mn.instructions;
            //遍历所有指令
            for (int i = 0; i < instructions.size(); i++) {
                AbstractInsnNode node = instructions.get(i);
                //字符串加载为LDC指令,因此在此用类型判断过滤
                if (node instanceof LdcInsnNode) {
                    LdcInsnNode ldcNode = (LdcInsnNode)node;
                    //创建一个新的LDC指令,并将其插入到之前"Hi Test"字符串加载指令之后
                    LdcInsnNode newLdcNode = new LdcInsnNode(new String("Hello World"));
                    instructions.insert(ldcNode, newLdcNode);
                    //然后删除原先那条指令
                    instructions.remove(ldcNode);
                    break;
                }
                
            }
            super.visitEnd();
            //让MethodVisitor采纳当前修改后MethodNode信息;
            mn.accept(chain);
        }
    }
}

整个流程大致总结如下:

  1. 创建一个输入流读取../src/Test.class字节码文件,创建一个输出流写入修改后的../out/Test.class字节码文件;
  2. 解析、封装类信息使用ClassReaderClassWriter
  3. 解析类时若需在解析到特定步骤进行修改,创建一个ClassVisitor的子类,并重写对应方法来完成;
  4. 如果要在特定方法内进行修改,在3的基础上,还要创建一个MethodVisitor的子类,并重写对应方法,并在上方创建的ClassVisitor子类的visitMethod方法中作为结果返回;
  5. MethodVisitor的方法中,最常用的是重写visitEnd()方法,因为此时整个方法的信息都解析完毕,可以进行各种修改;
  6. MethodVisitor的构造方法中,第二个参数传入一个MethodNode,可以保存MethodVisitor解析到的信息,这样有利于我们进行修改;
  7. InsnList为方法内的所有指令集合,具体指令种类与规范此处暂不展开;
  8. 字符串加载为LDC指令,因此过滤Test类中main方法内的LDC指令,并将其替换为我们想要的即可;

编译

ryan ~/workspace/tools $ javac StringModifier.java -cp ../asm/asm-6.0.jar:../asm/asm-tree-6.0.jar

运行

ryan ~/workspace/tools $ java -classpath ../asm/asm-6.0.jar:../asm/asm-tree-6.0.jar:./ StringModifier

验证

ryan ~/workspace/tools $ cd ../out/
ryan ~/workspace/out $ java Test
Hello World

后记

基于这个Demo,已经算是敲开了ASM的大门,至于怎么使用,各位根据需求修改即可;

关于指令类型、写法之类的,这里不展开,也没法展开,感兴趣的可以去翻
官方规范接口;

个人建议,除了本职工作需要深入了解JVM设计规范,其余人员可根据自己的需求,按需了解即可;

后面会再记录一个案例,这也是我探索出这两篇文章的最初动机;

更新时间待定……

举报

相关推荐

0 条评论