liunx(3)-内核模块编写与系统调用

文章目录

    • 问题
    • 运行环境
    • 程序组成
    • 实现思路
    • helllo.c
    • Makefile
    • main.c
    • 程序运行


问题

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


运行环境

      Ubuntu-20.04 64位虚拟机

程序组成

      1,采用hello.c实现新的系统调用的函数 ,按照内核模块编写的规范来编写程序(包含有两部分:1,修改系统调用表;2,编写自己的系统调用服务程序)
      2,main.c 用来进行调用所编写的中断例程

实现思路

      因为之前了解过汇编的中断向量表,有过一定的实践,所以直接采用修改linux系统调用表的方法。

1,如何在系统调用表中添加新的表项?
      先找到系统调用表的入口地址,由于现在的linux系统会对给表做一个安全的保护,不让用户轻易的修改系统调用表,所以需要去掉系统的保护之后,才能将我们自己编写的程序地址加载到系统调用表中,接下来才能被调用。

      a,查找系统调用表地址 sudo cat /proc/kallsyms | grep sys_call_table 演示如下:

cedric@ubuntu:~/Desktop/C$ sudo cat /proc/kallsyms | grep sys_call_table
ffffffffb7400280 R x32_sys_call_table
ffffffffb74013a0 R sys_call_table
ffffffffb74023e0 R ia32_sys_call_table

      其中中间的地址ffffffffb74013a0是我们后面会采用到的。注意:这个地址并非一成不变的,特别的,每次开机都需要执行该命令获得新的系统调用表的地址。
      b,需要修改cr0控制寄存器的第16位以便于用来写系统调用表,将该位清0,禁用系统对调用表的写保护。在此采用汇编语言来实现。注意:在使用内嵌汇编时,在linux下需要采用AT&T的汇编语法,并且在对寄存器操作时应该禁用中断响应,防止出错。禁用写保护的汇编代码如下:

    asm volatile
    (
        "cli;"       //禁止中断
        "push %rax;"
        "movq %cr0,%rax;"
        "and $0xfffffffffffeffff,%rax;"  //第16位置0,禁用写保护
        "movq %rax,%cr0;"
        "popq %rax;"
        //"sti;"

    );

      c,修改系统调用表项,将其替换为自己编写的程序。首先将得到的系统调用表地址保存到变量SYS_CALL_TABLE_ADDRESS中(每次开机都要进行修改),然后将系统调用表223号表项内容保存起来,禁用系统的写保护,将我们编写的函数sys_mycall地址替换223号表项,恢复系统的写保护,整个替换工作就此结束。代码如下:

    sys_call_table_my=(unsigned long*)(SYS_CALL_TABLE_ADDRESS); //初始化全局变量
    saveAddress=sys_call_table_my[NUM];  //保存223号表项
    clear_cr0();
    sys_call_table_my[NUM]=(unsigned long) &sys_mycall;  //设置223号表项为我们自己的函数
    restore_cr0();

2,实现具体想要的功能,将所有进程按照父子关系进行显示。
      采用list_for_each函数,从init_task的地址开始递归访问即可,代码如下:

static void getTasks(struct task_struct* root)
{
    struct task_struct* p;
    struct list_head* list;
    list_for_each(list,&root->children) 
    {
        p = list_entry(list,struct task_struct,sibling);
        printk("parent: id and name: %d %s    myself: id and name: %d %s\n",p->parent->pid,p->parent->comm,p->pid,p->comm);

        getTasks(p);
    }
}

helllo.c

      这里需要注意一下内核模块的编写规范,包括模块凭证,入口和退出函数,以及printk函数的使用。


#include 
#include 
#include   //定义task_struct
#include    //定义list_head
#include   //定义init_task
MODULE_LICENSE("Dual BSD/GPL");

#define SYS_CALL_TABLE_ADDRESS 0xffffffff87a013a0  //本电脑sys_call_table对应的地址 
#define NUM 223

static unsigned long saveAddress;//保存223号表项的入口地址
unsigned long* sys_call_table_my=0;//用来作为系统调用表的首地址 

static int clear_cr0(void)//AT&T 64位汇编
{
    asm volatile
    (
        "cli;"       //禁止中断
        "push %rax;"
        "movq %cr0,%rax;"
        "and $0xfffffffffffeffff,%rax;"  //第16位置0,禁用写保护
        "movq %rax,%cr0;"
        "popq %rax;"
        //"sti;"

    );

    printk(KERN_INFO " Hello World enter\n");
    return 0;
}



static void restore_cr0(void)
{
    asm volatile
    (
       //"cli;"
        "pushq %rax;"
        "movq %cr0,%rax;"
        "or $0x0000000000010000,%rax;"
        "movq %rax,%cr0;"
        "popq %rax;"
        "sti;"       //恢复中断

    );
    printk(KERN_INFO " Cruel World exit\n");
}


static void getTasks(struct task_struct* root)
{
    struct task_struct* p;
    struct list_head* list;
    list_for_each(list,&root->children) 
    {
        p = list_entry(list,struct task_struct,sibling);
        printk("parent: id and name: %d %s    myself: id and name: %d %s\n",p->parent->pid,p->parent->comm,p->pid,p->comm);

        getTasks(p);
    }
}

static int sys_mycall(void)//测试自己的系统调用
{
    printk(KERN_INFO"The pids will be show...:  \n");
	getTasks(&init_task);
  
    return current->pid;
}


static int call_init(void)
{
    sys_call_table_my=(unsigned long*)(SYS_CALL_TABLE_ADDRESS); //初始化全局变量
    printk(KERN_INFO"call_init......\n");
    saveAddress=sys_call_table_my[NUM];  //保存223号表项
    clear_cr0();
    sys_call_table_my[NUM]=(unsigned long) &sys_mycall;  //设置223号表项为我们自己的函数
    restore_cr0();

    return 0;
}

static void call_exit(void)
{
    printk(KERN_INFO"call_exit......\n");
    clear_cr0();
    sys_call_table_my[NUM]=saveAddress;   //恢复223号表项原有内容

    restore_cr0();
}

module_init(call_init);
module_exit(call_exit);

MODULE_AUTHOR("cedric");

Makefile

      这是一个可用的Makefile文件,文件名就是Makefile,不需要后缀名。它对hello.c程序进行编译。

ifneq ($(KERNELRELEASE),)  # call from kernel build system
	obj-m	:= hello.o
else
	KERNELDIR := /lib/modules/$(shell uname -r)/build
	PWD       := $(shell pwd)
default:
	make -C $(KERNELDIR) M=$(PWD) modules
endif

main.c

      它仅用来调用新的223号中断程序,测试最终结果。

#include
#include
#include

int main()
{

    int pid=syscall(223); //在此调用223号中断例程。
    printf("The current pid is: %d\n",pid);

    return 0;
}

程序运行

1,采用 sudo cat /proc/kallsyms | grep sys_call_table 命令获取当前电脑的系统调用表地址。
2,修改hello.c 中的对应语句,如下:
#define SYS_CALL_TABLE_ADDRESS 0xffffffff87a013a0
3,进入到root模式下make: sudo su + make
4,加载内核模块 hello.ko: insmod hello.ko
5,编译main.c: gcc –o main main.c
6,通过main调用223号服务例程: ./main
7,在系统日志中查看输出结果: cat /var/log/syslog
8,卸载内核模块: rmmod hello

你可能感兴趣的:(Linux,linux)