内核必须懂(一): 自定义系统调用和内核模块的使用(2019.6重编版)

目录

  • 前言
  • 内核模块

用内核模块进行系统调用

  • 自定义系统调用
  • 解压系统源码
  • 撰写自定义系统调用
  • 编译前准备
  • 去优化
  • 编译
  • 安装
  • 重启之后
  • 最后

前言

自定义系统调用和内核模块的使用是Linux内核编程的基础, 这篇就带大家来看看它们的hello, world例子吧.


内核模块

首先是源码部分, 这里由于是内核, 所以c库的函数就不能用了, 比如printf这样的, 要用printk替代, 这里的k就是指kernel.
然后__init__exit是初始化和卸载才会去执行函数, 也就是都只执行一次.
module_initmodule_exit理解为注册函数就行了.
以上这些都是一个内核模块必须要写的.

#include
#include
#include

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Sean Depp");

static int __init hello_init(void)
{
        printk("Hello, sean!\n")  ;
        return 0;
}

static void __exit hello_exit(void)
{
        printk("Exit, sean!\n");
}

module_init(hello_init);
module_exit(hello_exit);

Makefile常规写法就好, 没什么特别要说的. 当然, 你可以写的更有效一些, 比如编译完成之后删除除了.ko文件之外的其它生成文件. 下面给出常规写法改进写法:

obj-m:=helloKo.o

PWD:=$(shell pwd)
KER_DIR=/lib/modules/$(shell uname -r)/build

all :
        make -C $(KER_DIR) M=$(PWD) modules
clean :
        make -C $(KER_DIR) M=$(PWD) clean
ifneq ($(KERNELRELEASE),)
        obj-m := helloKo.o
else
        PWD := $(shell pwd)
        KER_DIR ?= /lib/modules/$(shell uname -r)/build
default:
        $(MAKE) -C $(KER_DIR) M=$(PWD) modules
        rm *.order *.symvers *.mod.c *.o .*.o.cmd .*.cmd .tmp_versions -rf
endif

来编译生成模块, 之后安装和卸载.

sudo make
sudo insmod helloKo.ko
sudo rmmod helloKo

你应该看到了一个提示Makefile:934: "Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel", 这里是缺了一个库, 但是你只是用apt安装是会导致编译失败的, 合理的解决方案就是apt安装之后再重新编译内核, 这样就不会有这个提示了. 类似问题都可以这样解决.

当然, 可以用改进的Makefile再操作一次, 这次用lsmod查看一下安装的模块, 用dmesg查看信息是否打印出来.

成功看到模块和打印的消息:


用内核模块进行系统调用

之前是最基础的内核模块的例子, 来看一个更加复杂的.
来到/usr/include/i386-linux-gnu/asm, 查看unistd_32.h, 注意这是32位Ubutnu12.04.5中的位置.
如果是64位Ubuntu16.04, 在arch/sh/include/uapi/asm/unistd_64.h中.

看到223了吗, 这很明显就是拿来自定义的. 当然了, 你可以用别的没使用过的系统调用号.

unistd_32.h

然后来到/boot, 要查看sys_call_table的内存位置, 注意, 要管理员权限.

sys_call_table

然后用vim搜索sys_call_table.

sys_call_table

开始写syscall.c. 这段代码不是我写的, 来自这篇文章, 写得很棒. 然后请原谅我不要脸地在自定义系统调用里面加了自己的Hello, world!(手动滑稽)

#include 
#include 
#include 
#include 
#include 

MODULE_LICENSE("Dual BSD/GPL");

#define SYS_CALL_TABLE_ADDRESS 0xc1697140  //sys_call_table对应的地址
#define NUM 223  //系统调用号为223
int orig_cr0;  //用来存储cr0寄存器原来的值
unsigned long *sys_call_table_my=0;

static int(*anything_saved)(void);  //定义一个函数指针,用来保存一个系统调用

static int clear_cr0(void) //使cr0寄存器的第17位设置为0(内核空间可写)
{
    unsigned int cr0=0;
    unsigned int ret;
    asm volatile("movl %%cr0,%%eax":"=a"(cr0));//将cr0寄存器的值移动到eax寄存器中,同时输出到cr0变量中
    ret=cr0;
    cr0&=0xfffeffff;//将cr0变量值中的第17位清0,将修改后的值写入cr0寄存器
    asm volatile("movl %%eax,%%cr0"::"a"(cr0));//将cr0变量的值作为输入,输入到寄存器eax中,同时移动到寄存器cr0中
    return ret;
}

static void setback_cr0(int val) //使cr0寄存器设置为内核不可写
{
    asm volatile("movl %%eax,%%cr0"::"a"(val));
}

asmlinkage long sys_mycall(void) //定义自己的系统调用
{   
    printk("Hello, world! Written by Sorrower\n");
    printk("模块系统调用-当前pid:%d,当前comm:%s\n",current->pid,current->comm);
    return current->pid;    
}

static int __init call_init(void)
{
    sys_call_table_my=(unsigned long*)(SYS_CALL_TABLE_ADDRESS);
    printk("call_init......\n");
    anything_saved=(int(*)(void))(sys_call_table_my[NUM]);//保存系统调用表中的NUM位置上的系统调用
    orig_cr0=clear_cr0();//使内核地址空间可写
    sys_call_table_my[NUM]=(unsigned long) &sys_mycall;//用自己的系统调用替换NUM位置上的系统调用
    setback_cr0(orig_cr0);//使内核地址空间不可写
    return 0;
}

static void __exit call_exit(void)
{
    printk("call_exit......\n");
    orig_cr0=clear_cr0();
    sys_call_table_my[NUM]=(unsigned long)anything_saved;//将系统调用恢复
    setback_cr0(orig_cr0);
}

module_init(call_init);
module_exit(call_exit);

MODULE_AUTHOR("25");
MODULE_VERSION("BETA 1.0");
MODULE_DESCRIPTION("a module for replace a syscall");

Makefile文件和之前差不多, 改下生成的.o文件名字就好.
然后要写一个用户态的程序来测试了.

#include
#include
int main()
{
        syscall(223);
        return 0;
}

gcc一下, 然后dmesg一下. 成功通过内核模块进行系统调用.

结果展示

自定义系统调用

自定义系统调用很麻烦, 因为要重编内核, 一次大约两个小时.

解压系统源代码

下载源码, 这里推荐阿里的镜像:

阿里镜像

这里我在Home下建立了目录, 解压源码到下面:

sudo tar -zxvf linux-4.15.tar.gz
源码

指令下载也行, 但是不推荐, 至少不能选版本.

sudo apt-get install linux-source
cd /usr/src/
sudo tar -jxvf linux-source-3.13.0.tar.bz2
解压源码

撰写自定义系统调用

用find指令在linux-source-4.10.0下查找文件, sys.c, syscalls.h, syscall_64.tbl.

find -name sys.c

64位Ubuntu16.04.3得到文件路径(内核和位数不同会有变化):

./kernel/sys.c
./include/linux/syscalls.h
./arch/x86/entry/syscalls/syscall_64.tbl

在sys.c中撰写函数代码:

函数代码

在syscalls.h中声明函数:

声明函数

在syscall_64.tbl中设置系统调用编号:

设置系统调用编号

编译前准备

首先补包:

sudo apt-get install build-essential kernel-package libncurses5-dev libssl-dev libelf-dev

中途可能会看到如图, 建议保持版本即可:

编译前准备

打开grub文件, 由于我的是单Ubuntu系统, 不是常见的Win+Ubuntu双系统, 所以开机选择系统的选项就默认隐藏了, 需要手动打开, 即注释掉第7行:

sudo vim /etc/default/grub
启动菜单

然后更新下grub:

sudo update-grub

然后拷贝配置文件, 其实这就是Ubuntu18.04.1LTS自带的配置文件, 这么做的好处就是稳, 如果你知道自己修改的内核配置是什么, 也同样可以在这个原有编译基础上修改. 当然, 现在你应该在解压的源码下面:

sudo cp /boot/config-4.15.0-46-generic .config
配置文件

去优化

以后还需要进行调试以及查看学习Linux内核源码, 所以需要去优化, 这里大致有两步:

  • 将Makefile中的-O2变成-O1, 这里直接vim全局替换:
:1,$s/-O2/-O1/g
  • 打开.config文件. 不设置CONFIG_CC_OPTIMIZE_FOR_SIZE, 设置CONFIG_DEBUG_SECTION_MISMATCH, 相当于-fno-inline-functions-called-once, 避免inline优化
去优化

去优化

编译

如果你之前编译过, 建议先sudo make mrproper进行清理, 尤其是之前编译失败了.
然后就开始编译, 至少两个小时吧:

sudo make mrproper
sudo make-kpkg clean
sudo make-kpkg --initrd kernel-headers kernel_image

安装

然后你会发现上层目录中多了两个deb包, 安装它们:

sudo dpkg -i *.deb
reboot
编译完成

重启之后

重启之后在grub界面选择4.15.0内核启动, 使用uname -r查看内核版本号, 发现已经改成4.15.0:

内核号

之前默认内核如图:

内核号

当然了, 不只是内核号变了, 还多了些内容, 在/usr/src下面多了源码文件夹和头文件文件夹:

变化

build和source都指向源码目录, kernel里面则是编译好的模块:

变化

/boot下同样增加了自编译内核的相关文件:

变化

最后看到/boot/grub/grub.cfg中多了自编译内核的启动信息:

变化

最后

这篇文章也是修改了多次, 自己也在不断学习. 有意见或者建议评论区见哦~

你可能感兴趣的:(内核必须懂(一): 自定义系统调用和内核模块的使用(2019.6重编版))