• Linux 访问进程地址空间函数 access_process_vm


    一、源码解析

    /**
     * get_task_mm - acquire a reference to the task's mm
     *
     * Returns %NULL if the task has no mm.  Checks PF_KTHREAD (meaning
     * this kernel workthread has transiently adopted a user mm with use_mm,
     * to do its AIO) is not set and if so returns a reference to it, after
     * bumping up the use count.  User must release the mm via mmput()
     * after use.  Typically used by /proc and ptrace.
     */
    struct mm_struct *get_task_mm(struct task_struct *task)
    {
    	struct mm_struct *mm;
    
    	task_lock(task);
    	mm = task->mm;
    	if (mm) {
    		if (task->flags & PF_KTHREAD)
    			mm = NULL;
    		else
    			atomic_inc(&mm->mm_users);
    	}
    	task_unlock(task);
    	return mm;
    }
    EXPORT_SYMBOL_GPL(get_task_mm);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    /*
     * Access another process' address space.
     * Source/target buffer must be kernel space,
     * Do not walk the page table directly, use get_user_pages
     */
    int access_process_vm(struct task_struct *tsk, unsigned long addr,
    		void *buf, int len, int write)
    {
    	struct mm_struct *mm;
    	int ret;
    
    	mm = get_task_mm(tsk);
    	if (!mm)
    		return 0;
    
    	ret = __access_remote_vm(tsk, mm, addr, buf, len, write);
    	mmput(mm);
    
    	return ret;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    其功能主要在__access_remote_vm函数:

    /*
     * Access another process' address space as given in mm.  If non-NULL, use the
     * given task for page fault accounting.
     */
    static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
    		unsigned long addr, void *buf, int len, int write)
    {
    	struct vm_area_struct *vma;
    	void *old_buf = buf;
    
    	down_read(&mm->mmap_sem);
    	/* ignore errors, just check how much was successfully transferred */
    	while (len) {
    		int bytes, ret, offset;
    		void *maddr;
    		struct page *page = NULL;
    
    		ret = get_user_pages(tsk, mm, addr, 1,
    				write, 1, &page, &vma);
    		if (ret <= 0) {
    			/*
    			 * Check if this is a VM_IO | VM_PFNMAP VMA, which
    			 * we can access using slightly different code.
    			 */
    #ifdef CONFIG_HAVE_IOREMAP_PROT
    			vma = find_vma(mm, addr);
    			if (!vma || vma->vm_start > addr)
    				break;
    			if (vma->vm_ops && vma->vm_ops->access)
    				ret = vma->vm_ops->access(vma, addr, buf,
    							  len, write);
    			if (ret <= 0)
    #endif
    				break;
    			bytes = ret;
    		} else {
    			bytes = len;
    			offset = addr & (PAGE_SIZE-1);
    			if (bytes > PAGE_SIZE-offset)
    				bytes = PAGE_SIZE-offset;
    
    			maddr = kmap(page);
    			if (write) {
    				copy_to_user_page(vma, page, addr,
    						  maddr + offset, buf, bytes);
    				set_page_dirty_lock(page);
    			} else {
    				copy_from_user_page(vma, page, addr,
    						    buf, maddr + offset, bytes);
    			}
    			kunmap(page);
    			page_cache_release(page);
    		}
    		len -= bytes;
    		buf += bytes;
    		addr += bytes;
    	}
    	up_read(&mm->mmap_sem);
    
    	return buf - old_buf;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    __access_remote_vm 函数用于访问另一个进程的地址空间。以下是对该函数的中文说明:
    函数开始时使用 down_read(&mm->mmap_sem) 获取内存描述符的 mmap 信号量的读取锁。这确保了同步,并防止对内存映射的并发修改。

    接着,函数进入一个循环,直到传输完整个长度 len 的数据或发生错误为止。

    在循环内部,函数调用 get_user_pages 来从目标进程的地址空间中检索与给定地址 addr 对应的页面。get_user_pages 函数尝试锁定页面并检索它们。如果返回值 ret 小于等于零,则表示出现错误或地址空间的末尾。

    如果 ret 小于等于零,则函数检查地址处的虚拟内存区域(VMA)是否是一种特殊类型(VM_IO | VM_PFNMAP),需要使用不同的访问方式。这个检查用于处理无法直接访问页面的特定情况。如果 VMA 是特殊类型,并且有关联的 access 函数,则调用该函数执行访问操作。如果 access 函数返回的值也是非正数,则终止循环。

    如果页面检索成功(ret 是正数),函数继续执行数据传输操作。它根据剩余长度 len 和当前页面的偏移量计算要传输的字节数。然后使用 kmap 将页面映射到内核地址空间以进行直接访问。

    如果设置了 write 标志,函数使用 copy_to_user_page 将数据从缓冲区 buf 复制到指定地址 addr 处的映射页面。它还使用 set_page_dirty_lock 将页面标记为脏页。

    如果未设置 write 标志,函数使用 copy_from_user_page 将数据从映射页面复制到指定地址 addr 处的缓冲区 buf。

    在完成数据传输后,函数使用 kunmap 解除页面在内核地址空间的映射,并使用 page_cache_release 释放页面。

    然后,函数更新下一次循环迭代的剩余长度 len、缓冲区指针 buf 和地址 addr。

    循环结束后,函数使用 up_read(&mm->mmap_sem) 释放对 mmap 信号量的读取锁。

    最后,函数返回传输的字节数(buf - old_buf),表示成功传输的总大小。

    __access_remote_vm 函数的作用是允许在内核中访问另一个进程的地址空间。通过该函数,内核可以直接读取或写入指定进程的内存数据,而无需通过用户空间或进程间通信来实现。

    这种功能在某些情况下非常有用,例如:
    (1)调试:允许调试器在内核级别访问目标进程的内存,以查看其状态、变量值和数据结构。

    (2)进程间通信:某些进程间通信机制可能需要在内核中进行数据交换,例如通过共享内存或管道进行高效的数据传输。

    (3)内核模块开发:内核模块可能需要读取或修改其他进程的内存数据,以实现特定的功能或扩展性。

    (4)效率优化:在某些情况下,直接在内核中访问另一个进程的内存可以提高性能,避免了用户空间和内核空间之间的数据复制开销。

    二、Linux内核 用途

    2.1 ptrace请求

    PTRACE_PEEKTEXT和PTRACE_PEEKDATA是用于在被跟踪进程的内存中读取数据的ptrace系统调用的请求选项。
    PTRACE_POKETEXT和PTRACE_POKEDATA是用于向被跟踪进程的内存写入数据的ptrace系统调用的请求选项。

    这两个选项父进程读取子进程内存地址空间数据或者写数据到子进程内存地址空间都是用到了access_process_vm函数。

    SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
    		unsigned long, data)
    {
    	struct task_struct *child;
    	child = ptrace_get_task_struct(pid);
    	
    	arch_ptrace(child, request, addr, data);	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    long arch_ptrace(struct task_struct *child, long request,
    		 unsigned long addr, unsigned long data)
    {
    	ptrace_request(child, request, addr, data);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    int ptrace_request(struct task_struct *child, long request,
    		   unsigned long addr, unsigned long data)
    {
    	switch (request) {
    	case PTRACE_PEEKTEXT:
    	case PTRACE_PEEKDATA:
    		return generic_ptrace_peekdata(child, addr, data);
    	case PTRACE_POKETEXT:
    	case PTRACE_POKEDATA:
    		return generic_ptrace_pokedata(child, addr, data);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    generic_ptrace_peekdata用来读取子进程的虚拟地址空间地址:

    int generic_ptrace_peekdata(struct task_struct *tsk, unsigned long addr,
    			    unsigned long data)
    {
    	unsigned long tmp;
    	int copied;
    
    	copied = access_process_vm(tsk, addr, &tmp, sizeof(tmp), 0);
    	if (copied != sizeof(tmp))
    		return -EIO;
    	return put_user(tmp, (unsigned long __user *)data);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对于读取子进程的虚拟地址空间,调用access_process_vm时,第五个参数 write = 0,代表是从该进程的虚拟地址空间读取内容。
    函数的主要逻辑如下:
    (1)声明一个局部变量 tmp,用于存储从目标进程内存中读取的数据。
    (2)调用 access_process_vm 函数,传递目标进程的 task_struct 指针(tsk)、目标虚拟地址(addr)、存储数据的缓冲区指针(&tmp)、要读取的数据大小(sizeof(tmp))和读取标志(0)。
    (3)access_process_vm 函数尝试访问目标进程的内存,并将读取的数据存储到 tmp 变量中。它返回实际复制的字节数。
    (4)检查返回的 copied 是否等于 sizeof(tmp),即判断是否成功复制了全部数据。如果不等于,则表示读取失败,返回错误码 -EIO。
    (5)调用 put_user 函数,将 tmp 变量的值复制到用户空间的 data 变量中。
    (6)返回复制操作的结果。

    这个函数的作用是从另一个进程的指定虚拟地址处读取数据,并将其存储到 data 变量中。它使用了 access_process_vm 函数来访问目标进程的内存,并使用 put_user 函数将读取的数据复制到用户空间。

    generic_ptrace_pokedata用来写入数据到子进程的虚拟地址空间地址:

    int generic_ptrace_pokedata(struct task_struct *tsk, unsigned long addr,
    			    unsigned long data)
    {
    	int copied;
    
    	copied = access_process_vm(tsk, addr, &data, sizeof(data), 1);
    	return (copied == sizeof(data)) ? 0 : -EIO;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    对于写入数据到子进程的虚拟地址空间地址,调用access_process_vm时,第五个参数 write = 1,代表是向该进程的虚拟地址空间写入新内容。

    函数的主要逻辑如下:
    (1)声明一个局部变量 copied,用于存储复制的字节数。
    (2)调用 access_process_vm 函数,传递目标进程的 task_struct 指针(tsk)、目标虚拟地址(addr)、源数据的指针(&data)、要复制的数据大小(sizeof(data))和写入标志(1)。
    (3)access_process_vm 函数尝试访问目标进程的内存,并将源数据复制到目标进程的指定地址处。它返回实际复制的字节数。
    (4)检查返回的 copied 是否等于 sizeof(data),即判断是否成功复制了全部数据。如果相等,则表示写入操作成功,返回0;否则,返回错误码 -EIO。

    这个函数的作用是向另一个进程的指定虚拟地址处写入数据。它使用了 access_process_vm 函数来访问目标进程的内存,并尝试将源数据复制到目标进程中。函数返回0表示写入操作成功,返回非零错误码表示写入失败。

    2.2 进程的命令行

    该函数用来获取进程的命令行:

    static int proc_pid_cmdline(struct task_struct *task, char * buffer)
    {
    	int res = 0;
    	unsigned int len;
    	struct mm_struct *mm = get_task_mm(task);
    	if (!mm)
    		goto out;
    	if (!mm->arg_end)
    		goto out_mm;	/* Shh! No looking before we're done */
    
     	len = mm->arg_end - mm->arg_start;
     
    	if (len > PAGE_SIZE)
    		len = PAGE_SIZE;
     
    	res = access_process_vm(task, mm->arg_start, buffer, len, 0);
    
    	// If the nul at the end of args has been overwritten, then
    	// assume application is using setproctitle(3).
    	if (res > 0 && buffer[res-1] != '\0' && len < PAGE_SIZE) {
    		len = strnlen(buffer, res);
    		if (len < res) {
    		    res = len;
    		} else {
    			len = mm->env_end - mm->env_start;
    			if (len > PAGE_SIZE - res)
    				len = PAGE_SIZE - res;
    			res += access_process_vm(task, mm->env_start, buffer+res, len, 0);
    			res = strnlen(buffer, res);
    		}
    	}
    out_mm:
    	mmput(mm);
    out:
    	return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    proc_pid_cmdline用于获取指定进程的命令行参数。

    struct mm_struct {
    	.....
    	unsigned long arg_start, arg_end, env_start, env_end;
    	.....	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    struct mm_struct 用于描述进程的地址空间,该结构体包含了和进程地址空间有关的全部细信息。每个进程都有一个对应的 mm_struct 结构体,它存储了进程的地址空间布局、内存映射、页表等关键信息。

    arg_start、arg_end、env_start 和 env_end 是用于描述进程的命令行参数和环境变量的起始地址和结束地址。以便访问和操作进程的命令行参数和环境变量。

    函数的主要逻辑如下:
    (1)调用 get_task_mm 函数获取目标进程的内存描述符(mm_struct)。
    (2)计算命令行参数的长度,即 arg_end - arg_start。如果长度超过了页面大小(PAGE_SIZE),将长度截断为页面大小。
    (3)调用 access_process_vm 函数,传递目标进程的 task_struct 指针(task)、命令行参数的起始地址(mm->arg_start)、存储命令行参数的缓冲区指针(buffer)、要复制的数据大小(len)和读取标志(0)。
    (4)检查返回的 res 是否大于0,并且缓冲区中最后一个字节是否不等于空字符(‘\0’),以及长度是否小于页面大小。如果满足这些条件,说明可能发生了 setproctitle 的情况,需要进一步处理:
    使用 strnlen 函数在缓冲区中查找第一个空字符(‘\0’),并将其位置存储在 len 变量中。
    如果找到的空字符位置小于 res,说明命令行参数中存在空字符,将其作为有效长度。
    否则,计算环境变量的长度(env_end - env_start)。
    如果环境变量长度超过了剩余缓冲区的大小(PAGE_SIZE - res),将长度截断为剩余缓冲区的大小。
    调用 access_process_vm 函数,从环境变量的起始地址(mm->env_start)开始,将环境变量的内容复制到缓冲区中(buffer+res),并返回实际复制的字节数。
    使用 strnlen 函数在缓冲区中查找第一个空字符(‘\0’),并将其位置存储在 res 变量中。

    这个函数的作用是获取指定进程的命令行参数,并将其存储在提供的缓冲区中。它首先获取进程的内存描述符,然后通过访问进程的内存来获取命令行参数。如果发现命令行参数中的空字符被覆盖,它还会尝试获取环境变量并将其添加到缓冲区中。最后,函数返回存储的命令行参数的长度。

    参考资料

    Linux 3.10.0

  • 相关阅读:
    地球系统模式(CESM)技术应用
    多线程安全的Queue
    Deep Learning for Detecting Robotic Grasps
    重生之初识svelte框架(1)
    Centos7 之PostgresSQL11.x 安装及插件安装
    计算机视觉学习记录(四):未有深度学习之前
    线程安全问题
    IDEA创建MavenWeb工程并发布到tomcat
    数据集笔记: Porto
    LeetCode刷题(12)
  • 原文地址:https://blog.csdn.net/weixin_45030965/article/details/132688643