适配开源地址:ohos_jadx:基于 OpenHarmony PC 与 ArkUI 的 APK 反编译工具项目 - AtomGit

上游项目:GitHub - skylot/jadx: Dex to Java decompiler · GitHub (Apache-2.0)

一、为什么要适配 jadx

jadx 是目前使用最广的 Dex → Java 反编译器,GitHub 上约 5 万 star。做 Android 逆向、安全审计、SDK 排查、竞品分析的人,桌面上几乎都装着它:把 APK 拖进去,左边是包树,右边是还原出来的 Java 源码,再配上类搜索和"查找引用",一个下午就能摸清一个陌生 App 的结构。

鸿蒙 PC 上目前还没有 jadx。社区在 Java 方向已经有了一些积累,这次适配也借鉴了其中的经验。jadx 的核心价值在反编译引擎,而引擎和界面在上游本来就是分开的,所以我们选择保留引擎、只重建界面,这样以后跟进上游版本也比较方便。

所以这次适配给自己定了一个很硬的目标:

反编译引擎一行不改,直接在鸿蒙 PC 上跑上游发布的 jar;只把界面换成原生 ArkUI。

最终结果:HAP 52.6 MB,已在鸿蒙 PC 真机上跑通"打开 APK → 包树 → 反编译 → 类搜索 → 查找引用"的完整链路;引擎侧改动为 0 行。

二、适配边界先明确:搬引擎,不搬 Swing

jadx 的工程本身就分得很清楚:jadx-core 是官方发布的库制品,jadx-gui 是基于 Swing 的桌面界面。这给了我们一条干净的分界线:

层

上游实现

鸿蒙版本

处理方式

反编译引擎

jadx-core + 输入插件(dex/smali/java/kotlin 元数据)

同一批 jar,来自 Maven Central

原样运行

运行时

桌面 JDK

内嵌 BiSheng JDK 17(aarch64 / musl),jlink 裁剪

裁剪 + 打进 HAP

桥接

无(Swing 直接调用 Java)

liblauncher.so:NAPI + JNI

新写

界面

Swing(jadx-gui)

ArkTS / ArkUI

重建,布局与交互对齐 jadx-gui

在动手之前,我们先确认了一件事:引擎到底依赖不依赖 AWT/Swing?如果引擎深度绑定 java.desktop,这条路就走不通。

扫描结果:jadx-core 的 568 个源文件中只有 5 个 import 了 AWT,占 0.9%。但 import 扫描不等于真实依赖——我们在另一个 Java 项目上吃过这个亏,按 import 估计依赖 45 个 jar,实际跑起来需要 215 个。所以这次所有依赖数字都是跑出来的:

  • 用 -verbose:class 在两个真实 APK 上跑完整反编译,统计实际加载的类:

模块

加载的类数

java.base

554

java.xml

169

java.desktop

0

  • 做了一次对照:在探针里显式调用 GraphicsEnvironment.isHeadless(),java.desktop 一行立刻从 0 变成 17——说明测量方法本身是灵敏的,0 是真的 0。

结论:运行时只需要 java.base + java.xml 两个模块,Swing 可以整个丢掉。

三、鸿蒙版本的整体架构

图 1  架构图(architecture.png)

ArkTS / ArkUI(pages/Index.ets · jvm/JadxBridge.ets)

   │  NAPI:jadxCall(method, args) → Promise<JSON>
   ▼

liblauncher.so(C++:启动 JVM · 转发调用 · JVM 日志 → hilog)

   │  JNI:唯一签名 call(String method, String[] args)
   ▼

HmJarBootloader → URLClassLoader → JadxEngine.dispatch()

   │  jadx.api.JadxDecompiler
   ▼

jadx-core + 输入插件(27 jar / 14 MB,上游制品,0 行改动)
运行于:内嵌 BiSheng JDK 17(jlink:java.base + java.xml,40.2 MB / 14 个 ELF)

桥的设计刻意做得很"窄":C++ 侧只认识一个 JNI 签名

static String call(String method, String[] args)

method 取 open、tree、code、search、usage、packages、resources、resource、status、close;参数一律是字符串,返回一律是 JSON,并且任何情况下都不抛异常,错误也以 {"ok":false,"error":"..."} 返回。

这样做的好处是:新增一个引擎能力 = 在 JadxEngine.dispatch() 里加一个 case,再在 Index.d.ts 里加一行类型声明,C++ 完全不用动。 后面"查找引用""资源列表"等功能都是这样加进来的。

ohos_jadx/
├── AppScope/
├── entry/
│   ├── libs/arm64-v8a/jre/        内嵌 JRE(脚本生成,不入库)
│   └── src/main/
│       ├── cpp/                   liblauncher.so:NAPI + JNI 桥
│       ├── ets/                   EntryAbility · JadxBridge · Index 页面
│       └── resources/resfile/     app.jar + lib/*.jar + JarLoader/(脚本生成)
├── java/
│   ├── bootloader/                HmJarBootloader
│   ├── app/                       JadxEngine(引擎薄封装)
│   ├── pom-engine.xml             引擎运行期依赖集
│   └── build.py
└── runtime/
    ├── embed-jre.py               jlink 裁剪并嵌入 JRE
    └── check-profile.py           检查签名 profile 的 ACL 权限

工程目录:

四、真机运行与核心功能

以下截图均取自鸿蒙 PC 真机。

1. File 菜单:与 jadx-gui 一致的菜单和快捷键

图 2  File 菜单(01-file-menu.jpg)

菜单项顺序、图标、快捷键都对齐 jadx-gui,老用户不需要重新学习。打开文件走系统文件选择器(picker),选中的 APK 先复制进应用沙箱,再把沙箱内路径交给引擎——JNI 层拿到的永远是一个普通文件路径,而不是 URI。

图 3  系统文件选择器(07-file-picker.jpg)

系统文件选择器按 .apk 过滤;应用只能访问用户选中的文件,这正是鸿蒙沙箱的设计。

2. 层级包树:大 APK 也能秒开

图 4  层级包树(02-package-tree.jpg)

包树支持层级和扁平两种视图,每个包显示类数。界面一律惰性加载:展开包只列类名,点击类才触发反编译。真机实测一个 60 MB、50,550 个类的 APK:open() 耗时 10.5 s,占用 2.67 GB RSS;之后展开包只是列出类名,不触发反编译。

3. 类搜索

图 5  类搜索(03-class-search.jpg)

工具栏内的类搜索,结果带类型图标和包名;在上面那个 50,550 个类的 APK 上同样可用。

4. 多标签编辑器、行号与语法高亮

图 6  标签页与语法高亮(04-tabs-and-highlighting.jpg)

反编译结果以多标签页展示,带行号和 Java 语法着色,支持前进/后退和 Select in Tree。单个类的 code() 调用在真机上从约 130 ms 到 4,172 ms 不等(最大一次输出 242,335 个字符)。

5. 查找引用(Find usage)

图 7  查找引用(05-find-usage.jpg)

右键类即可查找引用,图中是引用 Fragment 的 12 个类。这个功能就是按上面说的方式加的:引擎侧一个 case,ArkTS 侧一行类型声明。

6. 暗色主题

图 8  暗色主题(06-dark-theme.jpg)

跟随系统暗色模式,也可以手动切换。

五、适配中的主要困难

这一部分是本次适配真正花时间的地方。每一条都是真机上踩到、定位、修复并复测过的。

难点一:JVM 需要"可写可执行内存"权限,而且签名顺序不能反

HotSpot 的 JIT 需要把代码缓存标记为可执行。鸿蒙上这需要受限权限 ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY,否则 JVM 启动即失败:

Failed to mark memory page as executable

我们试过 -Xint(纯解释执行),同样无法绕过,真机已验证。

更隐蔽的是顺序问题:这个权限属于 ACL 权限,必须先在 module.json5 里声明,再在 DevEco Studio 里生成签名。顺序反了,生成的 profile 中 allowed-acls 为空,安装时被拒绝:

code:9568289 install failed due to grant request permissions failed

为此仓库里提供了 runtime/check-profile.py,签名后跑一下就能确认 profile 里是否真的带上了该权限。

难点二:musl 的 shortname 陷阱,JVM 找不到自己

这是最难定位的一个问题。JVM 启动时报:

Unable to load jimage library

原因:鸿蒙的加载器基于 musl,而 musl 不会给以绝对路径 dlopen 的库分配 shortname。我们用绝对路径打开了 .../jre/lib/server/libjvm.so,但 libjimage.so、libjava.so、libnio.so 的 DT_NEEDED 里写的都是 libjvm.so——按名字找,找不到。同样的布局在 Linux(glibc)上没问题,因为 glibc 会按 SONAME 匹配。

验证过无效的方案:

  • appEnvironments 里设置 LD_LIBRARY_PATH:变量能传进进程,但加载器在进程启动时已经确定了搜索路径;
  • dlns_set_namespace_lib_path:本平台 libc 未导出;
  • 把 libjvm.so 复制到 jre/lib:会破坏 HotSpot 根据自身 .so 路径推导 java.home 的逻辑。

最终方案:让 liblauncher.so 直接链接 libjvm.so,并设置

INSTALL_RPATH "$ORIGIN/jre/lib/server:$ORIGIN/jre/lib"

还有一个细节:CMake 里必须用 -L<目录> -l<名字>,不能写 .so 的绝对路径。否则 hvigor 会把 libjvm.so 再复制一份到 libs/arm64-v8a/ 根目录,HAP 平白多出约 15 MB,musl 还可能加载到错误的那一份。

难点三:jlink 压缩级别,--compress=2 会让 JVM 直接崩溃

为了压体积,我们用 jlink 裁剪运行时,并实测了三档压缩:

compress

JRE

HAP

结果

0

~50 MB

61.8 MB

正常

1

40.2 MB

52.6 MB

正常(默认)

2

33.9 MB

45.5 MB

启动崩溃

--compress=2 会对 lib/modules 中的资源做 ZIP 压缩,读取时需要 HotSpot 的 zip 解压器,而它在本环境下无法解析,JVM 在 JNI_CreateJavaVM 内部跳转到空地址。多出的 7 MB 不值得冒这个风险,默认固定为 --compress=1。

难点四:类加载器——app.jar 不能放进系统类路径

引导类 HmJarBootloader 以平台加载器为父加载器建立 URLClassLoader,把 app.jar 和 lib/*.jar 放进去;系统类路径里只有 resfile 目录,仅用来找到引导类本身。

如果顺手把 app.jar 也放进 -Djava.class.path,双亲委派会让系统加载器自己加载 JadxEngine,而它看不到 lib/*.jar,第一次调用引擎就会:

NoClassDefFoundError: jadx/api/JadxArgs

难点五:NAPI 线程上 ServiceLoader 找不到插件

修好类路径后,APK 能"打开",但一个类都没有。日志里是:

Resolved plugins: []

原因:ArkTS 的调用经 NAPI 异步任务落到线程池线程上,这个线程的上下文类加载器是系统加载器;而 jadx 正是用 ServiceLoader 通过上下文类加载器去发现 dex 输入插件的。于是插件一个都没找到,dex 根本没被读取。

修复:HmJarBootloader.call() 在分发期间把上下文类加载器切换为引擎自己的 URLClassLoader,结束后恢复。

难点六:平台自述和沙箱约定

几个容易被忽略、但会让 Java 库"莫名其妙"出错的地方(均为真机实测):

  • os.name 是 HarmonyOS,不是 linux;os.arch 是 aarch64。依据 os.name 判断平台的库可能走错分支;
  • user.home / user.name 默认值是 "?";
  • /tmp 只读;

因此 JVM 启动时显式传入 -Djava.io.tmpdir 和 -Duser.home。另外,JVM 的 stdout/stderr 要重定向到文件而不是管道——JVM 启动失败时进程会立即退出,管道里真正的错误信息会丢失。

难点七:两个 ArkUI 的坑

在把界面对齐 jadx-gui 的过程中,真机上还遇到两个 ArkUI 行为:

  • @Builder 以"值"的方式接收参数时会捕获该值,导致按钮不再响应状态变化;
  • ForEach 的 key 如果只用下标,切换标签页时正文内容不会更新。

两处都在代码注释中留了说明。

六、构建与安装

环境要求:DevEco Studio(compatibleSdkVersion 6.0.0(20),targetSdkVersion 6.0.1(21))、主机 JDK 17、BiSheng JDK 17 aarch64/musl 版(含 jmods/)、Maven、Python 3。

# 1) 内嵌运行时 —— 必须先执行:native 构建要从生成的 jre/ 中链接 libjvm.so

python runtime/embed-jre.py

#    默认模块 java.base,java.xml,compress=1 → 40.2 MB / 14 个 ELF

 

# 2) Java 侧:引擎 jar + app.jar + 引导器

python java/build.py

 

# 3) 打包 HAP

hvigorw --mode module -p product=default -p module=entry@default assembleHap

 

# 4) 安装、启动、看日志

hdc install -r entry/build/default/outputs/default/entry-default-signed.hap

hdc shell aa start -a EntryAbility -b com.example.ohosjadx

hdc shell hilog -x | findstr JadxJvm

签名注意:仓库中 signingConfigs 刻意留空,请按"难点一"的顺序生成自己的签名,再用 python runtime/check-profile.py <profile>.p7b 确认 allowed-acls 中包含 ALLOW_WRITABLE_CODE_MEMORY。

几个关于"这个 HAP 里有什么"的数字:

  • 引擎运行期依赖 27 个 jar / 14 MB。它与完整 CLI 依赖集(55 jar / 52 MB)的反编译输出逐字节一致;
  • 内嵌 JRE 40.2 MB / 14 个 ELF;
  • 不带任何自有 .so(除了桥本身),不监听任何端口。

七、当前能力和边界

已在真机上跑通:

功能

状态

打开 APK / DEX / JAR(含 xapk / apkm / apks)

可用

包 / 类树(层级与扁平)

可用

反编译显示 Java 源码(语法着色、行号)

可用

类名搜索

可用

查找引用

可用

资源列表与资源文本(XML / JSON / TEXT)

可用

多标签页、前进 / 后退、Select in Tree

可用

复制源码 / 复制引用

可用

明暗主题

可用

与 jadx-gui 一致的快捷键

可用

系统文件选择器 + 复制进沙箱后打开

可用

按 jadx-gui 布局保留、当前禁用: Export project、Decompile all classes、Text search、Deobfuscation、Debugger、Go to declaration、Rename,以及 Plugins 菜单。

必须说清楚的限制:

1. W^X 权限是硬门槛。 ALLOW_WRITABLE_CODE_MEMORY 属于受限权限。官方文档的说明是:该权限仅向允许列表中的企业应用开放,进入列表需联系企业技术支持。因此这个仓库当前的定位是可复现、可运行的工程范式与开发者工具,而不是可以无条件上架的应用。

2. Secure Shield 模式。 官方文档要求申请该权限的应用适配 Secure Shield 模式。真机实测:开启该模式后内嵌 JVM 无法启动。

3. 调试证书有效期。 带该权限的调试 profile 有效期取决于申请状态:申请未审批时为 120 小时,审批通过后为 365 天。过期后已安装的应用可能以 10106105 under control 拒绝启动,需要重新签名安装。

4. 9-patch 资源。 jadx 中唯一真正走到 java.desktop 的运行期路径是 Res9patchStreamDecoder。实测中从未触发,但不能保证所有 APK 都不会触发;若出现,用 embed-jre.py --with-desktop 把模块加回即可,架构不变。

5. 内存。 反编译对 CPU 和堆都很敏感,大型 APK 的 open() 可达数 GB RSS(50,550 类的 APK 实测 2.67 GB)。引擎目前固定单线程,避免与界面争抢资源。

八、总结

这次适配验证了一条和"重写"不同的路:对于引擎与界面本来就分离的 Java 项目,可以把引擎原样放进内嵌 JVM,只重建界面。 这样做最大的收益是可维护性——上游 jadx 发新版,我们要做的只是替换 jar,反编译质量与上游完全一致,不存在"鸿蒙版本的行为和桌面版不一样"的问题。

整个方案里真正和鸿蒙平台相关的知识,集中在几个点上:受限权限与签名顺序、musl 的 shortname 规则、jlink 压缩级别、NAPI 线程上的上下文类加载器,以及 os.name / user.home / /tmp 这些平台约定。这些问题一旦解决,同样适用于其他希望把 Java 桌面程序带到鸿蒙 PC 的项目。

欢迎试用、提 issue 和 PR:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐