如何使用ASM给android的某个函数做插桩?
源码:https://github.com/shinecjj/AMStest

1、AMStest项目创建
直接在Android Studio中,new project 就行,等待项目第一次编译完成
2.gradle插件创建
在项目的根目录中,创建buildSrc文件夹,然后构建一下项目,然后在buildSrc文件夹中创建build.gradle配置文件,如下:
plugins{
id 'java'
id 'groovy'
}
group 'com.julive.sam'
version '0.0.1'
sourceCompatibility = 1.8
repositories{
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/public' }
}
def asmVersion = '8.0.1'
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation "com.android.tools.build:gradle:4.1.0"
implementation 'org.apache.directory.studio:org.apache.commons.io:2.4'
implementation "org.ow2.asm:asm:$asmVersion"
implementation "org.ow2.asm:asm-util:$asmVersion"
implementation "org.ow2.asm:asm-commons:$asmVersion"
}
接下来创建插件代码目录,由于我们使用java写的插件,所以需要选中buildSrc,然后鼠标右键选择new,再选择directory,最后出现的对话框中选择 src/main/java,下图中是因为我的项目已经创建完了,所以只有groovy目录,如果你需要写groovy的实现就创建下图中文件夹路径,创建完这个下一步就是创建插件。

在java目录中,创建包名com.julive.sam,在该包路径下创建Plugins插件,代码如下:
public class Plugins implements Plugin<Project> {
@Override
public void apply(Project project) {
AppExtension android = project.getExtensions().getByType(AppExtension.class);
android.registerTransform(new TransformTest());
}
}
然后创建插件的配置resources文件夹,和java文件夹同级,在resources下创建文件夹META-INF/gradle-plugins/,最终在gradle-plugins中创建com.julive.sam.properties,意思是你的包名.properties ,一定要对应好包名,然后在该文件中加入代码
implementation-class=com.julive.sam.Plugins
com.julive.sam.Plugins 你点击后,看能否跳转至 上面创建的Plugins插件中,如果可以直接跳转那就ok了。
3.下一步在App的build.gradle中配置插件

4.创建gradle的Transform实现
Transform是在.class -> .dex转换期间,用来修改.class文件的一套标准API,所以你现在应该知道了,在transform中我们肯定要调用ASM的实现,来实现.class文件的修改,最终转换为.dex文件。创建Transform的实现如下:
public class TransformTest extends Transform {
@Override
public String getName() {
return "TransformSam";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
try {
doTransform(transformInvocation);
} catch (Exception e) {
e.printStackTrace();
}
}
看上面注释是不是就对Transform有了一定的了解呢,那么如何处理.class文件呢?我们来实现doTransform函数,来看如何处理
private void doTransform(TransformInvocation transformInvocation) throws IOException {
System.out.println("doTransform =======================================================");
Collection<TransformInput> inputs = transformInvocation.getInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
if (outputProvider != null)
outputProvider.deleteAll();
inputs.forEach(transformInput -> {
transformInput.getDirectoryInputs().forEach(directoryInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(directoryInput.getFile(), list);
list.forEach(file -> {
System.out.println("getDirectoryInputs =======================================================" + file.getName());
if (file.isFile() && file.getName().endsWith(".class")) {
try {
ClassReader classReader = new ClassReader(new FileInputStream(file));
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new TestClassVisitor(classWriter);
classReader.accept(visitor, ClassReader.EXPAND_FRAMES);
byte[] bytes = classWriter.toByteArray();
FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath());
fileOutputStream.write(bytes);
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
if (outputProvider != null) {
File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
try {
FileUtils.copyDirectory(directoryInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
}
});
transformInput.getJarInputs().forEach(jarInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(jarInput.getFile(), list);
list.forEach(file -> {
System.out.println("getJarInputs =======================================================" + file.getName());
});
if (outputProvider != null) {
File dest = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
try {
FileUtils.copyFile(jarInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
}
});
});
}
void getFileList(File file, ArrayList<File> fileList) {
if (file.isFile()) {
fileList.add(file);
} else {
File[] list = file.listFiles();
for (File value : list) {
getFileList(value, fileList);
}
}
}
从transformInvocation的api中,我们获取了两个东西,一个是inputs,一个是outputProvider,我们遍历inputs后发现,他有两个api getDirectoryInputs和getJarInputs 这俩是什么东西呢?我描述不太好,我加了日志,来看下日志输出: 

这下是不是看明白了,其实我对getDirectoryInputs做了一层文件筛选处理
transformInput.getDirectoryInputs().forEach(directoryInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(directoryInput.getFile(), list);
});
void getFileList(File file, ArrayList<File> fileList) {
if (file.isFile()) {
fileList.add(file);
} else {
File[] list = file.listFiles();
for (File value : list) {
getFileList(value, fileList);
}
}
}
好,从上面我们看出,已经找到了MainActivity的class文件,那么接下来给MainActivity.class的onCreate函数,插入两行代码,
5. 现在开始操作ASM的api
首先要实现ASM的 ClassVisitor 类来操作我们想要操作的类,它可以访问class文件的各个部分,比如方法、变量、注解等
基本的实现如下:
public class TestClassVisitor extends ClassVisitor{
private String className;
private String superName;
TestClassVisitor(final ClassVisitor classVisitor) {
super(Opcodes.ASM6, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
this.superName = superName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (className.equals("com/julive/samtest/MainActivity")) {
if (name.startsWith("onCreate")) {
return new TestMethodVisitor(Opcodes.ASM6, methodVisitor, access, name, descriptor, className, superName);
}
}
return methodVisitor;
}
}
这里集成AdviceAdapter,其实AdviceAdapter是继承自MethodVisitor,这是不是就跟ClassVisitor一一呼应呢,使用它是因为它比较方便的实现,提供了onMethodEnter,onMethodExit,正好是我们的需求。在onCreate的函数的前后各插入一行代码。但仔细看onMethodEnter的函数实现,你会发现一脸懵逼,不知道是啥玩意。往下看
public class TestMethodVisitor extends AdviceAdapter {
private String className;
private String superName;
protected TestMethodVisitor(int i, MethodVisitor methodVisitor, int i1, String s, String s1,String className,String superName) {
super(i, methodVisitor, i1, s, s1);
this.className = className;
this.superName = superName;
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
mv.visitLdcInsn("TAG");
mv.visitLdcInsn(className + "---->" + superName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
@Override
protected void onMethodExit(int opcode) {
mv.visitLdcInsn("TAG");
mv.visitLdcInsn("this is end");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
super.onMethodExit(opcode);
}
}
在这里推荐一个插件,https://plugins.jetbrains.com/plugin/14860-asm-bytecode-viewer-support-kotlin ,用插件测试代码如下:
public class Test {
void aa() {
Log.i("TAG", "this is end");
}
}
转换ASM代码如下:
public static byte[] dump() throws Exception {
ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;
classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/julive/samtest/Test", null, "java/lang/Object", null);
classWriter.visitSource("Test.java", null);
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(5, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(RETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("this", "Lcom/julive/samtest/Test;", null, label0, label1, 0);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(0, "aa", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(8, label0);
methodVisitor.visitLdcInsn("TAG");
methodVisitor.visitLdcInsn("this is end");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(9, label1);
methodVisitor.visitInsn(RETURN);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLocalVariable("this", "Lcom/julive/samtest/Test;", null, label0, label2, 0);
methodVisitor.visitMaxs(2, 1);
methodVisitor.visitEnd();
}
classWriter.visitEnd();
return classWriter.toByteArray();
}
是不是很长,哈哈,这段代码其实是将整个Test类的东西,都通过ASM的方式生成,我们只需要找到对应的日志如下:
methodVisitor.visitLdcInsn("TAG");
methodVisitor.visitLdcInsn("this is end");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
然后将其放入到onMethodExit函数中,就可以了。
6.Tranfrom结合ASM实现
现在万事具备只欠东风,就是将Tranform拿到的class文件通过ASM做修改,具体如何关联,请看,回到刚才的doTransform中,改成如下代码:
private void doTransform(TransformInvocation transformInvocation) throws IOException {
System.out.println("doTransform =======================================================");
Collection<TransformInput> inputs = transformInvocation.getInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
if (outputProvider != null)
outputProvider.deleteAll();
inputs.forEach(transformInput -> {
transformInput.getDirectoryInputs().forEach(directoryInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(directoryInput.getFile(), list);
list.forEach(file -> {
System.out.println("getDirectoryInputs =======================================================" + file.getName());
if (file.isFile() && file.getName().endsWith(".class")) {
try {
ClassReader classReader = new ClassReader(new FileInputStream(file));
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new TestClassVisitor(classWriter);
classReader.accept(visitor, ClassReader.EXPAND_FRAMES);
byte[] bytes = classWriter.toByteArray();
FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath());
fileOutputStream.write(bytes);
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
if (outputProvider != null) {
File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
try {
FileUtils.copyDirectory(directoryInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
}
});
transformInput.getJarInputs().forEach(jarInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(jarInput.getFile(), list);
list.forEach(file -> {
System.out.println("getJarInputs =======================================================" + file.getName());
});
if (outputProvider != null) {
File dest = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
try {
FileUtils.copyFile(jarInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
}
});
});
}
void getFileList(File file, ArrayList<File> fileList) {
if (file.isFile()) {
fileList.add(file);
} else {
File[] list = file.listFiles();
for (File value : list) {
getFileList(value, fileList);
}
}
}
7.反编译检查代码
好了,通过ASM的一顿操作,已经将代码插入到了MainActivity的onCreate函数中,我们如何验证?可以通过反编译来看,也可以通过日志,日志不太合理,因为一般我们不会插入很多日志来验证我们插入的正确性,太多了,照顾不过来,下面我们就反编译来看:这里推荐使用https://github.com/skylot/jadx 它提供了可视化操作,首先做如下操作:
git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist
执行成功后,可以执行如下:
jadx-gui
然后就会打来工具,如下: 
然后将 app的debug apk包拖到这个窗口就行,如下: 我们找到MainActivity如下: 
而我们原代码是这样,跟我们预想的效果一致。

源码:https://github.com/shinecjj/AMStest
|