这篇文章主要记录了基于最新版 Magisk 的 Zygisk,整理并移植到 r0zygisk 的完整过程,同时把 KernelSU / SukiSU 环境下的 WebUI 一并补齐。
完整源码和发布包已经开源在 GitHub Releases:
r0zygisk Releases
整个过程可以归纳成八条主线:
关于 Magisk 和 ZygiskNext 老版本源码的阅读,这次我用到了一个开源库:
093K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5N6h3q4F1P5i4g2S2L8Y4A6Z5K9h3k6W2L8X3N6Q4x3V1k6U0L8$3c8W2i4K6u0V1M7r3q4F1L8%4u0S2L8h3p5`.
这个工具对源码阅读和结构定位帮助很大。
文章整体也使用了 AI 辅助完成,具体环境是:codex cli + gpt5.4 模型。
结果测试覆盖了 Android 14 到 Android 16 机型。
项目初始目录结构如下:
r0zygisk 是参考 ZygiskNext 的内容继续做的,使用 Android Studio 编译,开发环境如下。
Android Gradle Plugin 和 Gradle Wrapper 版本:
JDK 配置在 gradle.properties 中:
项目打包过程中,前期遇到的问题主要有这些:
开发环境准备好之后,就开始进入正式工作:基于最新版 Magisk 的 Zygisk,做一个支持 Android 16 的 r0zygisk。
动手移植之前,先确认当前 Magisk-master 这版到底有没有完成 Android 16 适配。
我先让 AI 帮我判断这版 Magisk-master 的适配情况,提问如下:
得到的分析结果如下:
通过对 Magisk 仓库的查看和 AI 分析结果,可以确认这个版本已经适配 Android 16,那么后续就可以直接基于这版 Zygisk 来做迁移。
在开始改代码之前,先把最新版 Magisk Zygisk 的执行流程和关键入口梳理清楚。
阅读 Magisk 源码时,我先把前面提到的开源库本地安装并运行起来:
点一下生成全景图,就开始分析了:
生成结果如下: 借助这个视图,可以快速定位主要入口。
由于我主要关注的是 zygisk,因此后续单独针对这部分代码继续下钻:
使用这个库后,项目入口和主要结构会清晰很多。具体使用过程这里就不展开了。
在这个基础上,我再结合 codex 继续分析代码。
在 /Magisk-master/native/src/core/zygisk 目录下,打开 codex,给它下指令:
等待 AI 执行:
结果:
本文基于当前目录 /native/src/core/zygisk 中的源码编写,目标是把 Zygisk 的“如何被加载、如何接管 Zygote、如何加载模块、如何与 magiskd/zygiskd 协作、最后如何清理卸载”串成一条完整链路,并按文件解释代码。
目录内文件职责:
Zygisk 不是通过修改 Zygote 可执行文件直接进入的,而是借用了 Android native bridge 加载机制。
Rust 侧 ZygiskState::set_prop() 修改属性:
写入策略:
因此 Zygote 启动 native bridge 时会优先加载 libzygisk.so。如果系统原本就配置了 native bridge,Zygisk 后续会从属性字符串里剥出原值并重新加载真实 native bridge,避免破坏原系统行为。
Android 的 LoadNativeBridge 会查找动态库中的导出符号 NativeBridgeItf。Zygisk 在 entry.cpp 中导出同名符号:
当 isCompatibleWith() 被调用时,Zygisk 已经进入 Zygote 进程。这里执行:
hook_entry() 创建全局 HookContext,调用 HookContext::hook_plt()。
第一批 PLT hook 挂在两个库上:
AndroidRuntime 启动 Java 层 ZygoteInit 前会处理类名字符串。strdup("com.android.internal.os.ZygoteInit") 触发 Zygisk 的 new_strdup(),调用 hook_zygote_jni()。
hook_zygote_jni() 做三件事:
替换后的函数来自 jni_hooks.hpp。它们都是包装器:先构造 ZygiskContext,执行 pre 回调,再调用原始 JNI 函数,最后执行 post 回调。
当 Zygote 要创建 app 或 system_server 时,流程进入 Zygisk 包装器:
模块如果需要 root 权限,可以调用 Api::connectCompanion()。这不会让 app 进程变成 root,而是:
子进程 specialize 完成后,Zygisk 不希望一直留在 app/system_server 进程中。析构 ZygiskContext 时:
后续 JVM 创建线程时会调用 pthread_attr_destroy。Zygisk 在这个安全时机:
#pragma once 防止头文件重复包含。jni.h 提供 JNI 类型,core.hpp 提供 Magisk core 内部工具、日志、IPC 等声明。
ZYGISKLDR 是 Zygisk loader 库名:libzygisk.so。
NBPROP 是 native bridge 属性名:ro.dalvik.vm.native.bridge。Rust 与 C++ 两边都使用同一语义。
64 位编译时日志前缀为 zygisk64,32 位为 zygisk32。这样同一设备有双 Zygote 时,logcat 中可以区分是哪一套代码在运行。
ZLOGV 默认被定义成空操作。取消注释可把 verbose 日志转成 debug 日志,但正常构建不启用,避免刷屏和性能影响。
hook_entry() 是 native bridge 回调进入 bootstrap 的入口。
hookJniNativeMethods() 是 Zygisk API 暴露给模块的 JNI hook 实现入口,内部会转给当前全局 HookContext。
这两个结构是 Android native bridge 头文件中的 ABI 子集:
NativeBridgeRuntimeCallbacks 提供三类能力:
Zygisk 依赖它枚举并替换 Zygote native 方法。
NativeBridgeCallbacks 是 native bridge 动态库导出的 NativeBridgeItf 结构。Zygisk 只填了版本、padding、isCompatibleWith。
android/dlext.h 用于从 fd 加载 so。dlfcn.h 用于 dlsym。poll.h 用于 companion daemon 等待请求。
comp_entry 是模块 companion 函数类型:void (*)(int)。
exec_companion_entry 是 Rust 侧 mod.rs 导出的 C ABI 函数。C++ 不直接创建线程,而是把 companion 请求交给 Rust 线程池执行。
zygiskd(int socket) 是 root companion daemon 主循环。开头要求:
不满足直接退出。
64 位进程命名为 zygiskd64,32 位为 zygiskd32。这和 magiskd 中按 ABI 维护两个 socket 对应。
从 magiskd 传来的 socket 中接收一组模块 fd。每个 fd:
"/jit-cache" 只是提供给 linker 的伪路径,真实库来自 fd。
write_int(socket, 0) 告诉 magiskd:zygiskd 已经加载模块 companion 表,可以开始转发请求。
poll 阻塞等待 magiskd 发来 client fd。每次请求:
这里的 companion entry 不是直接同步执行,而是进入 Rust 线程池,允许并发处理。
这是 magisk applet 入口,只有内部执行 magisk zygisk companion <fd> 时使用。
参数匹配 argc == 3 && argv[1] == "companion" 时进入 zygiskd(parse_int(argv[2]))。
导出符号 NativeBridgeItf,让 Android native bridge loader 能找到。
isCompatibleWith lambda 是 Zygisk 第一次执行自己代码的关键点:
返回 false 是有意的:Zygisk 要借 native bridge 入口执行注入,而不是最终承担 native bridge 功能。
sys/mman.h、sys/resource.h、dlfcn.h、unwind.h 支撑内存映射扫描、fd 上限、动态符号、栈回溯。
lsplt.hpp 是 PLT hook 库。Zygisk bootstrap 和模块 API 的 PLT hook 都依赖它。
module.hpp 提供 ZygiskContext,jni_hooks.hpp 提供生成好的 JNI 包装函数列表。
这段注释是官方写在源码里的关键说明。它表达的核心是:
kZygoteInit 是 Java 层入口类全名。
kZygote 是 JNI 查找类名格式:com/android/internal/os/Zygote。
kForkApp、kSpecializeApp、kForkServer 是要替换的 native 方法名。
JNIMethods 是 std::span<JNINativeMethod>,方便对生成数组做统一处理。
JNIMethodsDyn 是动态分配的 JNI 方法表和数量。
HookContext 继承 JniHookDefinitions,因此直接持有三组生成的 JNI hook 数组:
字段含义:
方法分两类:
g_ctx 指向当前 specialize 流程中的 ZygiskContext。它通常指向栈对象,只在包装 JNI 函数执行期间有效。
g_hook 指向长期存在的 HookContext,从 native bridge 加载后一直存在到自卸载前。
get_defs() 返回 g_hook,供 jni_hooks.hpp 生成的包装函数获取原始 native 函数指针。
new_strdup(const char *str) 检查字符串是否等于 com.android.internal.os.ZygoteInit。
如果匹配,调用 g_hook->hook_zygote_jni()。
随后调用原始 strdup。这个 hook 的意义是抓住 AndroidRuntime 即将启动 ZygoteInit 的窗口,此时 JVM 已创建,Zygote native 方法也已经可枚举和替换。
new_fork() 的逻辑:
Zygisk 在 fork_pre() 中会提前执行真正 fork。随后原始 Zygote native 函数内部再调用 fork() 时,会被这个 hook 拦截并返回同一个 pid,避免 fork 两次。
Zygote specialize 过程中可能调用 unshare(CLONE_NEWNS) 创建私有 mount namespace。
如果当前在 Zygisk specialize 流程中,且创建 mount namespace 成功,并且 flags 中有 DO_REVERT_UNMOUNT,则调用 revert_unmount()。这用于 denylist 或模块强制隐藏时,从目标进程 mount namespace 中卸载 Magisk/模块痕迹。
secontext 改变后,进程权限和 SELinux 限制会发生变化。这里在切换前调用 zygisk_get_logd(),预先拿到 logd fd,避免后续上下文下无法访问或日志管道异常。
Zygote fork/specialize 时会关闭日志 fd。Zygisk 根据 SKIP_CLOSE_LOG_PIPE 控制是否关闭自己的 log 管道,然后调用原函数。
这是 native bridge bootstrap 的关键:
post_native_bridge_load() 会用这个 handle 记录 Zygisk 自身,并通过栈回溯找到真实 LoadNativeBridge 和 runtime callbacks。
Zygisk 不能直接在自己的函数栈上调用 dlclose(self_handle),否则代码被 unmap 后返回地址可能落在已卸载内存里。
解决方式:
这样不会返回到 Zygisk 的已卸载代码。
get_fd_max() 读取 RLIMIT_NOFILE,默认准备 32768。
ZygiskContext 构造函数保存 JNIEnv、specialize 参数、初始化 pid/flags/fd 位图和 mutex,并设置全局 g_ctx = this。
析构时先把 g_ctx 清空,因为它指向栈对象。
如果不是子进程,直接返回。父 zygote 不能卸载 Zygisk,否则后续 fork 无法继续被 hook。
子进程中:
封装 _Unwind_GetRegionStart。ARM32 下如果 PC 处于 Thumb 模式,要把最低位置 1,保证函数地址匹配真实调用地址。
目标是从栈回溯上下文中找 NativeBridgeRuntimeCallbacks *。
步骤:
这是 Zygisk 能枚举已注册 JNI 方法的基础。
被 dlclose hook 触发。
主要流程:
register_hook() 包装 lsplt::RegisterHook,成功后把 (dev, inode, symbol, old_func) 存入 plt_backup。
hook_plt() 扫描 maps 找:
然后注册:
最后 lsplt::CommitHook() 执行实际 patch,并删除没有成功备份 old_func 的记录。
hook_unloader() 扫描 maps 找 libart.so,注册 pthread_attr_destroy hook。这一步发生在子进程 specialize 完成后,而不是最初 bootstrap 时。
遍历 plt_backup,把每个 symbol 替回 old_func。若任何一步失败,设置 should_unmap = false,避免在 hook 状态不确定时卸载自己。
get_jni_methods() 通过 runtime callbacks:
register_jni_methods() 逐个调用 RegisterNatives,允许失败。失败时清 exception,并把该方法的 fnPtr 置空。
这是替换 JNI native 方法的核心。
流程:
注意源码中特别强调:runtime callbacks 返回的 signature 不是标准格式,所以不能只靠字符串提前判断,而是要直接 RegisterNatives 试。
对模块 API 提供的入口。若 callbacks/env/class 无效,则把所有 fnPtr 清空。否则 FindClass 后调用上面的核心替换函数。
这是“接管 Zygote specialize”的关键函数。
步骤:
把 fork_app_methods、specialize_app_methods、fork_server_methods 中保存的旧函数指针重新注册回 Zygote 类。
hook_entry() 创建 g_hook 并安装早期 PLT hook。
hookJniNativeMethods() 是公开 API 的 C++ 实现,转给 g_hook->hook_jni_methods。
包含 regex、list 和公开 API 头 api.hpp。模块 API 兼容层需要正则保存 PLT hook 规则。
Zygisk 支持多个 API/ABI 版本。这里用别名表达版本兼容关系:
这表示某些版本只扩展语义或参数对象,不改变模块 ABI 函数表布局。
AppSpecializeArgs_v3 保存 app specialize 的参数引用。必须用引用,因为模块可以修改这些参数,让原始 Zygote specialize 使用修改后的值。
必选字段包括 uid/gid/gids/runtime_flags/rlimits/mount_external/se_info/nice_name/instruction_set/app_data_dir。
可选字段用指针表示,因为不同 Android 版本和 OEM 签名不同:
AppSpecializeArgs_v5 在 v3 基础上增加 mount_sysprop_overrides。
旧 API v1/v2 没有 rlimits,构造函数从 v5 转换到 v1 视图。模块如果声明旧 API,Zygisk 会创建这个兼容对象再调用模块回调。
ServerSpecializeArgs_v1 保存 system_server 参数引用:
模块可在 pre 阶段修改这些值。
module_abi_v1 是模块注册给 Zygisk 的 ABI:
static_assert 保证内部 flag 与公开 API flag 值一致。
UNMOUNT_MASK 表示“进程在 denylist 且 denylist enforced”。
PRIVATE_MASK 是不应该暴露给模块的内部 flag,包括 enforced 状态和 Magisk app 标记。
api_abi_base 每个版本都有:
v1 提供:
v2 增加:
v4 改造 PLT hook API,从 regex 匹配改为 dev/inode 精确定位,并增加:
ApiTable union 让同一块内存按不同 ABI 版本解释。
ZygiskModule 表示一个已加载到目标进程的 Zygisk 模块。
重要字段:
重要方法:
g_ctx 在 hook.cpp 定义,供模块 API 访问当前 specialize 上下文。
old_fork 是 PLT hook 保存的原始 fork。
内部 flags:
ZygiskContext 是一次 specialize 流程的上下文,生命周期通常是 JNI 包装函数的栈帧。
字段:
方法按功能分组:
连接 magiskd 的 RequestCode::ZYGISK,写入具体 ZygiskRequest 子命令,然后返回 socket fd。
后续 GetInfo、ConnectCompanion、GetModDir 都从这里发起。
保存 id、handle、entry。清零 API 表,设置:
模块入口拿到 API 表后,会调用 registerModule 把模块 ABI 回填给 Zygisk。
模块注册入口。
流程:
v1 API:
v2 API:
v4 API:
模块必须满足:
否则模块会从 modules 列表删除。
向 magiskd 发 ConnectCompanion 请求。写入当前进程 ABI:
再写模块 id。返回的 fd 是连接到 companion handler 的 socket。
向 magiskd 发 GetModDir,写模块 id,接收一个目录 fd。这个 fd 指向模块根目录。
支持两个模块选项:
getFlags() 返回 info_flags 去掉内部私有位后的结果。
tryUnload() 如果模块设置 unload,就 dlclose(handle)。
call_app 宏负责兼容旧 API:
server 参数没有这种版本分流,直接调用。
v1/v2 模块用 regex 匹配库路径。Zygisk 保存:
exclude 规则也保存 regex 与 symbol。symbol 为空表示排除整个库。
plt_hook_process_regex() 扫描 maps:
plt_hook_commit() 加锁、处理 regex、释放 regex 对象、清空列表,然后 lsplt::CommitHook()。
向 magiskd 发 GetInfo:
返回 fd 是因为 system_server 流程后续还要把加载失败的模块 id 写回 magiskd。
先关闭 Zygisk logd fd。
父进程直接返回。子进程中:
目的是避免从 Zygote 继承不该进入 app 的 fd,防止崩溃和泄漏。
exempt_fd(fd):
can_exempt_fd() 要求当前是 APP_FORK_AND_SPECIALIZE,且 app 参数有 fds_to_ignore。
fork_pre() 是 Zygisk 避免第三方模块代码污染父 Zygote 的关键。
流程:
之后原始 Zygote 方法再调用 fork() 时,会被 new_fork() 返回缓存 pid,不会真的再 fork。
fork_post() unblock SIGCHLD。
加载并执行模块 pre 阶段。
第一轮遍历 fd:
第二轮:
第三轮:
设置 POST_SPECIALIZE,再对每个模块:
app_specialize_pre():
app_specialize_post():
server_specialize_pre():
server_specialize_post() 调 run_modules_post()。
这是 Android Q 以后部分流程使用的“不 fork,只 specialize 当前进程”的路径。
pre:
post:
pre:
post:
pre:
post:
这是面向模块开发者的公开 API 头。源码提示不要直接修改,并建议模块开发使用发布版 sample 仓库中的头文件。
包含 jni.h,定义当前 API 版本 ZYGISK_API_VERSION 5。
注释解释了 Zygisk 的模型:
模块作者继承此类,实现:
AppSpecializeArgs 暴露 app specialize 参数。必选字段保证所有支持 Android 版本存在;可选字段要先判空。
ServerSpecializeArgs 暴露 system_server 参数。
字段都是引用,模块可在 pre 阶段修改它们,影响后续原始 Zygote specialize 调用。
FORCE_DENYLIST_UNMOUNT:强制对当前进程执行 Magisk/模块文件 unmount。
DLCLOSE_MODULE_LIBRARY:post 后卸载模块库。注释明确警告:如果模块 hook 了进程函数,不应启用该选项,否则回调地址会变成悬空指针。
PROCESS_GRANTED_ROOT 表示当前进程 uid 已被授权 root。
PROCESS_ON_DENYLIST 表示当前进程在 denylist 中。
内部 flag 如 denylist enforced、Magisk app 不直接暴露给模块。
模块可用 API:
这些 API 在 post 后会失效,因为 Zygisk 会卸载并清空 API 表。
REGISTER_ZYGISK_MODULE(clazz) 导出 zygisk_module_entry,Zygisk 加载模块后查找这个符号。
REGISTER_ZYGISK_COMPANION(func) 导出 zygisk_companion_entry,zygiskd 加载模块 companion 后查找这个符号。
internal::module_abi 是模块侧构造的 ABI 表,保存 API 版本、模块对象和四个回调 trampoline。
internal::api_table 是 Zygisk 传入模块的函数表,布局对应 module.hpp 中的 api_abi_v4。
entry_impl<T>():
这些方法只是薄封装:检查函数指针是否存在,再通过 tbl 调用 Zygisk 内部实现。
声明两个 C 符号:
模块可以只注册其中一个,也可以两个都注册。
这是 gen_jni_hooks.py 生成的文件,不建议手写修改。它的职责是覆盖不同 Android 版本和厂商修改后的 Zygote native 方法签名。
声明 JniHookDefinitions 和 get_defs()。get_defs() 在 hook.cpp 中返回 g_hook,因此生成代码可以访问保存旧函数指针的数组。
数组包含 12 个 nativeForkAndSpecialize 签名:
每个元素结构相同:
lambda 统一流程:
注意第 5 步中 fnPtr 在 hook 完成后已经被替换成原始函数指针,这是 HookContext::hook_jni_methods() 做的。
数组包含 7 个 nativeSpecializeAppProcess 签名。
这个路径返回 void,因为它不 fork,只 specialize 当前进程。
lambda 流程与 fork app 类似,但调用:
数组包含 2 个 nativeForkSystemServer 签名:
lambda 流程:
JType 保存 C++ 类型名和 JNI 签名字符。
JArray 根据元素类型生成数组类型:
Argument 保存参数名、类型、是否要写入 AppSpecializeArgs 的可选字段。
Anon 表示 Zygisk 不关心但签名中必须保留的 OEM 参数,自动命名为 _0、_1 等。
Return 保存返回值表达式和返回类型。
JNIMethod 能生成:
JNIHook 是抽象基类。
ForkApp 生成 nativeForkAndSpecialize 包装体:
SpecializeApp 改目标名为 nativeSpecializeAppProcess,返回 void。
ForkServer 改目标名为 nativeForkSystemServer,初始化 server args。
定义 AOSP 与 OEM 签名中会出现的参数对象,例如 uid/gid/gids/runtime_flags、fds_to_ignore、is_child_zygote、mount_sysprop_overrides、server capabilities 等。
带 set_arg=True 的参数会被写入 AppSpecializeArgs_v5 可选字段。
逐个定义不同 Android 版本/OEM 的方法签名:
Samsung、Nubia、XR 等变体通过 Anon 或额外参数保持 ABI 匹配,但 Zygisk 只提取自己关心的参数。
gen_jni_def(field, methods) 生成一个 std::array<JNINativeMethod, N>。
每个元素包含:
脚本打开 jni_hooks.hpp 并写入:
引入模块根目录常量、MagiskD、FFI 枚举、resetprop、Unix socket 扩展、fd 工具、fork 工具、日志工具等。
NBPROP 与 C++ 一致,是 native bridge 属性。
ZYGISKLDR 与 C++ 一致,是 libzygisk.so。
UNMOUNT_MASK 表示 denylist enforced 且当前进程在 denylist。
zygisk_should_load_module(flags) 返回 true 的条件:
启动 companion daemon。
关键点:
这个参数最终进入 C++ zygisk_main()。
字段:
处理模块 connectCompanion() 请求。
流程:
Zygisk 状态重置:
如果 lib_name 不为空,说明已设置过,直接返回。
否则读取原 native bridge 属性:
写回 ro.dalvik.vm.native.bridge。
如果 Huawei Maple 开启,则设置 ro.maple.enable=0,因为 Maple 特殊 Zygote 可能绕开 native bridge 创建 system_server。
如果 lib_name 比 libzygisk.so 长,后半段就是原 native bridge;否则恢复为 "0"。
写回属性并清空 lib_name。
magiskd 收到 RequestCode::ZYGISK 后进入这里。
读取 ZygiskRequest,分发:
从模块列表取每个模块对应 ABI 的 zygisk so fd:
处理目标进程查询。
读取:
计算 flags:
然后:
读取模块 id,查模块列表,打开模块根目录并把 fd 发送给客户端。
FFI 辅助方法,返回 atomic zygisk_enabled 状态。
引入 daemon 子模块,并重新导出:
这是给 C++ entry.cpp 调用的 C ABI 函数。
参数:
执行流程:
这样做是为了避免模块 handler 自己关闭 fd 后,系统复用了相同 fd 数字,Zygisk 又误关了别的文件。
公开 API 注释也强调:模块只在 fork 后的 app/system_server 进程中加载。Zygisk 先 fork,再在子进程加载第三方模块,避免模块代码污染父 Zygote。
Zygisk 不是要成为真正 native bridge,它只是借这个入口完成 bootstrap。真正 native bridge 如存在,会在 post_native_bridge_load() 中补载。
Zygisk 需要在第三方模块加载前先真实 fork,确保模块只进入子进程。但原始 Zygote native 函数内部仍会调用 fork。new_fork() 返回缓存 pid,使原始逻辑继续按正常路径执行但不再创建新进程。
不同 Android 版本和厂商修改了 Zygote native 方法签名。Zygisk 不能假设固定签名,所以生成大量候选签名,逐个 RegisterNatives 尝试,成功后再反查旧函数指针。
Zygisk 的职责是在 specialize 前后提供模块回调。完成后继续常驻 app 进程会增加检测面、内存占用和 hook 风险。因此子进程完成后恢复 hook 并 dlclose 自己。
Zygisk 的核心是:magiskd 通过 native bridge 属性让 Zygote 加载 libzygisk.so,Zygisk 在 native bridge 初始化窗口安装 PLT hook,再在 ZygoteInit 前替换 Zygote 的 JNI specialize 方法;真正创建 app/system_server 时,它先 fork 到子进程,向 magiskd 获取模块 fd,加载模块执行 pre/post 回调,完成后恢复 hook 并从目标进程自卸载。
源码流程梳理清楚之后,就可以开始真正移植了。
指令:
AI执行过程:
文件:
改动:
意义:
文件:
改动:
意义:
文件:
改动:
改为:
意义:
文件:
改动:
新增生成结果中包含:
意义:
文件:
改动前:
改动后:
意义:
文件:
改动前:
改动后:
意义:
文件:
改动前:
改动后:
生成结果:
执行过的验证命令:
结果:
执行完整打包:
结果:
最终新产物:
项目已经开源,发布页在 GitHub Releases:
r0zygisk Releases
当前源码已经完成:
在 Android 14 的 SukiSU 环境刷入后,我又额外测试了 LSPosed 模块,确认加载正常。
到这里,移植主体已经基本完成,下一步开始补 WebUI,这部分同样结合 AI 一起推进。
指令:
AI 给出的结论如下:
实机刷入后,确认 WebUI 虽然已经生成,但界面和信息组织都比较粗糙,这也是后面重做界面的直接原因。这里当时没有保留截图。 指令:
弄清楚官方 ZygiskNext WebUI 提供哪些功能,再决定哪些功能能在当前 r0zygisk fork 中真实接入。
本地先检查项目结构,确认 WebUI 在:
再查看控制脚本:
实际内容是通过:
调用 native 里的 ctl start|stop|exit。
随后查看 loader/src/ptracer/main.cpp,确认当前后端只支持:
换句话说,当前 fork 并没有暴露官方 ZygiskNext 中那些设置项对应的配置接口。
通过 GitHub 官方仓库的 i18n 分支文案确认,ZygiskNext WebUI 功能大致包括:
但是当前 r0zygisk 后端没有这些设置项的持久化配置或控制命令。因此不能做“看起来能点,实际没有接线”的假开关。
把页面做成:
重写:
新增的页面结构包括:
初版 WebUI 通过管理器 WebUI bridge 执行 shell:
然后从 module.prop 的 description= 中解析:
执行:
构建通过,产物为:
刷入后界面看起来不错,但出现:
当时判断存在两个可能问题:
但实际刷入后模块目录可能在其他路径或更新目录中。
加入自动定位模块目录逻辑:
前端不再固定依赖 /data/adb/modules/r0zygisk,而是在刷新和点击按钮时动态查找真实模块目录。
一开始用过:
并尝试根据返回值判断 Promise、同步结果或 callback。
重新打包后,问题还没有彻底解决。
实机反馈如下:
从 index.html 删除:
同时从 app.js 删除 officialFeatures 和 renderFeatures(),从 styles.css 删除对应的 .feature-grid、.feature-item 样式。
继续查 KernelSU WebUI API 后确认:
KernelSU 的底层 bridge 不是:
而是:
关键差异是第三个参数传的是“全局回调函数名”,不是函数对象。
在 module/src/webroot/app.js 中实现:
同时保留降级:
最终执行顺序:
执行:
并检查 zip:
确认 zip 中包含:
实机反馈中,SukiSU 模块列表里出现了:
并询问这是什么意思。
这是 r0zygisk 原本把运行状态写进 module.prop 的 description= 中导致的。
在 loader/src/ptracer/monitor.cpp 原始逻辑中:
它会生成类似:
SukiSU 的模块列表直接显示 description,所以用户会看到这串运行状态。
这些字段含义:
这些状态本身说明模块运行正常,但并不适合直接显示在模块列表里。
这里的目标已经明确:
即:
修改文件:
主要变化:
修改:
刷新命令新增读取:
诊断信息增加:
前端逻辑改为:
执行:
并检查 native so:
确认包含:
并确认不再依赖 description= 状态拼接。
再次刷入后,SukiSU 模块列表只显示:
解释为:
也就是 r0zygisk 不依赖 Magisk 自带 Zygisk,而是自己实现一套 Zygisk 注入和守护进程机制,让 KernelSU / SukiSU 环境也能加载 Zygisk 模块。
这说明把运行状态迁移到 status.json 的方案已经生效。
后续实机反馈如下:
native 已经写出结构化 JSON:
但前端判断还主要依赖 raw 文本:
如果 raw 文本解析失败,或者和旧格式有细微差异,前端就会漏判 Zygote。
在 module/src/webroot/app.js 中把状态读取改成:
判断逻辑改成优先用 JSON 字段:
显示详情也改为优先显示 JSON:
执行:
并确认 zip 中包含:
输出包含:
最终 zip 时间:
截至本记录生成时,主要文件状态如下。
文件:
当前能力:
文件:
当前行为:
文件:
当前描述:
SukiSU 中显示这句话是正常现象,含义是:
补充过程中的一些截图和当前最终版本截图:
Android 15 测试:
Android 16 测试:
Android 14 SukiSU:
最后把模块加载修改为列表显示,不挤成一坨,当前最终版的 WebUI 样式:
测试后功能没有问题,但使用 Hunter 最新版 6.58 发现仍会被检测:
以上为使用 ptrace 的方式注入。
这里尝试了多种修改方式,最终仍被检测到;期间还出现过一次卡开机,随后重刷并重建环境后继续分析。
第一版基于 ptrace 的注入方案在功能上已经稳定,但用 Hunter 6.58 实测时仍会被检测,日志里出现:
这说明问题不在“是否能注入”,而在“注入行为特征是否被对方检测到”。所以第二版的核心工作不是继续堆规避技巧,而是先把 Hunter 的检测链路完整拆出来,再按检测点反推改造方向。
使用 apktool 解包 com.zhenxi.hunter.apk,全局搜索 Ptrace Check、find zygisk detected、zygote root pid 等关键字。
结果很快明确:
这一步很关键。因为如果误判成“Java 层静态字符串检测”,后续优化方向会完全跑偏。
接下来把谁在触发 checkZygisk() 这件事补齐。沿着 smali 回溯后,能定位到两条主要路径:
同时在 AndroidManifest.xml 中可以看到对应服务进程(包括 hunter_server_iso 与 twin 服务),和运行时日志中的进程标签可以对上。这意味着检测不是偶发调用,而是有明确的服务化触发链路。
因为目标 so 是 ELF aarch64 且有 strip,不能依赖常规符号名直接定位,所以采用了“字符串 + JNI 注册表 + 反汇编交叉”的方式:
到这一步,链路已经闭合:Java 触发点、JNI 绑定点、native 实现点三者一致。
checkZygisk 不是单一判断,而是“多分支命中后统一组装结果对象”的结构。结合反汇编可还原为:
也就是说,Hunter 在这一块并非只看某个固定文件是否存在,而是把“进程关系 + 特征路径 + 运行态信息”做了组合判定。
这一阶段的目标很明确:不再继续打补丁式修修补补,而是参考 ZygiskNext 1.3.4 的成熟路线,重做第二版运行链路。整体工作分成两部分:先改注入方式,再完成工程与模块命名收敛。
第一版是典型的 ptrace -> 远程 dlopen 路线:
在第七部分中确认 Hunter 检测是“多维组合判定”后,这条路线风险较高,所以第二版切换为 native bridge loader -> payload -> zygisk core -> daemon。
关键改动如下:
这次改造是架构级切换,不是启动命令替换,实际同时调整了注入入口、动态库产物、安装路径、启动时序与状态通道。
完成注入路线切换后,第二版继续做命名统一,避免构建、日志、脚本、WebUI 之间出现旧名残留。
指令:
执行过程:
执行结果:项目完成改名后刷入测试,仍被检测;原因是检测侧仍能发现 libzygisk.so。
接下来继续让 AI 进行修改:
指令:
执行结果:
指令:
执行结果:
刷入手机结果:
WebUI:
安装和打开lsposed模块也正常:
最新版 Hunter 检测结果全绿:
ruru结果:
至此,第二版大修结束,已通过新版 Hunter 的 Zygisk 检测。
这次做下来,最大的收获其实不只是把模块跑起来了,而是把一条完整的开发链路真正走通了:先确认最新版 Magisk 的 Zygisk 已经适配 Android 16,再借助 code-panorama 和 AI 快速梳理源码结构,后面一步步补齐 API、JNI 签名、打包链路、WebUI、守护进程状态和注入方式,最后再去处理真实设备上的兼容问题和检测问题。一路做下来,才把现在这个带 WebUI、功能上等效于 Zygisk 的 r0z 模块完整落地。
完整源码和发布包已经开源在 GitHub Releases:
r0zygisk Releases
这篇文章记录的是一个完整模块从源码阅读、迁移实现、联调修复,到最后对抗检测的过程。源码规模够大、调用链够长的时候,AI 能帮人省掉很多纯体力活,但真正决定结果的,还是自己去拆执行流程、判断注入链路、定位检测点、解决刷机和兼容性问题。至少对这次来说,目标已经完成了:模块做出来了,WebUI 跑起来了,Android 16 适配了,第二版也通过了新版 Hunter 的检测。
不过目前已经有朋友反馈,还是存在被检测到的情况。后续还会继续修改和完善,项目也会持续更新,新的进展我会继续补到后面的文章里。谢谢大家关注。
文件
作用
zygisk.hpp
Zygisk 内部公共声明、native bridge 常量、日志宏、NativeBridge ABI 结构
entry.cpp
Zygisk 的两个 C++ 入口:NativeBridge 注入口、zygisk companion 进程入口
hook.cpp
bootstrap 核心:PLT hook、JNI hook、真实 native bridge 续载、Zygisk 自卸载
module.hpp
模块 ABI、API 表、specialize 参数对象、ZygiskContext/ZygiskModule 类型定义
module.cpp
模块加载、API 适配、pre/post specialize 调度、FD 清理、denylist/unmount 处理
api.hpp
面向 Zygisk 模块作者的公开 API 头文件
jni_hooks.hpp
由脚本生成的 Zygote JNI native 方法包装器
gen_jni_hooks.py
生成 jni_hooks.hpp 的脚本,维护不同 Android/OEM JNI 签名
daemon.rs
magiskd 侧 Zygisk 状态、属性注入、模块 fd 下发、companion 连接管理
mod.rs
Rust 模块入口,导出 C++ 调用的 exec_companion_entry
目标库
被 hook 的符号
目的
libnativebridge.so
dlclose
捕获 native bridge 加载流程结束点,拿到 runtime callbacks,并补载真实 native bridge
libandroid_runtime.so
strdup
捕获 com.android.internal.os.ZygoteInit 字符串,触发 JNI 方法替换
libandroid_runtime.so
fork
Zygisk 提前 fork 后,让原始逻辑看到缓存 pid,避免重复 fork
libandroid_runtime.so
unshare
进入新 mount namespace 后执行 denylist unmount
libandroid_runtime.so
selinux_android_setcontext
secontext 切换前预取 logd
libandroid_runtime.so
__android_log_close
fork/specialize 过程中控制 log fd 关闭
字段
含义
plt_backup
记录已经成功注册的 PLT hook,用于后续恢复
runtime_callbacks
NativeBridgeRuntimeCallbacks,枚举 JNI native 方法时使用
self_handle
Zygisk 自身的 dlopen handle,用于最后 dlclose
should_unmap
是否允许自卸载
字段
含义
api_version
模块使用的 Zygisk API 版本
impl
模块对象指针
preAppSpecialize
app specialize 前回调
postAppSpecialize
app specialize 后回调
preServerSpecialize
system_server specialize 前回调
postServerSpecialize
system_server specialize 后回调
字段
含义
id
模块在 magiskd 模块列表中的下标,用于 companion/getModuleDir
unload
模块是否要求 post 后 dlclose
handle
dlopen handle
entry
zygisk_module_entry 函数
api
Zygisk 注入给模块的 API 表
mod
模块回填的 ABI 表
flag
含义
POST_SPECIALIZE
已进入 post 阶段
APP_FORK_AND_SPECIALIZE
当前是 fork app 流程
APP_SPECIALIZE
当前是 specialize app 流程
SERVER_FORK_AND_SPECIALIZE
当前是 system_server fork 流程
DO_REVERT_UNMOUNT
需要执行 denylist unmount
SKIP_CLOSE_LOG_PIPE
不关闭 Zygisk log pipe
字段
含义
env
当前 JNIEnv
args
app 或 server specialize 参数
process
进程名
modules
当前子进程加载的 Zygisk 模块列表
pid
Zygisk 预 fork 的结果,子进程为 0,父进程为 child pid
flags
当前流程内部状态
info_flags
magiskd 返回的进程状态
allowed_fds
fork 后允许保留的 fd 位图
exempted_fds
模块通过 API 申请豁免关闭的 fd
register_info
旧版 regex PLT hook 注册项
ignore_info
旧版 regex PLT hook 排除项
方法
调用时机
onLoad(Api*, JNIEnv*)
模块 so 被加载后立即调用
preAppSpecialize(AppSpecializeArgs*)
app specialize 前,仍接近 Zygote 权限
postAppSpecialize(const AppSpecializeArgs*)
app specialize 后,处于 app 沙箱
preServerSpecialize(ServerSpecializeArgs*)
system_server specialize 前
postServerSpecialize(const ServerSpecializeArgs*)
system_server specialize 后
字段
含义
lib_name
当前写入 native bridge 属性的值,用于恢复原属性
sockets
32 位和 64 位 zygiskd 的连接 socket
start_count
zygote crash 计数,用于回滚
函数/符号
文件
作用
NativeBridgeItf
entry.cpp
native bridge 注入入口
zygisk_main
entry.cpp
companion applet 入口
zygiskd
entry.cpp
root companion daemon 主循环
hook_entry
hook.cpp
初始化 HookContext 并安装 bootstrap PLT hook
HookContext::hook_plt
hook.cpp
hook dlclose/fork/unshare/strdup/...
HookContext::post_native_bridge_load
hook.cpp
找 callbacks 并补载真实 native bridge
HookContext::hook_zygote_jni
hook.cpp
替换 Zygote native specialize 方法
ZygiskContext::fork_pre
module.cpp
提前 fork 并记录 fd
ZygiskContext::run_modules_pre
module.cpp
dlopen 模块并调用 onLoad/pre
ZygiskContext::run_modules_post
module.cpp
调用 post 并按需卸载模块
ZygiskModule::RegisterModuleImpl
module.cpp
模块 ABI 注册和 API 表填充
ZygiskState::set_prop
daemon.rs
写入 native bridge 属性
MagiskD::get_process_info
daemon.rs
返回进程 flags 和模块 fd
ZygiskState::connect_zygiskd
daemon.rs
启动/复用 companion daemon
exec_companion_entry
mod.rs
在线程池中执行 companion handler
目录中的 Magisk-master 是最新版 Magisk,分析这个版本和之前版本的差异,是否适配了 Android 16。
• 结论:Magisk-master 这个版本已经适配 Android 16 ,并且适配点明确覆盖 Android 16 QPR2。更准确地说,它不是只把 App 的 targetSdk 改到 36 ,而是同时处理了 MagiskInit、MagiskPolicy、
Zygisk 三个对 root/注入影响最大的底层点。
本地版本判断
本地 Magisk-master/docs/changes.md:3 显示当前是 v30.7 (2026.2 .23 ),重点变更是:
- Magisk-master/docs/changes.md:5 :MagiskInit 支持 Android 16 QPR2 sepolicy format
- Magisk-master/docs/changes.md:7 :Zygisk 支持 Android 16 QPR2 and higher
- Magisk-master/docs/changes.md:29 :v30.3 已经加入 Android 16 QPR2 新 sepolicy binary format 支持
App 构建配置也对上了 Android 16 /QPR2:
- Magisk-master/app/build-logic/src/main/java/Setup.kt:46 :compileSdk 使用 release(36 )
- Magisk-master/app/build-logic/src/main/java/Setup.kt:48 :minorApiLevel = 1 ,对应 Android 16 QPR2 SDK 36.1
- Magisk-master/app/build-logic/src/main/java/Setup.kt:229 :targetSdk = 36
这些配置和 Google 官方 Android 16 /QPR2 SDK 文档一致:Android 16 是 API 36 ,QPR2 使用 release(36 ) { minorApiLevel = 1 }。参考 Android 官方 API level 表、Android 16 SDK 设置、
Android 16 QPR2 SDK 设置:
https:
https:
https:
和之前版本的主要差异
如果按 changelog 看,Android 16 相关差异主要集中在 v30.3 到 v30.7 :
- v30.3 :开始支持 Android 16 QPR2 引入的新 sepolicy binary format,并支持安装到 vendor_boot。
- v30.5 :改进命令行参数解析,resetprop 支持带 property overrides 的 Android 版本。
- v30.6 :回滚一个可能导致 bootloop 的改动。
- v30.7 :补齐 Android 16 QPR2 的关键适配,尤其是 MagiskInit 和 Zygisk。
本地还有一个 文章发布版本/Magisk-master,我对比了文件清单和关键版本信息,它同样是 v30.7 / versionCode 30700 ,不是一个明显的旧版目录。因此这里的“之前版本”更适合按 changelog 里的
v30.6 /v30.5 /v30.3 来分析,而不是把旁边那个目录当作旧版。
底层适配证据
Zygisk 适配不是简单判断 SDK_INT,而是维护多组 Zygote JNI 签名。Android 16 /QPR2 相关新增参数体现在:
- Magisk-master/native/src/core/zygisk/gen_jni_hooks.py:282 :fas_u
- Magisk-master/native/src/core/zygisk/gen_jni_hooks.py:303 :包含 mount_sysprop_overrides
- Magisk-master/native/src/core/zygisk/gen_jni_hooks.py:497 :spec_u
- Magisk-master/native/src/core/zygisk/gen_jni_hooks.py:516 :同样处理 mount_sysprop_overrides
sepolicy 编译/加载路径也按 split policy、mapping、compat CIL、vendor/product/system_ext/odm 等路径处理:
- Magisk-master/native/src/sepolicy/policydb.cpp:153 :读取并设置 policy version
- Magisk-master/native/src/sepolicy/policydb.cpp:163 :加载 platform mapping CIL
- Magisk-master/native/src/sepolicy/policydb.cpp:166 :加载 compat CIL
- Magisk-master/native/src/sepolicy/policydb.cpp:192 :继续加载 vendor 侧策略
官方上游 changelog 也和本地内容一致,v30.7 明确写了 Android 16 QPR2 sepolicy 和 Zygisk 支持:
https:
判断
可以认为这个 Magisk-master 已经适配 Android 16 ,且适配范围包含 Android 16 QPR2:
- App 层:compileSdk 36.1 、targetSdk 36
- MagiskInit:支持 Android 16 QPR2 sepolicy format
- MagiskPolicy:从 v30.3 开始支持 Android 16 QPR2 新 sepolicy binary format
- Zygisk:明确支持 Android 16 QPR2 and higher,并包含对应 JNI 签名变体
剩下的风险不在“代码是否声明适配”,而在具体机型:Android 16 /QPR2 的不同 ROM、内核、vendor_boot 布局、SELinux policy、OEM 改 Zygote 签名,都可能让某些设备还需要实机验证。代码层面
看,这版已经做了 Android 16 适配。
我现在要详细了解Magisk中zygisk的执行流程和加载过程,逐行详细解释目录中的代码,并生成到MAGISK-ZYGISK-SOURCE.md
ro.dalvik.vm.native.bridge
extern "C" NativeBridgeCallbacks NativeBridgeItf { ... };
libnativebridge.so: dlclose
libandroid_runtime.so: fork
libandroid_runtime.so: unshare
libandroid_runtime.so: selinux_android_setcontext
libandroid_runtime.so: strdup
libandroid_runtime.so: __android_log_close
fds_to_ignore
is_child_zygote
is_top_app
pkg_data_info_list
whitelisted_data_info_list
mount_data_dirs
mount_storage_dirs
uid, gid, gids, runtime_flags, permitted_capabilities, effective_capabilities
hookJniNativeMethods
pltHookRegister(regex 版本)
pltHookExclude
pltHookCommit
connectCompanion
setOption
getModuleDir
getFlags
exemptFd
api.base.impl = this
api.base.registerModule = RegisterModuleImpl
64 位写 true
32 位写 false
zygisk_module_entry
zygisk_companion_entry
ctx.nativeSpecializeAppProcess_pre()
原始 nativeSpecializeAppProcess
ctx.nativeSpecializeAppProcess_post()
magisk zygisk companion <fd>
ZygiskState
zygisk_should_load_module
magiskd set_prop(ro.dalvik.vm.native.bridge)
↓
zygote LoadNativeBridge("libzygisk.so...")
↓
NativeBridgeItf.isCompatibleWith()
↓
hook_entry()
↓
HookContext::hook_plt()
↓
dlclose hook -> post_native_bridge_load()
↓
保存 callbacks,必要时补载真实 native bridge
↓
strdup("com.android.internal.os.ZygoteInit")
↓
hook_zygote_jni()
↓
替换 Zygote native specialize 方法
Zygote.nativeForkAndSpecialize(...)
↓
Zygisk generated lambda
↓
ZygiskContext ctx
↓
ctx.nativeForkAndSpecialize_pre()
↓
old_fork() 先 fork
↓
子进程 get_module_info(uid, process)
↓
magiskd 返回 flags + module fds
↓
子进程 dlopen 模块
↓
模块 onLoad + preAppSpecialize
↓
调用原始 nativeForkAndSpecialize
↓
fork hook 返回缓存 pid,避免二次 fork
↓
原始 specialize 完成
↓
ctx.nativeForkAndSpecialize_post()
↓
模块 postAppSpecialize
↓
ctx 析构,恢复 JNI hook,准备自卸载
Zygote.nativeForkSystemServer(...)
↓
Zygisk generated lambda
↓
ctx.nativeForkSystemServer_pre()
↓
old_fork()
↓
子进程 get_module_info(1000, "system_server")
↓
加载模块并执行 preServerSpecialize
↓
把加载失败模块 id 写回 magiskd
↓
调用原始 nativeForkSystemServer
↓
ctx.nativeForkSystemServer_post()
↓
模块 postServerSpecialize
模块 Api::connectCompanion()
↓
ZygiskModule::connectCompanion()
↓
magiskd zygisk_handler ConnectCompanion
↓
connect_zygiskd()
↓
启动或复用 zygiskd32/zygiskd64
↓
zygiskd 收 module fds 并查 zygisk_companion_entry
↓
magiskd 把 client fd 发送给 zygiskd
↓
zygiskd 读 module_id
↓
exec_companion_entry(client, module_entry)
↓
Rust ThreadPool 调模块 companion_handler(client)
当前目录中有Magisk-master和r0zygisk两个项目。Magisk-master中已存在适配 Android 16的zygisk,我现在需要移植这个版本到r0zygisk,用作 KernelSU / Magisk 环境下的 Zygisk 替代实现。最终要形成打包文件:r0zygisk-v1.0.0-1-release.zip
1. 先检查 `Magisk-master` 是否体现 Android 16 支持。
- `app/build-logic/src/main/java/Setup.kt` 中 `compileSdk` / `targetSdk` 使用 API 36 。
- `scripts/avd.sh` 中 `Baklava) api=36 ;;`,说明脚本知道 Android 16 代号和 API 级别。
- `native/src/core/zygisk/gen_jni_hooks.py` 和生成的 `jni_hooks.hpp` 中有新的 Zygote JNI 签名候选,例如 `nativeForkAndSpecialize_b`。
2. 再检查 `r0zygisk` 的结构。
- `r0zygisk` 不是普通 Zygisk 模块,而是独立 Zygisk 实现,包含自己的 `loader`、`zygiskd`、ptrace 注入逻辑和模块分发逻辑。
- 因此不能直接把 Magisk 的 `hook.cpp/module.cpp` 整套替换进去,否则会破坏 r0zygisk 自己的架构。
3. 采用保守迁移策略。
- 同步公开 Zygisk API 到 Magisk 最新 API v5。
- 保留 r0zygisk 自己的 daemon / ptrace / 模块加载流程。
- 只把必要的 Zygote JNI 签名和参数结构迁移进 r0zygisk 的现有 hook 生成器。
4. 修改版本命名。
- 按要求改为固定输出 `r0zygisk-v1.0.0 -1 -release.zip`。
loader/src/include/api.hpp
#define ZYGISK_API_VERSION 5
loader/src/injector/module.hpp
jboolean *mount_sysprop_overrides = nullptr ;
using module_abi_v5 = module_abi_v1;
using api_abi_v5 = api_abi_v4;
loader/src/injector/hook.cpp
AppSpecializeArgs_v3 *app;
AppSpecializeArgs_v5 *app;
case 5 :
loader/src/injector/gen_jni_hooks.py
is_perception_app
use_fifo_ui
fas_b
fas_nubia_u
spec_xr_u
spec_nubia_u
loader/src/injector/jni_hooks.hpp
nativeForkAndSpecialize_b
nativeForkAndSpecialize_nubia_u
nativeSpecializeAppProcess_xr_u
nativeSpecializeAppProcess_nubia_u
build.gradle.kts
val verName by extra("v4-0.9.1.1" )
val verCode by extra(gitCommitCount)
val verName by extra("v1.0.0" )
val verCode by extra(1 )
module/build.gradle.kts
val zipFileName = "$moduleName -$verName -$verCode -$commitHash -$buildTypeLowered .zip" .replace(' ' , '-' )
val zipFileName = "$moduleName -$verName -$verCode -$buildTypeLowered .zip" .replace(' ' , '-' )
module/build.gradle.kts
"versionName" to "$verName ($verCode -$commitHash -$variantLowered )" ,
"versionName" to "$verName ($verCode -$variantLowered )" ,
version=v1.0.0 (1-release)
versionCode=1
./gradlew :loader:assembleRelease --no-daemon --console=plain
BUILD SUCCESSFUL
./gradlew zipRelease --no-daemon --console=plain
BUILD SUCCESSFUL
module/build/outputs/release/r0zygisk-v1.0.0-1-release.zip
build.gradle.kts
module/build.gradle.kts
loader/src/include/api.hpp
loader/src/injector/module.hpp
loader/src/injector/hook.cpp
loader/src/injector/gen_jni_hooks.py
loader/src/injector/jni_hooks.hpp
r0zygisk-v1.0 .0 -1 -release.zip 刷入并测试后,功能正常。但是当前版本中,我没有看到 WebUI,我希望增加 WebUI 的功能。
项目中已经存在一个最小 WebUI,位于:
- `module/src/webroot/index.html`
- `module/src/webroot/styles.css`
- `module/src/webroot/app.js`
初始页面很简陋,只显示:
- 当前状态原始输出
- `start`
- `stop`
- `exit `
`r0zygisk-v1.0 .0 -1 -release.zip ` 刷入并测试后,功能本身是正常的,也已经生成了 WebUI;但页面既不够直观,也缺少功能说明。因此我进一步参考了 GitHub 上 `KernelSU` 和 `ZygiskNext` 的 WebUI,想先弄清它们分别提供了什么功能,再决定当前项目里应该接哪些能力。
module/src/webroot/index.html
module/src/webroot/styles.css
module/src/webroot/app.js
module/src/zygisk-ctl.sh
exec $MODDIR /bin/zygisk-ptrace64 ctl $*
ctl start
ctl stop
ctl exit
version
cat /data/adb/modules/r0zygisk/module.prop
ps -A | grep -E "zygiskd|zygisk-ptrace|app_process"
for d in /data/adb/modules/*; do ...
[monitor:tracing, zygote64:injected, daemon64:running(...)]
node --check module/src/webroot/app.js
./gradlew :module:prepareModuleFilesRelease --no-daemon --console=plain
./gradlew :module:zipRelease --no-daemon --console=plain
module/build/outputs/release/r0zygisk-v1.0.0-1-release.zip
/data/adb/modules/r0zygisk
for base in /data/adb/modules /data/adb/modules_update /data/adb/ksu/modules /data/adb/ap/modules; do
...
if grep -q '^id=r0zygisk$' "$prop " || grep -q '^name=r0zygisk$' "$prop " ; then
MODDIR=${prop%/module.prop}
fi
done
bridge.exec (command, callback)
无法直接执行 WebUI 命令
exec 超时,管理器没有返回命令结果
<section class ="panel" >
...
<h2 > 官方 WebUI 功能地图</h2 >
...
</section >
exec (command, callbackFunction)
exec (command, JSON .stringify (options), callbackFunctionName)
const callbackName = `r0zygisk_exec_${Date .now()} _${callbackCounter++} ` ;
window [callbackName] = (errno, stdout, stderr ) => {
finish ({ errno, stdout, stderr });
};
bridge.exec (command, "{}" , callbackName);
bridge.exec (command, callbackName)
bridge.exec (command)
node --check module/src/webroot/app.js
./gradlew :module:zipRelease --no-daemon --console=plain
unzip -p module/build/outputs/release/r0zygisk-v1.0.0-1-release.zip webroot/app.js | rg "callbackName|exec\\(command"
bridge.exec (command, "{}" , callbackName)
tracing, zygote64: injected, daemon64: running(Root: KernelSU, module(3): ...)
fprintf (prop.get (), "%s[%s] %s" , pre_section.c_str (), status_text.c_str (), post_section.c_str ());
description=[monitor:tracing, zygote64:injected, daemon64:running(...)] Standalone implementation of Zygisk.
/data/adb/modules/r0zygisk/status.json
loader/src/ptracer/monitor.cpp
static std::string status_path;
status_path = "./status.json" ;
close (open (status_path.c_str (), O_WRONLY | O_CREAT | O_TRUNC, 0644 ));
updateStatus ();
{
"monitor" : "tracing" ,
"stop_reason" : "" ,
"zygote64" : "injected" ,
"daemon64" : "running" ,
"daemon64_info" : "Root: KernelSU,module(3): ..." ,
"zygote32" : "unsupported" ,
"daemon32" : "unsupported" ,
"daemon32_info" : "" ,
"raw" : "monitor:tracing, zygote64:injected, daemon64:running(...)"
}
tracing
stopped
exited
injected
not injected
running
crashed
unknown
module/src/webroot/app.js
cat "$MODDIR /status.json" 2>/dev/null || true
--- status.json ---
...
node --check module/src/webroot/app.js
./gradlew :module:zipRelease --no-daemon --console=plain
strings module/build/outputs/module/release/lib/arm64-v8a/libzygisk_ptrace.so | rg "status\\.json|description=|monitor:"
./status.json
Standalone implementation of Zygisk.
Zygisk 的独立实现。
"zygote64" : "injected"
const status = parseStatus (readStatusRaw (statusText, prop));
const zygoteOk = status.zygotes .some ((item ) => /:injected\b/ .test (item));
function readStatus (statusText, prop ) {
const text = statusText.trim ();
let json = null ;
let raw = prop.description || "" ;
if (text) {
try {
json = JSON .parse (text);
if (json && json.raw ) {
raw = String (json.raw );
}
} catch (_) {
raw = text;
}
}
const parsed = parseStatus (raw);
parsed.json = json;
return parsed;
}
const json = status.json || {};
const monitorOk =
json.monitor === "tracing" ||
status.monitor .includes ("tracing" ) ||
/zygisk-ptrace/ .test (processes);
const daemonOk =
json.daemon64 === "running" ||
json.daemon32 === "running" ||
status.daemons .some ((item ) => item.includes ("running" )) ||
/zygiskd/ .test (processes);
const zygoteOk =
json.zygote64 === "injected" ||
json.zygote32 === "injected" ||
status.zygotes .some ((item ) => /:injected\b/ .test (item));
zygote64 :injected
daemon64 :running
node --check module/src/webroot/app.js
./gradlew :module:zipRelease --no-daemon --console=plain
unzip -p module/build/outputs/release/r0zygisk-v1.0.0-1-release.zip webroot/app.js | rg "json\\.zygote64"
json.zygote64 === "injected"
2026-04-17 14:54
module/src/webroot/index.html
module/src/webroot/styles.css
module/src/webroot/app.js
loader/src/ptracer/monitor.cpp
/data/adb/modules/r0zygisk/status.json
module/src/module.prop
description=Standalone implementation of Zygisk.
Zygisk 的独立实现。
Ptrace Check Zygisk
find zygisk detected, zygote root pid ...
修改目录中 zygisk 字符串为 r0z,不要影响核心功能。
统一规则如下:
- 模块标识:`r0zygisk` -> `r0z`
- daemon:`zygiskd` -> `r0zd`
- 控制脚本:`zygisk-ctl.sh` -> `r0z-ctl.sh`
- 注入核心库发布命名:`libzygisk.so` -> `libr0zgk.so`
- 版本:`v1.0.1` ,`versionCode=2`
同步范围覆盖构建、打包、脚本、WebUI 与文档:
1. Gradle/Cargo 任务与逻辑名映射。
2. `module.prop` 、安装脚本、控制脚本文案统一 `r0z` 。
3. 打包签名与文件清单映射更新。
4. WebUI 标题与状态描述统一。
5. 发布包命名统一为 `r0z-v1.0.1-2-<buildType>.zip` 。
这一部分的目的不是“改展示文字”,而是把第二版运行链路与工程标识统一成同一套语义,保证后续排障和迭代基线稳定。
为什么生成产物中仍存在 `libzygisk.so`?它的功能是什么,是否可以改名为 `libr0zgk`?
改名并保持可运行
文件
作用
zygisk.hpp
Zygisk 内部公共声明、native bridge 常量、日志宏、NativeBridge ABI 结构
entry.cpp
Zygisk 的两个 C++ 入口:NativeBridge 注入口、zygisk companion 进程入口
hook.cpp
bootstrap 核心:PLT hook、JNI hook、真实 native bridge 续载、Zygisk 自卸载
module.hpp
模块 ABI、API 表、specialize 参数对象、ZygiskContext/ZygiskModule 类型定义
module.cpp
模块加载、API 适配、pre/post specialize 调度、FD 清理、denylist/unmount 处理
api.hpp
面向 Zygisk 模块作者的公开 API 头文件
jni_hooks.hpp
由脚本生成的 Zygote JNI native 方法包装器
gen_jni_hooks.py
生成 jni_hooks.hpp 的脚本,维护不同 Android/OEM JNI 签名
daemon.rs
magiskd 侧 Zygisk 状态、属性注入、模块 fd 下发、companion 连接管理
mod.rs
Rust 模块入口,导出 C++ 调用的 exec_companion_entry
目标库
被 hook 的符号
目的
libnativebridge.so
dlclose
捕获 native bridge 加载流程结束点,拿到 runtime callbacks,并补载真实 native bridge
libandroid_runtime.so
strdup
捕获 com.android.internal.os.ZygoteInit 字符串,触发 JNI 方法替换
libandroid_runtime.so
fork
Zygisk 提前 fork 后,让原始逻辑看到缓存 pid,避免重复 fork
libandroid_runtime.so
unshare
进入新 mount namespace 后执行 denylist unmount
libandroid_runtime.so
selinux_android_setcontext
secontext 切换前预取 logd
libandroid_runtime.so
__android_log_close
fork/specialize 过程中控制 log fd 关闭
字段
含义
plt_backup
记录已经成功注册的 PLT hook,用于后续恢复
runtime_callbacks
NativeBridgeRuntimeCallbacks,枚举 JNI native 方法时使用
self_handle
Zygisk 自身的 dlopen handle,用于最后 dlclose
should_unmap
是否允许自卸载
字段
含义
api_version
模块使用的 Zygisk API 版本
impl
模块对象指针
preAppSpecialize
app specialize 前回调
postAppSpecialize
app specialize 后回调
preServerSpecialize
system_server specialize 前回调
postServerSpecialize
system_server specialize 后回调
字段
含义
id
模块在 magiskd 模块列表中的下标,用于 companion/getModuleDir
unload
模块是否要求 post 后 dlclose
handle
dlopen handle
entry
zygisk_module_entry 函数
api
Zygisk 注入给模块的 API 表
mod
模块回填的 ABI 表
flag
含义
POST_SPECIALIZE
已进入 post 阶段
APP_FORK_AND_SPECIALIZE
当前是 fork app 流程
APP_SPECIALIZE
当前是 specialize app 流程
SERVER_FORK_AND_SPECIALIZE
当前是 system_server fork 流程
DO_REVERT_UNMOUNT
需要执行 denylist unmount
SKIP_CLOSE_LOG_PIPE
不关闭 Zygisk log pipe
字段
含义
env
当前 JNIEnv
args
app 或 server specialize 参数
process
进程名
modules
当前子进程加载的 Zygisk 模块列表
pid
Zygisk 预 fork 的结果,子进程为 0,父进程为 child pid
flags
当前流程内部状态
info_flags
magiskd 返回的进程状态
allowed_fds
fork 后允许保留的 fd 位图
exempted_fds
模块通过 API 申请豁免关闭的 fd
register_info
旧版 regex PLT hook 注册项
ignore_info
旧版 regex PLT hook 排除项
方法
调用时机
onLoad(Api*, JNIEnv*)
模块 so 被加载后立即调用
preAppSpecialize(AppSpecializeArgs*)
app specialize 前,仍接近 Zygote 权限
postAppSpecialize(const AppSpecializeArgs*)
app specialize 后,处于 app 沙箱
preServerSpecialize(ServerSpecializeArgs*)
system_server specialize 前
postServerSpecialize(const ServerSpecializeArgs*)
system_server specialize 后
字段
含义
lib_name
当前写入 native bridge 属性的值,用于恢复原属性
sockets
32 位和 64 位 zygiskd 的连接 socket
start_count
zygote crash 计数,用于回滚
函数/符号
文件
作用
NativeBridgeItf
entry.cpp
native bridge 注入入口
zygisk_main
entry.cpp
companion applet 入口
zygiskd
entry.cpp
root companion daemon 主循环
hook_entry
hook.cpp
初始化 HookContext 并安装 bootstrap PLT hook
HookContext::hook_plt
hook.cpp
hook dlclose/fork/unshare/strdup/...
HookContext::post_native_bridge_load
hook.cpp
找 callbacks 并补载真实 native bridge
HookContext::hook_zygote_jni
hook.cpp
替换 Zygote native specialize 方法
ZygiskContext::fork_pre
module.cpp
提前 fork 并记录 fd
ZygiskContext::run_modules_pre
module.cpp
dlopen 模块并调用 onLoad/pre
ZygiskContext::run_modules_post
module.cpp
调用 post 并按需卸载模块
ZygiskModule::RegisterModuleImpl
module.cpp
模块 ABI 注册和 API 表填充
ZygiskState::set_prop
daemon.rs
写入 native bridge 属性
MagiskD::get_process_info
daemon.rs
返回进程 flags 和模块 fd
ZygiskState::connect_zygiskd
daemon.rs
启动/复用 companion daemon
exec_companion_entry
mod.rs
在线程池中执行 companion handler
Android Gradle Plugin:8.9.1
Gradle Wrapper:8.11.1
org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home
nativeForkAndSpecialize
nativeSpecializeAppProcess
nativeForkSystemServer
对 fork 型方法,Zygisk 先调用真实 fork()。
子进程向 magiskd 查询当前 uid/process 的 flags。
如果进程不在 enforced denylist 且不是 Magisk app,则接收模块 so 的 fd。
使用 android_dlopen_ext(..., ANDROID_DLEXT_USE_LIBRARY_FD, ...) 从 fd 加载模块。
调用模块 zygisk_module_entry、onLoad、preAppSpecialize 或 preServerSpecialize。
调用模块 postAppSpecialize 或 postServerSpecialize。
根据模块选项可能 dlclose 模块。
恢复 Zygote JNI hook。
安装自卸载 hook。
arm64:查 r19-r28。
arm32:查 r4-r10。
x86:从 ebp 相对位置读第二参数。
x86_64:查 rbx/r12-r15 等 callee-saved 寄存器。
riscv:查 callee-saved x8/x9/x18-x27。
先 dlsym(RTLD_DEFAULT, ...)。
找不到就扫描并 dlopen libnativehelper.so。
遇到 nativeForkAndSpecialize,用 fork_app_methods 替换。
遇到 nativeSpecializeAppProcess,用 specialize_app_methods 替换。
遇到 nativeForkSystemServer,用 fork_server_methods 替换。
使用 Magisk-master/native/src/core/zygisk/api.hpp 覆盖 r0zygisk 旧版公开 API 头文件。
ZYGISK_API_VERSION 从旧版本升级为:
新编译的 Zygisk 模块会按 API v5 注册。
r0zygisk loader 需要能识别并加载 API v5 模块。
增加 AppSpecializeArgs_v5。
AppSpecializeArgs_v5 继承 AppSpecializeArgs_v3,并单独持有:
call_app 增加 case 5,让 API v5 模块按新版参数传入。
兼容 Magisk 最新 Zygisk API v5。
保留对旧 API v1-v4 模块的兼容。
ZygiskContext 中 app 参数指针从:
ZygiskModule::valid() 接受 API v5:
r0zygisk 的模块加载逻辑可以接受 v5 模块。
Zygote JNI wrapper 传入的是新版 specialize 参数对象。
wrapper 初始化参数从 AppSpecializeArgs_v3 改为 AppSpecializeArgs_v5。
增加 Magisk 最新 Zygisk 中已有的新参数:
nativeForkAndSpecialize_b 是 Android 16 / Baklava 相关适配的重要线索。
新增的 OEM / XR 签名提高了对新系统和厂商 ROM 的兼容范围。
项目从自己的第一版开始。
versionCode 固定为 1。
不再因为目录不是 git 仓库而输出 unknown。
最终文件名符合要求。
Zygisk API v5 迁移。
Android 16 / Baklava 相关 JNI 签名候选加入。
r0zygisk 自有 loader / zygiskd / ptrace 架构保留。
输出文件名改为 r0zygisk-v1.0.0-1-release.zip。
完整打包通过。
状态页
基本信息
设置页
Root 实现显示
Zygote monitor 状态
Zygisk module 状态
ZN Module 状态
KernelSU / Magisk / APatch 排除列表策略说明
日志写入 dmesg
非 root 应用作为排除列表
强制排除列表
匿名内存加载
Zygisk Next linker
Zygote 注入状态说明
模块问题提示
状态仪表盘
追踪器控制按钮
已发现 Zygisk 模块列表
每个按钮的说明
原始诊断输出
ZygiskNext 参考功能地图,并标注当前 fork 是否接入
module/src/webroot/index.html
module/src/webroot/styles.css
module/src/webroot/app.js
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2026-5-12 22:01
被fyrlove编辑
,原因: 补漏
上传的附件: