一、背景
在之前的 memfd配合跨进程传输fd的例子及原理-CSDN博客 博客里,我们讲了跨进程通过socket传递fd的底层实现原理,并且给了一个例子来替代socket传输和转换部分的逻辑,直接使用底层fd和file的接口来通过模块ko来实现跨进程的fd的转换和重新映射。
这篇博客里,我们继续fd的转换和映射的主题,我们改写一下实现,不再需要共享内存的创建线程去显式地调用ioctl或者socket的send去做转换和存储逻辑,而是由另外一个进程,也就是共享内存的使用者线程,以remote的方式,只根据remote的进程的pid和remote进程上对应的fd,来转换得到对应实际的file资源,并增加其引用计数,这样该remote进程上所管理的资源可以被另外一个进程接管管理起来,从而该remote进程就可以退出。关于这里说的remote,也就是指操作者所访问的内容是属于某个进程上下文的,且这个进程上下文并不是自己。为什么叫remote,内核里有一个可用于获取进程上下文(非自己)上的任意地址空间的函数,叫pin_user_pages_remote,具体细节参考 非gdb方式观察应用程序的运行时的变量状态-CSDN博客。
事实上,进行remote进程上fd对应资源的获取不仅可用于实现刚才说的接管共享内存的功能,还可以实现任何进程的fd对应资源的监控。fd的资源里除了“有名”内存,比如/dev/shm下的文件对应的共享内存,或者一个具体的物理上的文件,这些都是比较好跟踪和维护的,还有一种就是上篇博客里提到的memfd的共享内存,是一种特殊的匿名的共享内存,匿名共享内存不仅有memfd,还有dma-buf,dma-buf也是一个非常普通使用的一套共享内存机制,这个会在后面的博客里详细介绍其使用及原理。
当前这篇博客相比之前的 memfd配合跨进程传输fd的例子及原理-CSDN博客 博客里的实现方式,其一个明显的优势在于,当前这篇博客里的实现是无“嵌入”式的实现方式,也就是当前正在使用共享的进程并不需要因为做这个功能而去“埋下”共享内存相关资源的存储和转换的逻辑到其代码,而是由另外一个进程远程的去查找和获取,算是“解耦”的实现。
接下来,我们在第二章里,先会给出不使用非export的内核函数的方式来实现一个ko模块,来完成这个remote的fd的查找资源和获取资源的逻辑,实现的方法是把内核里没有export的symbol的一个函数把对应实现搬到ko里来。在第三章里,我们介绍一个相对通用的使用内核里非export的symbol的函数的方法,是基于kprobe的,所以它自然是依赖于kprobe选项打开的(所以它相比第二章的实现而言,是有一定局限性的),另外,我们在第三章里会提到kprobe的使用上的一些注意事项。
二、不使用非export的内核函数的方式来实现remote的fd对应资源的获取
我们先在 2.1 里讲解如何使用这个ko,然后在 2.2 里贴出源码,在 2.3 里做一些源码的介绍。
2.1 如何使用和效果展示
第二章这个例程里有三个部分,一个是用于获取remote的进程的fd对应的资源,返回当前进程的fd来对应这个资源的模块ko,一个是创建memfd匿名共享内存机制的进程,该进程与ko没有任何直接的代码上的联系,还有一个是借助该remote获取fd对应资源并转换成fd的这个ko,通过ioctl这个ko拿到转化后的fd,并通过拿到的fd,mmap出对应的内存,并读取共享内存上的内容的例子。
如下进行运行:
先insmod testremotefd.ko:
然后运行创建memfd匿名共享内存的进程:
然后,运行通过testremotefd.ko,传入remote进程的pid和进程上的fd号,获取remote进程上fd号对应的资源并转换成ioctl所在进程上的unused的fd资源,进行mmap,并读取的例程:
上图中./testgetremotefd后面的两个参数,第一个参数是remote进程的pid,第二个参数是remote进程上我们关心的资源对应的fd号。
例程里,创建memfd匿名共享内存的进程是打印了创建的memfd共享内存的fd(关于memfd的介绍,见之前的博客 memfd配合跨进程传输fd的例子及原理-CSDN博客 里 2.2 一节)。
下面这个截图展示了,在testmemfd创建了memfd共享内存以后,testgetremotefd程序接管了该共享内存以后,testmemfd退出后,testgetremotefd能继续使用该共享内存:
下图是表示,在testgetremotefd A接管了testmemfd创建的共享内存以后(testmemfd退出),再起了一个testgetremotefd B来通过读取testgetremotefd A里的是哪个fd,通过testremotefdko来获取到该共享内存资源,并能成功访问
2.1.1 关于如何获取任意一个进程的fd对应的mmap映射信息的方法
拿上面的运行的testmemfd的程序的例子,上面截图里显示该进程的pid是1892501,我们通过cat /proc/1892501/maps来查看,发现了一个我们感兴趣的映射的共享内存区域:
当然,我们可以用这个虚拟地址区域,来通过之前的博客 非gdb方式观察应用程序的运行时的变量状态_程序运行变量监控-CSDN博客 里的方法来访问这段内存,但是这个方法还是更偏向于调试,而不是服务接管功能。
我们看到这个/memfd:my_shared_mem (deleted)字样,去在/proc/<pid>/fd里去依次通过下图里的readlink方法去找fd对应的映射文件(下图里fd 3对应的是特殊的memfd的内存,它是unlinked的文件,所以显示的是deleted):
这样,我们就得知了remote进程上我们感兴趣的是fd是3对应的资源,就可以传入pid和fd号,来访问remote进程上资源对应的共享内存了:
2.2 实现源码
在 2.2.1 先贴出ko的实现,在 2.2.2 里贴出memfd的创建进程的实现,来展示其代码里是非常干净的仅仅使用了memfd的接口,没有其他代码,也没有通过socket进行传输fd,在 2.2.3 里贴出了使用ko来获取remote进程的fd对应资源的例程的代码。
2.2.1 testremoteko的ko源码
#include <linux/module.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/ioctl.h>
#include <linux/errno.h>
#include <linux/stddef.h>
#include <linux/lockdep.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <trace/events/workqueue.h>
#include <linux/sched/clock.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/file.h>
#include <linux/fdtable.h>
#include <linux/interrupt.h>
#include <linux/tracepoint.h>
#include <trace/events/sched.h>
#include <trace/events/irq.h>
#include <linux/kallsyms.h>
#include <linux/kprobes.h>
typedef struct remotefd_env {
struct proc_dir_entry* remotefd;
} remotefd_env;
static remotefd_env _env;
static int remotefd_proc_open(struct inode *i_pinode, struct file *i_pfile)
{
return 0;
}
#define REMOTEFD_NODE_TYPEMAGIC 0x69
#define REMOTEFD_IO_BASE 0x10
typedef struct remotefd_ioctl_converfd_get_of_pid_fd {
int i_pid;
int i_fd;
int o_fd;
} remotefd_ioctl_converfd_get_of_pid_fd;
#define REMOTEFD_IOCTL_REMOTEFD_GET_OF_PID_FD \
_IOWR(REMOTEFD_NODE_TYPEMAGIC, REMOTEFD_IO_BASE + 1, remotefd_ioctl_converfd_get_of_pid_fd)
static inline struct file *__fget_files_rcu(struct files_struct *files,
unsigned int fd, fmode_t mask)
{
for (;;) {
struct file *file;
struct fdtable *fdt = rcu_dereference_raw(files->fdt);
struct file __rcu **fdentry;
if (unlikely(fd >= fdt->max_fds))
return NULL;
fdentry = fdt->fd + array_index_nospec(fd, fdt->max_fds);
file = rcu_dereference_raw(*fdentry);
if (unlikely(!file))
return NULL;
if (unlikely(file->f_mode & mask))
return NULL;
/*
* Ok, we have a file pointer. However, because we do
* this all locklessly under RCU, we may be racing with
* that file being closed.
*
* Such a race can take two forms:
*
* (a) the file ref already went down to zero,
* and get_file_rcu() fails. Just try again:
*/
if (unlikely(!get_file_rcu(file)))
continue;
/*
* (b) the file table entry has changed under us.
* Note that we don't need to re-check the 'fdt->fd'
* pointer having changed, because it always goes
* hand-in-hand with 'fdt'.
*
* If so, we need to put our ref and try again.
*/
if (unlikely(rcu_dereference_raw(files->fdt) != fdt) ||
unlikely(rcu_dereference_raw(*fdentry) != file)) {
fput(file);
continue;
}
/*
* Ok, we have a ref to the file, and checked that it
* still exists.
*/
return file;
}
}
static struct file *__fget_files(struct files_struct *files, unsigned int fd,
fmode_t mask)
{
struct file *file;
rcu_read_lock();
file = __fget_files_rcu(files, fd, mask);
rcu_read_unlock();
return file;
}
struct file *fget_task(struct task_struct *task, unsigned int fd)
{
struct file *file = NULL;
task_lock(task);
if (task->files)
file = __fget_files(task->files, fd, 0);
task_unlock(task);
return file;
}
struct file* fget_by_pid_and_fd(int i_pid, int i_fd)
{
struct file* ret = NULL;
struct pid* pid_struct;
struct task_struct* ptask;
if (i_fd < 0) {
printk("fd is invalid\n");
return NULL;
}
pid_struct = find_get_pid(i_pid);
if (pid_struct) {
ptask = get_pid_task(pid_struct, PIDTYPE_PID);
if (ptask) {
ret = fget_task(ptask, i_fd);
put_task_struct(ptask);
}
else {
printk("No exist task of i_pid[%d]!\n", i_pid);
put_pid(pid_struct);
return NULL;
}
put_pid(pid_struct);
}
else {
printk("No exist task of i_pid[%d]!\n", i_pid);
return NULL;
}
return ret;
}
static long remotefd_proc_ioctl(struct file *i_pfile, u32 i_cmd, long unsigned int i_arg)
{
switch (i_cmd) {
case REMOTEFD_IOCTL_REMOTEFD_GET_OF_PID_FD:
{
void __user* parg = (void __user*)i_arg;
remotefd_ioctl_converfd_get_of_pid_fd get_of_pid_fd;
struct file* pfile = NULL;
if (copy_from_user(&get_of_pid_fd, parg, sizeof(get_of_pid_fd))) {
printk("copy_from_user failed\n");
return -EFAULT;
}
get_of_pid_fd.o_fd = get_unused_fd_flags(O_RDONLY | O_CLOEXEC);
if (get_of_pid_fd.o_fd < 0) {
printk("get_unused_fd_flags failed!\n");
break;
}
pfile = fget_by_pid_and_fd(get_of_pid_fd.i_pid, get_of_pid_fd.i_fd);
if (pfile == NULL) {
put_unused_fd(get_of_pid_fd.o_fd);
get_of_pid_fd.o_fd = -1;
}
else {
fd_install(get_of_pid_fd.o_fd, pfile);
}
if (copy_to_user(parg, &get_of_pid_fd, sizeof(get_of_pid_fd))) {
printk("copy_to_user failed\n");
return -EFAULT;
}
return 0;
}
default:
return -EINVAL;
}
return 0;
}
static int remotefd_proc_release(struct inode *i_inode, struct file *i_file)
{
return 0;
}
static const struct proc_ops remotefd_proc_ops = {
.proc_read = NULL,
.proc_write = NULL,
.proc_open = remotefd_proc_open,
.proc_release = remotefd_proc_release,
.proc_ioctl = remotefd_proc_ioctl,
};
#define PROC_REMOTEFD_NAME "remotefd"
static int __init remotefd_init(void)
{
_env.remotefd = proc_create(PROC_REMOTEFD_NAME, 0666, NULL, &remotefd_proc_ops);
return 0;
}
static void __exit remotefd_exit(void)
{
remove_proc_entry(PROC_REMOTEFD_NAME, NULL);
}
module_init(remotefd_init);
module_exit(remotefd_exit);
MODULE_LICENSE("GPL v2");
2.2.2 testmemfd的源码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
using namespace std;
int main() {
// Step 1: 创建 memfd 文件描述符
char str[20];
int memfd = memfd_create("my_shared_mem", MFD_CLOEXEC);
if (memfd == -1) {
perror("memfd_create failed");
return 1;
}
printf("memfd=%d\n", memfd);
// Step 2: 设置内存大小(比如 4096 字节)
size_t mem_size = 2*1024*1024;
#if 1
if (ftruncate(memfd, mem_size) == -1) {
perror("ftruncate failed");
close(memfd);
return 1;
}
#endif
// Step 3: 映射内存
void *paddr = mmap(NULL, mem_size, PROT_READ | PROT_WRITE, MAP_SHARED, memfd, 0);
if (paddr == MAP_FAILED) {
perror("mmap failed");
close(memfd);
return 1;
}
mlock(paddr, mem_size);
//for (int i = 0; i < mem_size; i++) {
// *((char*)addr + i) = 0;
//}
// Step 4: 向共享内存写入数据
int d = 0;
snprintf((char *)paddr, mem_size, "Hello, shared memory! [%d]", d++);
// 打印当前进程的内容
printf("Parent process: %s\n", (char *)paddr);
while (1)
{
snprintf((char *)paddr, mem_size, "Hello, shared memory! [%d]", d++);
sleep(1);
}
// Step 5: 解除映射
if (munmap(paddr, mem_size) == -1) {
perror("munmap failed");
close(memfd);
return 1;
}
close(memfd);
return 0;
}
2.2.3 testgetremotefd的源码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <sys/types.h>
#include <sys/ioctl.h>
using namespace std;
#define REMOTEFD_NODE_TYPEMAGIC 0x69
#define REMOTEFD_IO_BASE 0x10
typedef struct remotefd_ioctl_converfd_get_of_pid_fd {
int i_pid;
int i_fd;
int o_fd;
} remotefd_ioctl_converfd_get_of_pid_fd;
#define REMOTEFD_IOCTL_REMOTEFD_GET_OF_PID_FD \
_IOWR(REMOTEFD_NODE_TYPEMAGIC, REMOTEFD_IO_BASE + 1, remotefd_ioctl_converfd_get_of_pid_fd)
#define BUFFER_SIZE 100
int main(int i_argc, char* i_argv[]) {
int convertfd = open("/proc/remotefd", O_RDWR);
remotefd_ioctl_converfd_get_of_pid_fd get;
get.i_pid = std::atoi(i_argv[1]);
get.i_fd = std::atoi(i_argv[2]);
if (ioctl(convertfd, REMOTEFD_IOCTL_REMOTEFD_GET_OF_PID_FD, &get) < 0) {
perror("ioctl");
return 0;
}
if (get.o_fd < 0) {
printf("fd is not valid\n");
return 1;
}
void *mapped_memory = mmap(nullptr, BUFFER_SIZE, PROT_READ, MAP_SHARED, get.o_fd, 0);
if (mapped_memory == MAP_FAILED) {
perror("mmap");
close(convertfd);
return 1;
}
while (1) {
std::cout << "Data from memfd: " << (char *)mapped_memory << std::endl;
sleep(1);
}
munmap(mapped_memory, BUFFER_SIZE);
close(get.o_fd);
return 1;
}
2.3 代码分析
我们依次对 2.2 里的三个源码做分析。
2.3.1 ko源码分析
我们看到,ko源码里,只有一个ioctl的功能,即REMOTEFD_IOCTL_REMOTEFD_GET_OF_PID_FD,意思就是获取remote进程上指定fd号对应的资源并做当前进程的fd的映射。
在 memfd配合跨进程传输fd的例子及原理-CSDN博客 的博客里,我们讲到了fget_raw接口,用来获取本进程的fd对应的struct file指针。这篇博客说的是要获取remote进程,而不是本进程,所以不能用fget_raw接口,得用内核里的fget_task接口,但是fget_task接口并不是一个export symbol,在模块里无法直接使用,所以,我们把fget_task接口的内核里的实现一层层的搬到了模块里,fget_task用的是__fget_files接口:
__fget_files再调用了__fget_files_rcu:
__fget_files_rcu再通过task上的fdtable找到对应的file再返回:
有了这个通过task_struct的指针和其对应进程上下文上的fd号获取的file指针的函数以后,再配合通过pid找对应的task_struct指针,就可以完成要做的功能了:
2.3.2 testmemfd源码分析
这段源码其实就是为了展示里面啥也没做,仅仅调用了memfd_create的接口,ftruncate后,mmap和mlock,然后sleep 1不断地改写共享内存上的内容:
如上图,里面没有socket传输fd的逻辑,也没有ioctl到testremoteko的逻辑。
2.3.3 testgetremotefd源码分析
testgetremotefd的代码页是比较清晰的,仅仅是使用testremoteko的ioctl功能,根据程序的入参,ioctl传入pid和fd号,获取到资源,并分配一个本进程的fd作为ioctl的输出对应此资源,然后通过mmap进行映射,并打印共享内存上的值,如 2.1 里的成果展示,是可以看到共享内存上的值的变化的,因为testmemfd程序是每隔1秒变化一次共享内存上的内容的:
三、借助kprobe来使用非export的内核函数的方式来实现及kprobe使用注意事项
包括之前的博客,我们已经多次通过在模块里复制内核里实现的代码过来,来达到使用内核里非export的symbol的函数的目的。
这一章里,是展示了如何通过kprobe来获取没有export的symbol的函数指针,从而直接使用该函数。
在 3.1 一节里,我们给出了修改后的testremoteko的源码并进行分析和成果展示,在 3.2 里给出了kprobe使用中的注意事项。
3.1 通过kprobe来获取函数指针,从而直接使用内核里没有export的symbol的函数
在第二章里,我们已经说明了,我们需要使用内核里的非export symbol的fget_task函数,该函数,通过如下cat /proc/kallsyms是可以grep到的:
只要是能通过上图方法grep到的符号,就可以通过kprobe来获取到函数指针,见 3.1.1 里的源码。
3.1.1 源码及源码分析
#include <linux/module.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/ioctl.h>
#include <linux/errno.h>
#include <linux/stddef.h>
#include <linux/lockdep.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <trace/events/workqueue.h>
#include <linux/sched/clock.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/file.h>
#include <linux/fdtable.h>
#include <linux/interrupt.h>
#include <linux/tracepoint.h>
#include <trace/events/sched.h>
#include <trace/events/irq.h>
#include <linux/kallsyms.h>
#include <linux/kprobes.h>
typedef struct remotefd_env {
struct proc_dir_entry* remotefd;
} remotefd_env;
static remotefd_env _env;
static int remotefd_proc_open(struct inode *i_pinode, struct file *i_pfile)
{
return 0;
}
#define REMOTEFD_NODE_TYPEMAGIC 0x69
#define REMOTEFD_IO_BASE 0x10
typedef struct remotefd_ioctl_converfd_get_of_pid_fd {
int i_pid;
int i_fd;
int o_fd;
} remotefd_ioctl_converfd_get_of_pid_fd;
#define REMOTEFD_IOCTL_REMOTEFD_GET_OF_PID_FD \
_IOWR(REMOTEFD_NODE_TYPEMAGIC, REMOTEFD_IO_BASE + 1, remotefd_ioctl_converfd_get_of_pid_fd)
typedef struct file* (*fget_task_func)(struct task_struct *i_task, unsigned int i_fd);
fget_task_func _fget_task_func;
struct file* fget_by_pid_and_fd(int i_pid, int i_fd)
{
struct file* ret = NULL;
struct pid* pid_struct;
struct task_struct* ptask;
if (i_fd < 0) {
printk("fd is invalid\n");
return NULL;
}
pid_struct = find_get_pid(i_pid);
if (pid_struct) {
ptask = get_pid_task(pid_struct, PIDTYPE_PID);
if (ptask) {
ret = _fget_task_func(ptask, i_fd);
put_task_struct(ptask);
}
else {
printk("No exist task of i_pid[%d]!\n", i_pid);
return NULL;
}
put_pid(pid_struct);
}
else {
printk("No exist task of i_pid[%d]!\n", i_pid);
return NULL;
}
return ret;
}
static long remotefd_proc_ioctl(struct file *i_pfile, u32 i_cmd, long unsigned int i_arg)
{
switch (i_cmd) {
case REMOTEFD_IOCTL_REMOTEFD_GET_OF_PID_FD:
{
void __user* parg = (void __user*)i_arg;
remotefd_ioctl_converfd_get_of_pid_fd get_of_pid_fd;
struct file* pfile = NULL;
if (copy_from_user(&get_of_pid_fd, parg, sizeof(get_of_pid_fd))) {
printk("copy_from_user failed\n");
return -EFAULT;
}
get_of_pid_fd.o_fd = get_unused_fd_flags(O_RDONLY | O_CLOEXEC);
if (get_of_pid_fd.o_fd < 0) {
printk("get_unused_fd_flags failed!\n");
break;
}
pfile = fget_by_pid_and_fd(get_of_pid_fd.i_pid, get_of_pid_fd.i_fd);
if (pfile == NULL) {
put_unused_fd(get_of_pid_fd.o_fd);
get_of_pid_fd.o_fd = -1;
}
else {
fd_install(get_of_pid_fd.o_fd, pfile);
}
if (copy_to_user(parg, &get_of_pid_fd, sizeof(get_of_pid_fd))) {
printk("copy_to_user failed\n");
return -EFAULT;
}
return 0;
}
default:
return -EINVAL;
}
return 0;
}
static int remotefd_proc_release(struct inode *i_inode, struct file *i_file)
{
return 0;
}
static const struct proc_ops remotefd_proc_ops = {
.proc_read = NULL,
.proc_write = NULL,
.proc_open = remotefd_proc_open,
.proc_release = remotefd_proc_release,
.proc_ioctl = remotefd_proc_ioctl,
};
#define PROC_REMOTEFD_NAME "remotefd"
static int noop_pre_handler(struct kprobe *p, struct pt_regs *regs){
return 0;
}
static int __init remotefd_init(void)
{
int ret;
struct kprobe kp;
memset(&kp, 0, sizeof(kp));
kp.symbol_name = "fget_task";
kp.pre_handler = noop_pre_handler;
kp.addr = NULL; // 作为强调,提示使用symbol_name
ret = register_kprobe(&kp);
if (ret < 0) {
printk("register_kprobe fail!\n");
return -1;
}
printk("register_kprobe succeed!\n");
_fget_task_func = (void*)kp.addr;
unregister_kprobe(&kp);
_env.remotefd = proc_create(PROC_REMOTEFD_NAME, 0666, NULL, &remotefd_proc_ops);
return 0;
}
static void __exit remotefd_exit(void)
{
remove_proc_entry(PROC_REMOTEFD_NAME, NULL);
}
module_init(remotefd_init);
module_exit(remotefd_exit);
MODULE_LICENSE("GPL v2");
我们只看和第二章里不同的部分的代码:
通过上图中的register_kprobe来获取到fget_task的函数指针。
另外,如果我们有多个非export symbol函数需要去获取函数指针的话,倒是可以先获取到kallsyms_lookup_name的函数指针,再通过kallsyms_lookup_name来获取多个函数指针。这样可以省去register_kprobe再unregister_kprobe这些冗余的操作。
3.1.2 成果展示
一样的,先insmod ko:
然后运行testmemfd,再运行testgetremotefd,可以看到效果和第二章里的效果是一样的,是可以成功读取memfd的共享内存的内容的:
3.2 kprobe使用的注意事项
主要有三个注意事项,一个是在通过register_kprobe来进行注册kprobe时,要把struct kprobe结构体的内容都清零,再去做symbol_name的赋值,因为,如果struct kprobe结构的symbol_name和addr同时有内容的话,就会出错,因为register_kprobe无法知道要选择哪一个方式来注册(通过名字找,还是通过地址找),另外,struct kprobe里如flags变量等如果是非零的话都会造成逻辑上的混乱。
第二个注意事项,register_kprobe所用到的struct kprobe这个实例,生命周期必须一值存在到unregister_kprobe函数调用后,因为register_kprobe要使用struct kprobe这个结构体里的内容来进行管理的,它并没有再去赋值一份出来,而是直接用的传入register_kprobe里的指针对应的内存来管理。
第三个注意事项,是unregister_kprobe是会等使用kprobe所注册关联的回调函数执行完后,才返回的,所以,它是可能睡眠的,如下图:
unregister_kprobe调用了unregister_kprobes,unregister_kprobes里调用了synchronize_rcu去等所有的变更结束,因为如下图,unregister_kprobes里用到的__unregister_kprobe_top函数里,使用了list_rcu_del来删除链表:
关于rcu的使用和原理介绍,见之前的博客 rcu的实例、注意事项及原理讲解-CSDN博客 。其实,就是因为unregister_kprobe里会去等所有的注册的回调在回调函数清除后的宽限期结束,我们才可以在 3.1 里使用栈里的内存来作为注册kprobe所用到的struct kprobe的内存。