KernelSU 非 GKI 内核集成实战指南:kprobe 自动集成与手动源码补丁全解析
【免费下载链接】KernelSUA Kernel based root solution for Android项目地址: https://gitcode.com/GitHub_Trending/ke/KernelSU
本文基于 KernelSU 官方文档(俄文版)整理,并结合当前仓库源码进行深度印证。内容面向需要为旧款(非 GKI)Android 设备自编译内核、并希望集成 KernelSU 的开发者,涵盖
kprobe自动集成、手动修改内核源码两条完整路线,以及安全模式、path_umount回移(backport)等关键附加功能。
文档适用范围与版本警示
::: warning 存档说明 本文对应文档仅用于存档参考,自 KernelSU v1.0 起,官方已放弃对非 GKI 设备的支持。KernelSU 最后正式支持非 GKI 内核的版本为v0.9.5,集成时务必使用正确的版本(可参考同主题英文文档的版本提示)。本文所述流程面向旧版本 KernelSU 的集成场景。 :::
KernelSU 可以被集成进非 GKI 内核,官方曾将其移植到 Linux 4.14 及更早版本。由于非 GKI 内核碎片化严重,项目无法提供统一的构建方式,因此不会为非 GKI 设备发布现成的 boot.img;你需要自行编译集成 KernelSU 的内核镜像。
集成前必须满足一个硬性前置条件:
- 你应当能够从内核源码编译出一个可启动的内核;
- 如果设备内核不开源,则很难为其运行 KernelSU。
在满足上述条件后,有两条集成路线可选:
- 自动集成:使用
kprobe完成内核钩子; - 手动集成:直接修改内核源码,手工植入调用点。
集成方式一:使用 kprobe 自动集成
KernelSU 依赖kprobe完成内核钩子(kernel hooks)。如果你的内核中kprobe运行稳定可靠,官方推荐优先使用这种方式。
第一步:将 KernelSU 加入内核源码树
在内核源码根目录执行官方提供的集成脚本(脚本源码位于本仓库 kernel/setup.sh):
curl -LSs "https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh" | bash -该脚本的实际行为可以从 kernel/setup.sh 中直接确认:
- 自动定位
drivers/目录(支持common/drivers与drivers两种布局,见initialize_variables()); git cloneKernelSU 仓库到KernelSU/目录;- 无参数时自动检出最新 tag(
git checkout "$(git describe --abbrev=0 --tags)"),传入参数则检出指定 commit 或 tag; - 在
drivers/下创建指向KernelSU/kernel的符号链接kernelsu; - 向
drivers/Makefile追加obj-$(CONFIG_KSU) += kernelsu/; - 向
drivers/Kconfig的endmenu之前插入source "drivers/kernelsu/Kconfig"。
同时脚本还提供了--cleanup参数,用于一键回滚上述所有修改(删除符号链接、还原 Makefile 与 Kconfig、删除KernelSU/目录)。
第二步:确认并开启 kprobe 内核配置
KernelSU 的集成依赖 Kconfig 中的KSU选项,而该选项本身依赖 kprobe 支持。查看仓库 kernel/Kconfig 可确认:
config KSU tristate "KernelSU function support" depends on KPROBES && EXT4_FS default y因此,请检查你的内核配置文件(defconfig)中是否已开启 kprobe,若未开启则补充以下配置:
CONFIG_KPROBES=y CONFIG_HAVE_KPROBES=y CONFIG_KPROBE_EVENTS=y重新编译内核后,KernelSU 应当能够正常工作。
第三步:kprobe 未生效与启动循环的排查
如果发现KPROBES依然没有被激活,可以尝试开启CONFIG_MODULES;若仍然无效,则使用make menuconfig搜索 KPROBES 的其他依赖项。
如果在集成 KernelSU 后出现启动循环(bootloop),则可能意味着你的内核中kprobe 本身存在缺陷。此时应修复 kprobe 的 bug,或者改用下文的手动集成方式。
如何判断 kprobe 是否损坏?官方给出的排查方法:在KernelSU/kernel/ksu.c中注释掉ksu_sucompat_init()与ksu_ksud_init()两个初始化调用。如果注释后设备能正常启动,则说明问题很可能出在 kprobe 上(例如ksu_ksud_init()中注册的input_eventkprobe 失败,参考 ksud_integration.c 中register_kprobe(&input_event_kp)的用法)。
集成方式二:手动修改内核源码
如果 kprobe 在你的内核中不可用(可能是上游 bug,或内核版本低于 4.8),可以尝试手动集成。
第一步:将 KernelSU 加入内核源码树
根据需要的版本选择对应命令:
- 最新稳定 tag:
curl -LSs "https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh" | bash -- main 开发分支:
curl -LSs "https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh" | bash -s main- 指定 tag(例如 v0.5.2):
curl -LSs "https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh" | bash -s v0.5.2::: tip 关于版本选择的补充 结合英文母版文档的提示:KernelSU 1.0 及以后版本不再支持非 GKI 内核,最后支持版本为 v0.9.5,请务必使用正确版本。此外,部分设备的 defconfig 位于arch/arm64/configs,另一些则在arch/arm64/configs/vendor/your_defconfig。无论使用哪个 defconfig,都需要显式开启CONFIG_KSU(y启用 /n禁用),例如:
# KernelSU CONFIG_KSU=y该选项在仓库 kernel/Kconfig 中被定义为tristate,依赖KPROBES && EXT4_FS,默认值为y。 :::
第二步:在内核源码中植入 KernelSU 钩子调用
手动集成的核心,是在内核 VFS 层的关键路径上插入 KernelSU 的钩子函数。官方给出了四个补丁作为参考。以下补丁中的函数在当前仓库中均有对应实现,例如 sucompat.c 中的ksu_handle_faccessat_sucompat、sucompat.h 中导出的各 handler,以及 syscall_event_bridge.c 中的ksu_hook_faccessat、ksu_hook_execve/ksu_hook_execveat、ksu_hook_newfstatat等桥接逻辑。
补丁 1:fs/exec.c—— 挂钩 execve 路径
diff --git a/fs/exec.c b/fs/exec.c index ac59664eaecf..bdd585e1d2cc 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -1890,11 +1890,14 @@ static int __do_execve_file(int fd, struct filename *filename, return retval; } +extern bool ksu_execveat_hook __read_mostly; +extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv, + void *envp, int *flags); +extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, + void *argv, void *envp, int *flags); static int do_execveat_common(int fd, struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, int flags) { + if (unlikely(ksu_execveat_hook)) + ksu_handle_execveat(&fd, &filename, &argv, &envp, &flags); + else + ksu_handle_execveat_sucompat(&fd, &filename, &argv, &envp, &flags); return __do_execve_file(fd, filename, argv, envp, flags, NULL); }这里通过ksu_execveat_hook布尔开关选择进入普通 execve 钩子还是 su 兼容层钩子,unlikely()保证正常路径开销极小。
补丁 2:fs/open.c—— 挂钩 faccessat 路径
diff --git a/fs/open.c b/fs/open.c index 05036d819197..965b84d486b8 100644 --- a/fs/open.c +++ b/fs/open.c @@ -348,6 +348,8 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len) return ksys_fallocate(fd, mode, offset, len); } +extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, + int *flags); /* * access() needs to use the real uid/gid, not the effective uid/gid. * We do this by temporarily clearing all FS-related capabilities and @@ -355,6 +357,7 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len) */ long do_faccessat(int dfd, const char __user *filename, int mode) { const struct cred *old_cred; struct cred *override_cred; struct path path; struct inode *inode; struct vfsmount *mnt; int res; unsigned int lookup_flags = LOOKUP_FOLLOW; + ksu_handle_faccessat(&dfd, &filename, &mode, NULL); if (mode & ~S_IRWXO) /* where's F_OK, X_OK, W_OK, R_OK? */ return -EINVAL;补丁 3:fs/read_write.c—— 挂钩 vfs_read 路径
diff --git a/fs/read_write.c b/fs/read_write.c index 650fc7e0f3a6..55be193913b6 100644 --- a/fs/read_write.c +++ b/fs/read_write.c @@ -434,10 +434,14 @@ ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos) } EXPORT_SYMBOL(kernel_read); +extern bool ksu_vfs_read_hook __read_mostly; +extern int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr, + size_t *count_ptr, loff_t **pos); ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { ssize_t ret; + if (unlikely(ksu_vfs_read_hook)) + ksu_handle_vfs_read(&file, &buf, &count, &pos); + if (!(file->f_mode & FMODE_READ)) return -EBADF; if (!(file->f_mode & FMODE_CAN_READ))补丁 4:fs/stat.c—— 挂钩 stat 路径
diff --git a/fs/stat.c b/fs/stat.c index 376543199b5a..82adcef03ecc 100644 --- a/fs/stat.c +++ b/fs/stat.c @@ -148,6 +148,8 @@ int vfs_statx_fd(unsigned int fd, struct kstat *stat, } EXPORT_SYMBOL(vfs_statx_fd); +extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags); + /** * vfs_statx - Get basic and extra attributes by filename * @dfd: A file descriptor representing the base dir for a relative filename @@ -170,6 +172,7 @@ int vfs_statx(int dfd, const char __user *filename, int flags, int error = -EINVAL; unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT; + ksu_handle_stat(&dfd, &filename, &flags); if ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT | AT_EMPTY_PATH | KSTAT_QUERY_FLAGS)) != 0) return -EINVAL;这四个需要查找的函数及其通常所在文件:
do_faccessat,通常在fs/open.c;do_execveat_common,通常在fs/exec.c;vfs_read,通常在fs/read_write.c;vfs_statx,通常在fs/stat.c。
第三步:针对旧内核的适配变体
没有vfs_statx?改用vfs_fstatat
如果内核中没有vfs_statx函数,请改为在vfs_fstatat中植入调用:
diff --git a/fs/stat.c b/fs/stat.c index 068fdbcc9e26..5348b7bb9db2 100644 --- a/fs/stat.c +++ b/fs/stat.c @@ -87,6 +87,8 @@ int vfs_fstat(unsigned int fd, struct kstat *stat) } EXPORT_SYMBOL(vfs_fstat); +extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags); + int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat, int flag) { @@ -94,6 +96,8 @@ int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat, int error = -EINVAL; unsigned int lookup_flags = 0; + ksu_handle_stat(&dfd, &filename, &flag); + if ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT | AT_EMPTY_PATH)) != 0) goto out;内核早于 4.17,找不到do_faccessat?
直接找到faccessat系统调用定义处,把调用放进去:
diff --git a/fs/open.c b/fs/open.c index 2ff887661237..e758d7db7663 100644 --- a/fs/open.c +++ b/fs/open.c @@ -355,6 +355,9 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len) return error; } +extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, + int *flags); + /* * access() needs to use the real uid/gid, not the effective uid/gid. * We do this by temporarily clearing all FS-related capabilities and @@ -370,6 +373,8 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode) int res; unsigned int lookup_flags = LOOKUP_FOLLOW; + ksu_handle_faccessat(&dfd, &filename, &mode, NULL); + if (mode & ~S_IRWXO) /* where's F_OK, X_OK, W_OK, R_OK? */ return -EINVAL;第四步:启用内置安全模式(强烈建议)
要启用 KernelSU 内置的安全模式(Safe Mode),需要修改drivers/input/input.c中的input_handle_event函数:
::: tip 强烈建议开启该功能,它对防止启动循环(bootloop)非常有帮助! :::
diff --git a/drivers/input/input.c b/drivers/input/input.c index 45306f9ef247..815091ebfca4 100755 --- a/drivers/input/input.c +++ b/drivers/input/input.c @@ -367,10 +367,13 @@ static int input_get_disposition(struct input_dev *dev, return disposition; } +extern bool ksu_input_hook __read_mostly; +extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value); + static void input_handle_event(struct input_dev *dev, unsigned int type, unsigned int code, int value) { int disposition = input_get_disposition(dev, type, code, &value); + if (unlikely(ksu_input_hook)) + ksu_handle_input_handle_event(&type, &code, &value); if (disposition != INPUT_IGNORE_EVENT && type != EV_SYN) add_input_randomness(type, code, value);从源码看,安全模式的判定逻辑位于 ksud_integration.c:ksu_handle_input_handle_event统计KEY_VOLUMEDOWN(音量减)按键事件,连续按下 3 次(volumedown_pressed_count >= 3)即触发安全模式,随后通过ksu_is_safe_mode()被 userspace 查询确认,并立即卸载输入钩子(ksu_stop_input_hook_runtime())。
::: warning 关于误触发的补充说明 结合英文母版文档的提示:如果你使用手动集成且没有禁用CONFIG_KPROBES,用户可能在开机后通过长按音量减键意外触发安全模式。因此,使用手动集成时有必要禁用CONFIG_KPROBES。 :::
附加修复:终端中无法执行pm命令?
如果遇到该问题,需要修改fs/devpts/inode.c,参考补丁如下:
diff --git a/fs/devpts/inode.c b/fs/devpts/inode.c index 32f6f1c68..d69d8eca2 100644 --- a/fs/devpts/inode.c +++ b/fs/devpts/inode.c @@ -602,6 +602,8 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv) return dentry; } +extern int ksu_handle_devpts(struct inode*); + /** * devpts_get_priv -- get private data for a slave * @pts_inode: inode of the slave @@ -610,6 +612,7 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv) */ void *devpts_get_priv(struct dentry *dentry) { + ksu_handle_devpts(dentry->d_inode); if (dentry->d_sb->s_magic != DEVPTS_SUPER_MAGIC) return NULL; return dentry->d_fsdata;附加功能:手动回移path_umount(pre-GKI 的模块卸载支持)
如果你希望"卸载模块(Umount modules)"功能在 pre-GKI 内核上正常工作,而你的内核版本低于 5.9,则需要手动将path_umount从 5.9 回移到fs/namespace.c。参考补丁如下:
--- a/fs/namespace.c +++ b/fs/namespace.c @@ -1739,6 +1739,39 @@ static inline bool may_mandlock(void) } #endif +static int can_umount(const struct path *path, int flags) +{ + struct mount *mnt = real_mount(path->mnt); + + if (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW)) + return -EINVAL; + if (!may_mount()) + return -EPERM; + if (path->dentry != path->mnt->mnt_root) + return -EINVAL; + if (!check_mnt(mnt)) + return -EINVAL; + if (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */ + return -EINVAL; + if (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN)) + return -EPERM; + return 0; +} + +int path_umount(struct path *path, int flags) +{ + struct mount *mnt = real_mount(path->mnt); + int ret; + + ret = can_umount(path, flags); + if (!ret) + ret = do_umount(mnt, flags); + + /* we mustn't call path_put() as that would clear mnt_expiry_mark */ + dput(path->dentry); + mntput_no_expire(mnt); + return ret; +} /* * Now umount can handle mount points as well as block devices. * This is important for filesystems which use unnamed block devices.如果不回移path_umount,"卸载模块"功能将无法工作。该补丁将校验逻辑封装在can_umount()中(检查标志合法性、挂载权限、挂载点根目录、check_mnt、MNT_LOCKED标志以及MNT_FORCE所需的CAP_SYS_ADMIN能力),再调用do_umount()执行真正的卸载,并手动管理dentry/mount引用计数以避免清除mnt_expiry_mark。
源码层面的补充印证
手动集成补丁中引用的各钩子函数,在当前仓库源码中均有对应实现,可作为集成正确性的参照:
- execve 相关:
ksu_handle_execveat/ksu_handle_execveat_sucompat的声明与实现分布在 sucompat.h、sucompat.c(ksu_handle_execveat_sucompat)以及 syscall_event_bridge.c(ksu_hook_execve/ksu_hook_execveat桥接,内部通过static_branch控制 ksud 钩子与 su 兼容层分派); - faccessat 相关:
ksu_handle_faccessat_sucompat定义于 sucompat.c,桥接入口ksu_hook_faccessat位于 syscall_event_bridge.c; - 安全模式输入钩子:
ksu_handle_input_handle_event实现于 ksud_integration.c,其 kprobe 注册方式(input_event_kp)见同文件ksu_ksud_init(); - Kconfig 依赖:
CONFIG_KSU对KPROBES && EXT4_FS的依赖关系,可直接在 kernel/Kconfig 中核实,这解释了为何两种集成方式都绕不开 kprobe 相关配置; - 集成脚本:手动方式第一步使用的
setup.sh即仓库内的 kernel/setup.sh,支持 tag/分支参数与--cleanup清理。
总结
非 GKI 内核集成 KernelSU 的核心决策路径如下:
- 确认内核可自编译且开源,这是所有操作的前提;
- 优先尝试 kprobe 自动集成:运行
setup.sh添加 KernelSU → 确保CONFIG_KPROBES/CONFIG_HAVE_KPROBES/CONFIG_KPROBE_EVENTS开启 → 重新编译; - 若 kprobe 不可用(内核 < 4.8 或上游 bug)导致 bootloop,则改用手动集成:添加 KernelSU → 开启
CONFIG_KSU=y→ 在fs/exec.c、fs/open.c、fs/read_write.c、fs/stat.c四个文件中按上文补丁植入钩子(旧内核注意vfs_fstatat、faccessat变体); - 按需补充安全模式(
input.c)、pm命令修复(devpts/inode.c)、path_umount回移(fs/namespace.c,内核 < 5.9); - 手动集成时记得禁用
CONFIG_KPROBES,避免误触发安全模式。
完成上述全部修改后重新编译内核,KernelSU 即可在非 GKI 设备上正常工作。需要注意,以上能力以 KernelSU v0.9.5 及更早版本为准,新版本已不再维护非 GKI 支持,本指南仅作历史存档与旧设备维护参考。
【免费下载链接】KernelSUA Kernel based root solution for Android项目地址: https://gitcode.com/GitHub_Trending/ke/KernelSU
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考