通过添加系统调用的方式遍历当前进程

1.题目要求

  在Linux内核中增加一个系统调用,并编写对应的linux应用程序。利用该系统调用能够遍历系统当前所有进程的任务描述符,并按进程父子关系将这些描述符所对应的进程id(PID)组织成树形结构显示。

2.基础知识

2.1 什么是系统调用?

  系统调用,本质上就是内核态函数的调用。CPU在内核态下可以随意转成用户态,但在用户态下不能随意切换到内核态。(内核态和用户态是操作系统的两种运行级别,内核态下CPU可执行任何指令,用户态下CPU只能执行非授权指令)一般程序处于用户态下,如果想通过调用内核态函数使用系统资源时,必须调用软中断的方式,将用户态陷入内核态。
  例如,一个运行在用户态的进程,如果要执行文件写操作,必须要调用write等系统调用,也就是通过调用内核中的代码来完成操作。此时,进程从用户态切换到内核态,进入到内核地址空间执行相应的代码,(Linux进程的4GB地址空间,3G-4G部分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。)当内核函数执行完成后,再切换到用户态。

2.2 如何添加系统调用?

  对于每个系统调用都会有一个对应的系统调用号作为唯一标识,内核维护一张系统调用表(sys_call_table),而系统调用号就是系统调用在系统调用表里的偏移量。
  用户空间通过调用syscall函数,产生0x80号软中断,让程序从用户态切换到内核态。在内核空间中接收到软中断0x80信号,系统会执行system_call函数。当0x80号中断执行的时候,系统调用号会被放在eax寄存器中,system_call函数通过读取eax寄存器来获取系统调用号。将获取的系统调用号乘以4得到偏移地址,而偏移地址加上系统调用表(sys_call_table)的基地址就是所要执行服务程序的地址。也就是说,服务程序地址=(eax)×4+sys_call_table的基地址。具体流程如下图所示。
这里写图片描述
  

2.3 如何对系统中当前运行的进程遍历?

  Linux系统刚开始启动的时候,内核(kernel)只建立了一个init进程。Linux kernel并不提供直接建立新进程的系统调用。剩下的所有进程都是init进程通过fork机制建立的。新的进程要通过老的进程复制自身得到,这就是fork。fork是一个系统调用。进程存活于内存中。每个进程都在内存中分配有属于自己的一片空间 (address space)。当进程fork的时候,Linux在内存中开辟出一片新的内存空间给新的进程,并将老的进程空间中的内容复制到新的空间中,此后两个进程同时运行。
  老进程成为新进程的父进程(parent process),而相应的,新进程就是老的进程的子进程(child process)。一个进程除了有一个PID之外,还会有一个PPID(parent PID)来存储的父进程PID。如果我们循着PID不断向上追溯的话,总会发现其源头是init进程。所以说,所有的进程也构成一个以init为根的树状结构。这样只要获取了本进程的PID就可以探知系统中进程树,从而获取当前系统中所有进程。

3.程序设计与实现

3.1 程序设计思路

  添加一个新的系统调用来实现对当前进程遍历,其本质就是在内核中添加一个可以遍历当前进程的函数。可以采用添加内核模块的方法实现。
  linux内核模块是一些可以让操作系统在需要时载入和执行的代码,不需要时可以由内核卸载的程序代码。在加载内核模块时无需重启系统,这样可以使内核短小精悍。通过这种方法省去了重新编译带来的麻烦,而且避免了使内核臃肿。
通过内核模块实现添加系统调用,这种方法其实是对系统调用的拦截。系统调用服务程序的地址是放在sys_call_table中,可以通过系统调用号定位到具体的地址,那么我们通过编写内核模块将sys_call_table中的系统调用的地址修改为我们自己定义的函数的地址,这样调用该系统调用时就会执行我们自定义的函数。
下图为本程序的结构图。
这里写图片描述

3.2 系统调用模块设计与实现

  程序基本思路是,通过编写内核模块来修改sys_call_table中的系统调用的地址为我们自己定义的函数的地址,就可以实现系统调用的拦截。因此,按照程序思路,本部分的阐述分为程序基本框架、初始化函数,写保护设置三个部分。

3.2.1 程序基本框架

  开始时,我们先要包含一些常见的文件头,并用预定义的宏来描述模块。
  

#include <linux/init.h>  
#include <linux/module.h>  
#include <linux/kernel.h>  

module_init(init_addsyscall);  
module_exit(exit_addsyscall);
MODULE_LICENSE("GPL");

  模块入口函数为init_addsyscall(),由module_init()宏指定,在模块被加载的时候被调用向系统注册。入口函数的返回值:0表示成功,非0表示失败。模块的退出函数为exit-_addsyscall(),由module_exit()宏指定,在模块被卸载时被调用向系统注销,主要来完成资源的清理工作。它被调用完毕后,就模块就被内核清除了。一个模块最少需要有入口和退出函数。

3.2.2 内核初始化函数

  在这里使用系统空闲的223号空闲的系统调用号。首先保存原223号系统调用的地址,然后修改该系统调用的地址将其指向进程遍历函数。
  

static int __init init_addsyscall(void)  
{  
    sys_call_table = (unsigned long *)sys_call_table_address;//获取sys_call_table的首地址 
    anything_saved = (int(*)(void)) (sys_call_table[my_syscall_num]);//保存原始系统调用的地址  
    orig_cr0 = clear_and_return_cr0();//修改sys_call_table写属性
    sys_call_table[my_syscall_num]= (unsigned long)&sys_mycall;//将223号指向自己写的调用函数 
    setback_cr0(orig_cr0);//恢复页表只读属性 
    return 0;  
}  

移除内核模块时,将原有的系统调用进行还原。

static void __exit exit_addsyscall(void)  
{  
    //设置cr0中对sys_call_table的更改权限。  
    orig_cr0 = clear_and_return_cr0();//设置cr0可更改  
    //恢复原有的中断向量表中的函数指针的值。  
    sys_call_table[my_syscall_num]= (unsigned long)anything_saved;  
    //恢复原有的cr0的值  
    setback_cr0(orig_cr0); 
}  

3.2.3 写保护设置

  根据程序设计思路,首先要找到sys_call_table的地址。可以在终端输入命令:
grep sys_call_table/boot/System.map-uname-r’
  这样就得到了sys_call_table的地址0xc06224e0,但同时也得到了一个重要的信息,该符号对应的内存区域是只读的。所以我们要修改它,必须对它进行清除写保护。
我们知道控制寄存器cr0的第16位是写保护位。cr0的第16位置为了禁止超级权限,若清零了则允许超级权限往内核中写入数据,这样我们可以再写入之前,将那一位清零,使我们可以写入。然后写完后,又将那一位复原就行了。
  以下代码为清除写保护和恢复写保护

unsigned int clear_and_return_cr0(void) //清除写保护 
{  
    unsigned int cr0 = 0;  
    unsigned int ret;  
    asm("movl %%cr0, %%eax":"=a"(cr0));//读取cr0寄存器的值放入eax中,同时赋值给cr0  
    ret = cr0;  
    cr0 &= 0xfffeffff;  
    asm("movl %%eax, %%cr0"::"a"(cr0));  
    return ret;  
}
void setback_cr0(unsigned int val) 
{  
    asm volatile("movl %%eax, %%cr0"::"a"(val)); //读取val的值到eax寄存器,再将eax寄存器的值放入cr0中  
} 

3.3 进程遍历模块设计与实现

  Linux内核中所有进程以树形结构来表示进程之间的父子关系,首先,我们最容易得到的是当前进程的进程信息,通过使用current,我们可以获得只想当前进程的task_struct结构,并判断当前进程是否是跟进程,如果不是跟进程,则可以通过循环找到根进程,在得到根进程后,我们就可以对Linux进程进行遍历,为了能够进行树形打印,可以将遍历的进程信息进行保存,可以定义一个结构体,来储存当前进程信息,包括进程task_struct及其深度,如下所示。cp变量记录task_struct指针,dep记录该进程的深度。并将结构体记录在一个链表中,最后通过使用循环将其进行树形输出。
  

void processtree(struct task_struct * p,int b)  
{  
    struct list_head * l;  
    a[counter].pid = p -> pid;  
    a[counter].depth = b;  
    counter ++;  
    for(l = p -> children.next; l != &(p->children); l = l->next)  
    {  
        syscall(223,&a);
        for(i = 0; i < 512; i++)  
        {  
            for(j = 0; j < a[i].depth; j++)  
            printf("|-");  
            printf("%d\n",a[i].pid);  
            if(a[i+1].pid == 0)  
            break;  
        }  
        task_struct *t = list_entry(l,struct task_struct,sibling);  
        processtree(t,b+1);  
    }  
}  

  其中for ( p = current; p != &init_task; p =p->parent );语句即为通过当前current进行向上查找,知道p指向根进程init_task。其中getprocesstree(,)生成进程的树结构并进行保存。代码如下图所示。其中pi为储存进程信息的processinfo类型的数组。

3.4 用户态实例模块设计与实现

  在用户态空间我们使用syscall()这个函数去触发223的系统调用。
  

syscall(223,&a);
for(i = 0; i < 512; i++)  
{  
    for(j = 0; j < a[i].depth; j++)  
    printf("|-");  
    printf("%d\n",a[i].pid);  
    if(a[i+1].pid == 0)  
    break;  
}  

4.程序编译

4.1 Makefile文件的编写

KVERS = $(shell uname -r)
# Kernel modules  
obj-m += ProgressTreePrintKernel.o

# Specify flags for the module compilation.  
#EXTRA_CFLAGS=-g -O0  

build: kernel_modules user_test

kernel_modules:  
    make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
user_test:  
    gcc -o ProgressTreePrintUser ProgressTreePrintUser.c 

clean:  
    make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

5.程序运行

  1.编译代码
  在终端输入make即可。
这里写图片描述
  2.将编译出来的内核模块ProgressTreePrintKernel.ko加载到内核中
  这里写图片描述
  3. 运行测试程序,输出树状打印结果
  这里写图片描述
  4.卸载模块
  这里写图片描述

6.完整程序代码

完整程序代码包以及使用说明(包括.C,Makefile文件)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值