ReZygisk 守护进程 zygiskd 与 Root 适配
本文是 ReZygisk 源码解读系列第 1 篇。全系列:
00总览与整体架构01守护进程 zygiskd 与 Root 适配(本篇)02注入机制:monitor 监控与 ptracer 注入03注入体 injector 与反检测基石04外围:IPC 协议、模块脚本、WebUI 与构建代码引用格式为
文件:行号,基于撰写时的仓库状态(main分支)。
一、本篇定位
zygiskd(ReZygiskd)是 ReZygisk 的用户态大脑。如果说 monitor/ptracer(见 02 篇)负责"把代码塞进 zygote",那么 zygiskd 负责回答塞进去之后的一切问题:
- 系统当前装的是哪种 root(Magisk / KernelSU / APatch)?
- 有哪些 Zygisk 模块、它们的
.so在哪、哪些被禁用了? - 某个 APP 进程(按 UID / 进程名)是否被授予 root、是否在 DenyList、是不是管理器?
- 模块的 companion(伴生守护进程)如何按需拉起、如何把请求转发给它?
- 对需要"隐藏 root"的进程,如何给它一个干净的 mount namespace(卸载掉所有 root 挂载痕迹)?
它以 32 位 / 64 位两个独立二进制(zygiskd32 / zygiskd64)常驻运行,各自监听一个 Unix socket,服务对应位宽的被注入进程。
本篇覆盖两大块代码:
| 目录 | 内容 |
|---|---|
zygiskd/src/(main.c / zygiskd.c / companion.c / utils.c / constants.h) |
守护进程主体 |
zygiskd/src/root_impl/(common.* / kernelsu.* / magisk.* / apatch.*) |
Root 方案适配层 |
二、zygiskd 在系统中的位置
先建立全局观。ReZygisk 的运行时拓扑如下:
几条关键关系:
- monitor → zygiskd:monitor 在捕获到 zygote 后,通过
ensure_daemon_created()用fork+execl拉起zygiskd32/64(见 02 篇)。zygiskd 反过来通过init_monitor这个 datagram socket 把"root 实现 / 模块清单 / 错误信息"上报给 monitor,用于刷新模块描述与state.json。 - 注入体 → zygiskd:被注入进 zygote 的
libzygisk.so作为客户端,通过cp32/64.sock发起DaemonSocketAction请求(协议客户端在loader/src/common/daemon.c,服务端在本篇的zygiskd.c)。 - zygiskd → 模块 companion:每个模块可有一个 companion 进程,由 zygiskd 用双重 fork 拉起,APP 侧通过 zygiskd 中转拿到与 companion 直连的 socket。
三、进程模型与启动入口(main.c)
zygiskd 是一个多功能二进制,靠子命令区分角色(main.c:10-63):
int main(int argc, char *argv[]) {
LOGI("Welcome to ReZygiskd%s", LP_SELECT("32", "64"));
if (argc > 1) {
if (strcmp(argv[1], "companion") == 0) { /* companion <fd> */ ... companion_entry(fd); }
else if (strcmp(argv[1], "version") == 0) { /* 打印版本 */ }
else if (strcmp(argv[1], "root") == 0) { /* 探测并打印 root 实现 */ }
else { /* usage */ }
...
}
/* 无参数 = 守护进程主模式 */
if (switch_mount_namespace(1) == false) { ... return 1; }
root_impls_setup();
zygiskd_start(argv);
return 0;
}
四种角色:
| 调用 | 作用 |
|---|---|
zygiskd(无参) |
守护进程主模式:切到 init 的 mount namespace → 探测 root → 进入主循环 |
zygiskd companion <fd> |
作为某模块的 companion 子进程运行(见第六节) |
zygiskd version |
打印 ZKSU_VERSION |
zygiskd root |
一次性探测并打印当前 root 实现(调试用) |
为什么主模式第一步是 switch_mount_namespace(1)?
main.c:54 在进入主循环前,先把自己切进 pid 1(init)的 mount namespace:
// utils.c:26-48
bool switch_mount_namespace(pid_t pid) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "/proc/%d/ns/mnt", pid);
int nsfd = open(path, O_RDONLY | O_CLOEXEC);
...
if (setns(nsfd, CLONE_NEWNS) == -1) { ... return false; }
close(nsfd);
return true;
}
root 方案(尤其 Magisk)会给自己和模块创建独立的 mount namespace。zygiskd 要做的事——枚举挂载、为目标进程卸载 root 挂载——必须站在 init 这个"全局视角"的 namespace 里,才能看到完整、未被局部 namespace 遮蔽的挂载树。这是后面 umount_root / save_mns_fd 能正确工作的前提。
四、模块的发现与加载
主循环开始前,zygiskd_start 会先 load_modules 扫描模块目录(zygiskd.c:50-123)。
模块数据结构
// zygiskd.c:18-27
struct Module {
char *name; // 模块目录名(如 "zygisk-lsposed")
int lib_fd; // 已打开的 zygisk/<arch>.so 文件描述符
int companion; // 与该模块 companion 的连接 fd(-1 表示未启动)
};
struct Context {
struct Module *modules;
size_t len;
};
扫描规则
// zygiskd.c:64-72(节选)
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type != DT_DIR) continue;
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0
|| strcmp(entry->d_name, "rezygisk") == 0) continue;
char so_path[PATH_MAX];
snprintf(so_path, PATH_MAX, "/data/adb/modules/%s/zygisk/" ARCH_STR ".so", name);
if (access(so_path, R_OK) == -1) continue; // 没有本架构的 .so → 跳过
...
snprintf(disabled, PATH_MAX, "/data/adb/modules/%s/disable", name);
if (access(disabled, F_OK) == 0) continue; // 被禁用 → 跳过
...
}
要点:
- 遍历
/data/adb/modules/下所有目录; - 必须存在
zygisk/<ARCH>.so(ARCH_STR由编译期架构宏决定,zygiskd.c:36-47:arm64-v8a/armeabi-v7a/x86_64/x86);这天然实现了"32 位 daemon 只加载 32 位模块库"; - 跳过自身
rezygisk目录与带disable标记的模块; - 对每个有效模块
open其.so拿到lib_fd(O_RDONLY | O_CLOEXEC),companion 初始化为-1。
注意 zygiskd 此时并不 dlopen 模块——它只持有文件描述符。真正加载模块库的是被注入进 zygote 的 libzygisk.so(通过 ReadModules 拿到路径后用自定义 loader 加载,见 03 篇);companion 则由 zygiskd 在需要时通过 lib_fd 加载。
五、守护进程协议:DaemonSocketAction
这是 zygiskd 对外服务的核心。协议特征:Unix domain socket(SOCK_STREAM)、裸二进制、无包头、一次请求一次短连接;整型按主机字节序直接读写,字符串为"size_t 长度 + 原始字节(不含 \0)"。
socket 的建立与 SELinux 上下文
// zygiskd.c:135-139
static int create_daemon_socket(void) {
set_socket_create_context("u:r:zygote:s0");
return unix_listener_from_path(PATH_CP_NAME);
}
PATH_CP_NAME=/data/adb/rezygisk/cp32.sock或cp64.sock(zygiskd.c:32)。set_socket_create_context("u:r:zygote:s0")(utils.c:56-95):把后续创建的 socket 的 SELinux 上下文设为zygote,这样 zygote 进程才有权连接它。unix_listener_from_path(utils.c:174-213)创建监听 socket 后,还会chcon(path, "u:object_r:zygisk_file:s0")给 socket 文件打标签。
主循环骨架
// zygiskd.c:322-652(高度节选)
while (1) {
int client_fd = accept(socket_fd, NULL, NULL);
uint8_t action8 = 0;
read_uint8_t(client_fd, &action8);
enum DaemonSocketAction action = (enum DaemonSocketAction)action8;
switch (action) {
case ZygoteInjected: ... break;
case GetProcessFlags: ... break;
case GetInfo: ... break;
case ReadModules: ... break;
case RequestCompanionSocket:... break;
case GetModuleDir: ... break;
case ZygoteRestart: ... break;
case UpdateMountNamespace: ... break;
case RemoveModule: ... break;
}
close(client_fd);
}
协议全表
动作枚举定义在 constants.h:13-23,9 个动作(值 0..8)。下表把**服务端处理(zygiskd.c)与客户端封装(daemon.c)**对照列出:
| 动作 (值) | 客户端发送 | 服务端响应 | 语义 |
|---|---|---|---|
ZygoteInjected (0) |
u8 动作 |
无 | 注入体上报"已注入",zygiskd 转发 ZYGOTE_INJECTED 给 monitor(zygiskd.c:349-352) |
GetProcessFlags (1) |
u8+u32 uid+string 进程名 |
u32 flags |
计算进程标志位(root/denylist/manager/首进程/root 类型),见第九节 |
GetInfo (2) |
u8 |
u32 flags + u32 pid + size_t 模块数 + N×string |
返回 daemon pid、root 类型、模块名清单(供 info 命令/WebUI) |
ReadModules (3) |
u8 |
size_t 数量 + N×string 库路径 |
返回各模块 .so 绝对路径,供注入体加载(zygiskd.c:467-484) |
RequestCompanionSocket (4) |
u8+size_t index |
u8(1/0)(成功则后续直接复用此连接) |
按需拉起 companion,把客户端 fd 转交给它,见第六节 |
GetModuleDir (5) |
u8+size_t index |
经 SCM_RIGHTS 传一个目录 fd |
返回模块目录的 fd(zygiskd.c:548-581) |
ZygoteRestart (6) |
u8 |
无 | zygote 重启:关闭全部 companion fd(zygiskd.c:354-363) |
UpdateMountNamespace (7) |
u8+u32 pid+u8 state |
u32 our_pid + u32 ns_fd |
返回一个 Clean/Mounted 的 mount namespace fd,见第七节 |
RemoveModule (8) |
u8+size_t index |
u8(1/0) |
从 Context 移除某模块(加载失败时由注入体调用,保持两端一致) |
客户端侧(
daemon.c)每个动作都是rezygiskd_connect(1)→ 写动作字节 → 写参数 → 读响应 →close。RequestCompanionSocket是唯一例外:成功后不关闭 fd,而是把它升级为与 companion 通信的长连接。
一次 GetProcessFlags 的完整时序
六、Companion 进程模型
Zygisk 的 companion 机制:模块可以提供一个运行在有 root 权限的独立进程里的伴生服务(zygote 内的模块代码受 SELinux/降权限制,很多事做不了,就委托 companion)。zygiskd 负责这些 companion 的生命周期。
6.1 拉起 companion:双重 fork + exec
spawn_companion(zygiskd.c:141-258)的设计很讲究:
- 主进程先
socketpair得到daemon_fd/companion_fd,再fork。 - 子进程去掉
companion_fd的FD_CLOEXEC,把进程名设成<nice_name>-<模块名>,然后non_blocking_execv(ZYGISKD_PATH, {process_name, "companion", fd_str})——即重新 exec 自己、走companion子命令。non_blocking_execv内部又 fork 一次(utils.c:421-453),所以真正的 companion 是"孙进程",被 init 收养,与 zygiskd 主进程脱钩。 - 子进程随即
exit(0),主进程waitpid到它,确认 companion 已成功 exec。 - 之后主进程通过
daemon_fd把模块名和模块.so的 fd(write_fdviaSCM_RIGHTS)发给 companion,并读取一个字节:1= 模块有 companion entry,0= 没有(此时返回-2,主循环据此记录"该模块无 companion")。
6.2 companion 进程本体
companion_entry(companion.c:84-182):
void companion_entry(int fd) {
char name[256 + 1];
read_string(fd, name, sizeof(name)); // 模块名
int library_fd = read_fd(fd); // 模块 .so 的 fd
zygisk_companion_entry module_entry = load_module(library_fd); // dlopen + dlsym
close(library_fd);
if (module_entry == NULL) { write_uint8_t(fd, 0); goto cleanup; } // 无 entry
else write_uint8_t(fd, 1);
signal(SIGPIPE, SIG_IGN);
while (1) {
if (!check_unix_socket(fd, true)) break; // 阻塞等待 zygiskd 转发请求
int client_fd = read_fd(fd); // 收到一个 APP 客户端 fd
...
pthread_create(&thread, NULL, entry_thread, args); // 每个请求一个线程
pthread_detach(thread);
}
cleanup: close(fd); exit(0);
}
load_module(companion.c:26-47):dlopen("/proc/self/fd/<library_fd>")+dlsym("zygisk_companion_entry")。用/proc/self/fd/路径 dlopen 是经典技巧——无需把.so落地到可访问路径。- 之后进入循环:每收到一个被转交的 APP 客户端 fd,就起一个分离线程执行
entry_thread→ 调用模块的module_entry(client_fd)。 entry_thread(companion.c:50-81)有个细节:用fstat比对调用前后 fd 的 inode,只有当 fd 仍是原来那个文件时才close,避免模块自己已经关闭/复用了 fd 导致的双重关闭。
6.3 RequestCompanionSocket:把请求"接力"给 companion
主循环里 RequestCompanionSocket 的处理(zygiskd.c:485-547):
struct Module *module = &context.modules[index];
if (module->companion >= 0) {
if (!check_unix_socket(module->companion, false)) { // companion 崩了?
close(module->companion); module->companion = -1;
}
}
if (module->companion <= -1) {
module->companion = spawn_companion(argv, module->name, module->lib_fd); // 懒启动
}
if (module->companion >= 0) {
write_fd(module->companion, client_fd); // 把 APP 的连接 fd 转交给 companion
}
即:companion 是懒启动且带健康检查的——第一次请求时才 fork,崩溃后下次请求自动重启。拿到(或新建)companion 连接后,用 write_fd 把当前 APP 的 client_fd 经 SCM_RIGHTS 直接送进 companion,此后 APP 与 companion 就在这个 fd 上直连对话,zygiskd 不再介入数据面。
七、Mount Namespace 管理与 Root 隐藏
这是 zygiskd 最精巧的部分之一,对应 UpdateMountNamespace 动作(zygiskd.c:582-612)与 utils.c 的 save_mns_fd / umount_root。
7.1 为什么需要它
很多 APP(银行、游戏)会检测设备是否 root。隐藏手段之一:让这些 APP 运行在一个看不到任何 root 挂载的 mount namespace 里——/data/adb/modules 的 overlay、magisk 的 worker 挂载等统统不可见。
ReZygisk 维护两种 namespace(constants.h:42-45):
Clean:卸载了所有 root 挂载的"干净"命名空间,给需要隐藏 root 的进程(DenyList 命中)。Mounted:保留挂载的正常命名空间,给允许看到模块的进程。
7.2 save_mns_fd:fork 一个进程去"制备"命名空间
// utils.c:732-861(结构节选)
int save_mns_fd(int pid, enum MountNamespaceState mns_state, struct root_impl impl) {
static int clean_namespace_fd = -1; // 缓存:只制备一次
static int mounted_namespace_fd = -1;
if (mns_state == Clean && clean_namespace_fd != -1) return clean_namespace_fd;
if (mns_state == Mounted && mounted_namespace_fd != -1) return mounted_namespace_fd;
socketpair(...);
pid_t fork_pid = fork();
if (fork_pid == 0) {
switch_mount_namespace(pid); // 进目标进程的 ns
if (mns_state == Clean) {
unshare(CLONE_NEWNS); // 私有化挂载
umount_root(impl); // 卸载所有 root 挂载
}
/* 通过 socket 与父进程握手,保持存活直到父进程 open 了 /proc/<pid>/ns/mnt */
}
...
snprintf(ns_path, PATH_MAX, "/proc/%d/ns/mnt", fork_pid);
int ns_fd = open(ns_path, O_RDONLY); // 抓取这个 ns 的 fd
...
if (mns_state == Clean) clean_namespace_fd = ns_fd; // 缓存
else mounted_namespace_fd = ns_fd;
return ns_fd;
}
思路:fork 一个临时子进程,让它进入目标进程的 namespace;如果要 Clean,就 unshare 出私有挂载视图再 umount_root。父进程随后 open 这个子进程的 /proc/<pid>/ns/mnt 拿到 namespace 的 fd 并缓存(static,全局只制备一次)。这个 fd 最终经 UpdateMountNamespace 回传给注入体,注入体再 setns 进去。
注意 UpdateMountNamespace 处理时(zygiskd.c:595-596):当请求 Clean 时,会先顺手制备好 Mounted,再制备 Clean——保证两种都就绪。
7.3 umount_root:精确卸载 root 痕迹
umount_root(utils.c:667-730)先用 parse_mountinfo("self") 解析 /proc/self/mountinfo,再筛选要卸载的挂载点:
// utils.c:690-693(判定逻辑)
if (strcmp(mount.source, source_name) == 0
|| (impl.impl == Magisk && strcmp(mount.source, "worker") == 0)) should_unmount = true;
if (strncmp(mount.target, "/data/adb/modules", strlen("/data/adb/modules")) == 0) should_unmount = true;
if (strncmp(mount.root, "/adb/modules/", strlen("/adb/modules/")) == 0) should_unmount = true;
source_name按 root 实现取值:Magisk→magisk、KernelSU→KSU、APatch→APatch(utils.c:678-680);- 命中后逆序
umount2(target, MNT_DETACH)(utils.c:714-723,逆序是为先卸载子挂载再卸载父挂载)。
parse_mountinfo(utils.c:531-665)是一个相当完整的 /proc/<pid>/mountinfo 解析器,用 sscanf 的 %n 技巧定位各字段(id/parent/major:minor/root/target/options/type/source 等),并处理 shared:/master:/propagate_from: 可选字段。
八、Root 适配层 root_impl
root_impl/ 把"Magisk / KernelSU / APatch 三者各自不同的查询方式"统一到一组接口之后,zygiskd.c 只需面向抽象编程。
8.1 数据结构与分发
// root_impl/common.h:10-32
enum root_impls { None, Multiple, KernelSU, APatch, Magisk };
struct root_impl_state { // 各实现"自检"时填写
enum RootImplState state; // Supported / TooOld / Inexistent / Abnormal
uint8_t variant;
};
struct root_impl { // 对外汇总结果
enum root_impls impl;
uint8_t variant;
};
void get_impl(struct root_impl *uimpl);
bool uid_granted_root(uid_t uid);
bool uid_should_umount(uid_t uid, const char *const process);
bool uid_is_manager(uid_t uid);
注意区分两个结构体:
root_impl_state是每个实现做存在性/版本检测时填的"局部结果";root_impl是root_impls_setup汇总后保存的"全局结论"。(这一点正是本项目通读时最容易看错的地方——务必以common.h真实定义为准。)
root_impls_setup(common.c:12-65)的决策逻辑:
zygiskd_start 开头(zygiskd.c:267-283)若发现 None 或 Multiple,会向 monitor 上报 DAEMON_SET_ERROR_INFO 并直接 exit——这就是模块描述里看到"Unsupported environment"的来源。
三个查询接口(uid_granted_root / uid_should_umount / uid_is_manager)都是简单的 switch(impl.impl) 分发到对应实现(common.c:71-120)。root_impl_cleanup 只在 KernelSU 下需要(关闭 ioctl fd,common.c:122-124)。
8.2 KernelSU:ioctl 新接口 + prctl 旧接口双通道
kernelsu.c 是三者中最复杂的,因为 KernelSU 同时存在新旧两套内核交互接口。ksu_get_existence(kernelsu.c:73-164)的探测顺序:
几个关键设计点:
- Waydroid 特判(
kernelsu.c:77-82):在 Waydroid 上SYS_reboot会触发SIGSYS把 zygiskd 打崩,因此读ro.board.platform判断后直接跳到 prctl 老接口。 - 新接口探测(
kernelsu.c:84):syscall(SYS_reboot, KSU_INSTALL_MAGIC1=0xDEADBEEF, KSU_INSTALL_MAGIC2=0xCAFEBABE, 0, &ksu_fd),成功会回填一个 ioctl 用的 fd。 - 关闭内核 umount(
kernelsu.c:145-155):通过KSU_IOCTL_SET_FEATURE把kernel_umount关掉——因为 ReZygisk 要自己用umount_root精确控制卸载,不让内核插手。 - ksud 存在性校验(
kernelsu.c:100-106/135-141):有些定制 ROM 预装了 KernelSU 但用户实际用 Magisk,故要求/data/adb/ksu/bin/ksud存在才算 Supported。 - 变体判定:通过 hook mode(KernelSU Next 有
CMD_HOOK_MODE/KSU_IOCTL_GET_HOOK_MODE)区分KNext与KOfficial。 - manager 判定(
kernelsu.c:216-256):优先用CMD_GET_MANAGER_UID/ioctl 拿管理器 UID(对仿冒包名更可靠);回退到stat已知包名目录。还处理了 Private Space(UID10xxxxx用% 100000归一化)。
UID 查询同样区分新旧接口(kernelsu.c:166-214):新接口走 ioctl(ksu_fd, KSU_IOCTL_UID_GRANTED_ROOT/...),旧接口走 prctl(0xDEADBEEF, CMD_UID_GRANTED_ROOT/...),并用一个数组+回写标志位的约定读取结果。
8.3 Magisk:靠 magisk 二进制 + sqlite
magisk.c 通过执行 magisk 命令行来查询(exec_command 抓取 stdout):
- 存在性(
magisk.c:22-57):在/sbin/magisk[32/64]、/debug_ramdisk/magisk[32/64]等候选路径里找到 magisk 二进制,执行magisk -V取版本,与MIN_MAGISK_VERSION(magisk.h:9,26000)比较。 - granted root(
magisk.c:59-73):magisk --sqlite "select 1 from policies where uid=<uid> and policy=2 limit 1",有结果即已授权。 - should umount(
magisk.c:75-91):magisk --sqlite "SELECT 1 FROM denylist WHERE '<process>' LIKE process || '%' LIMIT 1"——注意是按进程名前缀匹配 DenyList。 - manager(
magisk.c:93-117):查strings表的requester拿到管理器包名,再stat其数据目录比对 UID。
这意味着 Magisk 模式下,每次进程查询都会 fork+exec 一个 magisk 子进程跑 sqlite——这是 Magisk 没有提供内核态查询接口的现实妥协。
8.4 APatch:解析 package_config
apatch.c:
- 存在性(
apatch.c:14-55):要求/data/adb/ap/bin/apd存在、PATH含/data/adb/ap/bin,执行apd -V取版本,与MIN_APATCH_VERSION(10657)比较。 - 配置来源:APatch 把每个包的策略写在 CSV 文件
/data/adb/ap/package_config(列:process, exclude(umount), allow(root), uid)。_apatch_get_package_config(apatch.c:78-143)解析它。 - granted root / should umount(
apatch.c:145-206):按 UID 匹配配置行读取allow/exclude。对隔离服务(IS_ISOLATED_SERVICE(uid),即90000 ≤ uid < 1000000)做了额外的进程名前缀匹配兜底,防止隔离进程绕过 DenyList。 - manager(
apatch.c:208-219):stat /data/user_de/0/me.bmax.apatch比对 UID。
8.5 三方案对比
| 维度 | KernelSU | Magisk | APatch |
|---|---|---|---|
| 查询机制 | 内核 ioctl / prctl(0xDEADBEEF) |
执行 magisk --sqlite |
解析 package_config CSV |
| 最低版本 | MIN_KSU_VERSION |
26000 | 10657 |
| 变体 | KOfficial / KNext | — | — |
| 授权查询 | ioctl/prctl 返回数组 | policies 表 |
CSV allow 列 |
| DenyList | ioctl/prctl | denylist 表(进程名前缀) |
CSV exclude 列 + 隔离服务兜底 |
| manager | ioctl GET_MANAGER_UID / stat 包名 | strings.requester + stat |
stat me.bmax.apatch |
| 需要 cleanup | 是(关 ioctl fd) | 否 | 否 |
| 特殊处理 | Waydroid SIGSYS、ksud 校验、Private Space | 多路径探测 | 隔离服务前缀匹配 |
九、ProcessFlags 与 UID 判定全流程
把第五节的 GetProcessFlags 与第八节的 root_impl 串起来,就是 ReZygisk 对"一个即将特化的 APP 进程该怎么对待"的完整决策。
进程标志位定义(constants.h:25-33):
enum ProcessFlags: uint32_t {
PROCESS_GRANTED_ROOT = (1u << 0),
PROCESS_ON_DENYLIST = (1u << 1),
PROCESS_IS_MANAGER = (1u << 27),
PROCESS_ROOT_IS_APATCH = (1u << 28),
PROCESS_ROOT_IS_KSU = (1u << 29),
PROCESS_ROOT_IS_MAGISK = (1u << 30),
PROCESS_IS_FIRST_STARTED = (1u << 31)
};
GetProcessFlags 的处理(zygiskd.c:364-419):
要点:
- 首进程标志:zygiskd 用一个
bool first_process(zygiskd.c:321)标记整个生命周期里第一个被查询的进程,置PROCESS_IS_FIRST_STARTED。 - manager 与普通进程互斥:是管理器就只标
PROCESS_IS_MANAGER,不再判 root/denylist(zygiskd.c:385-394)。 - root 类型位始终根据当前 impl 追加,注入体据此知道宿主是哪种 root。
注入体拿到这些 flags 后,决定是否给该进程切到 Clean namespace(DenyList 命中)、是否暴露模块等——这部分逻辑在 03 篇展开。
十、小结
zygiskd 是 ReZygisk 的服务中枢,本篇可以浓缩为五句话:
- 多角色单二进制:靠子命令区分守护进程 / companion / 调试,32/64 位各跑一份。
- 模块即文件描述符:扫描
/data/adb/modules,只持有<arch>.so的 fd,不在 daemon 内 dlopen 模块。 - 9 个动作的二进制 RPC:
DaemonSocketAction是注入体与 daemon 的全部对话;SCM_RIGHTS传 fd 是其中 companion / 模块目录 / namespace 交接的关键。 - companion 懒启动 + fd 接力:双重 fork 把 companion 脱钩为 init 的孩子,按需拉起、崩溃自愈,APP 与 companion 最终直连。
- root 抽象 + namespace 隐藏:
root_impl抹平三种 root 的差异;save_mns_fd/umount_root制备并缓存 Clean/Mounted 两种挂载命名空间,实现按进程隐藏 root。
下一篇(02)将进入"代码是如何被塞进 zygote 的"——monitor 如何 ptrace 追踪 init、捕获 zygote,以及 ptracer 如何用自定义链接器 CSOLoader 完成远程注入。
评论
- 还没有评论,来说点什么吧。