# Dex 编辑
一个 Android 应用的绝大部分代码(使用 Java/Kotlin 编写)都会编译打包到 dex 文件中,通过 MT 强大的 Dex 编辑器++ 功能你可以将其反编译为 Smali 代码并进行修改。
# 多 Dex 编辑
打开 dex 文件,选择 Dex 编辑器++,如果同目录下还有其它 dex 文件,你将会看到以下对话框,可以选择将它们一起打开进行编辑:
如果一次打开多个 dex 文件时出现了类名重复的情况,那么将根据 dex 文件名排序移除后面的类。
例如 classes.dex 和 classes2.dex 中均包含了类 androidx.annotation.Keep
,那么 classes2.dex 中的该类会被移除,并看到以下提示:
# Smali 编辑
在浏览界面中点击一个类,即可进入它的 Smali 代码编辑界面:
关于 Smali 的相关知识,可以在网上找到非常多的资料,这边不过多展开。
# 代码导航
在点击左上角的指南针图标可打开导航对话框,这里提供了当前代码中所有字段和方法的导航(也可以切换为字符串导航),直接点击可跳转到对应的代码位置,长按则可以弹出菜单进行相关操作。
相关内容:关于重写方法
# 代码跳转
# 跳转到类
选中一个类名,在弹出菜单中就会出现「跳转」,点击即可跳转到该类的 Smali 代码中。
# 跳转到方法
选中一个方法名,在弹出菜单中就会出现「跳转」,点击即可跳转到该方法的 Smali 代码处。
# 跳转到字段
选中一个字段名,在弹出菜单中就会出现「跳转」,点击即可跳转到该字段的 Smali 代码处。
# 跳转到标签
选中一个标签,在弹出菜单中就会出现「跳转」,点击即可看到该标签的定义和调用点。
# 指令查询
Smali 拥有两百多条指令,你可以点击右上角菜单的「指令查询」调用出查询窗口,查看各个指令的功能介绍和调用格式。如果当前光标所在行存在 Smali 指令,还会自动将其填入到搜索框,无需手动输入。
# 寄存器分析与扩充
Smali 是基于寄存器的语言,寄存器存放着运行时的各个变量值,在修改时若稍有不慎,错误覆盖了一个后续代码需要用到的寄存器的值,将会产生意想不到的错误。
在修改 Smali 代码时,特别是在插入调用方法指令的情况下,经常需要借用几个寄存器来存放参数值,为了避免产生上面提到的错误,你可以使用 MT 的「寄存器分析」功能来查找出指定位置中可用寄存器。
可用寄存器:你可以随意对其赋值,完全不会影响到下文的运行。
只需要将光标放在需用插入指令的位置,然后点击右上角的笔图标,选择「寄存器分析」:
MT 会通过控制流分析代码上下文,找出所有可使用的寄存器,以注释方式插入到代码中:
有时会出现所有寄存器都被占用,找不到可用寄存器的情况,这时候就需要增加寄存器数量。
寄存器数量在方法代码的开头以 .registers N
的形式指定,例如原先有 8 个寄存器,你需要额外使用 1 个寄存器,那就需要扩充为 9 个寄存器,即修改为 .registers 9
,这样再使用「寄存器分析」就可以找到可用寄存器了。
但是直接修改 .registers N
的方法仅在修改后的 N 不大于 16 的情况下推荐使用,因为有些指令只能使用 v0 .. v15 的寄存器,而修改 N 会导致参数寄存器变大,一旦参数寄存器超过 v15,又刚好被这些指令用到,将会导致编译失败。
对于 N 大于 16 的情况,可以使用 MT 的「寄存器扩充」功能:
# 反编译为 Java
VIP 功能
- 点击右上角菜单的「转成 Java」即可将当前编辑的整个 Smali 代码反编译为 Java 代码;
- 在代码导航的方法长按菜单中点击「转成 Java」可将选中的方法单独反编译为 Java 代码。
反编译后的 Java 代码仅供参考,无法修改!
Java 是高级语言,Smali 是低级语言,Java 代码到 Smali 代码之间经过编译器优化,一些代码流程逻辑已无法使用 Java 代码来描述,只要代码逻辑复杂点,反编译出来的 Java 代码就可能会存在一些错误,甚至一些反编译器因为自身 bug 生成了逻辑完全相反的代码。
因此反编译出来的 Java 代码只能参考,用于帮助你更快的理解 Smali 代码,而无法用于修改再转回 Smali 代码。
# 切换引擎
如果你觉得当前反编译出来的 Java 代码不理想,那么可以点击右上角菜单切换引擎,当前支持的反编译引擎有:
- Jadx
- Jadx (Simple)
- Jadx (Fallback)
- FernFlower
- JD-Core
- Procyon
- CFR
一般推荐使用 Jadx,其次 FernFlower,如果都不理想再尝试其它的。
Jadx (Simple) 和 Jadx (Fallback) 反编译出来的代码介于 Smali 代码与 Java 代码之间,比 Smali 可读性高,比 Java 错误少。
# 重新反编译
当你反编译为 Java 代码后再去修改 Smali 代码,前面的 Java 代码并不会随之刷新,你需要点击右上角菜单的「重新加载」,它会读取最新的 Smali 代码并进行反编译。
# 搜索与替换
Dex编辑器++ 支持搜索代码、类名、方法名、字段名、字符串、整数,可指定搜索路径、是否搜索子目录、是否区分大小写等,支持正则表达式,支持在当前搜索结果中搜索,支持撤销搜索。
发起搜索的方式有两种,一是切换到搜索界面,直接点击对应选项,二是在浏览界面长按文件夹,点击弹出菜单中的搜索。
# 搜索字符串和代码的区别
字符串包含于代码。代码就是 Smali 代码,而字符串是 Smali 代码中的 “xxxxxx”,即引号中的那部分内容(经过反转义)。
# 为什么搜不到内容?
- 检查下「搜索子目录」是否没有勾选;
- 正则表达式、完全匹配、区分大小写等选项是否正确勾选;
- 搜索内容是否输入正确。
如果以上都没有问题,那就是确实找不到你要搜索的内容了。
# 替换
要使用替换功能,需要先进行搜索,成功搜索到内容后,才可以看到「在当前结果中替换」按钮,替换功能的搜索类型仅支持代码与字符串。
# 工程说明
对于新打开的文件都会为其创建一个全新的临时工程,所有的工程数据(正在编辑的文件、历史记录、搜索记录等)都会在返回到主界面时被删除,当然你也可以选择将当前工程数据保存。
点击左上角菜单的「保存为工程」,输入工程名字后即可:
保存为工程后,你可以在主界面侧拉栏的工程分组中找到该工程并打开:
还可以向右滑动展开菜单进行删除、重命名、打开工程目录的操作:
# 临时工程与正式工程的主要区别
- 临时工程在编译时会覆盖原文件,正式工程则会将文件输出到工程目录;
- 临时工程退出时会删除所有数据,正式工程会保留数据并可以重新进入;
- 临时工程适合快速修改保存的场景,正式工程适合分析复杂文件的场景。
# 补充说明
# 关于重写方法
在代码导航的方法长按菜单中,「查找调用处」与「查找重写方法」均和重写方法有关。
重写方法的完整说法应该是重写父类中的方法,有 Java 开发经验的人肯定对 @Override
这个注解十分熟悉,其实它就是标注了这是一个重写方法,如果不是则编译器会报错。
先让我们看看以下代码:
class Parent {
public void run() {
System.out.println("I am parent");
}
}
class Child extends Parent {
@Override
public void run() {
System.out.println("I am child");
}
}
其中 Child 重写了 Parent 的 run() 方法,再来看看以下代码:
static void print(Parent obj) {
obj.run();
}
上面的 obj.run() 是会输出 I am parent
还是 I am child
呢?
答案是都有可能:
print(new Parent()); // 输出 I am parent
print(new Child()); // 输出 I am child
如果转到 Smali 代码的视角,那就是 invoke-virtual {v0}, Lparent;->run()V
不一定执行 parent.run()
方法,也可能执行 Child.run()
方法,主要就在于 v0
参数究竟是 Parent 的实例还是 Child 的实例。
因此如果你看到了 invoke-virtual {v0}, Lparent;->run()V
,然后跳转到 parent.run()
方法看了下代码,就觉得运行时会输出 I am parent
,这显然是不对的,你还需要去它的重写方法 Child.run()
看看。
而在方法的「查找调用处」功能中,如果选择了同时搜索此方法的重写方法的调用,假设你搜索的是 parent.run()
的调用处,那么 MT 还会同时搜索 Child.run()
方法的调用处,避免遗漏。
# 关于旧版 Dex 编辑器
在打开 dex 文件时除了 Dex 编辑器++ 外还有一个 Dex 编辑器,这是早期开发的功能,停更多年,技术已过时,除了特殊需求外,不再推荐使用,并且在未来版本中可能会将其移除。