内核必须懂(一): 用系统调用打印Hello, world!
内核必须懂(二): 文件系统初探
内核必须懂(三): 重编Ubuntu18.04LTS内核4.15.0
内核必须懂(四): 撰写内核驱动
内核必须懂(五): per-CPU变量
目录
- 前言
- 模块与系统调用
- 用模块打印Hello, world!
- 用模块添加自定义系统调用
- top指令
- 关闭Linux图形界面
- 重编内核添加系统调用
- 解压系统源代码
- 撰写自定义系统调用
- 编译内核
- 测试新内核
- 最后
前言
要自定义系统调用, 常规的两个方法是模块和重编内核, 一起来看看吧.
模块与系统调用
用模块打印Hello, world!
首先是源码部分, 这里由于是内核, 所以c库的函数就不能用了, 比如printf这样的, 要用printk替代, 这里的k就是指kernel.
然后 __init 和 __exit 意味着只有初始化和卸载才会执行函数, 也就是都只执行一次.
module_init 和 module_exit 理解为注册函数就行了.
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/module.h>
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”, 很明显这是一个内核编译的参数没生效, 但是编译成功了. 于是我好奇就装了一下libelf-dev, 反而就无法编译成功了. 这里如果有大佬可以告知我为什么, 评论区见, 提前笔芯. 所以这里暂时不管这个参数了.安装完libelf-dev, 重新编译内核即可.
当然, 可以用改进的Makefile再操作一次, 这次用lsmod查看一下安装的模块, 用dmesg查看信息是否打印出来.
成功看到模块和打印的消息:
用模块添加自定义系统调用
注意, 题目是用系统调用打印Hello, world!, 之前的只是熟悉一下模块的使用, 还不是系统调用打印出来的.
来到/usr/include/i386-linux-gnu/asm, 查看unistd_32.h, 注意这是32位ubutnu12.04.5中的位置, 不代表其他版本其他位数的.
看到223了吗, 这很明显就是拿来自定义的.
然后来到/boot, 要查看sys_call_table的内存位置, 注意, 要管理员权限.
然后用vim搜索sys_call_table. 我特意把行号标出来了, 你要是想手动找到, 祝你好运了.
开始写syscall.c. 这段代码不是我写的, 来自这篇文章, 写得很棒. 然后请原谅我不要脸地在自定义系统调用里面加了自己的Hello, world!(手动滑稽)
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/unistd.h>
#include <linux/sched.h>
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文件名字就好.
然后要写一个用户态的程序来测试了.
什么是用户态, 来快速解释一下. cpu有用户态和核心态, 系统调用以及中断和异常都会由用户态变成核心态. 上一张进程转换图(或者叫状态机?), 图片来自网络, 我觉得画得一般, 但是我不想再手动画一张了.
好了, 不皮了. 来写test.c吧. 简单粗暴, 就一个系统223调用.
#include<stdio.h>
#include<stdlib.h>
int main()
{
syscall(223);
return 0;
}
gcc一下, 然后dmesg一下. 这下真的就结束这一部分了.
top指令
中途休息一下, 来说些小技巧和指令.
mac下的top指令非常好用. 你输入top, 然后输入?, 就显示全部后续操作了. 比如这里top下输入o, 在输入cpu回车. 就是cpu占有排序.
关闭Linux图形界面
我没有很讨厌Linux的图形界面, 但是用了ssh之后, 你就发现确实用不到了. 我知道大家都会切换到tty的. mac是fn+ctrl+option+f3(当然了, 根据版本不同, fx有效范围不同, 12.04是f1-f6, f7图形界面, 测测就知道了)
但是还不够彻底, 要让它开机直接字符界面. 关闭/开启. 当然了, 12.04似乎不吃这个指令. 要再高版本一些.
sudo systemctl set-default multi-user.target
sudo reboot
sudo systemctl set-default graphical.target
sudo reboot
重编内核添加系统调用
重编的难点在于费时.
解压系统源代码
你可以使用指令下载源码, 也可以手动下载. 总之, 下完之后, 解压文件.
sudo apt-get install linux-source
cd /usr/src/
sudo tar -jxvf linux-source-3.13.0.tar.bz2
撰写自定义系统调用
用find指令在linux-source-3.13.0下查找文件, sys.c, syscalls.h, syscall_64.tbl.
find -name sys.c
得到文件路径:
./kernel/sys.c
./include/linux/syscalls.h
./arch/x86/syscalls/syscall_64.tbl
在sys.c中撰写函数代码:
在syscalls.h中声明函数:
在syscall_64.tbl中设置系统调用编号:
编译内核
清理和补库.
make mrproper
sudo apt-get install libncurses5-dev
sudo apt-get install libssl-dev
make mrproper
然后你可以设置编译参数, 如果你知道自己在干嘛的话. 默认直接save然后exit.
sudo make menuconfig
编译装载.
sudo make
sudo make modules
sudo make modules_install
sudo make install
重启.
sudo update-grub2
sudo reboot
查看内核版本号
uname -a
测试新内核
上几张之前实验时候截的效果图, 测试函数还是之前的test.c, 改下调用号就可以了.
最后
先来几个坑, 求人救救孩子~~
这是14.04.5中的, 说什么Invalid module format, StackOverFlow说是内核版本不一致, 但是我Makefile中是用’uname -r’的, 怎么会不一致呢.
问题已经解决, 如果出现上述错误, 只需要使用:sudo apt-get install linux-source-(uname -r得到的内核号)即可.
例如:
sudo apt-get install linux-source-4.15.0
之后使用如下指令, 可能会提示补库:
sudo make bzImage
sudo make modules
sudo make modules_install
reboot之后问题迎刃而解.
然后看一下默认的18.04, 不是我改过内核的那个. 也在google和StackOverFlow看了解决方案, 还是解决不能.
解决方案除了重编内核, 就是重新安装镜像, 目前我新装的18.04.1测试没问题.
这次也是新开一个篇章, 和以往分享操作不同, 文章更偏向探索, 去学习更深的知识. 喜欢记得点赞, 有意见或者建议评论区见, 暗中关注我也是可以的~